Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/astro/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,10 @@ export default defineConfig({
label: "Navigation",
slug: "guide/development/navigation",
},
{
label: "Federated Navigation",
slug: "guide/development/navigation-federation",
},
"guide/development/best-practices",
"guide/development/third-party-libraries",
],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
---
title: Federated Navigation
description: Compose an app from independently-authored parts using navigation contracts, mounts, and host capabilities.
---

Large apps are often built by several teams, each owning a feature and shipping
on its own release cycle. Federation lets each team declare its screens and the
contract at its boundary, and lets an integration layer compose them into one
app, without any team editing another's code.

It builds on the [`navigator`](navigation) construct: each part runs its own
navigator internally, and the integrator mounts the part at a route.

:::caution[Experimental]
These constructs are experimental. Build with
`SLINT_ENABLE_EXPERIMENTAL_FEATURES=1` (or set `enable_experimental` in the
compiler configuration) to use them.
:::

## The navigation contract

A contract is an `interface` that declares the routes a part exposes at its
boundary. It is the only thing the integrator and the part have to agree on.

```slint
export interface FeatureNav {
@version(1)
@uri("app://feature") route Main;
}
```

- `route Main;` declares an entry route. A route may take parameters, for
example `route Details(id: int);`, for deep links that carry data.
- `@version(n)` versions the contract, so an integrator can tell which revision
a part was built against.
- `@uri("…")` gives a route a stable deep-link address.

A contract may also declare the capabilities the host provides to a mounted
part:

```slint
export interface HostServices {
callback log(string);
callback go-home();
}
```

## Implementing a contract

A part conforms with `implement <Contract> <=> self;`, which checks at compile
time that its navigator covers every route the contract declares. It declares the
capabilities it `needs` from its host, and runs its own navigator internally:

```slint
import { FeatureNav, HostServices } from "contract.slint";

enum MediaRoute { Main, Player }

export component MediaFeature inherits Rectangle {
implement FeatureNav <=> self;
needs HostServices;
in-out property <MediaRoute> current-route: MediaRoute.Main;

init => { root.log("media: mounted"); }

navigator (current-route) {
MediaRoute.Main: Library {
play => { root.navigate(MediaRoute.Player); }
home => { root.go-home(); }
}
MediaRoute.Player: Player {
go-back => { root.back(); }
}
}
}
```

`needs HostServices` makes `log` and `go-home` usable inside the part as if it
declared them, but leaves them unbound: the integrator binds them at the mount.

## Mounting a part

The integration shell mounts a part at one of its own routes with
`mount … via Contract`, and binds the capabilities the part needs right there:

```slint
import { FeatureNav } from "contract.slint";
import { MediaFeature } from "media.slint";

enum Shell { Home, Media }

export component App inherits Window {
in-out property <Shell> current: Shell.Home;

navigator (current) {
Shell.Home: HomeScreen {
open-media => { root.navigate(Shell.Media); }
}
Shell.Media: mount MediaFeature via FeatureNav {
log(message) => { debug(message); }
go-home => { root.navigate(Shell.Home); }
}
}
}
```

`mount MediaFeature via FeatureNav` instantiates the part directly and checks it
against the contract at build time. An unbound `needs` capability is a compile
error at the mount site, so the integration never ships a part missing a service
it requires.

## External parts

A part built and shipped on its own cycle is not a compile-time dependency.
Mount it with `mount extern via Contract` and hand it a `ComponentFactory` at
runtime:

```slint
export component App inherits Window {
in property <component-factory> plugin-factory;
// ...
navigator (current) {
// ...
Shell.Plugin: mount extern via FeatureNav {
component-factory: root.plugin-factory;
}
}
}
```

The host supplies the factory. Here it loads the part from source at runtime
with the interpreter, but it could equally be a separately compiled binary:

```rust
let factory = slint::ComponentFactory::new(|ctx| {
let compiler = slint_interpreter::Compiler::new();
let def = spin_on::spin_on(
compiler.build_from_source(plugin_source, "plugin.slint".into()),
)
.component("Plugin")
.unwrap();
def.create_embedded(ctx).ok()
});
app.set_plugin_factory(factory);
```

The external part renders into the mount's slot; the seam is a
`ComponentContainer`, so nothing about the shell has to be recompiled when the
part changes.

