In-app routing
Describe pages as an enum, expose the current page and its params as two Signals, and get back / forward from a fixed-capacity history stack.
The Router model
ui.fx.Router(Page) is an enum-driven, in-memory router, not a web URL router. It exposes two Signals, current: *Signal(Page) and params: *Signal(RouteParams), and keeps a history stack of up to 64 entries.
const std = @import("std");
const ui = @import("ui");
const Page = enum { welcome, editor, settings };
const Router = ui.fx.Router(Page);
fn mountApp(cx: *ui.Cx, scope: *ui.Scope) anyerror!*ui.Node {
// The router's two Signals are created in `scope`, so pass a scope that
// lives as long as the window. The router value itself is large (~290 KB of
// inline history), so keep it in Cx-owned state and pass the pointer around.
const router = try cx.bindState(Router, try Router.init(scope, .welcome));
const shell = try ui.box(cx, .{
.width = .fill(),
.height = .fill(),
.direction = .column,
}, .{});
// ...append a toolbar and the page host (see "Rendering pages")
_ = router;
return shell;
}Navigation and params
var params: ui.fx.router.RouteParams = .{};
try params.set("file", "draft.md"); // copies the bytes; errors on overflow
router.navigate(.editor, params); // page + params
router.navigateTo(.settings); // page, empty params
_ = router.back(); // false when already at the oldest entry
_ = router.forward(); // false when there is no forward branch
if (router.canGoBack()) {
// e.g. enable a Back button
}RouteParams.set copies the key and value bytes into its own buffers, so later changes to the caller’s buffer don’t affect it; setting an existing key overwrites the value. Capacity is fixed, and overflow returns an error instead of truncating:
max_params = 8error.TooManyParamsmax_key_len = 64error.KeyTooLongmax_value_len = 512error.ValueTooLongnavigate, back and forward all write both Signals. canGoBack() / canGoForward() are plain reads, not Signals. To drive the router from a handler, keep the pointer in a cx.bindState struct:
const Nav = struct {
router: *Router,
fn goBack(self: *Nav) void {
_ = self.router.back();
}
};
const nav = try cx.bindState(Nav, .{ .router = router });
const back_button = try ui.widgets.Button(.{
.label = "Back",
.on_click = cx.on(Nav, nav, Nav.goBack),
}).mount(scope, cx);Rendering pages
The current page is a Signal(Page); switch page subtrees on it with ui.Match. Match gives each page a child Scope; on a switch it disposes the old Scope first (its Signals, Effects and resources go with it), then frees the old nodes and mounts the new page.
const page_host = try ui.box(cx, .{
.width = .fill(),
.height = .fill(),
}, .{});
// Match appends the page to page_host and rebuilds it when router.current
// changes; each page gets a fresh child Scope that is disposed on leave.
_ = try ui.Match(Page, scope, page_host, router.current, cx, struct {
fn build(child_scope: *ui.Scope, c: *ui.Cx, page: Page) anyerror!*ui.Node {
return switch (page) {
.welcome => mountWelcome(c, child_scope),
.editor => mountEditor(c, child_scope),
.settings => mountSettings(c, child_scope),
};
}
}.build);Match rebuilds only when the enum value changes: navigating from .editor to .editor with different params keeps the page mounted. A page that depends on params should subscribe to router.params itself:
// editor -> editor with other params does not rebuild the page,
// so subscribe to the params Signal inside the page.
try scope.createEffect(.{ .params = router.params }, struct {
fn run(ctx: anytype) void {
// get() returns a copy; keep it in a local so the slices stay valid.
const params = ctx.params.get();
const file = params.get("file") orelse "untitled.md";
std.log.info("editor shows {s}", .{file});
}
}.run);History rules
- ✓
New navigation truncates the forward branch. Navigating after back discards the old forward entries.
- ✓
Every navigate pushes. The same page with different params (or even an identical target) becomes a new entry.
- ✓
At 64 entries, the older half goes. Before the 65th push the newest 32 are kept and the cursor shifts with them.
- ✓
No URLs. Deep links and system URL schemes must be mapped to
navigateby the app.
Tracing navigation
The router doesn’t log on its own. To debug navigation, write the relevant transitions to the current Cx’s Console under a router scope; DevTools Console can then filter by scope and show the level and call site.
router.navigate(.editor, params);
cx.console().scoped("router").debug("navigate -> {s}", .{@tagName(Page.editor)});See DevTools for the full Console API.