Skip to main content

shell_route

Function shell_route 

Source
pub fn shell_route<State>(
    inner: &NavigatorController<State>,
    builder: impl Fn(&BTreeMap<String, String>) -> AnyView<State> + 'static,
    children: Vec<Route<State>>,
) -> Route<State>
where State: 'static,
Expand description

The declarative router vocabulary (a go_router-subset layer), flat-re-exported from frust-widgets so app code never names that crate directly: Router resolves a location against a Route table (built via RouteBuilder) into Resolution/ResolvedPages driving a NavigatorController, with :param/query parsing (Location/PathPattern/RouteParams), per-route/top-level Redirects (loop-guarded at DEFAULT_REDIRECT_LIMIT), and an ErrorBuilder fallback for an unmatched location.

A page builder receives the location’s query merged under its path captures, so /terminal?session=abc reads its own parameter.

RouteNavigator is the seam a screen navigates through: a Send + Sync queue of NavRequest data (paths and names, never closures) that rides provide_context — the Router itself cannot, since it holds Rc page builders. RouterDeepLinks::track drains it every rebuild, so a request queued in an event handler (on any thread — the queue never panics off-thread) applies on the next frame.

shell_route is the nested-navigator binding: its children resolve onto a second NavigatorController the app owns and its own page — the chrome wrapping that inner navigator — stays retained while they do, so navigating between siblings inside the shell never rebuilds the chrome. See its docs for the keep rule and the per-verb table. A shell route: a pathless route whose children resolve onto inner, a second NavigatorController the app owns, while this route’s own page stays retained on the enclosing stack (go_router’s ShellRoute).

builder builds the shell page — chrome plus the inner navigator, e.g. scaffold(any(navigator(&inner, || …))).app_bar(…) — and must mount a navigator driven by the same inner clone; that navigator is where the children land.

ⓘ
// The app owns `inner` in its `Component::State`, exactly like the outer one.
Router::with_controller(&outer, vec![
    Route::new("/", connect_page),               // declared BEFORE the shell
    shell_route(&inner, shell_page, vec![
        Route::new("/sessions", sessions_page),
        Route::new("/terminal", terminal_page),
    ]),
])

§Pathless, and why order matters

A shell route consumes no path segments (like go_router’s ShellRoute, which has no path at all), so its children keep flat public paths. A zero-consuming route also matches the empty segment list, so a root route (/) must be declared before the shell — Router::resolve takes the first match. To scope a shell under a prefix, nest it: a Route::new("/dash").child(shell_route(…)) puts /dash’s own page on the enclosing stack ahead of the shell page.

§How a chain that crosses this route is applied

The resolved chain splits at the boundary. With the shell page already on the enclosing stack — the keep rule — the enclosing controller gets zero ops, because replacing that page would drop the retained inner navigator and every page in it:

VerbEnclosing controllerInner controller
goshell already placed → keep (zero ops); else replace the top with the shell page and push the restreplace the top with the leaf
pushshell already placed → keep; else push the shell page (the page below — a / connect screen, say — is retained)push the leaf
replaceas go’s rulereplace the top with the leaf
poppops the router’s own controller — see the module docs’ deferred note—

One structural op per controller per navigation, so nothing here rests on stacking two transitions in one apply_ops pass.

A placement resets the verb below it. When the shell page has to be placed, the inner navigator is brand new — its stack is just the root page its navigator(…) call names — so there is no in-shell history to stack onto and every segment below the placement applies as a go (replace) whatever the caller asked for. That is what makes push("/sessions") from a bar-less / land as [/, shell] outside and [sessions] (depth 1) inside, so a back press at /sessions leaves the shell instead of popping to a placeholder root.

§Matched state reaching the shell page

builder receives the resolved chain’s merged RouteParams like any other route — but only as of the navigation that placed it. A navigation within the shell issues no op on the enclosing controller by design, so the shell page is neither rebuilt from a new builder nor restamped: its published route entry keeps naming the location that placed it. Live in-shell state is read from the inner navigator instead — its own route_stack (or the facade’s RouteObserver over it), which is authoritative at every publish. Chrome inside the shell page (a title bar) reads that, never the enclosing stack.

A go that runs before the shell page exists — a cold-start deep link resolved from Component::init — queues the inner segment’s ops on inner while it has no widget at all. They are drained by the inner navigator’s first build (the same pre-first-frame drain a top-level controller gets), so the shell’s first painted frame is already the linked child: no flash, no second navigation. If a shell page defers mounting its navigator (rendering a loading state first), the ops simply wait on the queue and land on whichever build mounts it.

§Back

Nothing here wires back. The shell page’s builder mounts the inner navigator, so the inner navigator wires after the enclosing one and the facade’s innermost-first arbitration reaches it first: back pops the inner stack while it is poppable, and an inner navigator at depth 1 claims no interest, so the press falls through and pops the shell page itself.