## Why federation

The contract is the whole coupling surface. Because it is declared structure
checked by the compiler, an integration team can compose parts it did not write,
each part can be versioned and deep-linked, and a part can be swapped, at build
time or at runtime, without either side reaching into the other's internals.

A runnable version lives in `examples/navigation-federated` in the Slint
repository.
50 changes: 49 additions & 1 deletion examples/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions examples/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ members = [
'mcu-board-support',
'memory',
'navigation',
'navigation-federated',
'navigation-std',
'native-gestures',
'opengl_texture',
Expand Down
20 changes: 20 additions & 0 deletions examples/navigation-federated/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Copyright © SixtyFPS GmbH <info@slint.dev>
# SPDX-License-Identifier: MIT

[package]
name = "navigation-federated"
version = "1.18.0"
authors = ["Slint Developers <info@slint.dev>"]
edition.workspace = true
publish = false
license = "MIT"
rust-version.workspace = true

[[bin]]
path = "main.rs"
name = "navigation-federated"

[dependencies]
slint = { path = "../../api/rs/slint" }
slint-interpreter = { path = "../../internal/interpreter" }
spin_on = { version = "0.1" }
43 changes: 43 additions & 0 deletions examples/navigation-federated/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Navigation (federated, multi-team)

A multi-screen app composed from **independently-authored feature modules** using
the experimental `navigator` federation seams. It mirrors how separate teams,
each on their own release cycle, plug features into one app that an integration
team assembles.

## Who owns what

| File | Owner | Role |
| --- | --- | --- |
| `ui/contract.slint` | platform team | the `FeatureNav` contract (`@version` / `@uri`) + `HostServices` capabilities |
| `ui/media.slint` | Media team | `MediaFeature` with `implement FeatureNav <=> self`, `needs HostServices`, its own navigator |
| `ui/settings.slint` | Settings team | `SettingsFeature`, same contract |
| `ui/app.slint` | integration team | mounts each feature at a shell route and binds the capabilities |
| `main.rs` | integration team | supplies the external plugin as a `ComponentFactory` |

The example shows every federation seam:

- **build-time mount** — `mount MediaFeature via FeatureNav { ... }` instantiates a
compile-time feature and binds the capabilities it `needs`.
- **external mount** — `mount extern via FeatureNav { component-factory: ... }` mounts
a plugin delivered at runtime. `main.rs` builds it via `slint_interpreter` and passes
it in as a `slint::ComponentFactory`; it could equally be shipped as its own binary.
- **versioned, deep-linkable contract** — `@version(1)` and `@uri("app://feature")` on
the contract's routes.

No team edits another team's file.

## Experimental features

`navigator` and its federation seams are experimental. `build.rs` enables them
for the `slint!` macro:

```rust
println!("cargo:rustc-env=SLINT_ENABLE_EXPERIMENTAL_FEATURES=1");
```

Run it with:

```sh
cargo run -p navigation-federated
```
6 changes: 6 additions & 0 deletions examples/navigation-federated/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT

fn main() {
println!("cargo:rustc-env=SLINT_ENABLE_EXPERIMENTAL_FEATURES=1");
}
41 changes: 41 additions & 0 deletions examples/navigation-federated/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT

slint::slint!(export { App } from "ui/app.slint";);

// The external plugin is an independently-delivered part. Here it is loaded from
// source at runtime via the interpreter, but it could equally be a separately
// compiled binary. The shell mounts it through `mount extern via FeatureNav`.
fn plugin_factory() -> slint::ComponentFactory {
slint::ComponentFactory::new(|ctx| {
let compiler = slint_interpreter::Compiler::new();
let def = spin_on::spin_on(
compiler.build_from_source(
r#"
export component Plugin inherits Rectangle {
background: #2b2b40;
VerticalLayout {
alignment: center;
Text {
text: "External plugin\n(loaded via ComponentFactory)";
color: white;
horizontal-alignment: center;
}
}
}
"#
.into(),
std::path::PathBuf::from("plugin.slint"),
),
)
.component("Plugin")
.unwrap();
def.create_embedded(ctx).ok()
})
}

pub fn main() {
let app = App::new().unwrap();
app.set_plugin_factory(plugin_factory());
app.run().unwrap();
}
Loading