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:
| Verb | Enclosing controller | Inner controller |
|---|---|---|
go | shell already placed → keep (zero ops); else replace the top with the shell page and push the rest | replace the top with the leaf |
push | shell already placed → keep; else push the shell page (the page below — a / connect screen, say — is retained) | push the leaf |
replace | as go’s rule | replace the top with the leaf |
pop | pops 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.
§Deep links landing mid-shell
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.