Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/facett

.nornir/app-reasoning.md

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, which by rule mentions none of this.

Why a nav framework at all

facett started as a component library: 30+ independent Facets, 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.