Occlusion and stacked views
Which screens on the path render, and how a prompt stays on top of the screen it belongs to.
The path can hold several screens at once: home › note › discard. Usually only the deepest one should show. Sometimes the one beneath should stay visible too, such as an editor under its "discard changes?" prompt. Occlusion is how each node says what it hides.
The ladder
Occlusion has three values, in order:
| Value | Ancestors' drawing | Ancestors' views |
|---|---|---|
Occlusion::None | Runs | Render behind this one |
Occlusion::View | Runs | Hidden |
Occlusion::Surface | Hidden | Hidden |
View is the default and the common case: an ordinary child screen replaces its parent's view. None is for prompts and cards that sit over the screen they belong to. Surface is for a screen that brings its own world and hides everything, including whatever your app paints behind the UI.
It is one ordered ladder rather than two independent flags, and Surface implies View. That is a claim about real apps: nothing so far has wanted an ancestor's background hidden while its UI still shows. If your app does, that is the design decision to challenge.
Two lists
The router turns the path and the ladder into two lists, both outermost first.
let views = router.visible_views(); // ids whose views render
let drawing = router.drawing(); // ids whose own drawing runs
visible_views()starts at the deepest node whose occlusion isViewor more, and runs to the end of the path. These are the screens your UI draws.drawing()starts at the deepest node whose occlusion isSurface, and runs to the end of the path. Use it for anything you paint that is not part of a view: a canvas, a map, a 3D viewport, a background.
For the path home › note › discard with discard declared None:
| Method | Result |
|---|---|
path() | home, note, discard |
visible_views() | note, discard |
drawing() | home, note, discard |
home is hidden by note, which is a View. note stays because discard hides nothing.
Declaring occlusion
Declare it in the table:
t.under("discard", "note")
.occludes("discard", Occlusion::None); // the editor stays visible beneath
Where the table declares a node's occlusion, the router reads the table and never calls the node's occludes() method. Where it does not, the router asks the node. RouteTable::occlusion_of(id) answers from the table alone, with View for undeclared ids.
Prefer the table. Occlusion is a fact about the node, and the table can answer it before any handler exists. The occludes() method is a fallback for older code and is expected to retire.
Drawing stacked views
When visible_views() returns more than one id, draw them in order. Each later view goes on top of the one before it:
for id in router.visible_views() {
match id {
"note" => note(ctx, &mut view),
"discard" => discard(ctx, &mut view),
_ => {}
}
}
In the notes example the prompt is an egui window anchored to the centre, so it naturally lies over the editor's panel. In a retained UI you would render the views as stacked children of one container, the deeper one painted last.
Occlusion is not input
Occlusion says what draws. It says nothing about who gets the keyboard or the pointer. A stacked prompt over an editor is visible together with the editor, and Carte does not decide which one a click reaches. That is your UI layer's job. With stacked views, offer input to the top view first.
Escape is the exception, because it goes through the router: it always reaches the deepest node first. See Escape, popping and departure.
If a screen should take input exclusively, such as a modal, have your binding route input only to the deepest view. The path already tells you which one that is: router.deepest().
What is a route and what is not
A useful test for whether something belongs in the table: a surface is a route if, and only if, it takes input exclusively. Settings, a prompt and a pause menu are routes. A toast, a tooltip or a status message is not. Those are ordinary state, shown alongside whatever is on the path, with no node and no lifetime of their own. Keeping them out keeps the table small.
Next
Guards: refusing entry with a reason.