facett-demo — the showcase app
facett-demo is one eframe (glow) binary that exercises every facett component with fake-but-realistic data, switchable across every palette in Theme::ALL. It is the living "everything works together" proof and the D4 compliance check: if a component cannot be slotted into this one window, that is a flag (see Compliance below), not a silent omission.
It is the gallery a reviewer opens to see the whole library at once, and the acceptance artifact (tests/snapshots/facett_demo.png) a CI run produces.
It is the reference driver for the look & feel (look::Theme)
facett-demo is now the reference app that drives the unified look::Theme (.nornir/look-and-feel.md). The top toolbar carries a preset switcher over LookTheme::preset_names() — Windows L/D, macOS L/D, Device — and draw_app calls DemoState::apply_look(ctx) → LookTheme::apply(ctx) every frame:
src/main.rsstarts onLookTheme::from_os(ctx.os())(Linux → windows-dark).DemoState { look_preset: usize }+look_theme()/set_look_preset(i)/apply_look(ctx).state_json["look"] = { preset, preset_index, presets, mode }.
apply installs the full egui::Style AND publishes the derived legacy palette, so the legacy per-facet palette deck (deck.palette_picker) still works while the new look re-themes every component coherently (COH-1). Inject-assert test: look_switcher_drives_every_preset_and_publishes_coherent_palette drives every preset and asserts the published palette name matches the active preset.
cargo run -p facett-demo # the live window
cargo test -p facett-demo # self-check + snapshot + mega matrix + robot drive
cargo test -p facett-demo --test robot_demo -- --nocapture # D5b2: drive the whole app
What it shows
A single tabbed FacetDeck holds 56 Facet components (the deck vector in FacetDeck::new(...) in src/lib.rs — that vector plus the coverage-gated LEGEND registry are the single source of truth; the self-check asserts facets.len() == 56), plus a non-Facet command menu overlay (facett-menu CommandPalette, Ctrl+P) and an about/legend side panel.
Rather than duplicate the 56-row registry here (it would rot — LEGEND in src/lib.rs carries the authoritative one-line blurb per tab), the deck spans these families:
- Graphs / DAGs —
graph,graphview,Graphs(the graph navigator over the Swedish 11M population graph),metro,depgraph,graphpan,decograph,plandag(Spark plan DAG),pipeline+synthetic-sdp,SDP/brokkreditors. - 3-D —
graph3d,rabbit3d,avocado3d(glTF),OSM 3D(extruded city),helix/dna/timehelix(parametric time-axis / genome / vortex scenes). - Maps —
map(shared byfacett-osm),geomap,OSM 2D(slippy GeoParquet),OSM 3D— all over the ONEfacett-map::MapLayertrait + renderer. - Data —
grid,table,warehouse,card,form,matrix,icebergview(Iceberg time-travel),warehousedeck(multi-pane lakehouse WALL). - Charts —
plot,boxplot,violin,heatmap,treemap,candles,stat,syschart,timeline(swimlane Gantt),pop(ingest progress). - App-framework (facett-app) —
nav,shell,overlay,widgets,primitives(Fas 1–4 backbone: NavModel + router + AppShell + overlays + widgets). - Adopted panes (lifted whole from nornir's viz) —
log,console,video,Git,Open File,docview,security,jobs,tabbar,Velo L1.
facett-arrow is exercised as the RecordBatch builder that feeds grid/table; the effects crate (RavenSprite / glow) is exercised through the deck fx (the glow toggle and send-raven button).
The one fake dataset (src/data.rs)
Everything is generated from a small, deterministic (no RNG, no files) "fleet / issues" world so the self-check test can assert exact counts and values:
- 48 vessels (region, status, crew, score, progress, 7-point trend, position) → an Arrow
RecordBatchfeeding grid + table, a card/form for one vessel, and pop progress rows. - an 8-node service DAG (
Ingest → Decode → … → Audit) → graph, graph3d, depgraph, pipeline. - telemetry series (per-region throughput) → plot and the plotters family.
- the vessels' lon/lat → map, and (with per-region hotspots) the scrollable/zoomable geomap.
- the same 8-node DAG laid out as free-positioned peers with event badges → syschart.
- two Iceberg snapshots of the fleet table (an older half-fleet
#100, the full-fleet#200) → icebergview time-travel + paging.
Live UI (src/main.rs + DemoState::draw_app)
The entire toolbar + deck UI now lives in DemoState::draw_app(ui) (in src/lib.rs), and src/main.rs is a thin eframe shell that just calls it. This is deliberate: the eframe binary and the D5b2 robot-UI test render the exact same UI (CLI/GUI parity), so the robot drives precisely what a human sees. The shared UI offers:
- a palette switcher (
FacetDeck::palette_picker) overTheme::ALL— the whole deck and every facet re-theme live; - a glow toggle and send-raven button (deck fx);
- a command palette (facett-menu,
Ctrl+P) whose invocations route throughDemoState::run_command— the single command sink the toolbar buttons share; - an about / legend toggle;
- a node-select toolbar (labelled
node:<id>) shown when the syschart tab is active, so the chart's selection is robot-addressable (its own canvas click-select is position-based).
Auto-demo / attract mode (#82) + the responsive hamburger menu (#39)
Auto-demo (attract / kiosk loop). facett-demo/src/attract.rs holds a pure, Copy AttractMode state machine: after IDLE_ARM_SECS (5 s) of no interaction it arms, then auto-advances through a TOUR of showcase tabs, dwelling DWELL_SECS (5 s) each; any interaction cancels it and re-arms the idle timer. draw_app pumps update(dt, interacted) -> AttractAction (gated on attract_enabled && attract.running()) and draws an on-screen chip — "auto-demo · move to take control · {tab}". Public API: new/running/idle_secs/dwell_secs/tour_index/current_tab/update/interrupt/ state_json; the live state rides in state_json.attract. (Distinct from MapFacet::with_auto_rotate, the 3D-camera idle spin in .nornir/idle-auto-rotate.md.)
Hamburger main-menu drawer (responsive fold). The deck's tab bar is the demo's MAIN menu (60+ facets). On a phone-class viewport draw_app drives the device class into the deck — self.deck.set_menu_collapsed(phone) — so the wide tab bar collapses to a ≡ hamburger + drawer (canvas-first); tablet/desktop keep the inline wrapped bar. The per-frame ShellReport (device class · controls_folded · canvas fractions) is the observable data in state_json.shell, so the fold is robot-assertable. (This is the deck menu's fold; the scene shell's own activity-rail → drawer fold is the separate facett-app scene layer, .nornir/scene-design.md.)
The per-frame lag fix (the "stutter on a 5080" bug)
facett-demo was reported to lag / stutter on a discrete GPU + 12 cores while the sibling sigtuna_demo was buttery smooth. A 5080 has no business dropping frames on a map demo, so the delta between the two binaries WAS the bug.
Root cause — the per-frame full-deck state dump. main.rs called DemoState::dump_state() every native frame. dump_state builds the WHOLE ~27-facet deck's aggregated state_json() (a deep serde tree) and fs::writes it to disk each frame. That tree is not static: the live sim accrues one Iceberg snapshot (and grows the warehouse table) on every tick, so the per-frame serialise grows without bound — the classic "starts okay, then stutters" shape. Measured (the demo_frame test): the serialised tree is ≈ 112 KB and rising, and building + serialising it costs ≈ 160 ms/frame in a debug build — plus a full-file fs::write (fsync) — paid unconditionally every frame while the GPU idled. That is CPU-side frame stall, not a GPU limit; sigtuna_demo never did it (it only reads map.state_json() for the on-screen panel).
Fix (L2/L4). dump_state is a robot-UI channel seam, not interactive work. DemoApp now only pays it when an external driver actually asked for the channel ($FACETT_DEMO_STATE set); a human interactive run leaves it unset → zero per-frame serialise + disk write → the frame is GPU-bound again. The channel (and every control_matrix / robot test that drives it in-process) is unchanged.
Measured smoothness, as data. DemoState now carries a FrameClock (an EWMA of egui's stable_dt), surfaced in the toolbar as x.x ms · yyy fps (green ≤ 16.6 ms) and in state_json.perf (ms_per_frame / fps / smooth_60fps, testmatrix-gated). The facett.demo_frame bench arm (dense town, street-level pose, the full CPU paint pipeline) is the frame budget the Loki bencher re-runs every push to guard against a regression back into per-frame stutter.
The street-level tour (parity with sigtuna_demo)
The user also reported the OSM-3D tab "has a similar tour but the walk doesn't go down to street level." It auto-played the framed flythrough, which orbits the city HIGH and never lands. osm3d_view now flies the same shared facett_map3d::SigtunaTour the sigtuna_demo bin does (air-circle so you SEE THE HOUSES → descend → drive the streets), reusing the ONE street picker (osm_tabs::street_route) and the town-core crop (crop_to_town_core) — L5, no second tour, no forked picker. The crop is load-bearing: on the raw 16 km clip the huge metres-per-design-unit pushes the camera's hard min-distance floor ~480 m over the road (you never get down among the houses); cropping to the dense core drops metres-per-unit to ~481, so the drive reaches a real ~30 m street-level chase. Proof (osm3d_street_tour, RED-when-broken): the tab walks circle → descend → drive, descends bird → street (air ≈ 514 m ≫ street ≈ 30 m), and 2473 house verts are in frame at the circle. The camera-framing/z-fight unit tests disable the auto-pilot (set_tour(false)) exactly as they already did for the flythrough.
Headless heart (src/lib.rs)
DemoState::new() builds the entire app — the populated FacetDeck (all 56 Facets), the menu, and the fleet — without a window, so the self-check test constructs it directly. DemoState::state_json() folds in the deck's state_json (every facet + the caps map), the menu, and the legend.
Two host-driving helpers back the robot row: run_command(id) (the shared command sink) and select_syschart_node(id) (forwards a selection to the boxed SystemChart via the deck's typed facet_mut::<T>(title) — see as_any_mut below). facet_mut works because SystemChart and IcebergView opt into the new defaulted Facet::as_any_mut (everything else keeps the None default), giving a host typed access to a concrete component inside a FacetDeck.
Laws covered
- (1 + 6) self-check —
demo_state_has_every_component_populatedasserts every component is present and populated with the fake data viastate_json(real counts/values: 56 facets (assertedfacets.len() == 56), 48 grid/table rows, per-region plot series, non-zero pop progress, one caps entry per facet, the demo command set, 8 syschart nodes, geomap points, 2 iceberg snapshots), not "didn't panic".deck_cycles_through_every_palettewalks all ofTheme::ALLand wraps. - (1) D5b1 static mega matrix —
tests/mega_matrix.rscrosses every component × data × palette (58 components × 2 data × 7 palettes — the 57MatrixCells plus the menu cell) and a coverage gate forces it to track the demo'sLEGEND. See.nornir/mega-matrix.md. - (1) D5b2 robot-UI live drive —
tests/robot_demo.rsfires up the whole demo headless (overDemoState::draw_app, the same path as the binary) and drives every tab + every component's controls (theme picker; grid/table scroll; syschart node-select; geomap pan/zoom; icebergview pager + snapshot time-travel; menu open/Escape), asserting each component'sstate_jsonresponds. Every step is onerobot-uimatrix row (the run gates on every row green), printed as a grid and persisted totarget/robot-matrix.jsonl. A fidelity test proves a wrong expectation is a red cell, not a panic. See.nornir/mega-matrix.md(D5b2). - (4) screenshot proof —
tests/snapshot.rsrenders the populated, themed (nordic-aurora + glow) demo withegui_kittest(wgpu / lavapipe) to the goldentests/snapshots/facett_demo.png, asserting it is non-trivial (> 5 KB). Usesrun_steps(notrun) because the glow + 3D spin request a repaint every frame by design and never reach a steady state. - (2) this doc — design + guide, in-repo.
Compliance (D4 flags)
Every component slots in without a flag — including syschart, geomap and icebergview, which were added as their own tabs so the demo is the full robot-drivable showcase (D5b2). Notes on how a few are exercised rather than re-implemented:
- osm now has its own tabs —
OSM 2D(facett-geomap::OsmView, a slippy GeoParquet viewer) andOSM 3D(facett-map3d::Map3D, an extruded lit city) — in addition tofacett-osmstill yielding the same sharedMapViewasfacett-mapon the map tab. All maps ride the ONEfacett-map::MapLayertrait + renderer, so this is layer selection, not re-implementation. - menu (facett-menu
CommandPalette) is an overlay, not aFacet— it is not tabbed into the deck but driven on top (toolbar button /Ctrl+P), which is its intended host integration.
No component had to be omitted.
Robot-UI driver caveat
D5b2 is the documented nornir-robotui consumer, but robotui 0.1.0 pins egui 0.33 (it serves nornir's egui-0.33 viz) while facett is egui 0.34 — two egui majors cannot share an egui_kittest::Harness. So tests/robot_demo.rs carries a byte-faithful in-crate port of robotui's API over egui_kittest 0.34; swapping to the published crate is a one-line use once robotui ships a 0.34 build (the dep is listed, commented, in facett-demo/Cargo.toml). The only fix robotui needs is the egui-0.34 Event::MouseWheel { phase } field.