Nodes and the walk
The Node trait, the router that holds your nodes, and how the path is derived each frame.
The table is static. The path is live. The router turns one into the other each frame by asking the nodes on the path which child, if any, they are showing.
The router
let routes: Vec<(RouteId, Box<Screen>)> = vec![("home", Box::new(Home))];
let router = Router::new(table, routes, |_: &Model| "home")?;
Router<Cx, A, R> has three type parameters:
Cxis your context: the model the nodes read. In the examples it isModel.Ais your action type, the thing nodes push onto the outbox.Ris what each id resolves to. Usuallydyn Node<Cx, A>, so each id can have its own handler type.
Router::new takes the table, one handler per id, and a root function. The root function picks which root a path starts at. In a one-root app it returns a constant. In a larger app it is your lifecycle: signed out, loading, ready.
The walk
router.resolve(cx, store) derives the path:
- Call the root function. That id is the first segment.
- Ask that node's
resolve_child_in(cx, store). By default this callsresolve_child(cx). - If it returns
None, the path ends here. - If it returns a child, check the child is registered beneath this node and that the child's guard allows entry. If so, the child is the next segment. Go to 2.
The path is derived, never pushed. There is no stack to get out of step with your state, because the path is recomputed from your state every time.
A node names at most one child, because resolve_child returns an Option<RouteId>. That makes the walk total and ordered with nothing above it to arbitrate. If you have four overlays that must never show together, you do not need four booleans and a rule; you need one Option that cannot hold two values.
fn resolve_child(&self, m: &Model) -> Option<RouteId> {
m.home_child
}
If a node names a child the table does not register beneath it, or names an id already on the path, the walk stops there and prints one warning per bad pair to stderr. The frame still renders, as if the node had named nothing.
Reading claims from the store
resolve_child_in gets the store as well as your context. Override it when the claim is a store field rather than a model field, for example when another part of the app writes "show this child" into a node's scope. The store it sees is the last commit, never a half-finished tick.
The Node trait
| Method | Called when |
|---|---|
resolve_child(&self, cx) | Every walk, while the node is on the path |
resolve_child_in(&self, cx, store) | Every walk. Defaults to resolve_child |
occludes(&self) | When the table declares no occlusion for this id |
update(&mut self, cx, out, dt) | Router::update, outermost first |
escape(&mut self, cx, out) | Escape is offered to this node |
take_leave_request(&mut self) | Your binding asks after delivering an event |
child_popped(&mut self, out, child) | A child this node was claiming popped |
enter(&mut self, cx) | The id joined the path |
leave(&mut self, cx) | The id left the path |
depart(&mut self, to) | This node is the deepest one leaving |
Methods that run during a frame (update, escape, child_popped) get &Cx and an &mut Outbox<A>. They can read and they can ask, but they cannot change your model. Only enter and leave get &mut Cx.
The trait is scaffolding. It was cut from Masque's routing layer at the seam where the remaining methods named UI types, and some methods (
occludes,take_leave_request) are expected to move or retire. Expect it to change before 1.0.
Lifecycle
enter and leave mean the id joined or left the path, not that it became or stopped being the deepest. When a prompt opens above an editor, the editor stays on the path, so its leave does not run and whatever it was holding stays put.
resolve runs leave deepest-first and enter outermost-first, so a child lets go before its parent, and a parent is in place before its child. It returns true when the path changed.
Node objects themselves are not dropped when they leave. The router holds one handler per id for its whole life. Use leave to reset anything that should not survive. If state must outlive the screen it is edited in, it is not node state: put it in your model or a longer-lived scope.
Reading the router
| Method | Returns |
|---|---|
path() | The active path, outermost first |
deepest() | The deepest active id |
is_active(id) | Whether id is on the path |
drawing(), visible_views() | What renders, after occlusion |
refused() | The door the last walk was refused at |
last_departure() | How the last path change asked to leave |
get(id), get_mut(id) | The handler for id |
table() | The table |
Next
Escape, popping and departure: how a screen leaves when the path is derived.