アプリ内ルーティング
ページを enum で表し、現在のページとパラメーターを 2 つの Signal として公開し、固定容量の履歴スタックで戻る / 進むを実現します。
Router のモデル
ui.fx.Router(Page) は enum 駆動のインメモリルーターで、Web の URL ルーターではありません。current: *Signal(Page) と params: *Signal(RouteParams) の 2 つの Signal を公開し、内部で最大 64 件の履歴スタックを保持します。
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;
}ナビゲーションとパラメーター
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 は key と value のバイト列を自身のバッファにコピーするため、呼び出し側のバッファが後で変わっても影響しません。既存の key を設定すると値が上書きされます。容量は固定で、あふれた場合は切り詰めずにエラーを返します。
max_params = 8error.TooManyParamsmax_key_len = 64error.KeyTooLongmax_value_len = 512error.ValueTooLongnavigate、back、forward はいずれも 2 つの Signal に書き込みます。canGoBack() / canGoForward() は通常の読み取りで、Signal ではありません。ハンドラーから Router を操作するには、ポインターを 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);ページの描画
現在のページは Signal(Page) です。ui.Match で enum 値に応じてページのサブツリーを切り替えます。Match はページごとに子 Scope を作成し、切り替え時にはまず古い Scope を dispose し(その中の Signal、Effect、リソースもまとめて解放)、次に古いノードを解放して新しいページをマウントします。
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 は enum 値が変わったときだけ再構築します。.editor から別のパラメーター付きの .editor へ移動してもページは再マウントされません。パラメーターに依存するページは、自分で router.params を購読してください。
// 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);履歴のルール
- ✓
新しいナビゲーションは進む側の分岐を切り捨てます。back の後に navigate すると、以前の forward エントリは破棄されます。
- ✓
navigate は毎回プッシュします。同じページでパラメーターが違う場合も、まったく同じ行き先でも、新しい履歴エントリになります。
- ✓
64 件に達すると古い半分を破棄します。65 回目のプッシュの前に最新の 32 件を残し、カーソルもそれに合わせて移動します。
- ✓
URL はありません。ディープリンクやシステムの URL スキームは、アプリ側で
navigateに対応付ける必要があります。
ナビゲーションの記録
Router 自体はログを出力しません。ナビゲーションを調べるときは、重要な遷移を現在の Cx の Console に router scope 付きで書き出してください。DevTools Console で scope ごとに絞り込み、レベルと呼び出し位置を確認できます。
router.navigate(.editor, params);
cx.console().scoped("router").debug("navigate -> {s}", .{@tagName(Page.editor)});Console の API 全体は DevTools を参照してください。