facett-app — the application-framework — guide
Class:
guide· audience: a techie composing an app from facett components. The what is inapp-design.md; the why inapp-reasoning.md. This file is the workflow.
f0 — the end-to-end workflow: model → router → shell → demo
Building a navigable facett app is four moves, in order:
- Declare a
NavModel— the tree every surface reads from. - Pick a shell —
NavShell(body text per route) orAppShell(realFacetpanes per route, plus menubar/palette). - Drive it headlessly —
navigate/select/invokecalls, asserted viastate_json(), with zero window needed. - Wire it into
facett-demo— a deck tab so it renders in the live app and the web demo, and shows up in the mega-matrix + robot-UI coverage.
use facett_app::{NavItem, NavModel, NavShell};
let model = NavModel::new([
NavItem::new("home", "Home").icon("🏠"),
NavItem::new("data", "Data").icon("🗃").children([
NavItem::new("data.grid", "Grid").route("grid").badge("12k"),
NavItem::new("data.table", "Table").route("table"),
]),
NavItem::new("settings", "Settings").icon("⚙"),
NavItem::new("admin", "Admin (locked)").disabled(),
]);
let mut shell = NavShell::new("nav", model)
.with_body("home", "Welcome — pick something on the left.")
.with_body("grid", "The Arrow grid would render here.")
.with_body("table", "The data table would render here.");
// Headless drive, no ui() call needed:
shell.navigate("table");
assert_eq!(shell.current_route(), "table");
assert_eq!(shell.breadcrumb(), vec!["Data", "Table"]);
f1 — NavModel + Router: building and querying the tree
Build the tree once with NavModel::new(items); NavItem::new(id, label) plus the builders .icon(...), .route(...), .badge(...), .disabled(), .child(item) / .children(items) compose it. A leaf with no .route(...) navigates to its own id.
Query it directly when you need the tree without a shell wrapping it:
model.first_route(); // Some("home") — router start
model.find_route("data.table"); // the NavItem targeting that route
model.breadcrumb_items("table"); // [("Data", "data"), ("Table", "table")] — clickable trail
model.routes(); // every enabled navigable route id, in order
model.state_json(); // the whole tree as json (golden-file / robot-UI friendly)
Router is usable standalone if you don't want a shell at all — e.g. driving a bare FacetDeck::set_active_by_title by hand:
use facett_app::Router;
let mut r = Router::new("home");
r.navigate("data");
r.navigate("table");
r.back(); // → "data"
assert!(r.can_forward());
r.forward(); // → "table"
r.to_hash(); // "#table" — write to window.location.hash on wasm
Router::route_from_hash("#data%20table"); // Some("data table") — parse it back
f2 — choosing a shell, and wiring the extra surfaces
| you need… | use |
| routed body text only, a rail + breadcrumb | NavShell |
| routed real components (grids, charts, 3D views) per route | AppShell::with_pane(route, Box::new(component)) |
| a menubar over the same nav tree | AppShell::with_menubar() (or facett_menu::menu_bar(ui, &nav_commands(&model)) directly) |
| a Ctrl-K fuzzy "go to…" palette | AppShell::with_command_palette() (or a standalone NavPalette) |
| two components side by side under one route | SplitPane::new(title, Box::new(a), Box::new(b)), mounted as one AppShell pane |
| a per-route control (e.g. a settings dropdown) | NavShell::with_route_dropdown(route, label, Dropdown::new(...).options([...])) |
AppShell composition, mirroring facett-demo's shell tab (facett-demo/src/lib.rs:395-421):
use facett_app::{AppShell, NavItem, NavModel, SplitPane};
let model = NavModel::new([
NavItem::new("home", "Home").icon("🏠"),
NavItem::new("models", "3D Models").icon("🧊").children([
NavItem::new("m.a", "Avocado").route("avocado"),
NavItem::new("m.b", "Skull").route("skull"),
NavItem::new("m.c", "Compare").route("compare"),
]),
]);
let compare = SplitPane::new("compare", Box::new(view_a), Box::new(view_b));
let mut shell = AppShell::new("shell", model)
.with_body("home", "Pick a model on the left.")
.with_pane("avocado", Box::new(view_a_owned))
.with_pane("skull", Box::new(view_b_owned))
.with_pane("compare", Box::new(compare))
.with_command_palette()
.with_menubar();
shell.navigate("compare");
assert!(shell.active_pane().is_some());
Overlays (Modal, Toasts, Popover) and input widgets (Tree, TabView, ColorPicker, DatePicker, Accordion, Stepper, Pagination) are drawn inside whatever Facet::ui hosts them — they are not shells themselves. The pattern is always: keep the widget as a struct field, call its .show(ui) (or .ui(ui) for Facets) each frame, and read the Option<T> it returns for "did something change this frame":
// Inside your Facet::ui:
if let Some(action) = self.modal.show(ui) { // "ok" / "cancel" / "dismiss"
if action == "ok" { self.toasts.push(Level::Success, "Saved."); }
}
self.toasts.show(ui); // stacked top-right, self-expiring
f3 — running the demo, native and web
cargo run -p facett-demo # the live native window (the nav/shell/overlay/widgets
# tabs sit alongside every other component)
cargo test -p facett-app # NavModel/Router/shell/widget unit tests (facett-app/src/tests.rs)
cargo test -p facett-demo # self-check + snapshot + mega matrix + robot drive
cargo test -p facett-demo --test robot_demo -- --nocapture # drive the whole app headlessly
Building and serving the web (WASM) build:
cargo xtask demo-wasm # → ./dist (index.html + JS glue + .wasm)
(cd dist && python3 -m http.server 8080) # → http://localhost:8080
cargo xtask deploy-pages # build, then publish dist/ to the `pages` branch
#TabName in the URL deep-links to a tab on load (facett-demo/src/main.rs:73 reads window.location().hash()); from inside the running app, DemoState::set_active_tab(title) does the same thing programmatically. The wasm build drops region-clip/map-wgpu/ map3d-wgpu (native-only), so those surfaces degrade gracefully rather than panicking — see app-for-idiots.md if you're checking the deployed site behaves the same as native.
To read what actually ran in a shipped wasm bundle (no filesystem to dump to), open the browser devtools console on the deployed page and call:
JSON.parse(window.__facett_state())
This returns the same DemoState::state_json() a native test asserts on — every tab, every control, the nav/shell/overlay/widgets state included.