# 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

```rust
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 to `Router::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](/docs/routing/occlusion).
- `guard(id, guard)` registers a node's precondition. See [Guards](/docs/routing/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 with `mounts(id, document)`, naming what to mount, or `mounts_supplied(id)` when your code builds the instance itself.
- `Tier::Structural`. The node mounts nothing and selects nothing. It exists to own a [scope](/docs/state/scopes) that must outlive all of its children. Declare it with `structural(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:

```text
route `trash` is registered under `hom`, which is not a registered route
```

## Next

[Nodes and the walk](/docs/routing/nodes) turns this static table into a live path.
