Escape, popping and departure
How a screen leaves when the path is derived rather than pushed, and how it says goodbye.
If the path is derived from your state, a screen cannot simply remove itself. Leaving has to change the state the walk reads. Carte gives you one route for that, and uses it for Escape, Back buttons and anything else that closes a screen.
Escape goes to the deepest screen first
let outcome = router.escape(&model, &mut outbox);
Router::escape offers Escape to the deepest node on the path, then to each node above it in turn. Each node answers with an Escape:
| Answer | Meaning |
|---|---|
Escape::Pop | Leave this screen. The ordinary case |
Escape::Prompt | This screen has unsaved work and raises its own prompt instead |
Escape::Ignore | The key means something here (a text field clearing itself) and must go no further |
Escape::Fall | Not this screen's key. Offer it to the parent |
Nothing above the deepest screen intercepts. The first answer other than Fall ends the search, and escape tells you what happened with an EscapeOutcome:
| Outcome | When |
|---|---|
Popped(id) | id answered Pop |
Prompted(id) | id answered Prompt. The path is unchanged this frame |
Consumed(id) | id answered Ignore |
Unclaimed | Every node answered Fall |
Unclaimed is the only case where anything outside the router hears about Escape. Your app may do what it likes with it, such as asking to quit.
How a pop shortens the path
A pop does not edit the path. When a node answers Pop, the router calls child_popped(out, child) on its parent, the node that was claiming it. The parent pushes an action that stops the claim:
fn child_popped(&mut self, out: &mut Outbox<Action>, _: RouteId) {
out.push(Action::Closed { parent: "home" });
}
Then the frame carries on as usual. The tick applies Closed, which sets home_child to None in your model, and the next resolve derives a shorter path. The child cannot shorten the path itself, because it does not derive it. Only the parent's claim does.
All of this lands in the same frame, because the tick runs after Escape is handled and resolve runs after the tick. See The frame.
A root that answers Pop has nobody to ask. escape still reports Popped, but nothing changes: your root function put it there, and only your root function can take it away. Have roots answer Fall.
Prompting instead of leaving
Prompt is for "you have unsaved changes". The router does not raise the prompt. The node does, by pushing the action that makes it claim a prompt child:
fn escape(&mut self, model: &Model, out: &mut Outbox<Action>) -> Escape {
match model.dirty() {
true => {
out.push(Action::AskDiscard); // sets note_child = Some("discard")
Escape::Prompt
}
false => Escape::Pop,
}
}
The prompt is an ordinary child route, so the walk finds it after the tick. Because the editor stays on the path, its leave does not run and its draft survives. Escape in the prompt pops the prompt, which returns you to the editor. Declare the prompt Occlusion::None to keep the editor visible under it. See Occlusion and stacked views.
Back buttons
A Back or Close button can reuse Escape. The notes example sets a flag when the button is clicked and calls router.escape as if the key had been pressed. The deepest screen decides what leaving means, whatever started it.
Leaving for other reasons
A node may also want to leave in response to an event that is not Escape. It records that in its own state and answers true from take_leave_request, which must clear the request as it reads it. Carte does not dispatch your UI's events, so your binding asks after delivering one, and reports a yes with pop_at:
let path = router.path().to_vec();
for (depth, id) in path.iter().copied().enumerate() {
if router.get_mut(id).is_some_and(|node| node.take_leave_request()) {
router.pop_at(&mut outbox, depth);
}
}
pop_at does exactly what Escape's Pop does: it tells the parent to stop claiming. Depth 0 is ignored.
Departure
When the path changes, the deepest node that left is asked how it leaves the frame:
fn depart(&mut self, to: Option<RouteId>) -> Departure {
Departure::Bleed { seconds: 0.3 }
}
to is the new deepest id. The answer is a Departure:
Departure::Cut: gone this frame. The default, and it costs nothing.Departure::Bleed { seconds }: ask the binding to hold the outgoing screen's last frame for that long, composited under the incoming one.
Carte only records the answer. Read it after resolve:
if router.resolve(&mut model, &mut store) {
if let Some((id, Departure::Bleed { seconds })) = router.last_departure() {
// keep `id`'s last frame and fade it out over `seconds`
}
}
Capturing and compositing the frame is your UI layer's job. A page turn, a cross-fade, a blur: Carte has no opinion. last_departure is None on frames where the path did not change.
Next
Occlusion and stacked views: what the deeper screens hide.