Route tables
Declaring what may appear beneath what, once, at startup.
A RouteTable is a static description of your screens: which ids exist, which may appear beneath which, and a few facts about each. You build it once at startup and hand it to the router. It never changes while the app runs.
Registered is not active. The table says what may appear beneath what. Which ids are on the path right now is the router's job, derived every frame.
Building a table
use carte::{Occlusion, RouteTable};
fn table() -> RouteTable {
let mut t = RouteTable::new();
t.at_root("home")
.under("note", "home")
.under("discard", "note")
.under("settings", "home")
.under("settings", "note") // one node, two parents
.under("trash", "home")
.occludes("discard", Occlusion::None) // the editor stays visible beneath
.guard("trash", trash_has_something);
t
}
at_root(id)registers an id a path may start at. A table may have several roots. Which one a path starts at each frame is decided by the root function you pass toRouter::new.under(id, parent)registers one edge. Call it once per parent.occludes(id, occlusion)declares what a node hides beneath it. See Occlusion and stacked views.guard(id, guard)registers a node's precondition. See Guards.
Every builder method returns &mut Self, so they chain.
Ids
A route id is a RouteId, which is a &'static str. Ids are compared on every frame's walk, and the table is built once, so a static string is enough. There are no parameters inside an id: you cannot register note/:id. Which note is open belongs in your model, not in the path.
One node, several parents
The table is a directed acyclic graph, not a tree. settings reachable from home and from note is two edges and one handler, not two routes. The walk records how you arrived, so leaving settings returns to whichever parent was claiming it. You never remember an origin by hand.
Asking the table
The table answers questions with no router and no node objects:
| Method | Answers |
|---|---|
ids() | Every registered id, in registration order |
contains(id) | Whether id is registered |
is_root(id) | Whether a path may start at id |
is_child_of(child, parent) | Whether that edge exists |
children_of(parent) | The ids registered beneath parent, sorted |
parents_of(id) | The parents id is registered beneath, sorted |
occlusion_of(id) | Its declared occlusion, or Occlusion::View |
guard_of(id) | Its guard, if any |
children_of is useful for a menu or a sidebar that derives its entries from the table instead of repeating them, so it can never offer a destination nobody registered.
Tiers
Each node has a Tier, which says what standing on it means:
Tier::View, the default. The node selects a view inside whatever encloses it. Most nodes are views.Tier::InstanceRoot. The node mounts a separate UI instance of its own, such as a separate document or workspace your UI layer stands up. Declare it withmounts(id, document), naming what to mount, ormounts_supplied(id)when your code builds the instance itself.Tier::Structural. The node mounts nothing and selects nothing. It exists to own a scope that must outlive all of its children. Declare it withstructural(id).
at_root is placement and mounts is meaning. They are independent: an instance root may sit at the top of the graph or beneath a structural node.
The table answers tier questions with tier_of, document_of, is_instance_root, and enclosing_instance(path), which finds the deepest instance root on a path. Carte itself does nothing with a document name. It is data for your binding.
An instance root may also carry an Area (another &'static str) with in_area(id, area): a label for the broad place the user is, for host reactions that depend on it. If you declare any area, you must also choose a fallback with default_area(area). area_of(id) and area_of_path(path) read them back.
TODO: tiers and areas exist for the binding layer described in the design record, and the notes example uses neither. Treat them as early.
Validation at startup
RouteTable::validate checks the whole table and returns a TableError naming the culprit. Router::new calls it for you. It refuses:
| Error | Cause |
|---|---|
UnknownParent | An edge names a parent nobody registered |
DuplicateId | An id is registered twice |
Cycle | Some path would never end |
NoRoot | Nothing is registered at the root |
AttributeOnUnknown | An occlusion, guard, tier or area names an unregistered id |
Redeclared | A node declares the same attribute twice, or two guards |
AreaOnNonInstanceRoot | An area on a view or structural node |
NoDefaultArea | Areas declared with no fallback |
Attributes never register an id. If they did, a typo would quietly become a real node.
Router::new adds one more check: every registered id must have a handler. A missing one is reported as UnknownParent with the parent <no handler registered>.
Every TableError implements Display and std::error::Error, so ? in your setup function prints a sentence such as:
route `trash` is registered under `hom`, which is not a registered route
Next
Nodes and the walk turns this static table into a live path.