# facett-app — the application-framework — reasoning
> **Class:** `reasoning`. This file holds the *why*, the trade-offs, and **every design we
> rejected** for the nav backbone and demo harness. The descriptive shape we actually run is
> in [`app-design.md`](app-design.md), which by rule mentions none of this.
## Why a nav framework at all
facett started as a component library: 30+ independent `Facet`s, each excellent alone, with no
shared vocabulary for "the app around them" — no sidebar, no routing, no menu that wasn't
bespoke per host. But facett already proved the pattern it needed to generalise:
`facett-menu`'s **one `Command` set drives three surfaces** (palette, context-menu, menu bar).
The founding idea of `facett-app` (from the original roadmap doc,
`nav-menu-framework-roadmap.md`, now folded here) was to generalise that from a flat command
list to a **hierarchical navigation model** that drives *every* nav/menu surface — sidebar,
breadcrumb, tree, palette, context-menu, dropdown — so new surfaces become cheap (they consume
the same model) and the app gets one navigational vocabulary instead of N bespoke ones.
This was scoped deliberately to egui's immediate-mode world: composition + navigation +
overlays. Reactive signals/state-diffing (a Leptos-style model) was explicitly **out of
scope** — a separate, larger axis that would fight immediate-mode's re-render-everything
model rather than complement it. Async task runners, undo/redo, i18n, and persistence were
likewise deferred: they can be layered on top of `Facet`/`FacetDeck` later without breaking
the nav contract, so building them now would have been solving problems nobody had yet.
## Why phase the build as Fas 1 → 4
The roadmap ranked the backbone (`NavModel` + `Router`) as **Wave 0**, deliberately ahead of
every visible surface, on the reasoning that shipping Wave 1 (sidebar, dropdown) *before* the
backbone would mean rebuilding each surface's data model once the backbone landed anyway.
Paying that cost once, first, made every later surface "mostly rendering the same tree a new
way" rather than a fresh design each time. The realized phases in the crate doc
(`facett-app/src/lib.rs:9-11`) follow that ordering: Fas 1 (nav backbone: `NavModel`, `Router`,
`Sidebar`, `NavShell`) shipped first; `AppShell`, split-panes, and the overlay system were
explicitly deferred to "later phases" rather than bolted onto Fas 1's `NavShell`.
Within Wave 1, **Sidebar** was ranked highest-value ("the single most 'app-like' addition")
over dropdown/context-menu/breadcrumb because a persistent nav rail is the one surface a user
*always* sees — it does the most to make a component pile read as an app. The alternative
"ship the smallest thing first" (a standalone dropdown, no backbone) was named explicitly in
the roadmap as a fallback only if visible progress mattered more than the model — the backbone
path was taken instead because it was expected to unlock everything downstream cheaply, which
it did: `AppShell`'s menubar and `NavPalette` both reuse the identical `NavModel` the sidebar
already reads.
## Rejected: `egui::ComboBox` for `Dropdown`
`egui::ComboBox` was the obvious base for a select widget — it's already in egui, already
themed. It was rejected because it hides its open/closed state in egui's internal memory
(keyed by widget id), which breaks FC-1 (the component contract's rule that *all* observable
state lives in the component's own struct): a host can't read "is this dropdown open right
now" from `state_json`, can't set it programmatically for a test, and can't replay a script
that opens it deterministically. `Dropdown` reimplements the popup by hand over a plain
`egui::Area` specifically so `open: bool` is a normal struct field (`dropdown.rs:16-18`) —
more code, but headless-addressable (`select`, `set_open`) and fully observable for free.
## Rejected: `view` mutating self directly in the nav widgets
Every nav widget in this crate (`Sidebar`, `Tree`, `Modal`, `Toasts`, `Popover`, `Dropdown`,
`NavPalette`, the Fas-4 widgets) could have taken the simpler shape of "just call `self.field
= x` wherever a click happens" — the natural egui idiom. Instead every `show`/`ui` method
returns an `Option<T>` (the newly-clicked route, the newly-picked date, the newly-chosen
button id) or performs the mutation through a small headless method (`select`, `navigate`,
`invoke`, `toggle`) that a test can call without drawing anything. This buys the same payoff
the Elm/`FC` contract buys elsewhere in facett (see `elm-reasoning.md`): a robot-UI driver and
a unit test exercise the *exact* code path a click does, with no separate "test-only" API to
drift out of sync.
## Rejected: mounting `Modal`/`Toasts` folded into `NavShell`/`AppShell` directly
The overlay system (`Modal`, `Toasts`, `Popover`) could have been built as a fixed part of the
shell types — every `AppShell` automatically gets a toast tray, say. It was kept as separate,
composable structs instead (Fas 3, demoed via the standalone `overlay` tab / `OverlayDemo`,
not as a shell field) because not every host wants the same overlay policy (some want sticky
toasts only, some want no modal at all), and because `Modal` is explicitly a generalisation of
the **pre-existing** `facett-menu` `CommandPalette` overlay pattern (scrim `Area` at
`Order::Background` + a panel `Area` at `Order::Foreground`, Esc-dismissable) — pulling that
pattern out once as `Modal` let both the command palette's shape and any future dialog share
one proven overlay recipe, rather than coupling it to one shell type.
## Rejected: index-based selection in `Tree` / `Sidebar` / `NavPalette`
As with the rest of facett (see `elm-reasoning.md`'s "Rejected: index-based selection"), every
selection here is a **stable string id** (`Tree::selected: Option<String>`,
`NavPalette::invoked: Option<String>`), never a row/child index. The nav tree is exactly the
kind of structure that reorders under filtering (`NavPalette`'s fuzzy search re-ranks hits
every keystroke) and expand/collapse (a `Tree` row's on-screen position moves when a sibling
folds) — an index-based selection would silently point at the wrong node the moment either
happened. Carrying the id costs one extra `to_string()`/lookup; the alternative would have
been intermittent, hard-to-reproduce wrong-selection bugs.
## Rejected: coupling `facett-demo`'s app-framework tabs to native-only rendering
The `nav`/`shell`/`overlay`/`widgets` demo tabs, and the live web demo they ship inside, could
have been native-only (skip WASM, since egui's `wgpu` paint-callback path for the map/3D tabs
isn't available there anyway). This was rejected in favour of `cargo xtask demo-wasm` building
`--no-default-features` (dropping only the wgpu-dependent map passthroughs and the
filesystem-dependent region-clip), because the whole point of the demo is "one binary, every
component, drivable by anyone with a browser" — carving app-framework tabs out of that
contradicts the demo's own reason to exist (the D4 compliance check: if a component can't be
slotted into the one gallery window, that's a flag). The cost is graceful-degradation code in
the few native-only features (`region_picker.rs` shows a "desktop-only" message instead of
calling `thread::spawn` on wasm); the app-framework tabs themselves needed no such gating —
they were wasm-clean from the start.
## Road here
`facett-menu`'s `Command`/`CommandPalette`/`context_menu`/`menu_bar` (one model, three
surfaces) came first and is the proof-of-pattern this whole crate generalises. The roadmap
(`nav-menu-framework-roadmap.md`) named the backbone (`NavModel`+`Router`) as Wave 0 and
sidebar as the top of Wave 1; both shipped as Fas 1, followed by `AppShell` (Fas 2,
`facett-app/src/appshell.rs`), the overlay system (Fas 3, `modal.rs`/`toast.rs`/`popover.rs`),
and the input/data widgets (Fas 4, `widgets.rs`/`tree.rs`/`datepicker.rs`/`colorpicker.rs`).
Each phase was wired into `facett-demo` as its own tab as soon as it landed
(`nav`/`shell`/`overlay`/`widgets` in the `LEGEND`, `facett-demo/src/lib.rs:112-115`) rather
than batched, so every phase got mega-matrix and robot-UI coverage (see
`facett-demo/tests/mega_matrix.rs:366-439`) the moment it existed, instead of accumulating an
untested backlog. Split-panes (`SplitPane`) and nested in-content tabs (`TabView`) — named in
the roadmap as Wave 2/3 "structure" items — landed alongside Fas 2/4 rather than as their own
phases, once it was clear they were small enough to fold into the shell/widgets work already
in flight.