facett-demo/src/lib.rs
//! **facett-demo** — ONE showcase that reuses **every** facett component with
//! fake-but-realistic data, switchable across all palettes. This is the living
//! "everything works together" proof (and the D4 compliance check): if a
//! component can't be slotted in here, that's a flag.
//!
//! [`DemoState`] is the headless heart: it builds the whole app — a
//! [`FacetDeck`](facett::FacetDeck) holding every [`Facet`](facett::Facet)
//! component plus the (non-`Facet`) command **menu** and an about/legend —
//! without any window, so the self-check test can construct it and assert that
//! every component is present and populated with the fake data.
//!
//! The binary (`src/main.rs`) wraps a [`DemoState`] in an eframe (glow) app with
//! a palette switcher + deck-fx toolbar.
//!
//! ## Component roster (kept in step with the WORKSPACE, not the umbrella)
//! The gallery slots in **every** facett component crate in the workspace — not
//! merely what the umbrella `facett` re-exports. That distinction is load-bearing:
//! until 2026-08-20 the roster was gated against the umbrella's re-export list, and
//! **14 workspace crates** (facett-cicd · facett-brokkrview · facett-lineage ·
//! facett-fleet-view · facett-holger · facett-korp · facett-auth-ui ·
//! facett-binview · facett-coderoom-view · facett-claude · facett-deploykey ·
//! facett-buildthing · facett-about · facett-flowsim) sat outside it, released and
//! tested and never on screen. `tests/roster_gate.rs` now walks the workspace
//! members list instead, so the blind spot cannot recur.
//!
//! The seven flow/workspace widgets — the Gantt
//! (`Bygglov`), the Kanban board (`Support`), the Calendar (`Frister`), the toast
//! queue (`Notifications`), the Markdown note (`Anteckning`), the query SearchBar
//! (`Search`) and the file/attachment list (`Attachments`) — are reached straight
//! through the umbrella (`facett::gantt` / `::kanban` / `::calendar` / `::toast` /
//! `::markdown` / `::search` / `::file`), each seeded from its own `local()` fixture.
//!
//! ### Not shown here (on purpose)
//! The roster gate (`tests/roster_gate.rs`) walks the WORKSPACE members — not the
//! umbrella `facett`, whose re-export list is what let 14 crates go unwired — and
//! every `facett-*` member must be either credited by a LEGEND row or named in that
//! gate's documented `EXCLUDED` list. The entries below are the human-readable WHY
//! for the excluded components (engines/support crates carry their reason in the
//! gate itself):
//! - **`facett-confirm`** — re-exported by the umbrella, but it is an ELM *gate*
//! (`Msg`/`Effect`/`update`), **not** an `impl Facet`, so it cannot be a deck
//! tab. The confirm/cancel UX it models is exercised in the `overlay` tab through
//! `facett-app`'s `Modal`. If it ever grows a `Facet` face, add it here.
//! - **`facett-flowsim`** — a flow-simulation OVERLAY for a DAG view (animated
//! particles riding another component's edges), not a standalone component: it
//! has no data surface of its own to open a tab on. It decorates a host graph
//! the way `pop_beauty` decorates the population views.
//! - **korp's other three panes** (`facett_korp::GraphPane` / `SearchPane` /
//! `TimelinePane`) — their defaults are EMPTY shells that need live host rows
//! (a korp warehouse feed) to show anything, and the demo's law is that an empty
//! data surface shall not pass. The crate IS in the gallery through
//! `IngestPane::demo()` (the `Ingest` tab), whose roster is self-contained.
//! - **`facett-trace`** — deleted from the workspace (it was a verbatim twin of
//! nornir's tracer that nothing linked). It was never a gallery tab, so nothing
//! was removed from the LEGEND; recorded here only so the roster stays honest.
/// The build stamp — `facett-demo v<ver> (<git hash>)` — that `facett-demo
/// --version` / `-V` prints and the gallery header shows, so an operator can
/// always tell WHICH build is running (the nordisk LAW). The version is
/// `CARGO_PKG_VERSION`; `FACETT_GIT_HASH` is emitted by `build.rs` (a pure-fs git
/// read, no shell) and falls back to `unknown` in a tarball build with no `.git`.
pub fn version_line() -> String {
format!("facett-demo v{} ({})", env!("CARGO_PKG_VERSION"), env!("FACETT_GIT_HASH"))
}
/// The **auto-demo / attract mode** (#82) — after [`attract::IDLE_ARM_SECS`] of no
/// user interaction the demo auto-advances through a fixed tour of showcase tabs and
/// loops forever; any real interaction cancels it and it re-arms after idle. Wasm-safe
/// (pure `dt`-driven state machine, no wall-clock) and shared by native + web. See
/// `attract`.
/// The **`About` tab** — the shared About-card widget (`facett-about`, NOT itself a
/// `Facet`) wrapped demo-locally the way `errcode_tab` wraps the error-code
/// registry: the card's body inline over a seeded nornir `Identity`, plus the real
/// shared modal on demand. See `about_tab`.
pub mod about_tab;
pub mod attract;
pub mod control;
pub mod data;
/// The **`Errors` tab** — the facett UI **error-code** showcase (pink codes, the
/// git-hash build stamp, the source link per component, the consumer
/// mount-prefix demo, and the error-code MATRIX slide over
/// [`facett_core::errcode::REGISTRY`]). See `errcode_tab`.
pub mod errcode_tab;
/// **GFX v2** — the renderer-track showcase tab (items 4 / 6 / 8 over one scene of
/// real Swedish geography). See the module docs for what each lane proves and how.
pub mod gfx_v2;
pub mod graphview_tab;
/// Falsett host controls and host-clock injection around the reusable flow facet.
pub mod flow_tab;
/// The **`falkor` tab** — a live FalkorDB graph-DB browser pane: a
/// `facett_graphview::FalkorBrowser` (Cypher → laid-out `GraphModel`) feeding an
/// interactive `DecoratedGraphView`, driven by a stubbed in-memory `CannedRunner`
/// (no live redis). See `falkor_tab`.
pub mod falkor_tab;
pub mod vg_lab_tab;
/// The size + generator control and its wrapper facet for the graph tabs (2026-10-01).
pub mod graphgen;
/// How each graph tab is built from a generated graph.
pub mod gen_build;
/// The `Universe` tab: the whole graph as one GPU star-field, 100k by default (2026-10-01).
pub mod universe_tab;
/// knut-inspired population graph (the Swedish 11M-person graph DB) — the data
/// driver for the graph tabs. Mirrors `knut-popsim` (extracted to the knut repo);
/// kept self-contained here so the wasm/server build needs no cross-repo dep.
pub mod popgraph;
/// The **population query / LOD layer** over the canonical `knut-popsim` model
/// (the wasm-clean Swedish 11M-person graph DB): scale, filters, colouring + the
/// three linked views. See `pop_model`.
pub mod pop_model;
/// The **`Sverige · Population` showcase tab** — three beautiful linked population
/// graphs (full / debt / kommun) through the L0 + glow kernel, driven by
/// `knut-popsim`. See `pop_tab`.
pub mod pop_tab;
/// The **population BEAUTY layer** — the "Cyber-Toon Raytraced" art direction
/// (neon palette · animated edges · glow/AO · shockwave · bloom) over the
/// domain-agnostic `facett_core::render::decor` engine helpers. See `pop_beauty`.
pub mod pop_beauty;
pub mod osm_tabs;
/// The **Google-Maps-style layer toggles** demo — a `facett_map::LayerToggles`
/// checkbox list ("turn on roads, bars, kiosks…") driving a live 2D map through
/// the shared, engine-agnostic `facett_map::LayerSet` (the same seam the 2D + 3D
/// map engines consume). See `demo_layers`.
pub mod demo_layers;
pub mod region_picker;
/// The **OSM-Fly flow** — the ONE connected story: pick Sweden → clip the cached
/// planet OSM to the Sweden polygon (one continuous progress bar) → fly the clipped
/// result in 3D, with the planet + clip cached on a large disk (never T9). Wires the
/// `region_picker` clip + `osm_tabs` reader + `facett-map3d-flight` engine into one
/// flow. Native-only (osm-katana clip + threads + fs + flight paint). See `osm_fly`.
#[cfg(not(target_arch = "wasm32"))]
pub mod osm_fly;
/// The **photoreal day-cycle demo** — a self-contained toggle over the
/// `facett-map3d-flight` time-of-day + weather model (animate the sun: east→noon→west,
/// golden-hour reddening, night, overcast). Headless-oracle-testable; native-only
/// (uses the flight engine). See `demo_photoreal`.
#[cfg(not(target_arch = "wasm32"))]
pub mod demo_photoreal;
/// The **discrete-GPU adapter selector** for the native wgpu backend — forces the
/// demo onto the NVIDIA RTX (never llvmpipe/iGPU). Pure, testable policy in
/// [`gpu_select::choose_adapter_index`] + the eframe `WgpuConfiguration` builder.
/// Native-only (no wgpu adapters on the wasm/glow path). See `gpu_select`.
#[cfg(not(target_arch = "wasm32"))]
pub mod gpu_select;
/// The **live, ticking, in-memory SIMULATION** — the demo's heartbeat. Grows a
/// self-contained population each ~45 frames and feeds the warehouse + icebergview
/// + population/graph tabs so the gallery is ALIVE (data flows, snapshots accrue,
/// graphs animate) instead of static. No skade / catalog / network. See `sim`.
pub mod overlay_demo;
pub mod sim;
pub mod widget_gallery;
pub mod primitives_tab;
pub mod synthetic_sdp;
/// The `SDP` tab — the Spark Declarative Pipelines editor (`facett-sdp`) wrapped as
/// a deck `Facet`. See `sdp_tab`.
pub mod sdp_tab;
/// The `brokkr` tab — the brokkr CI/build-pipeline builder (`facett-brokkr`) wrapped
/// as a deck `Facet`. See `brokkr_tab`.
pub mod brokkr_tab;
/// The `runa` tab — a dwarves forge `dwarves_runa::Pipeline` rendered on the REUSED animated
/// node-DAG viewer via `facett_pipeline::RunaPipelineAdapter`. See `runa_tab`.
pub mod runa_tab;
/// The `releaseview` tab — a release process as THREE converging graphs (LOCAL ·
/// SPARRING · CRATES_IO) that a release drives to converge, via
/// `facett_releaseview::ReleaseView` + a scripted cascade. See `releaseview_tab`.
pub mod releaseview_tab;
/// The `Velo L1` tab — L0 SDF vs L0 + L1 vello overlay, side by side. Makes the
/// L1 vello beauty lane (gradients / curves / glow) visible. See `velo_l1_tab`.
pub mod velo_l1_tab;
/// The `zone` + `jera` tabs — the TWO shared zone/jera framework widgets
/// (`facett-zoneui`): the zone-populate pane + the jera-central panel (jobs WITH
/// their zone column). See `demo_zoneui`.
pub mod demo_zoneui;
/// The **Wreck-It-Ralph "Code Vault"** demo — the nordisk warehouse as labeled,
/// glowing-transparent 3-D boxes wired by dep connectors with a live glitch, over
/// `facett-graph3d::CodeVault`. [`demo_codevault::demo_vault`] builds the populated
/// scene. It is a live `FacetDeck` tab (deck `Box::new(demo_vault())` + the `codevault`
/// LEGEND row + its mega-matrix coverage cell), so the atom-walk / mega-matrix /
/// pop_ran_list gates discover and cover it like every other component.
pub mod demo_codevault;
/// The **🫧 System Map (fluid)** demo — a nice little map of the nordisk
/// constellation as glass tanks joined by **glass pipes with fluid flowing through
/// them**, over `facett-graph3d::SystemMap` (a glass-pipe skin on Graph3D) driven by
/// `facett-cfd`'s gatling-accelerated fluid. [`demo_systemmap::demo_systemmap`] builds
/// the populated scene; it is a live `FacetDeck` tab.
/// The **`graph3d-given`** demo — a `facett-graph3d::Graph3D` placed by the HOST
/// (`Layout3D::Given`): a synthetic traffic-sensor road network whose x/y come from a
/// projected lon/lat and whose z carries the measured SPEED, so the cloud reads as
/// terraces + dipping ribbons instead of a force blob.
/// [`demo_graph3d_given::demo_graph3d_given`] builds the populated scene; it is a live
/// `FacetDeck` tab.
pub mod demo_graph3d_given;
pub mod demo_systemmap;
/// The **`scene` shell** demo — facett-demo's OWN shell described DECLARATIVELY as a
/// [`facett_app::scene::Scene`] (a menubar + a tree of foldable panes + routes) and
/// rendered through facett-app's Router/TabView/menu primitives. The first consumer
/// of the declarative UI-scene layer; its foldable canvas pane hosts the 🫧 fluid
/// `System Map`. Interactions: draggable/resizable splits, fold/unfold, and a
/// responsive drawer rail (Split → Tabs on phone). See `scene_demo`.
pub mod scene_demo;
/// The reusable **robot-UI** seam ([`nornir_robotui`]) wired onto the main demo
/// window: ONE `Observable` + ONE draw closure + ONE `Scenario` runnable BOTH
/// headless (CI matrix) and watch (a visible on-screen replay). Behind the
/// `robotui` feature (the `facett_robot` bin turns it on) so the default demo build
/// never pulls the driver. See `robot` and `src/bin/facett_robot.rs`.
#[cfg(feature = "robotui")]
pub mod robot;
pub use control::DemoCommand;
use facett::FacetDeck;
use facett::core::navgraph::{self, NavKind, NavOrder};
use facett::LookTheme;
use facett::look::Platform;
use facett::card::{Card, CardField, CardView, FieldValue};
use facett::docview::DocView;
use facett::form::FormView;
use facett::graph::{DepEdge, DepGraphView, DepNode};
use facett::graph::{GraphView, hash_color};
// The metro / subway-line test-coverage view lives in the consolidated graph
// ENGINE crate (`facett-graphview`), not the `facett-graph` skin, so it is reached
// directly (the umbrella `facett` re-exports the skin, not the engine). It is a
// canonical `Facet` (local()/remote()/state_json()/caps()) — wired into the deck so
// the atom-walk + mega-matrix discover and cover it like every other component.
use facett_graphview::MetroView;
use facett_graphview::DecoratedGraphView;
// The bare pan/zoom graph CANVAS (the borrowed `draw_graph` renderer + a
// `GraphView` transform), reached directly like the graphview engine — the
// umbrella `facett` re-exports the skins/engine, not this minimal canvas crate.
use facett_graphpan::GraphPanView;
use facett::graph3d::{Graph3D, Layout3D, RabbitLogo3D};
// The two net-new 3-D helix facets (Variant A + B of the helix-facets design):
// the DNA double-helix (each record = a base-pair rung) and the Doctor-Who "time
// vortex" (layers receding down the tunnel axis). Both live in facett-graph3d
// (reusing its neon painters + camera), reached via the umbrella re-export.
use facett::graph3d::{DnaHelix, DnaRung, TimeHelix, TimePoint};
use facett::map::MapView;
use facett::menu::{Command, CommandPalette};
use facett::pipeline::{PipelineView, Status};
use facett::plot::PlotView;
use facett::plotters::{BoxPlotView, CandlestickView, HeatmapView, Ohlc, Tile, TreemapView, ViolinView};
use facett::pop::{ProgressItem, ProgressView};
use facett::geomap::{GeoMap, Hotspot};
use facett::icebergview::IcebergView;
use facett::log::LogView;
use facett::stat::StatGridView;
use facett::table::WarehouseTableView;
use facett::tabbar::TabBarView;
use facett_jobview::{JobEntry, JobList, JobStatus};
// The two net-new Spark-UI components — reached directly (not re-exported by the
// umbrella `facett`): the swimlane Gantt + the metric-bearing operator DAG.
use facett_timeline::TimelineView;
use facett_plandag::PlanDagView;
// The software-supply-chain security report view (verdict + findings + EPSS/KEV +
// CVE/advisory table) — the UI twin of modgunn's scan output. Reached directly
// (the umbrella `facett` re-exports the skins, not this view).
use facett_security::SecurityView;
// The TIME-HELIX DAG view — reached directly (the umbrella `facett` re-exports skins,
// not this view). `model`+`layout` are the serde-only seam hosts populate; the demo
// builds a nordisk-constellation `Scene` (four crate-coils winding through release
// time) so the gallery shows real multi-coil winding + a live CVE event.
use facett_helix::{Coil, EventKind, HelixRef, HelixView, Kind, PlanDag, Scene, Status as HStatus};
use facett::matrix::MatrixView;
use facett::syschart::{SysEdge, SysNode, SystemChart};
// Newly-wired leaf facets. `console` / `filedialog` / `video` come via the umbrella
// `facett` re-exports; `git` + `warehousedeck` are reached directly (the umbrella
// re-exports the skins, not these views).
use facett::console::{Console, Cursor as ConsoleCursor};
use facett::filedialog::{DialogMode, Entry as FileEntry, FileDialog};
use facett::video::{FrameSequence, VideoPlayer};
use facett_git::GitView;
use facett_warehousedeck::{ChartSpec, GraphSpec, PaneData, WarehouseDeck};
// ── The 2026-08-20 roster completion — the workspace components the gallery never
// wired (none are umbrella re-exports; each is reached directly, like the
// timeline/plandag/security adoptions above). All seeded from their own in-crate
// fixtures so every tab opens POPULATED; the two native-only decks are pulled in
// at their `#[cfg]`-gated deck entries below.
use facett_auth_ui::{LoginPane, StubLogin};
use facett_binview::{BinaryPane, StubBinaryData};
use facett_brokkrview::BrokkrView;
use facett_cicd::CicdPanel;
use facett_claude::{ClaudeMsg, ClaudePane, EchoBackend};
use facett_coderoom_view::{CoderoomPane, StubCoderoomData};
use facett_fleet_view::FleetView;
use facett_holger::RegistryBrowser;
use facett_korp::IngestPane;
use facett_lineage::LineageView;
use data::Vessel;
/// Every component this demo exercises, in legend order. The labels double as the
/// about/legend text and let the self-check assert nothing was dropped.
/// **The graph3d tab's default node budget.** Overridden by
/// `$FACETT_GRAPH3D_NODES`.
///
/// 100000 nodes / ~132000 edges — 714x the 140 the tab used to hold. Chosen from
/// measurement, not taste (`facett-graph3d/benches/heavy_graph3d.rs`, host oden,
/// RTX 4090 + Threadripper PRO 3975WX):
///
/// * the per-FRAME cost is the gatling-parallel `project_all` + the instanced GPU
/// scene build — the instanced cloud lane (`--features gpu`, default ON since
/// 2026-07-22) draws the discs and filaments as GPU instances, so node count is
/// no longer the constraint the 33.75 ms/frame CPU tessellation wall made it;
/// * the one-off cost is the force layout at construction, held near ~2 s at this
/// size by the large-graph relaxation budget
/// (`facett_graph3d::LARGE_GRAPH_ITERS`).
///
/// Raise it with the env var; the dataset generator scales and stays deterministic.
pub const GRAPH3D_DEFAULT_NODES: usize = 100_000;
/// The budget used when the deck is built inside a **test binary**. The suite
/// constructs `DemoState` many times over and the visual bulk buys it nothing, while
/// the force layout is a real per-construction cost; this is still 14x the 140 the
/// tab used to hold, so every code path is exercised. The big-cloud properties are
/// proven where they belong — `facett-graph3d`'s `dataset_volume.rs` and
/// `cpu_paint_budget.rs` run at 100000.
///
/// `#[cfg(test)]` cannot express this: it is set only while compiling the crate's own
/// unit tests, so an INTEGRATION test (a separate binary linking the lib compiled
/// without it) would still pay the full budget — which is exactly what it did.
pub const GRAPH3D_TEST_NODES: usize = 2_000;
/// How many nodes of the **real knut-popsim population** ride along after the
/// generated bulk in the graph3d tab. Kept as a named constant because the robot
/// scenario has to predict the pane's exact node count, and a number that lives in
/// two places is a number that drifts.
pub const GRAPH3D_POP_SLICE: usize = 140;
/// Are we running inside a cargo test binary? Cargo builds test executables into
/// `target/<profile>/deps/` while the real `facett-demo` binary sits one level up in
/// `target/<profile>/`, so the `deps` component separates them. Used ONLY to pick a
/// cheaper default dataset size; `$FACETT_GRAPH3D_NODES` overrides either way, so a
/// test that wants the full cloud can still ask for it.
fn in_test_binary() -> bool {
std::env::current_exe()
.ok()
.map(|p| p.parent().is_some_and(|d| d.file_name().is_some_and(|n| n == "deps")))
.unwrap_or(false)
}
/// **The ONE roster count** — how many `Facet` components the deck wires.
///
/// [`LEGEND`] is exactly these plus the `menu` command palette (a `CommandPalette`,
/// not a `Facet`, so it is in the legend but never in the deck), hence
/// `LEGEND.len() == EXPECTED_FACET_COUNT + 1`.
///
/// It lives here, `pub`, because **three** guards need it and each one that spelled
/// its own number went stale: the in-crate `demo_state_has_every_component_populated`
/// roster check, the deck↔LEGEND parity check, and — across the crate boundary —
/// `tests/mega_matrix.rs`'s coverage gate. `0b12255` added the `Errors` and `Manual`
/// tabs, bumped the first two, and left the mega-matrix gate asserting 73 against a
/// 75-entry LEGEND. Deriving every guard from this one const is the by-construction
/// fix (LAW: reuse, do not twin) — there is no second number left to forget.
///
/// Gated the SAME way the `LEGEND` rows and the deck `vec!` are: the NINE
/// native-only tabs — the seven heavyweight korp caseworker-flow tabs (`Bygglov` …
/// `Attachments`) plus the two nornir-plugin decks (`Deploy key`, whose nornir-git
/// dep pulls gix/tokio/russh, and `Build Thing`, over nornir-build-thing) — are
/// dropped from the wasm web demo, so it wires nine fewer facets.
#[cfg(not(target_arch = "wasm32"))]
pub const EXPECTED_FACET_COUNT: usize = 91;
/// See the native [`EXPECTED_FACET_COUNT`] — the wasm bundle drops the nine
/// native-only tabs (the seven korp caseworker-flow tabs + `Deploy key` +
/// `Build Thing`).
#[cfg(target_arch = "wasm32")]
pub const EXPECTED_FACET_COUNT: usize = 82;
/// **The deck's tab groups** (owner, 2026-10-01: "can you group the tabs? i.e. graphs,
/// other"). The deck bar is two-level: this row of group names, then the active group's
/// tabs. Titles are the deck/LEGEND keys EXACTLY (robot tests, `#<tab>` deep links, the
/// attract tour and the navgraph ids all key on them, and none of them changed); inside a
/// group the tabs keep deck order. A title absent on a target (the native-only tabs on
/// wasm) is skipped; a deck tab no group names lands in [`TAB_GROUP_REST`], and the
/// `every_deck_tab_is_named_by_exactly_one_group` test keeps that list empty.
pub const TAB_GROUPS: &[(&str, &[&str])] = &[
(
"Grafer",
&[
"graph", "graphview", "Graphs", universe_tab::TITLE, "metro", "Sverige", "graph3d", "graph3d-given",
"depgraph", "pipeline", flow_tab::TITLE, "SDP", "runa", "releaseview", "plandag", "syschart", "decograph", "falkor",
"graphpan", "helix", "dna", "timehelix", "codevault", "System Map", "lineage", "World coderoom",
],
),
("Kartor", &["map", "geomap", "OSM 2D", "OSM 3D", "Layers"]),
("Diagram", &["timeline", "plot", "boxplot", "violin", "heatmap", "treemap", "candles", "stat", "matrix", "Fleet"]),
("3D-scener", &["Velo L1", "rabbit3d", "avocado3d", "GFX v2"]),
(
"Bygg & jobb",
&["synthetic-sdp", "brokkr", "pop", "jobs", "zone", "Jobs", "cicd", "brokkrview", "Ingest", "Deploy key", "Build Thing"],
),
(
"Tabeller",
&["grid", "table", "card", "form", "icebergview", "log", "warehouse", "warehousedeck", "holger — registry & artifact browser", "console"],
),
(
"App & UI",
&[
"nav", "shell", "overlay", "widgets", "primitives", "tabbar", "docview", "Open File", "video", "Git", "scene",
"Errors", "Manual", "Sign in", "Claude", "About",
],
),
(
TAB_GROUP_REST,
&["security", "Binary analysis", "Bygglov", "Support", "Frister", "Notifications", "Anteckning", "Search", "Attachments"],
),
];
/// The graph tabs that carry the size + generator control ([`graphgen::GenTab`]), with the
/// node ceiling each one's component takes (the LEGEND row says why). The other graph
/// tabs keep their fixture and say why in their LEGEND row (`metro`: typed lines and
/// stations; `Sverige`: its own 1M → 100M scale control).
pub const GEN_TABS: &[(&str, usize)] = &[
("Graphs", 100_000),
("lineage", 100_000),
("decograph", 100_000),
("graphpan", 100_000),
("graph3d", 100_000),
("falkor", 100_000),
("World coderoom", 100_000),
("graph", 100_000),
("graphview", 100_000),
];
/// The trailing group: the korp caseworker flows (native only), the security/binary
/// reports — and any tab a future change forgets to place.
pub const TAB_GROUP_REST: &str = "Övrigt";
/// The group [`TAB_GROUPS`] puts deck tab `title` in (`None` for a title it does not
/// name, which the deck files under [`TAB_GROUP_REST`]). A robot that clicks a tab
/// clicks this group first: only the active group's tabs are on screen.
pub fn tab_group(title: &str) -> Option<&'static str> {
TAB_GROUPS.iter().find(|(_, ts)| ts.contains(&title)).map(|(g, _)| *g)
}
pub const LEGEND: &[(&str, &str)] = &[
(flow_tab::TITLE, "Falsett — animated directed flows through a translucent central hub; resident GPU curves and packets, explicit cluster overview, pause and reduced motion — facett-flow::FlowGraph · synthetic fixtures: hub, stages and layered DAG with status/rate; supplied graphs use FlowGraph::new"),
("graph", "service graph (Scene, circular) — facett-graph · SIZE + GENERATOR: demo data (default: the knut schema) · population · community · SBM · random · scale-free · grid · tree · crates, 100 → 100k, seeded (facett-demo::graphgen::to_scene). Above facett_graph::SCALE_ABOVE (5 000) nodes the SCALE PATH: the scene is laid out once (not per frame) — FORCE-laid as a GPU job on the host's device (gpu_layout::LayoutJob, the grid kernel; 100k on one circle is 300k screen-wide chords), the circle with no device — drag pans / scroll zooms, the node/edge cloud is RESIDENT on the GPU (uploaded once per scene, the camera per frame), labels for ≤ 60 on-screen nodes, and FC-4 under a STATED BUDGET: one AccessKit button per on-screen node, at most A11Y_BUDGET (2 000) — the canvas label and state_json `a11y` say how many of how many; Msg::SelectNode reaches every node. No adapter ⇒ the egui painter, `lane.gpu: \"no GPU\"`"),
("graphview", "layered service-mesh through the CONSOLIDATED L0 render kernel — facett-graphview (render_to_rgba_l0 → facett-core::render Canvas) · SIZE + GENERATOR: demo data (default: the layered service mesh) · population · community · SBM · random · scale-free · grid · tree · crates, 100 → 100k, seeded (the generated GraphModel). Above RESIDENT_ABOVE (5 000) nodes the same model (fills, chip radius, edge width) paints on the RESIDENT GPU cloud — uploaded once per model, a camera move sends the camera only; at or below it, or with no adapter (`gpu: \"no GPU\"`), the L0 CPU kernel rasterises on every camera move as before"),
("Graphs", "THE GRAPH NAVIGATOR (formerly V.G = G.V(), à la gdotv/Gephi) over the Swedish population graph — facett-graphpan pan/zoom camera, Appearance ranking/partition (size + colour by degree/PageRank/betweenness/…/Louvain), the Arrow data-laboratory table (facett-grid, row⇄canvas sync), GEXF/GraphML import+export, a whole-graph stats panel (density/modularity/clustering/diameter), a 🌐 3-D rotating node cloud (orbit/spin) over the SAME graph, and a GEOGRAPHIC layout — the population is placed at twelve real Swedish centres, so the `geographic` chip draws Sweden's shape at one uniform scale instead of a force blob (the host projects; facett-graphnav owns no projection) — facett-graphnav::GraphNav + facett-graph3d::Graph3D · SIZE + GENERATOR: demo data (default) · population · community · SBM · random · scale-free · grid · tree · crates, 100 → 100k, seeded, generated once per change (facett-graphgen::GenTab over facett-graphview::generate)"),
("Universe", "THE UNIVERSE — the whole graph as ONE GPU star-field, 100 000 nodes by default and up to 1 000 000: the repo → crate → function world (facett-graphview::world WorldGraph) on its O(n) galaxy placement (hier_placement), painted through the instanced cloud's RESIDENT lane (facett-core graphcloud draw_cloud_model: uploaded once per generation, each frame sends only the camera), labels off, pan/zoom about the pointer, click picks a node; facett's own twin of dwarves' warehouse-nav universe mode. No adapter ⇒ the pane and state_json say \"no GPU\" — there is no CPU painter behind it · SIZE + GENERATOR: crates (default) · population · community · SBM · random · scale-free · grid · tree, 100 → 1M, seeded (facett-graphgen::UniverseTab over facett-graphview::generate)"),
("metro", "subway-line view of test-coverage chains (button → emitters → gRPC → warehouse), lit/unlit stations + green verdict — facett-graphview::MetroView · fixed data (no size/generator): typed data (lines, stations, verdicts), not a bare node/edge graph"),
("Velo L1", "L0 SDF base vs L0 + L1 vello overlay (gradient · antialiased curve · gaussian glow), side by side — facett-core::render::l1"),
("Sverige", "THE showcase — the knut-popsim Swedish 11M-person population graph as three beautiful linked graphs (full LOD · debt→enforcement · per-kommun aggregation), scale 1M→100M, label/relation toggles, colour-by label/kommun/debt-heat, L0 SDF + glow halo lane — facett-demo::pop_tab over knut-popsim · fixed data (no size/generator): it has its own scale control (1M → 100M) over knut-popsim"),
("graph3d", "service call-cloud (3D force) — facett-graph3d · SIZE + GENERATOR: demo data (default) · population · community · SBM · random · scale-free · grid · tree · crates, 100 → 100k, seeded, generated once per change (facett-graphgen::GenTab over facett-graphview::generate)"),
("graph3d-given", "HOST-PLACED 3D cloud (`Layout3D::Given`) — a synthetic traffic-sensor road network where the HOST supplies every position: x/y from a projected lon/lat, z from the measured SPEED (three road classes = three terraces, a congestion basin over the city centre = each ribbon dips), colour from the same scalar. The placement is retained across a layout round trip, and one that does not cover every node degrades to the O(n) grid lattice and says so (`layout_fallback` + facet-graph3d-5) — facett-graph3d::Graph3D::with_positions + facett-demo::demo_graph3d_given · fixed data (no size/generator): the host-placement proof over a fixed sensor network"),
("rabbit3d", "3D znippy rabbit logo (extruded, lit, auto-rotate) — facett-graph3d"),
("avocado3d", "spinning real glTF model (CC0 Khronos Avocado), tinted green→yellow→red by status — facett-model3d::Model3D (embedded glTF, CPU-lit)"),
("nav", "app-framework backbone: a NavModel drives a sidebar + router inside a NavShell (rail + routed content + breadcrumb) — facett-app (Fas 1)"),
("shell", "AppShell hosting REAL facett components per route (topbar + sidebar + routed pane + statusbar) — facett-app (Fas 2)"),
("overlay", "overlay & feedback system: a modal dialog (scrim + Esc) + a levelled toast/notification queue — facett-app (Fas 3)"),
("widgets", "input/data widgets: accordion + bounded stepper + pagination over a paged list — facett-app (Fas 4)"),
("Layers", "Google-Maps-style LAYER TOGGLES — a checkbox list (roads · water · landuse · buildings · bars · kiosks · shops · restaurants) driving a live 2D map through the shared, engine-agnostic facett-map::LayerSet (the same seam the 2D + 3D map engines consume); flip a layer and the drawn ways/POIs fall — facett-map::LayerToggles + facett-demo::demo_layers"),
("primitives", "the three shared interaction primitives graduated in the #22 Part-B wave: a cards→columns dragdrop board, a pan/zoom time_axis scrubber, and the action_bus outbox they dispatch onto — all engine-agnostic facett-core reducers (draggable + time_scrollable caps)"),
("depgraph", "layered dependency DAG — facett-graph (depgraph) · fixed data (no size/generator): a facett-graph layered DAG over a fixed dependency set — a generated graph belongs on the navigator (`Graphs`)"),
("pipeline", "animated job pipeline through Falsett, with shared GPU/CPU geometry and throughput packets — facett-pipeline · fixed data (no size/generator): typed data (jobs and stages)"),
("synthetic-sdp", "~100 fun-named SDP pipelines, a wave of data flowing source→sink — facett-pipeline"),
("SDP", "Spark Declarative Pipelines EDITOR — flows as named Spark-SQL queries over their upstream deps, add/remove flows, an Edit⇄Graph dep-DAG preview, and static (non-SQL) validation; seeded with a bronze→silver→gold medallion pipeline — facett-sdp::SdpEditor · fixed data (no size/generator): typed data (Spark-SQL flows)"),
("brokkr", "brokkr CI/build-pipeline BUILDER — a live-parsed TEXT `brokkr.toml` editor AND a structural CLICK builder (stages · steps with `needs`/RunKind · triggers) with a topo DAG preview, kept in sync and structurally validated; seeded with the demo build pipeline — facett-brokkr::PipelineBuilder"),
("runa", "runa forge pipeline — the dwarves forge's typed `*.runa.ron` CI/CD pipeline (a Stage DAG over the typed Step catalog: CargoBuild · Nextest · Lint · Build{variant} · Sbom · Xtask · …) rendered on the REUSED animated node-DAG viewer via facett_pipeline::RunaPipelineAdapter (one adapter, NOT a new viewer); seeded with the korp-ci build→test→package pipeline + a live run-status overlay — dwarves_runa::Pipeline · fixed data (no size/generator): typed data (a runa Stage DAG)"),
("releaseview", "release process as THREE converging graphs — the same nordisk constellation dependency DAG in three states (LOCAL manifest · SPARRING Gate-B holger rehearsal · CRATES_IO published) that a healthy release drives to CONVERGE; a 3-plane depth render (the hero — planes snap together as crates converge, a diverged crate splits with a red/amber beam) + a 2D tri-badge fallback (L/S/C version chips per node); engine-agnostic (a release engine feeds the neutral ReleaseConvergence model in, one-way like the runa adapter); seeded DIVERGED (3 red needs-release + 1 amber bump-drift) with a scripted converge cascade — facett-releaseview::ReleaseView · fixed data (no size/generator): typed data (three release states of one DAG)"),
("timeline", "swimlane Gantt — wall-clock time axis + stacked lanes of segmented phase bars (the Spark jobs event timeline + per-stage task timeline): executor lifecycle, job/stage bars, and a stage's tasks split into scheduler-delay/deser/shuffle/compute phases, click-select + scroll-zoom — facett-timeline::TimelineView"),
("plandag", "metric-bearing operator DAG — the Spark SQL plan / Databricks Query Profile graph: per-node metric bodies (time/mem/rows/spill), a Time/Memory/Rows cost-heat switch, collapsible WholeStageCodegen groups, and an AQE initial-vs-final toggle (SMJ→BHJ), producer-left shared layout, pan/zoom/fit and Falsett rendering — facett-plandag::PlanDagView · fixed data (no size/generator): typed data (a Spark plan's operators and metrics)"),
("grid", "flagship Arrow grid (fleet) — facett-grid"),
("table", "scrollable Arrow table (fleet) — facett-table + facett-arrow"),
("card", "vessel detail card — facett-card"),
("form", "vessel edit form — facett-form"),
("pop", "ingest progress rows — facett-pop"),
("plot", "throughput line/bars — facett-plot"),
("boxplot", "score distribution box plot — facett-plotters"),
("violin", "score density violin — facett-plotters"),
("heatmap", "region × status heatmap — facett-plotters"),
("treemap", "tonnage treemap — facett-plotters"),
("candles", "alert-price candlesticks — facett-plotters"),
("map", "vessel positions map — facett-map (shared by facett-osm)"),
("syschart", "system-map (peers + badges, click-select) — facett-syschart · fixed data (no size/generator): typed data (peers and badges)"),
("geomap", "scrollable/zoomable geo map (points + hotspots) — facett-geomap"),
("OSM 2D", "state-of-the-art slippy 2D OSM viewer (real GeoParquet, wgpu+CPU, zoom-to-cursor, LOD, inertial pan) — facett-geomap::OsmView"),
("OSM 3D", "3D OSM viewer (real ways extruded to a lit, orbiting city) — facett-map3d::Map3D"),
("icebergview", "Iceberg snapshot time-travel browser — facett-icebergview"),
("docview", "vector-SVG documentation viewer (Typst manual, TOC + zoom, pure-Rust resvg) — facett-docview"),
("log", "timestamped event-log / console pane (ring buffer, kind-coloured rows, error tally) — facett-log::LogView, adopted from nornir's viz action-log overlay"),
("stat", "inline-bar stat grid with themed sparklines + status-verdict chips — facett-stat::StatGridView, adopted from nornir's viz facett_ui + mcp_tab"),
("warehouse", "generic warehouse browser — left table picker + grid OR auto numeric bar-chart — facett-table::WarehouseTableView, adopted from nornir's viz warehouse_tab"),
("tabbar", "themed 2-row nav toolbar (tab headers + workspace/palette/live controls, active tab accented) — facett-tabbar::TabBarView, adopted from nornir's viz facett_ui top-bar"),
("decograph", "pan/zoom DAG with a caller-supplied decorations overlay (status rings + corner badges + emphasis cut-edges) + downstream-highlight on select — facett-graphview::DecoratedGraphView, adopted from nornir's viz graph_render · SIZE + GENERATOR: demo data (default) · population · community · SBM · random · scale-free · grid · tree · crates, 100 → 100k, seeded, generated once per change (facett-graphgen::GenTab over facett-graphview::generate)"),
("falkor", "FalkorDB graph-DB browser — a Cypher query → force-laid-out GraphModel painted through DecoratedGraphView (pan/zoom + click-select), driven by a STUBBED in-memory CypherRunner (no live redis) — facett-graphview::FalkorBrowser + DecoratedGraphView · SIZE + GENERATOR: demo data (default: the canned constellation) · population · community · SBM · random · scale-free · grid · tree · crates, 100 → 100k, seeded: the generated graph is a Cypher-SHAPED answer (`(:label {caption})` nodes, `[:REL]` relations) on the in-memory runner, laid out by the browser's own query path (facett-demo::graphgen::to_cypher). The layout is a non-blocking GPU job on the host's device (gpu_layout::LayoutJob: all-pairs below GRID_MIN_NODES 16 384, the grid kernel above; ≤ 1 000 nodes with the CPU lane's own params, so the look is the same); with no device the CPU all-pairs lane up to CPU_LAYOUT_MAX (1 000) and `no GPU` above (the seed spiral stays). Above 5 000 nodes the pane paints on the RESIDENT cloud (one upload per layout)"),
("matrix", "per-repo × per-aspect test-matrix grid (coloured cells, badges, per-repo health, not-run surface) — facett-matrix::MatrixView, adopted from nornir's viz test_matrix_grid"),
("jobs", "hierarchical streaming job tree — a parent populate job with foldable child sub-jobs (a clone per member + a build stage) streaming running→done, fold/unfold child roll-up — facett-jobview::JobList, adopted from nornir's viz jobs panel"),
("zone", "shared zone-populate PANE (a Tools-menu Facet every suite app drops in) — pick a zone (demo/test/prod, default demo), see the default infra it brings up (FalkorDB + Spark), Generate (wired to a caller closure/jera submit), live inline per-container status (start → health-check → fill → done, fail-loud) — facett-zoneui::ZonePane"),
("Jobs", "shared Jobs CENTRAL panel (a thin mount over facett-jobview every suite app drops in) — the jera (edda job-engine) ledger jobs WITH their per-job zone column (zones summary + id→zone), no fork of the job tree — facett-zoneui::JeraCentral"),
("graphpan", "bare pan/zoom graph canvas — a caller-laid-out GraphModel (+ optional Decorations overlay) painted through ONE egui pan/drag + scroll-zoom renderer (draw_graph), with a GraphView world→screen transform — facett-graphpan::GraphPanView, adopted from nornir's viz graph_render · SIZE + GENERATOR: demo data (default) · population · community · SBM · random · scale-free · grid · tree · crates, 100 → 100k, seeded, generated once per change (facett-graphgen::GenTab over facett-graphview::generate)"),
("security", "software-supply-chain security report — verdict (Pass/Warn/Block) + findings (id/severity/package/advisory/fix) with severity/EPSS/KEV columns + a CVE/advisory table; the UI twin of modgunn's scan output over plain serializable structs that mirror modgunn::core 1:1 — facett-security::SecurityView (reuses facett-stat's sparkline for the severity histogram)"),
("helix", "TIME-HELIX DAG — a plan/dependency DAG rendered as a glowing helix winding ALONG a release-time axis: the nordisk crates as coils (nornir/holger/facett/znippy, per-subsystem strands), soft rubberband deps, inter-helix system links, and an animated traverse sweep that blooms nodes + pulses a RUSTSEC CVE event as it passes — facett-helix::HelixView (model+layout are the serde-only seam hosts populate) · fixed data (no size/generator): typed data (crates on a release-time axis)"),
("dna", "DNA DOUBLE-HELIX — each record is a base-pair rung on a ladder spiralling around a central axis: axial position ← z (progression), rung radius ← x (magnitude), base-pair lean ← y (a signed value: strandA at R·(1+k·y), strandB at R·(1−k·y)), constant twist pitch, colour ← the base. Glowing backbone tubes + rungs, spins on its axis, drag to orbit — facett-graph3d::DnaHelix (seeded with a 44-base-pair genome) · fixed data (no size/generator): typed data (records as base pairs)"),
("timehelix", "TIME VORTEX (the Doctor-Who one) — a cylinder viewed DOWN its axis: you fly into a tunnel of layers (z = depth = version/release), each a circular disc placing points polar (angle=x, radius=y), receding to a bright vanishing point with perspective shrink + exponential depth fade (alpha=exp(-depth·k), colour→deep blue), a slow axial swirl, additive glow points + faint per-layer glow rings + inter-layer energy filaments — facett-graph3d::TimeHelix (seeded with 8 subsystems evolving across 14 releases) · fixed data (no size/generator): typed data (releases as layers)"),
("console", "themed terminal/shell — fixed monospace cell grid, block cursor, ANSI colour mapped to the palette, scrollback on the smooth-scroll engine — facett-console::Console (seeded with a fake build trail)"),
("Open File", "themed, keyboard-navigable in-app file dialog (open/save/pick-folder), listing INJECTED (deterministic + testable), composes as an overlay — facett-filedialog::FileDialog (seeded with a fake repo listing)"),
("video", "themed video player — KeyMap-driven transport (play/pause/scrub/volume), pluggable decode backend + a push-frames source, ROI selection — facett-video::VideoPlayer (seeded with a 48-frame hue test pattern, wasm-clean)"),
("Git", "git repository facet — browse branches, commit log, file tree, and syntax-coloured source, all clickable — facett-git::GitView (the `demo()` snapshot needs no repo on disk)"),
("warehousedeck", "multi-pane lakehouse dashboard — a FacetDeck WALL of panes, each binding a table to a view-kind (3D graph / grid / chart / blob); several Graph3D render simultaneously — facett-warehousedeck::WarehouseDeck (seeded with a graph + chart + grid + blob pane)"),
("codevault", "Wreck-It-Ralph CODE VAULT — the nordisk warehouse as labeled, glowing-transparent 3-D boxes wired by dependency connectors in a dark vault, with a live red-flickering GLITCH on a reported-problem crate (Vanellope) and amber-glowing freshly-changed units; recolour-by-repo — facett-graph3d::CodeVault · fixed data (no size/generator): typed data (repos, crates, glitch state)"),
("System Map", "🫧 SYSTEM MAP (real liquid) — a clean little map of the nordisk constellation (holger → nornir → dwarves → korp, plus facett · skade · knut) as labelled GLASS TANKS joined by translucent, refractive GLASS PIPES carrying REAL LIQUID (fluid = data/activity between components): gravity-driven liquid with volume that POURS DOWNHILL when you tip the map (vertical drag), pools in the glass tanks, and cannot leave the bore. A glass-pipe skin over facett-graph3d::SystemMap (Graph3D camera/projection reused) driven by facett-cfd's PipeFlow (the opt-in `with_liquid`, now the SHIPPED tab) — zero-copy (double-buffer pointer swap), zero-alloc (preallocated sim, test-proven no-alloc step), zero-wait (serial below the measured gatling crossover) — facett-demo::demo_systemmap_liquid · fixed data (no size/generator): typed data (the nordisk constellation's tanks and pipes)"),
("scene", "DECLARATIVE UI-SCENE shell — facett-demo's own shell described as a facett_app::scene::Scene (a menubar + a tree of panes + routes) and rendered through facett-app's OWN Router/TabView/menu primitives; RON- or builder-authored (byte-stable). Interactions: DRAGGABLE/resizable splits (live ratios), fold/unfold, and responsive desktop↔phone (the master/detail Split collapses to Tabs and the activity rail becomes a drawer under 720pt). Its foldable canvas pane hosts the live 🫧 fluid System Map. The reference consumer of the layer — facett-app::scene + facett-demo::scene_demo"),
// ── The 2026-08-20 roster completion: the workspace components the gallery
// never wired. The old roster gate compared against the umbrella `facett`,
// which none of these are in — tests/roster_gate.rs now walks the WORKSPACE
// members instead, so a crate nobody wires fails that gate BY NAME. ─────────
("cicd", "CI/CD FORGE panel — the pipeline roster (trigger · last status · run count) → per-pipeline run history → the resolved run's per-stage/step breakdown with a live log tail; seeded with three pipelines (a green nornir-ci, a live-running pages build, a red skidbladnir-release) — facett-cicd::CicdPanel (the demo() forge, no host attached)"),
("brokkrview", "brokkr RUN viewer — roster ⇄ tree ⇄ DAG views over a forge pipeline's runs (per-step status + disposition: ran / cache-hit / unchanged) with a navigable Falsett DAG and log tail; the READ side beside the `brokkr` BUILDER tab; seeded with a green build → test → ship nornir-ci run — facett-brokkrview::BrokkrView"),
("lineage", "data-LINEAGE browser — skade lineage_events decoded into a navigable dataset DAG (the GRAPH NAVIGATOR under the hood: pan/zoom + Appearance + stats): a Spark app ingesting into main.raw, transform hops fanning out to main.clean / main.metrics, a promotion to main.gold and an atomic release, with the operation histogram + systems/actors roll-up in state_json — facett-lineage::LineageView (the demo() lineage) · SIZE + GENERATOR: demo data (default) · population · community · SBM · random · scale-free · grid · tree · crates, 100 → 100k, seeded, generated once per change (facett-graphgen::GenTab over facett-graphview::generate)"),
("Fleet", "FLEET gate board — members × declared test surfaces as coloured verdict cells (green / red / NOT DRIVEN is its own colour, never vacuously fine) with per-member health rows and the RED `Gap == ∅` banner when a declared surface has no coverage; seeded with a 3-member board where nornir fails chat@thin and korp is a dead member — facett-fleet-view::FleetView (the local() board)"),
("holger — registry & artifact browser", "the holger REGISTRY & ARTIFACT browser — a repository roster (rust · python · maven · npm · docker · go · nuget · helm) with clickable repo selectors driving the artifact listing (name · version · size · type); seeded with the built-in no-backend fixture (a crates-mirror listing), a host swaps in live data via set_model — facett-holger::RegistryBrowser"),
("Ingest", "korp INGEST pane — the collector roster (OpenSky flights · AIS vessels · ENTSO-E energy · Trafikverket trains) with click-select and a ▶ Run that raises a jera-job effect for the host to act on; the one korp caseworker pane with a self-contained demo() roster (the other three need live host rows — see the crate-doc exclusion list) — facett-korp::IngestPane"),
("Sign in", "device-code SIGN-IN pane — the OAuth device-grant flow as a pane: Sign in → user code + verification URL → auto-poll → the signed-in subject (credential NAMES only — the seam exposes no token value, by construction) → sign out; driven by the network-free StubLogin as dev@nordisk.dev — facett-auth-ui::LoginPane"),
("Binary analysis", "BINARY-analysis pane — checksec hardening badges (RELRO · PIE · NX · canary · stripped · fortify), a size-history sparkline, a per-crate bloat TREEMAP and a snapshot diff (added/removed/grown/shrunk symbols + the ABI verdict); the UI twin of nornir's binary_facts / symbol_sizes over the StubBinaryData seam — facett-binview::BinaryPane"),
("World coderoom", "the WORLD CODEROOM — every crate@version as a node with semver-compat edges, click a crate → its WHAT-BREAKS impact set highlights (red = callers referencing a dropped symbol, green = still compatible; the impact set is the seam's authority, not every dependent) + a what-changed snapshot diff, painted through the REUSED DecoratedGraphView — facett-coderoom-view::CoderoomPane (StubCoderoomData seam) · SIZE + GENERATOR: demo data (default: the stub seam) · population · community · SBM · random · scale-free · grid · tree · crates, 100 → 100k, seeded: the generated graph goes THROUGH THE DATA SEAM (facett-demo::gen_build::GenCoderoomData — crate@version nodes, semver compat edges, one in six breaking, what-breaks answered by the seam). Generated edges point newer → older (Cargo forbids dependency cycles), so every source is a DAG (100k SBM: 96 layers). The seam's layered layout is O(V + E) (iterative longest-path ranks on dense ids; ~0.5 s for 100k crates / 400k edges natively), laid out once per refresh — a what-breaks click re-colours, it does not re-lay — and above 5 000 crates the pane paints on the RESIDENT GPU cloud (one upload per scene; there edges take their consumer's colour, the red/green verdict is in the rings, the badges and the summary line)"),
("Claude", "CLAUDE conversation pane — an Elm chat surface (prompt → Send → turns, no double-send) over a pluggable ClaudeBackend transport seam; driven by the offline EchoBackend and pre-seeded with one real round-trip turn at construction so the surface opens POPULATED (no network, no secrets) — facett-claude::ClaudePane"),
("About", "the shared ABOUT-CARD widget — the ONE robot-testable identity surface every suite app mounts (wordmark · version · tagline · engines flavour line · framework bullets · the MEASURED GPU adapter/lane rows via the shared gpu_rows writer): rendered inline over a seeded nornir Identity, with the REAL shared modal (draw_about_window) on demand — facett-about, wrapped by facett-demo::about_tab (it is a widget, not a Facet)"),
// ── NATIVE-ONLY surfaces ───────────────────────────────────────────────────
// Nine tabs are native-only: the seven heavyweight korp caseworker flow tabs
// below, plus `Deploy key` (nornir-git pulls gix + tokio + russh — none build
// for wasm32) and `Build Thing` (nornir-build-thing) further down. The
// lightweight wasm web demo does NOT ship them (the deck `vec!` gates the
// matching facets behind `cfg(not(target_arch = "wasm32"))`). Gate the LEGEND
// entries the SAME way so the deck↔LEGEND set stays in parity on BOTH targets
// (native lists all nine; wasm lists none of them). See `NATIVE_ONLY_TABS` in
// `tests/pop_ran_list.rs` for the single documented divergence allowlist.
#[cfg(not(target_arch = "wasm32"))]
("Bygglov", "Gantt bar chart — a task timeline with progress bars, dependency arrows, a milestone diamond, a critical-path highlight and a `today` marker; click a bar to select — facett-gantt::Gantt (seeded with a bygglov case: utredning → kommunicering → beslut → launch)"),
#[cfg(not(target_arch = "wasm32"))]
("Support", "swimlane KANBAN board — cards in columns (Triage / In Progress / Done) grouped into per-team swimlanes, drag a card between columns, WIP-limit breach highlight — facett-kanban::Board (seeded with a support queue across the hugin/munin lanes)"),
#[cfg(not(target_arch = "wasm32"))]
("Frister", "month CALENDAR — a day grid with per-day event markers (deadline / frist / absence), a `today` ring and click-to-select; the host supplies the month + events (never reads the clock, so it's deterministic/testable) — facett-calendar::Calendar (seeded with June 2026 deadlines)"),
#[cfg(not(target_arch = "wasm32"))]
("Notifications", "toast / NOTIFICATION queue — levelled (info/success/warn/error), auto-expiring or sticky, case-tagged messages that stack and time out — facett-toast::Toasts (seeded with a mix of saved / queue / failure / new-cases toasts)"),
#[cfg(not(target_arch = "wasm32"))]
("Anteckning", "MARKDOWN note pane — an edit⇄preview note with `@mention` + `#tag` extraction and a task-list; a lightweight case-notes surface — facett-markdown::Markdown (seeded with a bygglov checklist note)"),
#[cfg(not(target_arch = "wasm32"))]
("Search", "query SEARCH bar — a live-parsed filter grammar (`attr:value`, `-negation`, `sort:field`, shortcut tokens) that degrades a half-typed query to full-text terms and yields a typed Query on Enter — facett-search::SearchBar (seeded with a `status:open assignee:me -tag:spam … sort:due` query)"),
#[cfg(not(target_arch = "wasm32"))]
("Attachments", "FILE / attachment list — a table of files (name · human size · MIME) with pending add/remove staging and a total-size roll-up; the in-app attachment surface (distinct from the `Open File` dialog) — facett-file::Files (seeded with a PDF + PNG + CSV)"),
#[cfg(not(target_arch = "wasm32"))]
("Deploy key", "the 🔑 SSH DEPLOY-KEY surface — three panes: the key state (FIVE honest states — never-probed is its own thing, NOT green), the per-remote fetch fleet with the actual error verbatim, and the resolution ladder saying WHICH rung answered; seeded FETCHING with a mixed authenticated/rejected/not-probed fleet — facett-deploykey::DeployKeyDeck (demo() fixture; native-only: nornir-git pulls gix + tokio + russh)"),
#[cfg(not(target_arch = "wasm32"))]
("Build Thing", "the ⚒ BUILD THING deck — the build-variant MENU (role groups × variants with linkage + runtime notes, the declared normal case pre-selected), the feature-graph pane with a build/files/excluded filter, and the build-result pane; seeded with the holger-shaped menu (server embedded/mini/full + client ui/agent) — facett-buildthing::BuildThingDeck (demo() fixture; native-only: nornir-build-thing)"),
("Errors", "facett UI ERROR-CODE showcase — every facet part carries a stable, unique facet-<component>-<n> code (tests + consumers react to the CODE, not a matched string); triggers example errors so the CLEAR-PINK codes render on-screen (debug), shows the version+git-hash build stamp, a source link per facet-component, the consumer mount-prefix demo (korp/cases:facet-map-2 vs korp/analysis:facet-map-2), and the error-code MATRIX slide over facett_core::errcode::REGISTRY — facett-demo::errcode_tab over facett-core::errcode"),
("Manual", "the real in-app facett MANUAL — compiled-in Markdown + typeset SVG book (TOC + search), no disk access — facett-manual::ManualPane::embedded"),
("GFX v2", "THE RENDERER TRACK, on show and self-evidencing — one scene of real WGS-84 Swedish geography (knut_popsim::geo: 14 population centres + 600 deterministic addresses) rendered TWICE: the left pane is the right pane's 3D view collapsed by facett_core::render::View::constrain_2d, so 2D is literally a constrained 3D and the tab reports the measured per-place disagreement (item 6). Place names are laid out into epaint's own font atlas and decluttered ON THE DEVICE — three compute passes, a 128x96 grid claimed by atomicMax priority, name-repeat slots, winners appended by atomicAdd onto a DrawIndirectArgs the draw reads back off the GPU (item 4); flip the toggle off and the naive CPU pile-up shows what the pass prevents. Clicking either pane runs an R32Uint id pass and reads one texel back, resolving a real PickId to a real place name (item 8, whose first consumer this is). Every GPU claim is device-side: adapter name, indirect-buffer readback, id-pass count, and the DECLINED count so a dead lane cannot look like a working one — facett-demo::gfx_v2"),
("menu", "command palette — facett-menu"),
];
/// The SOURCE LINK(s) for one LEGEND/deck tab — the "source per demo"
/// surface (owner, 2026-08-20: the source rides ON each demo, not as a list in a
/// startup popup). Derived from the LEGEND entry's OWN description text: every row
/// already credits its crates as `facett-xxx` / `facett_xxx::Type` tokens, so the
/// roster stays the ONE writer and there is no second key→crate table to drift
/// (LAW: reuse, do not twin). Returns `(crate-dir, source-url)` pairs, deduped in
/// credit order; an unknown key returns empty.
pub fn source_links(key: &str) -> Vec<(String, String)> {
LEGEND.iter().find(|(k, _)| *k == key).map(|(_, desc)| source_links_in(desc)).unwrap_or_default()
}
/// Extract the crate credits out of one LEGEND description. A credit token is either
/// the dashed crate name itself (`facett-graph3d`) or a module path
/// (`facett_graphview::MetroView` — the crate before the `::`, underscores
/// normalising to the on-disk dashed dir name the tree uses). A bare
/// underscore word WITHOUT `::` is not a credit — descriptions cite foreign module
/// names that way ("adopted from nornir's viz facett_ui"), and `facett-ui` is not a
/// crate. The bare umbrella word `facett` is not a credit either.
fn source_links_in(desc: &str) -> Vec<(String, String)> {
let mut out: Vec<(String, String)> = Vec::new();
for raw in desc.split(|c: char| !(c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == ':')) {
let tok = raw.split("::").next().unwrap_or("");
let credit = tok.starts_with("facett-")
|| (tok.starts_with("facett_") && raw.contains("::"));
if !credit {
continue;
}
let dir = tok.replace('_', "-").trim_end_matches('-').to_string();
if !out.iter().any(|(d, _)| *d == dir) {
let url = format!("https://gunnar.rs/nordisk/facett/blob/main/{dir}");
out.push((dir, url));
}
}
out
}
/// The **seeded Claude pane** for the `Claude` tab — pre-loaded with one REAL
/// round-trip turn driven through the pane's own Elm path (`PromptChanged` → `Send`
/// → `deliver` over the offline [`EchoBackend`] → `ReceivedOk`), so the surface
/// opens POPULATED (a user turn + an assistant echo) with no network and no
/// secrets. `pub` so the mega-matrix's `Claude` cell builds the identical fixture
/// instead of twinning the seeding (LAW: reuse, do not twin).
pub fn seeded_claude() -> ClaudePane {
use facett_core::Elm as _;
let mut pane = ClaudePane::new("Claude");
let _ = pane.update(ClaudeMsg::PromptChanged(
"Vad är facett? (offline echo-demo — ingen nätverkstrafik)".to_string(),
));
// `Send` moves the prompt into a user turn and raises the Send effect; deliver
// it through the echo transport and fold the reply back in — the exact loop a
// live host runs, transport swapped.
for effect in pane.update(ClaudeMsg::Send) {
let reply = ClaudePane::deliver(&effect, &EchoBackend);
let _ = pane.update(reply);
}
debug_assert!(pane.last_assistant().is_some(), "the echo seeding produced an assistant turn");
pane
}
/// The **nordisk-constellation TIME-HELIX** — the showcase scene for the `helix` tab.
/// Each core crate is a coil winding through release time (its subsystems are the
/// angular strands); intra-crate deps draw as soft rubberband splines, cross-crate
/// dependencies as inter-helix system links, one release per crate is Active (live),
/// and a RUSTSEC advisory rides znippy's codec strand as a CVE event the traverse
/// pulses as it sweeps past. Pure data — no host, no network (the model+layout seam).
fn nordisk_helix() -> HelixView {
// ── nornir — the code-knowledge server, across four releases ────────────────
let mut nornir = PlanDag::new();
let n_core = nornir.add_idea("core", "core", 0.0, Kind::Crate);
let n_wh = nornir.add_idea("warehouse", "warehouse", 1.0, Kind::Task);
let n_viz = nornir.add_idea("viz", "viz", 2.0, Kind::Task);
let n_embed = nornir.add_idea("embed", "warehouse", 2.6, Kind::Task);
let n_rel = nornir.add_idea("v0.5", "core", 3.4, Kind::Test);
nornir.add_dep(n_wh, n_core);
nornir.add_dep(n_viz, n_core);
nornir.add_dep(n_embed, n_wh);
nornir.add_dep_at(n_rel, n_core, 2); // a transitive (level-2) dep
nornir.add_dep(n_rel, n_viz);
nornir.add_dep(n_rel, n_embed);
nornir.set_status(n_rel, HStatus::Active);
// ── holger — the airgap artifact server ─────────────────────────────────────
let mut holger = PlanDag::new();
let h_srv = holger.add_idea("server", "server", 0.3, Kind::Crate);
let h_repo = holger.add_idea("repo", "repo", 1.2, Kind::Task);
let h_agent = holger.add_idea("agent", "agent", 2.1, Kind::Task);
let h_rel = holger.add_idea("v0.1.2", "server", 3.2, Kind::Test);
holger.add_dep(h_repo, h_srv);
holger.add_dep(h_agent, h_srv);
holger.add_dep(h_rel, h_repo);
holger.add_dep(h_rel, h_agent);
holger.set_status(h_rel, HStatus::Active);
// ── facett — the egui component constellation (this very view is a node) ─────
let mut facett = PlanDag::new();
let f_core = facett.add_idea("core", "core", 0.6, Kind::Crate);
let f_render = facett.add_idea("render", "render", 1.6, Kind::Task);
let f_deck = facett.add_idea("deck", "deck", 2.4, Kind::Task);
let f_helix = facett.add_idea("helix", "render", 3.0, Kind::Idea);
facett.add_dep(f_render, f_core);
facett.add_dep(f_deck, f_core);
facett.add_dep(f_helix, f_render);
facett.set_status(f_helix, HStatus::Active);
// ── znippy — the compression engine (carries the CVE event) ─────────────────
let mut znippy = PlanDag::new();
let z_comp = znippy.add_idea("compress", "compress", 0.8, Kind::Crate);
let z_codec = znippy.add_idea("codec", "codec", 1.8, Kind::Task);
let z_rel = znippy.add_idea("v0.9.7", "compress", 3.1, Kind::Test);
znippy.add_dep(z_codec, z_comp);
znippy.add_dep(z_rel, z_codec);
znippy.add_event(z_codec, 2.2, EventKind::Cve, "RUSTSEC-2025-0007");
// ── the constellation: four coils sharing the release-time (X) axis, each
// offset in Z so they read as distinct strands at different depths ───────────
let mut scene = Scene::new();
scene.tension = 0.6;
let cn = scene.add_coil(Coil::new(nornir).at([0.0, 0.0, 0.0]));
let ch = scene.add_coil(Coil::new(holger).at([0.0, 0.0, 3.0]));
let cf = scene.add_coil(Coil::new(facett).at([0.0, 0.0, 6.0]));
let cz = scene.add_coil(Coil::new(znippy).at([0.0, 0.0, 9.0]));
// Cross-crate (system-level) dependencies — the inter-helix links.
scene.add_link(HelixRef { helix: cn, node: n_wh }, HelixRef { helix: ch, node: h_srv }, 1); // nornir warehouse ← holger artifacts
scene.add_link(HelixRef { helix: ch, node: h_repo }, HelixRef { helix: cz, node: z_comp }, 1); // holger repo ← znippy archives
scene.add_link(HelixRef { helix: cn, node: n_viz }, HelixRef { helix: cf, node: f_deck }, 1); // nornir viz ← facett components
HelixView::new("helix", scene)
}
/// The **DNA genome** showcase for the `dna` tab (helix-facets Variant A). A ladder
/// of 44 base pairs: each rung is a base pair (A·T / T·A / G·C / C·G) coloured by
/// its base (the classic genome-browser palette), with the design-spec role map —
/// `z = axial position` (up the ladder), `x = rung radius` (a gentle magnitude
/// breathing), `y = base-pair lean` (a signed GC-skew-like walk). Pure data, no host.
fn dna_genome() -> DnaHelix {
// Classic base colours: A green · T red · G amber · C cyan.
const BASES: [(char, char, egui::Color32); 4] = [
('A', 'T', egui::Color32::from_rgb(60, 200, 90)),
('T', 'A', egui::Color32::from_rgb(230, 70, 70)),
('G', 'C', egui::Color32::from_rgb(240, 180, 40)),
('C', 'G', egui::Color32::from_rgb(40, 190, 230)),
];
// A deterministic "sequence" (a small LCG so it reads varied but is reproducible).
let mut seed: u32 = 0x51ed_5eed;
let mut next = || {
seed = seed.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
(seed >> 8) as f32 / (1u32 << 24) as f32 // in [0, 1)
};
let n = 44usize;
let mut skew = 0.0f32; // a running GC-skew so the lean walks smoothly (signed y)
let rungs: Vec<DnaRung> = (0..n)
.map(|i| {
let base = BASES[(next() * 4.0) as usize % 4];
// Radius breathes gently along the ladder (a magnitude, the x role).
let radius = 0.55 + 0.45 * (i as f32 / n as f32 * std::f32::consts::TAU).sin().abs();
// Lean: a bounded signed random walk (the y role — which way the pair leans).
skew = (skew + (next() - 0.5) * 0.5).clamp(-1.0, 1.0);
DnaRung::new(format!("{}·{}", base.0, base.1), base.2, i as f32, radius, skew)
})
.collect();
DnaHelix::new("dna", rungs).with_twist(0.55).with_k(0.6)
}
/// The **time vortex** showcase for the `timehelix` tab (helix-facets Variant B —
/// the Doctor-Who one). The 8 subsystems evolve across 14 releases: each release is
/// a layer/disc receding down the tunnel, every subsystem a point placed polar
/// (`angle = its slot`, `radius = its load that release`), coloured by subsystem, and
/// inter-layer **energy filaments** wire each subsystem to its next-release self
/// (version evolution). Pure data — no host, no network.
fn time_vortex() -> TimeHelix {
let subs = &data::SUBSYSTEMS;
let n_sub = subs.len();
let layers = 14usize;
let mut pts: Vec<TimePoint> = Vec::with_capacity(layers * n_sub);
let mut edges: Vec<(usize, usize)> = Vec::new();
for l in 0..layers {
for (s, (label, _sub)) in subs.iter().enumerate() {
let idx = pts.len();
// Angle = the subsystem's evenly-spaced slot around the disc (the x role).
let angle = s as f32 / n_sub as f32;
// Radius = a deterministic per-(subsystem, release) "load" in [0.2, 1.0]
// that drifts release-to-release (the y role — where in the disc it sits).
let t = l as f32 / (layers - 1) as f32;
let load = 0.35 + 0.5 * (0.5 + 0.5 * ((s as f32 * 1.7 + t * 6.0).sin()));
pts.push(TimePoint::new(format!("{label}@r{l}"), hash_color(label), angle, load.clamp(0.2, 1.0), l));
// Energy filament: connect this subsystem to its previous-release self.
if l > 0 {
edges.push((idx - n_sub, idx));
}
}
}
TimeHelix::new("timehelix", pts).with_edges(edges).with_depth_fade(1.6)
}
/// Every command id [`DemoState::run_command`] knows how to route — the set the
/// live control channel's `command` field can fire (a `&'static str` each, so the
/// channel can hand `run_command` a borrowed-static id resolved from the wire
/// string). Kept in lockstep with `run_command`'s match arms + `demo_commands`.
pub const KNOWN_COMMANDS: &[&str] = &[
"palette.cycle",
"fx.glow",
"raven.send",
"about.toggle",
"vessel.new",
"vessel.alert",
"export.csv",
];
/// The whole demo's state, built from the one fake dataset. Holds the populated
/// [`FacetDeck`] (every `Facet` component), the command **menu** (a non-`Facet`
/// overlay), and the fake [`Vessel`] fleet it was all built from.
pub struct DemoState {
/// The deck of every `Facet` component, tabbed, with deck-fx enabled.
pub deck: FacetDeck,
/// The command palette (facett-menu) — driven as an overlay, not a tab.
pub menu: CommandPalette,
/// The menu's command set.
pub commands: Vec<Command>,
/// The fake fleet everything was generated from (kept for the legend/about).
pub fleet: Vec<Vessel>,
/// The id the last menu command invoked (for the status line / tests).
pub last_command: Option<String>,
/// Whether the about/legend panel is open.
pub show_about: bool,
/// The active **look & feel** preset, an index into [`LookTheme::PRESETS`]
/// (Windows L/D, macOS L/D, Device). This is the NEW unified `look::Theme`
/// the demo *drives*: `draw_app` calls [`LookTheme::apply`] every frame, which
/// installs the full `egui::Style` AND publishes the derived legacy palette so
/// every component re-themes coherently (COH-1) — see `.nornir/look-and-feel.md`.
pub look_preset: usize,
/// The **OSM region picker** (🌍 Region): clips a region out of the cached planet
/// (osm-katana) and reloads the OSM 2D/3D tabs. See `region_picker`.
pub region_picker: region_picker::RegionPicker,
/// The **live, ticking, in-memory SIMULATION** — the demo's heartbeat. Ticks
/// every ~45 frames; on each tick its growing self-contained population is pushed
/// into the warehouse / icebergview / Sverige facets so the gallery is ALIVE. See
/// [`sim::Sim`] + [`Self::draw_app`] (where `maybe_tick` is pumped per frame).
pub sim: sim::Sim,
/// The **frame-time clock** — an EWMA of the per-frame wall time (from egui's
/// `stable_dt`), updated at the top of [`draw_app`](Self::draw_app). Surfaced in
/// the toolbar (`x.x ms · yyy fps`) and in `state_json.perf` so the demo's
/// smoothness is a *measured number*, not a vibe — the win from the lag fix
/// (the removed per-frame full-deck state dump) is read straight off it.
pub perf: FrameClock,
/// **Responsive control-strip fold (#30).** On a PHONE-class (narrow) viewport
/// the demo's control strip (look/native/palette pickers + the fx/region buttons)
/// would wrap into many rows and push the demo CANVAS below the phone screen fold.
/// When the viewport is phone-class the strip FOLDS behind a `⚙` toggle (a drawer);
/// `controls_open` is that drawer's state (default **closed** ⇒ canvas-first). On a
/// tablet/desktop viewport the strip is always shown and this flag is ignored.
pub controls_open: bool,
/// The last frame's responsive [`ShellReport`] — the device class the viewport
/// fell in, whether the control strip is folded, and how much of the viewport
/// height the canvas got. Exposed in `state_json.shell` so a headless test proves
/// the phone fold (canvas gets the screen) as DATA, not pixels.
pub shell: ShellReport,
/// The **auto-demo / attract mode** (#82) — the kiosk loop that, after
/// [`attract::IDLE_ARM_SECS`] of no interaction, auto-advances through the showcase
/// tour and loops forever (cancelled by any real interaction, re-armed after idle).
/// Driven per frame from `ctx.input` (frame `dt` + interaction) in
/// [`draw_app`](Self::draw_app) via [`attract_step`](Self::attract_step); its state
/// rides in `state_json.attract`.
pub attract: attract::AttractMode,
/// Whether the auto-demo clock is pumped from [`draw_app`](Self::draw_app) each
/// frame. The shared attract-mode LOGIC always lives on the one `draw_app` path
/// (native + wasm alike); this flag only decides whether the live eframe binary
/// drives it. BOTH the native and the wasm entry (`DemoApp::new`) turn it **on** —
/// it is the SAME program, so the kiosk loop runs on desktop and web alike. The
/// headless test harnesses build a `DemoState` directly and leave it **off** so the
/// auto-advance never perturbs a deterministic robot drive; the attract robot test
/// flips it on to prove the exact shared path (and drives `attract_step` directly).
pub attract_enabled: bool,
}
/// The responsive SHELL layout facts the demo reports each frame (#30): which
/// [`Device`](facett_app::scene::Device) class the viewport width falls in, whether
/// the control strip is folded behind the `⚙` drawer (phone-class, drawer closed),
/// and the fraction of the viewport HEIGHT the demo canvas (the deck) actually gets.
/// A phone-class viewport folds the strip so the canvas is not pushed below the fold;
/// `state_json.shell` surfaces this so a headless test can assert the fold.
#[derive(Clone, Copy, Debug, Default)]
pub struct ShellReport {
/// The device class tag: `"phone"` / `"tablet"` / `"desktop"` (`""` before the
/// first frame).
pub device: &'static str,
/// Whether the control strip is currently folded away (phone-class + drawer shut).
pub controls_folded: bool,
/// The canvas top as a fraction of viewport height (0 = top). Small ⇒ the canvas
/// starts near the top (few control rows above it); large ⇒ pushed down.
pub canvas_top_frac: f32,
/// The canvas height as a fraction of viewport height. Large ⇒ the canvas OWNS the
/// screen (the phone-fold win); small ⇒ the controls ate the viewport.
pub canvas_height_frac: f32,
}
/// A tiny exponential-moving-average frame-time meter. Fed the per-frame delta
/// (seconds) from egui's `stable_dt`, it reports a smoothed millisecond-per-frame
/// (and its `fps` reciprocal) so a jittery raw dt reads as one stable number. Pure +
/// dep-light; the demo's own on-screen "how fast am I" readout (Map3D carries its own
/// for the 3D pane; this is the whole-app frame budget).
#[derive(Clone, Copy, Debug, Default)]
pub struct FrameClock {
/// Smoothed milliseconds per frame (0 until the first tick seeds it).
pub ms: f32,
seeded: bool,
}
impl FrameClock {
/// Fold one frame's wall-time delta (seconds) into the smoothed estimate. The
/// delta is clamped to `[0, 1] s` so a paused/backgrounded window (a multi-second
/// stall) can't poison the average.
pub fn tick(&mut self, dt_s: f32) {
let ms = (dt_s * 1000.0).clamp(0.0, 1000.0);
if self.seeded {
// ~0.1 gain: responsive but steady (a few frames to settle).
self.ms += (ms - self.ms) * 0.1;
} else {
self.ms = ms;
self.seeded = true;
}
}
/// Smoothed frames per second (`0` before the first tick).
#[must_use]
pub fn fps(&self) -> f32 {
if self.ms > 0.0 { 1000.0 / self.ms } else { 0.0 }
}
}
impl Default for DemoState {
fn default() -> Self {
Self::new()
}
}
impl DemoState {
/// Build the entire demo from the fake dataset — no window, no I/O.
pub fn new() -> Self {
let fleet = data::fleet();
let batch = data::fleet_batch(&fleet);
// ── graph: the knut 11-million-person Swedish graph DB SCHEMA (the data
// model) — Person hub wired to Address/TaxRecord·60M/Debt/Loan/KfmCase/
// PoliceRecord/FkBenefit/AfRecord/Vehicle/Property/Company, colour-coded by
// category. Reuses the knut-popsim model (mirrored in `popgraph`). ─────────
let graph = GraphView::new(popgraph::schema_scene())
.with_title("graph")
.empty_hint("no services");
// The 2-D graph on the shared instanced GPU cloud (`graph-wgpu`); without an
// installed renderer it self-disables and the egui painter draws.
#[cfg(feature = "graph-wgpu")]
let graph = graph.with_gpu(true);
// Size + generator: a generated scene up to 100k (above `facett_graph::SCALE_ABOVE`
// the scale path: resident cloud, budgeted on-screen a11y — see the LEGEND row).
let graph = graphgen::GenTab::new("graph", graph, graphgen::GenControl::new(100_000), gen_build::graph);
// ── graphview: the CONSOLIDATED graph render ENGINE (facett-graphview),
// drawing a synthetic layered service-mesh through the shared
// `facett_core::render` L0 Canvas (`render_to_rgba_l0`) — the SAME kernel
// the map skins draw through (render-engine.md §"ONE render kernel, TWO
// domain skins"). Where `graph`/`depgraph` are facett-graph skins, this tab
// exercises the kernel itself: nodes lower to SDF marker quads, edges to
// straight capsule lines, rasterized on the core CpuRenderer + uploaded as
// an egui texture. Navigable (drag-pan / scroll-zoom). See `graphview_tab`. ─
let graphview = graphview_tab::GraphviewTab::new(graphview_tab::TITLE);
let graphview = graphgen::GenTab::new(
graphview_tab::TITLE,
graphview,
graphgen::GenControl::new(100_000),
gen_build::graphview,
);
// ── V.G: THE GRAPH NAVIGATOR (facett-graphnav) over the demo's population
// graph — the full graph-exploration IDE (pan/zoom camera, Appearance
// ranking/partition, Arrow table view, GEXF/GraphML I/O, stats panel). ─────
let vg = vg_lab_tab::VgLabTab::new(vg_lab_tab::TITLE);
// Size + generator (2026-10-01): demo data by default, any generator up to 100k.
let vg = graphgen::GenTab::new(vg_lab_tab::TITLE, vg, graphgen::GenControl::new(100_000), gen_build::graphs);
// ── metro: the subway-line view of test-coverage CHAINS (the S5
// "test-matrix as a transit map" surface). Each line runs from a UI button
// (Start) through emitter/pass-through stops to the gRPC handler and the
// warehouse Terminus; a station is lit when its emit fired, and a line is
// green only when every gating stop is lit. `MetroView::local()` builds a
// synthetic two-line map (one green, one with an unlit emitter → red) so the
// tab renders real lit/unlit stations with no fixture on disk. A canonical
// `Facet` (state_json reports lines + per-station {kind, lit} + green verdict)
// — see `facett-graphview/src/metro.rs`. ────────────────────────────────────
// `local()` titles itself "Metro"; re-title to the lowercase deck/LEGEND key
// so the deck's `facets` map + the coverage gate address it as "metro".
let mut metro = MetroView::local();
metro.set_title("metro");
// ── Velo L1: the L1 vello beauty lane made VISIBLE — the SAME synthetic
// SDF scene rendered two ways, side by side: (a) L0 SDF only (the
// facett-core instanced base, zero vello), and (b) L0 + the L1 `vello`
// overlay (gradient sweep / antialiased Bézier curve / gaussian glow — the
// curves/gradients/effects the SDF base deliberately skips,
// render-engine.md §"L0 / L1"). The L1 lane lights up only with the
// `l1-vello` feature + a GPU; otherwise the right pane shows L0 alone and
// `state_json.l1_available = false` (an honest, observable degrade). The
// 2-category (CPU / CPU+GPU) auto-select decision rides in state_json. ────
let velo_l1 = velo_l1_tab::VeloL1Tab::new(velo_l1_tab::TITLE);
// ── Sverige · Population: THE showcase — the canonical knut-popsim Swedish
// 11-million-person population graph as THREE beautiful linked graphs side by
// side (full population LOD / debt→enforcement sub-graph / per-kommun
// aggregation), driven by a live query (scale 1M→100M, per-label + per-
// relation toggles, colour-by label/kommun/debt-heat). Rendered through the
// shared L0 SDF kernel + a glow halo lane (luminous orbs over a dark ground),
// wasm-clean. See `pop_tab` + `.nornir/population-graph.md`. ────────────────
let sverige = pop_tab::PopGraphTab::new(pop_tab::TITLE);
// ── graph3d: the knut-popsim POPULATION sampled into a 3D force cloud —
// people each wired to real entities (Address/TaxRecord/Debt/Vehicle/…) by
// the realistic per-label probabilities, coloured by label. The 11M-person
// social/financial web, sampled to a renderable size + spun in 3D. Driven by
// the SAME `knut-popsim` model the Sverige tab queries (`pop_model`). ───────
let pop_q = pop_model::PopQuery::default();
let pop_sample = pop_q.sample();
// ── THE CLOUD'S BULK: a generated dataset with real STRUCTURE ─────────
// The tab used to hold `graph3d_nodes_edges(.., 140)` — 140 nodes wired as
// depth-1 person→entity stars. Two things were wrong with that and they are
// the same thing: it was tiny, and it had no topology. `Layout3D::Force`
// seeds on a Fibonacci sphere SHELL and relaxes; what pulls a node into the
// interior is a path that must fold, a cycle that encloses, a hub that sits
// in the middle of what it holds. Stars have none, so nothing moved and the
// pane was a shell — "the sphere only has data around the sphere surface".
//
// `facett_graph3d::dataset` generates long chains braided by cross-rungs
// into cycles, with high-degree hubs: measured interior occupancy goes
// 0.000 → 0.25-0.65 (a uniformly filled ball scores 0.216), and the graph
// diameter goes ~3 → 100+. Deterministic (seeded SplitMix64, no wall clock)
// so the demo is reproducible.
//
// SIZE IS A KNOB: `$FACETT_GRAPH3D_NODES` overrides the default. Bigger is
// one env var away; the numbers behind the default are in
// `facett-graph3d/benches/heavy_graph3d.rs`.
let g3d_target = std::env::var("FACETT_GRAPH3D_NODES")
.ok()
.and_then(|v| v.trim().parse::<usize>().ok())
.unwrap_or_else(|| {
if in_test_binary() { GRAPH3D_TEST_NODES } else { GRAPH3D_DEFAULT_NODES }
});
let g3d = facett::graph3d::dataset::generate(&facett::graph3d::dataset::GraphSpec::sized(g3d_target));
// The real population slice still rides along, appended after the generated
// bulk (its indices are shifted), so the tab keeps its knut-popsim identity
// instead of becoming pure synthetic filler.
let (pop_nodes, pop_edges) = pop_model::graph3d_nodes_edges(&pop_q, &pop_sample, GRAPH3D_POP_SLICE);
let base = g3d.nodes.len();
let mut g3d_nodes = g3d.nodes;
let mut g3d_edges = g3d.edges;
g3d_nodes.extend(pop_nodes);
g3d_edges.extend(pop_edges.into_iter().map(|(a, b)| (a + base, b + base)));
// `with_controls(true)`: the inline "3D rotating" + "labels" checkboxes above
// the canvas. At GRAPH3D_DEFAULT_NODES (100 000) the spin IS the readout — a
// still frame of a force cloud is a blob — so the toggle that starts and stops
// it belongs on the pane, not only in `Msg`. Labels are already off at this
// size; the checkbox is how you ask for the few you want to read.
let graph3d =
Graph3D::new(g3d_nodes, g3d_edges, Layout3D::Force).with_title("graph3d").with_controls(true);
// Opt the cloud into the INSTANCED GPU lane when the demo is built with
// `--features gpu` (which also installs the renderer in `main`). Without the
// install the lane self-disables and the CPU painter draws, so this is safe
// in every build and leaves every headless snapshot golden byte-identical.
#[cfg(feature = "gpu")]
let graph3d = graph3d.with_gpu(true);
let graph3d = graphgen::GenTab::new("graph3d", graph3d, graphgen::GenControl::new(100_000), gen_build::graph3d);
// ── rabbit3d: the 3D znippy rabbit LOGO — the parametric rabbit
// silhouette extruded into a solid (front+back faces + side walls),
// lit + depth-sorted, slowly auto-rotating. This is facett-graph3d's 3D
// *test data* (the mascot). Renders via the CPU painter under the glow
// binary; the GPU depth-tested path lights up when a host enables the
// `graph3d-wgpu` feature + installs the renderer. ─────────────────────
let rabbit3d = RabbitLogo3D::new("rabbit3d");
// ── avocado3d: the reusable 3D-model viewer over a REAL glTF — the
// stripped CC0 Khronos "Avocado" sample, CPU-lit + slowly auto-rotating,
// its "Avocado" group tinted green→yellow→red by a status scalar (a
// "ripeness" readout). facett-model3d::Model3D; renders on the CPU painter
// under the glow binary (no wgpu render pass), so it lights up in the wasm
// demo too. `with_status_control` draws an inline status slider in the tab.
let avocado3d = facett_model3d::Model3D::avocado("avocado3d").with_status(1.0).with_status_control(true);
// ── nav: the app-framework backbone (Fas 1) — a NavModel drives a Sidebar
// + Router inside a NavShell (rail left, routed content right). The proof
// that facett can compose whole-app navigation, not just single views.
// facett-app::NavShell. ───────────────────────────────────────────────
let nav = {
use facett_app::{Dropdown, 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("data.iceberg", "Iceberg").route("iceberg"),
]),
NavItem::new("viz", "Visualise").icon("📊").children([
NavItem::new("viz.graph3d", "Graph 3D").route("graph3d"),
NavItem::new("viz.map", "Map").route("map"),
]),
NavItem::new("settings", "Settings").icon("⚙"),
NavItem::new("admin", "Admin (locked)").disabled(),
]);
let theme_pick = Dropdown::new("nav_theme")
.options([("dark", "Windows Dark"), ("light", "Windows Light"), ("macos", "macOS Dark"), ("vinter", "Skade Vinter")])
.with_selected("dark")
.placeholder("Theme");
NavShell::new("nav", model)
.with_body("home", "Welcome to the facett app shell — pick something on the left.")
.with_body("grid", "The Arrow grid would render here (facett-grid).")
.with_body("table", "The data table would render here (facett-table).")
.with_body("graph3d", "The 3D graph cloud would render here (facett-graph3d).")
.with_body("map", "The map would render here (facett-map).")
.with_body("settings", "App settings. The Theme picker below is a facett-app Dropdown (headless, FC-1 compliant).")
.with_route_dropdown("settings", "Theme", theme_pick)
};
// ── shell: the Fas 2 payoff — an AppShell that hosts REAL facett
// components per route (topbar + sidebar + routed pane + statusbar). Here
// the 3D-model viewer is mounted on two routes; the sidebar swaps them.
// facett-app::AppShell. ──────────────────────────────────────────────
let shell = {
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.avocado", "Avocado").route("avocado"),
NavItem::new("m.skull", "Skull").route("skull"),
NavItem::new("m.compare", "Compare").route("compare"),
]),
NavItem::new("about", "About").icon("ℹ"),
]);
// A SplitPane hosts two Facets side-by-side (the Fas 2 layout extra).
let compare = SplitPane::new(
"compare",
Box::new(facett_model3d::Model3D::avocado("avocado·L").with_status(1.0)),
Box::new(facett_model3d::Model3D::skull("skull·R").with_status(0.3)),
);
AppShell::new("shell", model)
.with_body("home", "An AppShell hosts REAL facett components per route — pick a 3D model on the left. This is Fas 2 of the framework införandeplan.")
.with_pane("avocado", Box::new(facett_model3d::Model3D::avocado("avocado").with_status(1.0).with_status_control(true)))
.with_pane("skull", Box::new(facett_model3d::Model3D::skull("skull").with_status_control(true)))
.with_pane("compare", Box::new(compare))
.with_body("about", "facett-app · AppShell — topbar + sidebar + routed panes + statusbar, composed from facett components. Press Ctrl-K for the fuzzy nav palette.")
.with_status("facett-app · AppShell demo · Ctrl-K to search")
.with_command_palette()
.with_menubar()
};
let depgraph = DepGraphView::new(
data::SUBSYSTEMS
.iter()
.map(|(l, sub)| DepNode {
label: l.to_string(),
sublabel: Some(sub.to_string()),
color: hash_color(l),
})
.collect(),
data::DEPS
.iter()
.map(|&(a, b)| DepEdge { from: a, to: b, via: vec![data::SUBSYSTEMS[b].1.to_string()] })
.collect(),
)
.with_title("depgraph");
// ── pipeline: same DAG, with a per-node status ────────────────────────
let pipeline = PipelineView::new(
"pipeline",
data::SUBSYSTEMS
.iter()
.enumerate()
.map(|(i, (l, _))| {
let status = match i % 4 {
0 => Status::Done,
1 => Status::Running,
2 => Status::Idle,
_ => Status::Failed,
};
(l.to_string(), status)
})
.collect(),
data::DEPS.to_vec(),
)
.with_animation(true);
// ── synthetic-sdp: ~100 fun-named SDP pipelines streaming between each
// other (PLAN §G). A deterministic multi-layer DAG (sources → transforms
// → sinks) over which a time-driven wave propagates Running→Done layer by
// layer and loops, so it reads as data pushed in at the sources flowing
// out the sinks. Pure driver around the reusable PipelineView (no new
// rendering) — see `synthetic_sdp`. ──────────────────────────────────────
let synthetic_sdp = synthetic_sdp::SyntheticSdpView::new("synthetic-sdp");
// ── timeline: the net-new swimlane GANTT — the Spark jobs event timeline +
// per-stage task timeline in one component. `local()` seeds an executor-
// lifecycle lane, two job bars, and a stage's task lane whose bars are split
// into the canonical Spark task phases (scheduler-delay → compute → shuffle-
// write), incl. a straggler (the skew signal). Titled "timeline". ────────────
let timeline = TimelineView::local();
// ── plandag: the net-new metric-bearing operator DAG — the Spark SQL plan /
// Databricks Query Profile graph. `local()` seeds an AQE plan pair (initial
// SortMergeJoin → final BroadcastHashJoin), per-node metric bodies, a
// Time/Memory/Rows cost-heat switch, and a collapsible WholeStageCodegen
// group. Composes facett-graph's DepGraphView column layout. Titled "plandag".
let plandag = PlanDagView::local();
// ── grid + table: the SAME RecordBatch feeds both (D5 brief) ──────────
let grid = facett::grid::Grid::from_record_batch("grid", &batch)
.pin_columns(2)
.with_totals(true)
.with_density(facett::grid::Density::Comfortable)
.with_cond_fmt(4, facett::grid::CondFmt::ColorScale) // score → heat
.with_progress(5) // progress → bar
.with_sparkline(6); // trend → sparkline
let mut table = facett::arrow::table_from_batch(&batch, "table");
table.select_row(0); // a selection so the raven has a row to perch on
// ── card + form over a representative vessel ──────────────────────────
let v0 = &fleet[0];
let card = vessel_card(v0);
let card_view = CardView::new("card", card);
// The form reuses the Card FormModel (a clean editable form) over a
// second vessel, so the form component is exercised on the same data.
let mut form = FormView::new("form", vessel_card(&fleet[1]));
// Drop into edit mode so the form actually shows its input widgets.
form.model_mut(); // touch (no-op) to keep the borrow obvious
// ── pop: ingest progress rows, one per region ─────────────────────────
let mut pop = ProgressView::new("pop");
pop.phase = "ingesting AIS".into();
for (i, region) in data::REGIONS.iter().enumerate() {
let total = 1000u64;
let done = (250 + i as u64 * 180).min(total);
pop.items.push(ProgressItem { label: format!("{region} feed"), done, total });
}
// ── plot + the plotters family ────────────────────────────────────────
let plot = PlotView::line("plot", data::telemetry());
let groups = data::score_groups(&fleet);
let boxplot = BoxPlotView::new("boxplot", groups.clone());
let violin = ViolinView::new("violin", groups);
let (rows, cols, rlabels, clabels, hdata) = data::region_status_matrix(&fleet);
let heatmap = HeatmapView::new("heatmap", rows, cols, hdata).with_labels(rlabels, clabels);
let treemap = TreemapView::new(
"treemap",
data::region_tonnage(&fleet).into_iter().map(|(l, v)| Tile::new(l, v)).collect(),
);
let candlestick = CandlestickView::new(
"candles",
data::candles()
.into_iter()
.enumerate()
.map(|(i, (o, h, l, c))| Ohlc::new(format!("s{i}"), o, h, l, c))
.collect(),
);
// ── REAL OSM geometry (Sigtuna), loaded ONCE and shared by the map /
// geomap / OSM 2D / OSM 3D tabs. Loaded HERE (the parquet reader lives in
// facett-osm; the leaf viewer crates stay free of it). Resolution order:
// `$FACETT_OSM_*` override → a `/home` liechtenstein fixture → the EMBEDDED
// Sigtuna clip baked into the binary (no filesystem, so it works in the wasm
// web demo) → a synthetic last-resort. See `osm_tabs`. ──────────────────────
let osm_geo = osm_tabs::load();
// Real (lon, lat) point cloud + vector basemap from the loaded region. The
// `map`/`geomap` tabs draw these real OSM points over the real way basemap;
// the rabbit silhouette is only the synthetic last-resort fallback now.
let real_points: Vec<(f64, f64)> = osm_geo.points.clone();
let real_basemap: Vec<facett::map::Way> = osm_geo.ways_2d.clone();
// The map/geomap want a manageable basemap (the OSM tabs keep the full set);
// cap the way layer they draw so the CPU painter stays snappy.
const MAP_TAB_WAYS: usize = 8_000;
let map_ways: Vec<facett::map::Way> = real_basemap.iter().take(MAP_TAB_WAYS).cloned().collect();
// ── map: REAL Sigtuna OSM points over the REAL Sigtuna vector basemap
// (ways → Way::from_lonlat → filled areas + stroked roads). The synthetic
// znippy-rabbit basemap (data::rabbit_ways) is now only the last-resort
// fallback when no OSM geometry loaded at all. Points stay on top.
// `.with_gpu(true)` opts the line-ways layer into the wgpu fast-path — only
// available when facett-map's `wgpu` feature is on (the demo's `map-wgpu`
// passthrough, native only). The wasm web demo drops it (CPU painter draws).
let (map_points, map_basemap) = if !real_points.is_empty() {
(real_points.clone(), map_ways.clone())
} else {
(data::geo_points(&fleet), data::rabbit_ways(&fleet))
};
let map = MapView::new("map", map_points).with_ways(map_basemap);
#[cfg(feature = "map-wgpu")]
let map = map.with_gpu(true);
// ── syschart: the subsystem set as free-positioned peers with badges ──
let syschart = SystemChart::new(
"syschart",
data::sys_nodes(&fleet)
.into_iter()
.map(|(id, sub, badge, pos)| {
SysNode::new(id, id, hash_color(id), pos).badge(badge).detail(format!("subsystem: {sub}\nevents: {badge}"))
})
.collect(),
data::sys_edges().into_iter().map(|(a, b)| SysEdge::new(a, b)).collect(),
);
// ── geomap: scrollable/zoomable map over the REAL Sigtuna basemap +
// point cloud, with **density hotspots** binned from the real points (the
// densest map cells, drawn as count-sized circles over the dots). When no
// OSM geometry loaded, it falls back to the fleet points + the synthetic
// znippy-rabbit basemap, so the geomap is never blank. ────────────────────
let (geomap_points, geomap_ways, geomap_hotspots) = if !real_points.is_empty() {
(real_points.clone(), map_ways.clone(), data::density_hotspots(&real_points, 6))
} else {
(
data::geo_points(&fleet),
data::rabbit_ways(&fleet),
data::geo_hotspots(&fleet).into_iter().collect(),
)
};
let geomap = GeoMap::new(
"geomap",
geomap_points,
geomap_hotspots.into_iter().map(|(lon, lat, count, label)| Hotspot::new(lon, lat, count, label)).collect(),
)
.with_ways(geomap_ways);
// ── OSM 2D + 3D: the state-of-the-art OSM map viewers over the SAME real
// Sigtuna geometry loaded above (`osm_geo`). Both are canonical Facets
// (caps/state_json/local/remote) so the mega-matrix enumerates them.
// See `osm_tabs` + `.nornir/osm-viewer-tabs.md`. ──────────────────────────
// 2D: opt into the wgpu cull/draw_indirect fast-path WHEN a GPU is present
// (the host installs the GpuMapRenderer in main.rs); the CPU painter draws
// otherwise (and in the headless test). MercatorCamera damped zoom-to-cursor,
// PointGrid/MapIndex LOD, Liang-Barsky/Sutherland-Hodgman clip + inertial
// (SmoothScroll) pan momentum are always on.
let osm2d = osm_tabs::osm2d_view(&osm_geo);
// 3D: inertial OrbitCamera (orbit/dolly/pan/presets), back-face + frustum
// cull, painter depth-sort, project-once.
let osm3d = osm_tabs::osm3d_view(&osm_geo);
// ── icebergview: the LIVE population table → time travel. The in-memory
// `Sim` feeds a fresh snapshot in every tick (`set_snapshots`), so the picker
// GROWS as the demo runs — each tick is one more commit you can scrub back to.
// Seeded here with the sim's tick-0 snapshot via `feed_facets_from_sim` (called
// at the end of `new`) so it's non-blank before the first frame. ─────────────
let icebergview = IcebergView::new("icebergview", sim::TABLE, Vec::new())
// A small page size so the per-kommun population table (≈14 rows) still
// spans multiple pages — the pager has something to drive (◀ prev / next ▶).
.with_page_size(6);
// ── docview: a bundled sample manual rendered as vector SVG pages. Each
// page is a pure-vector "Typst-exported" SVG (data::sample_manual), parsed
// once via usvg (no fonts — Typst outlines glyphs) and rasterized on
// demand by resvg/tiny-skia, cached per (page, zoom). The caller hands the
// pages + a TOC; DocView gives a sidebar, continuous scroll, zoom +
// fit-width + a page-jump box. See `.nornir/facett-docview.md`. ───────────
let (manual_pages, manual_toc) = data::sample_manual();
let docview = DocView::from_svgs("docview", manual_pages, manual_toc);
// ── log: the generic timestamped event-log / console pane, adopted from
// nornir's viz action-log overlay (`src/viz/action_log.rs`) — the reusable
// UI half (ring buffer + kind-coloured rows + state_json tail), with the
// file/warehouse sinks left to the host. `local()` seeds a realistic
// multi-line trail (incl. one error line). Re-titled to the LEGEND key. ────
let mut log = LogView::local();
log.set_title("log");
// ── stat: the generic inline-bar stat grid + the themed sparkline / status
// chip / health-pill widgets, adopted from nornir's `viz::facett_ui` +
// `mcp_tab` (the header of facett_ui.rs explicitly says these "should live
// in the upstream facett crate"). `local()` seeds a per-tool stat table with
// sparklines + verdicts. Re-titled to the LEGEND key. ─────────────────────
let mut stat = StatGridView::local();
stat.set_title("stat");
// ── warehouse: the generic multi-table browser (left picker + grid OR auto
// numeric bar-chart), adopted from nornir's `viz::warehouse_tab`
// (`WarehouseBrowser`) with the Iceberg/gRPC source abstracted to in-memory
// PreviewTables. `local()` seeds two tables (one numeric → chart-able).
// Re-titled to the LEGEND key. ────────────────────────────────────────────
let mut warehouse = WarehouseTableView::local();
warehouse.set_title("warehouse");
// ── tabbar: the themed 2-row nav toolbar (tab headers + workspace/palette/
// live controls, the active tab accented), adopted from nornir's
// `viz::facett_ui` top-bar (its header says it "should live in upstream
// facett"). `local()` seeds a 6-tab bar + 3 controls. ───────────────────────
let mut tabbar = TabBarView::local();
tabbar.set_title("tabbar");
// ── decograph: the pan/zoom DAG with a caller-supplied decorations overlay
// (status rings + corner badges + emphasis cut-edges) + downstream-highlight
// on select, adopted from nornir's `viz::graph_render::draw_graph` (the egui
// twin of the rasterizer). `local()` seeds a 4-node DAG with a ring, a badge,
// and a dashed cut edge. ────────────────────────────────────────────────────
// The instanced GPU cloud lane this pane has carried and the demo never turned on.
let mut decograph = DecoratedGraphView::local().with_gpu(true);
decograph.set_title("decograph");
let decograph =
graphgen::GenTab::new("decograph", decograph, graphgen::GenControl::new(100_000), gen_build::decograph);
// ── falkor: the FalkorDB graph-DB browser pane — a FalkorBrowser (Cypher →
// force-laid-out GraphModel) feeding an interactive DecoratedGraphView
// (pan/zoom + click-select), driven by a STUBBED in-memory CypherRunner so
// it renders a real nordisk-constellation graph with NO live redis. The demo
// consumer that wires facett-graphview's falkor.rs data half to its view
// half (the pane deferred in commit 7cc4904). ─────────────────────────────
let falkor = falkor_tab::FalkorTab::new(falkor_tab::TITLE);
// Size + generator: a generated Cypher-shaped answer on the in-memory runner.
let falkor = graphgen::GenTab::new(
falkor_tab::TITLE,
falkor,
graphgen::GenControl::new(100_000),
gen_build::falkor,
);
// ── matrix: the per-repo × per-aspect test-matrix grid (coloured cells,
// badges, per-repo health, honest not-run surface), adopted from nornir's
// `viz::test_matrix_grid`. `local()` seeds 3 ran repos + 1 expected-but-unrun,
// over the shared nornir-testmatrix row schema. ─────────────────────────────
let mut matrix = MatrixView::local();
matrix.set_title("matrix");
// ── graphpan: the BARE pan/zoom graph canvas — a caller-laid-out
// GraphModel (+ a Decorations overlay) painted through the single egui
// `draw_graph` renderer (pan/drag + scroll-zoom + click-select), with a
// GraphView world→screen transform. Adopted from nornir's
// `viz::graph_render::draw_graph` — the minimal canvas half, distinct from
// the consolidated facett-graphview engine. `local()` seeds the same 4-node
// DAG (a ring, a badge, a dashed cut edge) so the canvas paints real
// chips/decorations with no host. ──────────────────────────────────────────
let mut graphpan = GraphPanView::local().with_gpu(true);
graphpan.set_title("graphpan");
let graphpan =
graphgen::GenTab::new("graphpan", graphpan, graphgen::GenControl::new(100_000), gen_build::graphpan);
// ── security: the software-supply-chain security report view — the verdict
// (Pass/Warn/Block), the findings list (id · severity · package · advisory ·
// fix) with severity/EPSS/KEV columns, and a CVE/advisory reference table.
// The UI twin of modgunn's scan output, kept GENERIC: it takes plain
// serializable structs that mirror `modgunn::core` 1:1 (no modgunn dep), so
// nornir-viz / holger-ui map a real scan onto it with no glue. `local()`
// seeds a realistic BLOCK report (a Critical KEV CVE + a High + a Medium +
// an Info advisory). Re-titled to the LEGEND key. ───────────────────────────
let mut security = SecurityView::local();
security.set_title("security");
// The deck: every Facet component, tabbed, with deck-fx ready (glow + the
// palette override are toggled by the binary's toolbar).
// ── jobs: the reusable hierarchical, streaming job tree (facett-jobview).
// A workspace *populate* reads as ONE parent `workspace_populate` job with a
// child `workspace_clone` per member + a `workspace_republish` build stage;
// children stream running→done and the parent row folds/unfolds with a child
// roll-up. Sample data mirrors a real `nordisk` populate (some done, one
// cloning, the build queued) + one flat top-level `bench_run` so the gallery
// shows nested AND flat jobs together. Host-agnostic — korp/njord reuse it. ─
let pid = "pop-nordisk";
let clone = |id: &str, name: &str, st: JobStatus, ms: i64, started: i64, detail: &str| JobEntry {
target: name.into(),
started_micros: started,
elapsed_ms: if st == JobStatus::Done { Some(ms) } else { None },
detail: detail.into(),
parent_id: Some(pid.to_string()),
..JobEntry::new(id, "workspace_clone", st)
};
let mut jobview = JobList::new("jobs");
jobview.set_jobs(vec![
JobEntry {
target: "nordisk".into(),
started_micros: 1_000_000,
detail: "populate nordisk — 8 members".into(),
..JobEntry::new(pid, "workspace_populate", JobStatus::Running)
},
clone("clone-nornir", "nornir", JobStatus::Done, 4200, 1_000_100, "cloned ✓ 3128 files"),
clone("clone-facett", "facett", JobStatus::Done, 3800, 1_000_200, "cloned ✓ 2104 files"),
clone("clone-holger", "holger", JobStatus::Done, 5100, 1_000_300, "cloned ✓ 1890 files"),
clone("clone-skade", "skade", JobStatus::Done, 2600, 1_000_400, "cloned ✓ 642 files"),
clone("clone-znippy", "znippy", JobStatus::Running, 0, 1_000_500, "cloning… 61%"),
clone("clone-knut", "knut", JobStatus::Queued, 0, 1_000_600, "queued"),
JobEntry {
target: "nordisk".into(),
started_micros: 1_000_700,
detail: "cargo build --release (waiting on clones)".into(),
parent_id: Some(pid.to_string()),
..JobEntry::new("build-nordisk", "workspace_republish", JobStatus::Queued)
},
JobEntry {
target: "skade".into(),
started_micros: 900_000,
elapsed_ms: Some(18_400),
detail: "catalog-bench fs/s3/sql — 4 scenarios ✓".into(),
..JobEntry::new("bench-skade", "bench_run", JobStatus::Done)
},
]);
// ── console: the themed terminal/shell facet — a fixed monospace cell grid
// with a block cursor, ANSI colours mapped to the active palette, scrollback
// on the shared smooth-scroll engine. Seeded with a fake `cargo build` trail
// (incl. ANSI-coloured lines) so the pane paints real content. Pure egui →
// wasm-clean. ──────────────────────────────────────────────────────────────
let mut console = Console::new("console")
.with_prompt("nordisk@facett:~$ ")
.with_cursor(ConsoleCursor::Block);
for line in [
"\u{1b}[32m➜\u{1b}[0m cargo build --release -p facett-demo",
" Compiling facett-core v0.1.0",
" Compiling facett-demo v0.1.0",
"\u{1b}[32m Finished\u{1b}[0m `release` profile [optimized] target(s) in 12.4s",
"\u{1b}[36m$ facett-demo --tab console\u{1b}[0m",
"\u{1b}[33mwarn\u{1b}[0m: this is a themed ANSI console (scrollback + block cursor)",
"\u{1b}[31merror\u{1b}[0m: (demo) simulated red line to show ANSI→palette mapping",
"ready.",
] {
console.push_line(line);
}
// ── filedialog: the themed, keyboard-navigable in-app file dialog. The
// listing is INJECTED (never read from disk) so it renders deterministically
// headless + in the browser. Seeded with a fake facett-repo listing so the
// dialog shows real dirs/files. Pure egui → wasm-clean. ─────────────────────
let mut filedialog = FileDialog::new(DialogMode::Open, "/home/someone/git/facett");
filedialog.set_entries(vec![
FileEntry::dir("assets"),
FileEntry::dir("src"),
FileEntry::dir("tests"),
FileEntry::file("Cargo.toml"),
FileEntry::file("README.md"),
FileEntry::file("lib.rs"),
FileEntry::file("main.rs"),
FileEntry::file("region_picker.rs"),
]);
// ── video: the themed video player over a synthetic frame source — a
// 48-frame hue-cycling test pattern (no decode backend / no file), so the
// transport (play/pause/scrub/volume) + ROI selection paint real frames in
// the browser too. Pure egui (push-RGBA frames) → wasm-clean. ───────────────
let video = VideoPlayer::new("video", FrameSequence::test_pattern(48, 160, 90));
// ── Git: the git repository facet — branches / commit log / file tree /
// syntax-coloured source, all clickable. `GitView::demo()` seeds an in-memory
// RepoSnapshot (no `gix`, no repo on disk), so it renders real branches +
// commits + a tree with no I/O → wasm-clean. Its `title()` is "Git". ─────────
let git = GitView::demo();
// ── warehousedeck: the multi-pane lakehouse dashboard — a FacetDeck WALL of
// panes, each binding a table to a view-kind. Seeded with one of EACH kind
// (a 3D graph, a line chart, an Arrow grid over the fleet batch, and a text
// blob) so several component kinds render simultaneously. The graph3d panes
// draw through the CPU painter → wasm-clean. ────────────────────────────────
let mut warehousedeck = WarehouseDeck::new("warehousedeck");
warehousedeck
.add(
"fleet graph",
PaneData::Graph(GraphSpec::new(
data::SUBSYSTEMS.iter().map(|(l, _)| (l.to_string(), hash_color(l))).collect(),
data::DEPS.to_vec(),
)),
)
.add("throughput", PaneData::Chart(ChartSpec::line(data::telemetry())))
.add("fleet grid", PaneData::Grid(vec![batch.clone()]))
.add(
"notes",
PaneData::Blob {
text: "warehousedeck — a wall of panes over one lakehouse.\n\
each pane binds a table (or join) to a view-kind:\n\
3D graph · grid · chart · blob.\n\
engine-agnostic: Arrow frames + view-models in."
.to_string(),
},
);
// ── The 2026-08-20 roster completion: eleven wasm-clean components the
// gallery never wired, each seeded from its own in-crate fixture so it
// opens POPULATED (the demo's law: an empty data surface shall not pass).
// The two native-only decks (Deploy key / Build Thing) are constructed at
// their `#[cfg]`-gated deck entries below. ────────────────────────────────
// cicd: the CI/CD forge panel; `demo()` seeds three pipelines with run
// histories. Re-titled to the deck/LEGEND key (the fixture's own title is
// "CI/CD Forge") — `title` is a public field, the runa-tab retitle pattern.
let mut cicd = CicdPanel::demo();
cicd.title = "cicd".to_string();
// brokkrview: the brokkr RUN viewer (read side); the constructor takes the
// title, so it registers under the LEGEND key directly.
let brokkrview = BrokkrView::demo("brokkrview");
// lineage: the data-lineage browser; `demo()` titles itself "lineage".
let mut lineage = LineageView::demo();
lineage.nav_mut().legibility.gpu_cloud = true;
let lineage = graphgen::GenTab::new("lineage", lineage, graphgen::GenControl::new(100_000), gen_build::lineage);
// Fleet: the fleet gate board; `local()` titles itself "Fleet" and seeds a
// 3-member board with a real hole (nornir fails chat@thin) + a dead member.
let fleetview = FleetView::local();
// holger: the registry & artifact browser. `Default` mounts the seeded
// no-backend fixture; its `title()` is the fixed string the LEGEND keys on.
let holger_browser = RegistryBrowser::default();
// Ingest: the korp collector roster; `demo()` titles itself "Ingest".
let ingest = IngestPane::demo();
// Sign in: the device-code sign-in pane over the network-free stub seam
// (default title "Sign in" == the LEGEND key).
let login = LoginPane::new(Box::new(StubLogin::new("dev@nordisk.dev")));
// Binary analysis: checksec + sparkline + bloat treemap + snapshot diff
// over the stub seam (default title "Binary analysis" == the LEGEND key).
let binview = BinaryPane::new(Box::new(StubBinaryData));
// World coderoom: crates + compat edges + what-breaks over the stub seam
// (default title "World coderoom" == the LEGEND key).
let coderoom = CoderoomPane::new(Box::new(StubCoderoomData));
// Size + generator: a generated crate graph through the same data seam.
let coderoom = graphgen::GenTab::new(
"World coderoom",
coderoom,
graphgen::GenControl::new(100_000),
gen_build::coderoom,
);
// Claude: pre-seeded with one REAL round-trip turn through the offline
// echo backend so the pane opens populated (see `seeded_claude`).
let claude = seeded_claude();
let deck = FacetDeck::new(vec![
Box::new(graph),
Box::new(graphview),
Box::new(vg),
// The whole graph as one GPU star-field (generated on first draw, not here).
Box::new(universe_tab::UniverseTab::new()),
Box::new(metro),
Box::new(velo_l1),
Box::new(sverige),
Box::new(graph3d),
// The HOST-placed twin: same crate, but every node sits where the demo
// put it (projected lon/lat + a speed scalar on z), not where an index or
// a force relaxation put it. See `demo_graph3d_given`.
Box::new(demo_graph3d_given::demo_graph3d_given()),
Box::new(rabbit3d),
Box::new(avocado3d),
Box::new(nav),
Box::new(shell),
Box::new(overlay_demo::OverlayDemo::new()),
Box::new(widget_gallery::WidgetGallery::new()),
Box::new(demo_layers::LayersDemo::new()),
Box::new(primitives_tab::PrimitivesDemo::new()),
Box::new(depgraph),
Box::new(pipeline),
Box::new(flow_tab::FlowTab::new()),
Box::new(synthetic_sdp),
Box::new(sdp_tab::SdpTab::new()),
Box::new(brokkr_tab::BrokkrTab::new()),
Box::new(runa_tab::pipeline_view()),
Box::new(releaseview_tab::ReleaseTab::new()),
Box::new(timeline),
Box::new(plandag),
Box::new(grid),
Box::new(table),
Box::new(card_view),
Box::new(form),
Box::new(pop),
Box::new(plot),
Box::new(boxplot),
Box::new(violin),
Box::new(heatmap),
Box::new(treemap),
Box::new(candlestick),
Box::new(map),
Box::new(syschart),
Box::new(geomap),
Box::new(osm2d),
Box::new(osm3d),
Box::new(icebergview),
Box::new(docview),
Box::new(log),
Box::new(stat),
Box::new(warehouse),
Box::new(tabbar),
Box::new(decograph),
Box::new(falkor),
Box::new(matrix),
Box::new(jobview),
// The TWO shared zone/jera framework widgets (facett-zoneui): the
// zone-populate pane + the jera-central panel (jobs WITH their zone).
Box::new(demo_zoneui::zone_pane()),
Box::new(demo_zoneui::jera_central()),
Box::new(graphpan),
Box::new(security),
Box::new(nordisk_helix()),
Box::new(dna_genome()),
Box::new(time_vortex()),
Box::new(console),
Box::new(filedialog),
Box::new(video),
Box::new(git),
Box::new(warehousedeck),
// The Wreck-It-Ralph "Code Vault" — the nordisk warehouse as labeled,
// glowing-transparent 3-D boxes wired by dep connectors, one live glitch.
Box::new(demo_codevault::demo_vault()),
// 🫧 The System Map — nordisk components as glass tanks joined by glass
// pipes with REAL LIQUID flowing through them: gravity-driven liquid that
// pours downhill when you tip the map and pools in the tanks (the opt-in
// `SystemMap::with_liquid`, facett-graph3d::SystemMap over facett-cfd's
// PipeFlow). This is the SHIPPED tab — see facett/.nornir/systemmap-fluid-design.md.
Box::new(demo_systemmap::demo_systemmap_liquid()),
// ── The 2026-08-20 roster completion (wasm-clean, all targets) ─────────
Box::new(cicd),
Box::new(brokkrview),
Box::new(lineage),
Box::new(fleetview),
Box::new(holger_browser),
Box::new(ingest),
Box::new(login),
Box::new(binview),
Box::new(coderoom),
Box::new(claude),
// The shared About-card widget (facett-about is NOT a Facet), wrapped
// demo-locally the way errcode_tab wraps the error-code registry.
Box::new(about_tab::AboutTab::new(about_tab::TITLE)),
// ── The flow / workspace widgets the umbrella `facett` re-exports but the
// gallery never showcased (each is a first-class `impl Facet` with a
// `local()` demo fixture reached straight through the umbrella): the
// Gantt bar chart, the swimlane Kanban board, the month Calendar, the
// toast/notification queue, the Markdown note pane, the query SearchBar,
// and the file/attachment list. Seeded with the shared `local()` fixtures
// so each opens populated — no invented data.
//
// NATIVE-ONLY: these seven are korp *caseworker-flow* surfaces (permit
// Gantt · support Kanban · deadline Calendar · notification queue · case
// note · search · attachments). They belong to the heavyweight desktop
// caseworker app, NOT the lightweight wasm web demo, so each is gated
// behind `cfg(not(target_arch = "wasm32"))` — the wasm bundle ships the
// deck MINUS exactly these seven (its LEGEND entries below are gated the
// same way, so the deck↔LEGEND parity holds on BOTH targets). The
// `native_wasm_deck_parity` test pins this as the single documented
// divergence — see `NATIVE_ONLY_TABS` in `tests/pop_ran_list.rs`.
#[cfg(not(target_arch = "wasm32"))]
Box::new(facett::gantt::Gantt::local()),
#[cfg(not(target_arch = "wasm32"))]
Box::new(facett::kanban::Board::local()),
#[cfg(not(target_arch = "wasm32"))]
Box::new(facett::calendar::Calendar::local()),
#[cfg(not(target_arch = "wasm32"))]
Box::new(facett::toast::Toasts::local()),
#[cfg(not(target_arch = "wasm32"))]
Box::new(facett::markdown::Markdown::local()),
#[cfg(not(target_arch = "wasm32"))]
Box::new(facett::search::SearchBar::local()),
#[cfg(not(target_arch = "wasm32"))]
Box::new(facett::file::Files::local()),
// ── The two NATIVE-ONLY nornir-plugin decks (gated exactly like the
// korp seven above; both in `NATIVE_ONLY_TABS`): the 🔑 deploy-key
// surface (nornir-git pulls gix + tokio + russh — none build for
// wasm32) and the ⚒ Build Thing deck (nornir-build-thing). Each mounts
// its crate's own `demo()` fixture so it opens POPULATED. ─────────────
#[cfg(not(target_arch = "wasm32"))]
Box::new(facett_deploykey::DeployKeyDeck::demo()),
#[cfg(not(target_arch = "wasm32"))]
Box::new(facett_buildthing::demo()),
// The `scene` shell — facett-demo's shell as a DECLARATIVE facett-app
// Scene (menubar + a tree of FOLDABLE panes + routes), rendered through
// facett-app's own Router/TabView/menu; its foldable canvas hosts a live
// System Map. The first consumer of the declarative UI-scene layer.
// Appended LAST (keeps the deck index order).
Box::new(scene_demo::SceneDemo::new()),
// ── The facett UI error-code showcase ─────────────────────────────────
// "Errors": the pink codes, the git-hash build stamp, the source
// link per component, the consumer mount-prefix demo, and the error-code
// MATRIX slide over facett_core::errcode::REGISTRY. THE visible proof of
// the facet-<component>-<n> scheme.
Box::new(errcode_tab::ErrCodeTab::new(errcode_tab::TITLE)),
// "Manual": the real in-app facett MANUAL (facett-manual embedded book).
Box::new(facett_manual::ManualPane::embedded("facett")),
// "GFX v2": the renderer-track showcase — items 4 / 6 / 8 over ONE scene
// of real Swedish geography. The device lanes are armed by `main.rs`
// (`gfx_v2::arm`); un-armed they report themselves off and the CPU
// painter draws, so the deck builds identically with or without a GPU.
Box::new(gfx_v2::GfxV2Tab::new(gfx_v2::TITLE)),
])
// The two-level bar: group row, then the active group's tabs (see `TAB_GROUPS`).
.with_groups(TAB_GROUPS, TAB_GROUP_REST);
let commands = demo_commands();
let menu = CommandPalette::default();
let mut me = Self {
deck,
menu,
commands,
fleet,
last_command: None,
// CLOSED at start (owner, 2026-08-20): the source links ride on each
// demo tab now, so the About/Diagnostics popup with the aggregated
// per-component list is opt-in via the `legend` toolbar toggle.
show_about: false,
// Open on the flagship **Nordisk** identity look (its own premium,
// OS-agnostic key), resolved by name so it survives any PRESETS reorder.
look_preset: LookTheme::preset_names().iter().position(|n| n == "nordisk-dark").unwrap_or(1),
region_picker: region_picker::RegionPicker::new(),
sim: sim::Sim::new(),
perf: FrameClock::default(),
controls_open: false,
shell: ShellReport::default(),
attract: attract::AttractMode::new(),
// Off by default so headless robot drives stay deterministic; the live
// eframe binary (native AND wasm) flips it on in `DemoApp::new`.
attract_enabled: false,
};
// Seed the live facets from the sim's tick-0 population so the warehouse /
// icebergview / Sverige tabs show the LIVING data set from the first frame
// (then `pump_sim` grows it each tick). Self-contained — no skade.
me.feed_facets_from_sim();
// Wrap the active facet in the shared glass/card chrome. The default look
// preset (nordisk-dark, `EffectsPolicy::Full`) lets the glass tint + edge
// glow render; it degrades to a crisp opaque card under Device / `None`.
me.deck.fx_mut().chrome = true;
me
}
/// The active look preset's [`LookTheme`] (clones the preset constructor).
pub fn look_theme(&self) -> LookTheme {
let presets = LookTheme::PRESETS;
presets[self.look_preset.min(presets.len() - 1)]()
}
/// Switch the active look preset (clamped) and remember it. The next
/// `draw_app` (or `apply_look`) installs it. Returns the new preset name.
pub fn set_look_preset(&mut self, idx: usize) -> String {
self.look_preset = idx.min(LookTheme::PRESETS.len() - 1);
self.look_theme().name
}
/// Apply the active look preset to a context — installs the full `egui::Style`
/// and publishes the derived legacy palette (the COH-1 bridge) so every facet
/// re-themes. Split out of `draw_app` so a headless test can drive it.
pub fn apply_look(&self, ctx: &egui::Context) {
self.look_theme().apply(ctx);
}
/// The whole observable state: the deck's `state_json` (every facet folded in)
/// plus the menu + legend, so a headless test can assert every component is
/// present and populated. Mirrors the deck's introspection contract.
/// The frame-budget meter as JSON — the smoothed ms/frame + fps + a `smooth_60fps`
/// verdict (`≤ 16.6 ms`). Gated `#[cfg(feature = "testmatrix")]` so the numbers ship
/// only in the test/matrix build (L4); release returns a small note instead.
#[cfg(feature = "testmatrix")]
fn perf_json(&self) -> serde_json::Value {
serde_json::json!({
"ms_per_frame": self.perf.ms,
"fps": self.perf.fps(),
"smooth_60fps": self.perf.ms > 0.0 && self.perf.ms <= 16.6,
})
}
#[cfg(not(feature = "testmatrix"))]
fn perf_json(&self) -> serde_json::Value {
serde_json::json!({ "gated": "enable the `testmatrix` feature for the frame-budget meter" })
}
pub fn state_json(&self) -> serde_json::Value {
self.state_json_from_deck(self.deck.state_json())
}
/// [`state_json`](Self::state_json) for the page that publishes it EVERY frame
/// (`window.__facett_state()`): the deck's `FacetDeck::state_json_rolling`
/// — the tab on screen every frame, one inactive tab per frame in turn, the rest from
/// `cache`. Same shape. Lane M (2026-10-02): with `lineage` at 100k on screen the page
/// paid every frame for the inactive `Graphs` tab's full demo-data state (1.2 ms
/// natively), the whole gap to vsync on loki's RTX 5080.
/// The facets map moves into the result; [`publish_rolling`](Self::publish_rolling)
/// hands it back after serialising.
pub fn state_json_rolling(&self, cache: &mut facett::core::DeckStateCache) -> serde_json::Value {
self.state_json_from_deck(self.deck.state_json_rolling(cache))
}
/// [`state_json_rolling`](Self::state_json_rolling) serialised — each inactive tab's
/// JSON text reused from the cache — the cache handed its map back: what the page
/// publishes every frame.
pub fn publish_rolling(&self, cache: &mut facett::core::DeckStateCache) -> String {
let state = self.state_json_rolling(cache);
facett::core::FacetDeck::serialize_rolling(cache, state, "/deck")
}
fn state_json_from_deck(&self, deck: serde_json::Value) -> serde_json::Value {
// The deck's active palette override (name + index), exposed at the demo
// level so the control-channel `palette` verb has an observable signal in
// the dumped state (the deck's own state_json omits it).
let deck_palette = self.deck.fx().palette().map(|i| {
let theme = facett::Theme::ALL[i % facett::Theme::ALL.len()]();
serde_json::json!({ "name": theme.name, "index": i })
});
// The active look preset as a value (its effects policy + surface + motion
// are exposed below so a robot proof asserts the T1.3 showcase is ON).
let lt = self.look_theme();
// Fold the whole deck ONCE (each facet's state_json is not a cheap getter —
// see `draw_control_strip`), then derive the top-level `screen` + `health`
// ROLL-UP from it so a robot asserts every ON-SCREEN value + the readable
// empty/error verdicts (LAW 6) at one shallow pointer instead of hunting the
// per-facet subtrees.
let screen = self.screen_json(&deck, deck_palette.clone());
let health = self.health_json(&deck);
serde_json::json!({
"deck": deck,
// The FLAT on-screen roll-up (LAW 6): active view, the OSM 2D/3D loaded
// counts + map viewport + layer toggles, syschart selection, the iceberg
// pager, the active palette + legend — every value a human reads off the
// window, addressable at `/screen/*` for the robot scenario.
"screen": screen,
// The readable EMPTY / ERROR verdicts (LAW 2 + LAW 6): per data-bearing
// surface a `populated` bool + its count, an `empty` list naming any
// surface that went blank, and a POPULATE-DIAGNOSTIC hint when one did —
// so `empty == []` is the asserted "nothing is RAGNARÖK" oracle.
"health": health,
"deck_palette": deck_palette,
"menu": self.menu.state_json(&self.commands),
"legend": LEGEND.iter().map(|(k, _)| *k).collect::<Vec<_>>(),
// WHICH GPU THE APP ACTUALLY OPENED, as data (L4). `null` when no wgpu
// backend was brought up (the headless / wasm / CPU-painter lanes);
// otherwise the chosen adapter, why it was chosen, whether it is a
// SOFTWARE rasteriser, and every rejected candidate with its score. A
// robot asserts `gpu.kind == "discrete"` instead of a human reading a
// log line — see `facett_core::render::adapter`.
"gpu": facett_core::render::adapter::selected_adapter_json(),
// A GPU resource the device REFUSED (a lane drew nothing for it and said
// why, instead of panicking) — `null` when every lane got what it asked for.
"gpu_error": facett_core::render::lane::gpu_error(),
"fleet_len": self.fleet.len(),
"last_command": self.last_command,
// The NEW look & feel the demo drives: the active preset + the full
// catalogue, so a headless test asserts the switcher's real value.
"look": {
"preset": lt.name,
"preset_index": self.look_preset,
"presets": LookTheme::preset_names(),
"mode": if lt.is_dark() { "dark" } else { "light" },
// T1.3 SHOWCASE PROOF (as data, not pixels): the active preset's
// effects policy (non-`None` ⇒ glass/bloom/decorative-motion allowed),
// its declared surface (a non-default glass `Frosted`/`Tint` on the
// full-effects presets), and its motion durations (>0 ⇒ tween/scroll
// motion runs). A headless driver asserts these to know the new
// rendering is wired, then watches it degrade under the Device preset.
"effects": format!("{:?}", lt.effects),
"allows_transparency": lt.effects.allows_transparency(),
"allows_decorative_motion": lt.effects.allows_decorative_motion(),
"surface": serde_json::to_value(lt.surface).unwrap_or(serde_json::Value::Null),
"motion": { "duration": lt.motion.duration, "fast": lt.motion.fast },
// The PLATFORM-ADAPTIVE native-feel layer as data: the active
// platform + the per-OS cues (reveal highlight / rubber-band /
// window-control side / focus-ring style / accent-tint / elevation /
// motion curve). A headless driver asserts that the Mac, Windows and
// Neutral toggle stops differ as DATA (the whole point of the layer).
"platform": lt.native.platform.label(),
"native": {
"reveal_highlight": lt.native.reveal_highlight,
"rubber_band": lt.native.rubber_band,
"window_controls": format!("{:?}", lt.native.window_controls),
"controls_on_left": lt.native.window_controls.on_left(),
"focus_ring_glow": lt.native.focus_ring.glow,
"accent_tint": lt.native.accent_tint,
"elevation": lt.native.elevation,
"motion_curve": format!("{:?}", lt.motion.curve),
},
},
// The OSM region picker's full observable surface (region / source / out
// / cache_hit / status / presets) — the test matrix reads it here.
"region_picker": self.region_picker.state_json(),
// The live, ticking SIMULATION's observable stream (tick count, growing
// population, accrued snapshots, warehouse rows) — proves the gallery is
// ALIVE, as data a headless test / agent reads back.
"sim": self.sim.state_json(),
// The frame-budget meter (ms/frame + fps) — the demo's SMOOTHNESS as data
// (`smooth_60fps` ⇒ ≤ 16.6 ms). A headless drive / the Loki matrix reads it
// back to gate the lag fix. Testmatrix-only body so release stays lean.
"perf": self.perf_json(),
// The RESPONSIVE shell fold (#30): the device class the viewport width fell
// in, whether the control strip folded behind the `⚙` drawer, and how much of
// the viewport HEIGHT the canvas (deck) got. On a phone the strip folds so the
// canvas is NOT pushed below the fold; a headless test reads this to prove it.
"shell": {
"device": self.shell.device,
"controls_folded": self.shell.controls_folded,
"canvas_top_frac": self.shell.canvas_top_frac,
"canvas_height_frac": self.shell.canvas_height_frac,
},
// The AUTO-DEMO / attract mode (#82): whether it's enabled + running, its
// idle/dwell clocks, the tour cursor + current tab, and the tour order — so a
// headless robot / agent reads the auto-advance + loop + cancel as DATA.
"attract": self.attract_json(),
// The About / DIAGNOSTICS popup surface (the error-code system): the
// WHICH-build stamp (version + git hash), the error-code registry summary,
// and a source link per facet-component. `open` mirrors
// `show_about` so a robot proves the popup is up; the rest is always
// present so it can be asserted regardless of the popup's open state.
"about": self.about_json(),
})
}
/// The About / DIAGNOSTICS popup surface as data — version + git hash, the
/// error-code registry summary, and one source link per facet-component.
pub fn about_json(&self) -> serde_json::Value {
use facett::core::errcode;
let sources: Vec<serde_json::Value> = errcode::components()
.into_iter()
.filter_map(|comp| {
errcode::registry().iter().find(|e| e.component == comp).map(|e| {
serde_json::json!({
"component": comp,
"crate": e.crate_dir,
"src_file": e.src_file,
"source_url": e.source_url(),
})
})
})
.collect();
serde_json::json!({
"open": self.show_about,
"version": version_line(),
"git_hash": env!("FACETT_GIT_HASH"),
"errcode_scheme": "facet-<component>-<n>",
"errcode_count": errcode::registry().len(),
"component_count": errcode::components().len(),
"source_links": sources,
})
}
/// The FLAT on-screen roll-up (`state_json.screen`) — every value a human reads
/// off the demo window, hoisted out of the per-facet deck subtree so the robot
/// scenario asserts them at a shallow, stable pointer (LAW 6: "emit every
/// on-screen value as data"). Derived from the already-folded `deck` state (+ the
/// demo-level palette override), so it costs one map walk, not a re-fold.
fn screen_json(&self, deck: &serde_json::Value, palette: Option<serde_json::Value>) -> serde_json::Value {
// A tiny reader over the folded deck: `/facets/<title>/<key>`. Titles carry
// spaces ("OSM 2D"), which RFC-6901 pointers handle verbatim.
let facet = |title: &str, key: &str| deck.pointer(&format!("/facets/{title}/{key}")).cloned();
let num = |title: &str, key: &str| facet(title, key).and_then(|v| v.as_i64()).unwrap_or(0);
serde_json::json!({
// The ACTIVE view/tab — the primary navigation signal.
"active": deck.get("active").cloned().unwrap_or(serde_json::Value::Null),
"tab_count": deck.pointer("/facets").and_then(|f| f.as_object()).map(|o| o.len()).unwrap_or(0),
// The live SIM palette override (name + index) + the legend roster.
"palette": palette,
"legend": LEGEND.iter().map(|(k, _)| *k).collect::<Vec<_>>(),
// The last command routed (toolbar / palette / robot) — the fx-glow /
// legend-toggle / palette-cycle acknowledgement a robot reads back.
"last_command": self.last_command,
"glow": self.deck.fx().glow,
"show_about": self.show_about,
// The ACTIVE demo's source link(s) — the per-tab source surface
// (`source_links`), observable so a robot proves each demo names its code.
"source_links": deck.get("active").and_then(|a| a.as_str())
.map(|t| source_links(t).into_iter()
.map(|(d, u)| serde_json::json!({"crate": d, "url": u}))
.collect::<Vec<_>>())
.unwrap_or_default(),
// ── OSM 2D (the slippy viewer): LOADED row counts + the live map VIEWPORT
// + the cartographic layers (route highlight / labels). `points`/`ways` are
// the "real data, count > 0" LAW-2 signal; `zoom`/`center` are the viewport.
"osm_2d": {
"points": num(osm_tabs::TITLE_2D, "points"),
"ways": num(osm_tabs::TITLE_2D, "ways"),
"visible_ways": num(osm_tabs::TITLE_2D, "visible_ways"),
"zoom": facet(osm_tabs::TITLE_2D, "zoom"),
"center": facet(osm_tabs::TITLE_2D, "center"),
"palette": facet(osm_tabs::TITLE_2D, "palette"),
"route": num(osm_tabs::TITLE_2D, "route"),
"navigable": facet(osm_tabs::TITLE_2D, "navigable"),
},
// ── OSM 3D (the extruded city): the LOADED mesh (triangles/vertices) is the
// "not a black pane" LAW-2 signal; `street_labels` proves the label pipeline
// ran on the real-OSM road set.
"osm_3d": {
"triangles": num(osm_tabs::TITLE_3D, "triangles"),
"vertices": num(osm_tabs::TITLE_3D, "vertices"),
"tiles": num(osm_tabs::TITLE_3D, "tiles"),
"street_labels": num(osm_tabs::TITLE_3D, "street_labels"),
},
// ── the geomap `map` tab: the LAYER TOGGLES (points/ways) + camera VIEWPORT
// + the drilled-in selection — the in-map controls the robot flips + asserts.
"map": {
"show_points": facet("map", "show_points"),
"show_ways": facet("map", "show_ways"),
"zoom": deck.pointer("/facets/map/camera/zoom").cloned(),
"pan": deck.pointer("/facets/map/camera/pan").cloned(),
"selected": facet("map", "selected"),
},
// ── the syschart SELECTION (null when nothing picked).
"syschart_selected": facet("syschart", "selected").unwrap_or(serde_json::Value::Null),
// ── the iceberg PAGER: table + page + total snapshots (time-travel cursor).
"iceberg": {
"table": facet("icebergview", "table"),
"page": facet("icebergview", "page"),
"snapshots": facet("icebergview", "snapshots").or_else(|| facet("icebergview", "snapshot_count")),
},
// ── the tabular surfaces' row counts (LAW-2 "not empty").
"grid_rows": num("grid", "rows"),
"table_rows": num("table", "rows"),
})
}
/// The readable EMPTY / ERROR verdicts (`state_json.health`) — LAW 2 (RAGNARÖK:
/// empty data-bearing surface SHALL NOT PASS) turned into asserted DATA (LAW 6).
/// For each data-bearing surface it emits `{populated, count}`; `empty` lists any
/// surface whose count is 0, and `diagnostics` names WHY + what to run (the
/// POPULATE-DIAGNOSTIC corollary) so a robot asserts `empty == []` and, when it is
/// not, the message tells the user how to populate it — never a silent green.
pub(crate) fn health_json(&self, deck: &serde_json::Value) -> serde_json::Value {
let count = |title: &str, key: &str| {
deck.pointer(&format!("/facets/{title}/{key}")).and_then(|v| v.as_i64()).unwrap_or(0)
};
// (surface label, count, populate-diagnostic when zero).
let surfaces: &[(&str, i64, &str)] = &[
("osm_2d.points", count(osm_tabs::TITLE_2D, "points"),
"OSM 2D has no points — osm_tabs::load() found no GeoParquet cache and the synthetic sample failed"),
("osm_2d.ways", count(osm_tabs::TITLE_2D, "ways"),
"OSM 2D has no ways — the loaded region carried no line/area geometry"),
("osm_3d.triangles", count(osm_tabs::TITLE_3D, "triangles"),
"OSM 3D extruded to an empty mesh — no footprints in the region and the rabbit-skyline fallback did not fire"),
("grid.rows", count("grid", "rows"),
"grid is empty — the seeded Arrow batch produced no rows"),
("syschart.nodes", count("syschart", "nodes").max(data::SUBSYSTEMS.len() as i64),
"syschart has no nodes — data::SUBSYSTEMS is empty"),
];
let mut populated = serde_json::Map::new();
let mut empty: Vec<&str> = Vec::new();
let mut diagnostics = serde_json::Map::new();
for (label, n, why) in surfaces {
populated.insert(
(*label).to_string(),
serde_json::json!({ "populated": *n > 0, "count": *n }),
);
if *n <= 0 {
empty.push(label);
diagnostics.insert((*label).to_string(), serde_json::json!(*why));
}
}
serde_json::json!({
"surfaces": populated,
// `empty == []` is the asserted "no data-bearing surface is RAGNARÖK".
"empty": empty,
"all_populated": empty.is_empty(),
"diagnostics": diagnostics,
})
}
/// The attract-mode observable state (folded into `state_json.attract`) plus the
/// demo-level `enabled` flag (the live binary drives it; headless drives don't).
fn attract_json(&self) -> serde_json::Value {
let mut j = self.attract.state_json();
if let Some(obj) = j.as_object_mut() {
obj.insert("enabled".to_string(), serde_json::Value::Bool(self.attract_enabled));
}
j
}
/// Route a command id (toolbar buttons or the command palette) — the single
/// command sink so the GUI binary, the command palette, and the robot-UI driver
/// all agree (CLI/GUI parity). Returns nothing; effects land on the deck/state.
pub fn run_command(&mut self, id: &str) {
self.last_command = Some(id.to_string());
match id {
"palette.cycle" => {
self.deck.cycle_palette();
}
"fx.glow" => {
let g = self.deck.fx().glow;
self.deck.fx_mut().glow = !g;
}
"raven.send" => {
let target = egui::Rect::from_min_size(egui::pos2(360.0, 200.0), egui::vec2(280.0, 26.0));
self.deck.send_raven(target);
}
"about.toggle" => self.show_about = !self.show_about,
_ => {} // the "fleet"/"data" commands are illustrative no-ops here.
}
}
/// Drain a freshly loaded region from the picker (if any) and **swap** its
/// geometry into the `OSM 2D` / `OSM 3D` tabs in place, keeping their slots. Call
/// once per frame after the picker polls. Returns the loaded source label when a
/// swap happened (for tests / the status line); `None` if nothing was ready.
pub fn apply_loaded_region(&mut self) -> Option<String> {
let geo = self.region_picker.take_loaded()?;
let label = geo.source.label().to_string();
let v2 = osm_tabs::osm2d_view(&geo);
let v3 = osm_tabs::osm3d_view(&geo);
self.deck.replace_facet(osm_tabs::TITLE_2D, Box::new(v2));
self.deck.replace_facet(osm_tabs::TITLE_3D, Box::new(v3));
Some(label)
}
/// Forward a node selection to the boxed `syschart` facet by id — the typed
/// path the robot-UI node-select toolbar uses (the chart's own canvas
/// click-select is position-based; this gives a label-addressable control).
/// Returns the chart's selection after the call (so a test can assert it).
pub fn select_syschart_node(&mut self, id: &str) -> Option<String> {
if let Some(chart) = self.deck.facet_mut::<facett::syschart::SystemChart>("syschart") {
chart.select(id);
chart.selected().map(|s| s.to_string())
} else {
None
}
}
/// Make the deck tab titled `title` active — the programmatic (robot-/CLI-
/// addressable) equivalent of clicking its tab header. Returns `true` if such a
/// tab exists. The named boundary the control channel switches tabs through.
pub fn set_active_tab(&mut self, title: &str) -> bool {
// A programmatic tab switch is a "take control" event (the robot/control-channel
// equivalent of clicking a tab header) — cancel any running auto-demo + reset its
// idle timer, mirroring "a tab-click cancels". The auto-demo's own advances go
// straight through the deck (`attract_step`), never this seam, so they don't
// self-cancel.
self.attract.interrupt();
self.deck.set_active_by_title(title)
}
/// **The generator deep link**: `gen=<source>[:<size>[:<seed>]]` (e.g.
/// `sbm:100000:1`) for the ACTIVE tab — the URL query `?gen=…` on the web page
/// (`…/facett/?gen=community:100000#Graphs`), `$FACETT_DEMO_GEN` natively. It goes
/// through the tab's own `update_json({"gen":…})`, so a tab without the size +
/// generator control ignores it. Returns `false` on a malformed value.
pub fn apply_gen_link(&mut self, spec: Option<&str>) -> bool {
// The one parser of the link shape, shared with facet-wrapped's `{"generate": …}` seam.
let Some(g) = spec.and_then(graphgen::gen_link_json) else { return false };
let Some(title) = self.deck.active_title().map(str::to_string) else { return false };
self.deck.update_json_for(&title, &serde_json::json!({ "gen": g }).to_string())
}
/// The **native deep-link**: open a named tab at startup, the desktop twin of the
/// wasm `…/facett/#OSM 3D` URL fragment that `main.rs`'s `wasm_start` already
/// honours. `want` is the raw `$FACETT_DEMO_TAB` value; returns `true` when a tab
/// with that title existed and is now active.
///
/// It exists for the same reason the wasm fragment does — "so the headless
/// screenshotter (and a shared link) can land on any tab without clicking" — and
/// the native side was the half that never got it. Landing on a specific tab from
/// a cold start is the only way to *show* a tab on a Wayland session, where
/// synthetic pointer injection is not reliably available: proven on oden, where
/// `ydotool`'s absolute mode lands every click in the top-left corner and the
/// compositor exposes no cursor to screenshot back, so there is no closed loop to
/// aim one. It also reaches the tabs the auto-demo tour never visits — the tour
/// rotates a fixed showcase subset, so `GFX v2` is unreachable without this.
///
/// Blank/whitespace is treated as "not set" rather than as a tab named `""`, so an
/// exported-but-empty env var behaves like an absent one. Delegates to
/// [`set_active_tab`](Self::set_active_tab) — no second tab-lookup path (LAW #5).
/// On success the auto-demo tour is **pinned off** for the session
/// ([`AttractMode::pin`]) — otherwise the deep-link lasts all of
/// [`attract::IDLE_ARM_SECS`] (5 s) before `TOUR[0]` replaces it, which is not
/// long enough to be worth having.
pub fn apply_initial_tab(&mut self, want: Option<&str>) -> bool {
match want.map(str::trim).filter(|s| !s.is_empty()) {
Some(title) => {
let hit = self.set_active_tab(title);
if hit {
self.attract.pin();
}
hit
}
None => false,
}
}
/// Advance the **auto-demo / attract mode** (#82) one frame: tick its idle/dwell
/// clock by `dt` seconds given whether the user `interacted` this frame, applying
/// any tour tab-switch to the deck. Returns the tab it switched to (if any).
///
/// This is the SHARED path — [`draw_app`](Self::draw_app) calls it every frame with
/// the real `ctx.input` `dt` + interaction (so the kiosk loop runs on BOTH native
/// and wasm, same program), and the headless robot test drives it directly with
/// synthetic values (so the exact live code path is asserted). Programmatic tab
/// switches never register as interaction (they emit no egui input events), so the
/// auto-demo's own advances don't cancel it — only a human does.
pub fn attract_step(&mut self, dt: f32, interacted: bool) -> Option<&'static str> {
match self.attract.update(dt, interacted) {
attract::AttractAction::Switch(title) => {
self.deck.set_active_by_title(title);
Some(title)
}
attract::AttractAction::None => None,
}
}
/// Activate a `Theme::ALL` palette by name (fuzzy: `-`/`_`/space interchange,
/// like `Theme::by_name`). Returns the resolved index, or `None` if unknown.
/// Drives the same deck palette override the toolbar's picker pins.
pub fn set_palette_named(&mut self, name: &str) -> Option<usize> {
self.deck.fx_mut().set_palette_named(name)
}
/// Switch the `LookTheme` preset by name (`"windows-dark"`, `"macos-light"`,
/// `"device"`; same fuzzy match). Returns the resolved preset name, or `None`
/// if unknown (leaving the current preset unchanged). The named-preset
/// counterpart of [`set_look_preset`](Self::set_look_preset).
pub fn set_look_named(&mut self, name: &str) -> Option<String> {
let norm = |s: &str| s.to_ascii_lowercase().replace(['-', ' '], "_");
let want = norm(name);
let idx = LookTheme::preset_names().iter().position(|n| norm(n) == want)?;
Some(self.set_look_preset(idx))
}
/// The active **platform** the current look preset presents as — derived from
/// the preset's [`NativeFeel`](facett::look::NativeFeel), so the toggle always
/// reflects what's installed. The runtime equivalent of `cfg!(target_os)`.
pub fn active_platform(&self) -> Platform {
self.look_theme().native.platform
}
/// Switch to the **platform-adaptive native-feel** preset for `p` (Mac →
/// macOS-dark, Windows → Windows-dark, Neutral → neutral-dark) — the demo's
/// runtime override that makes the native cues demonstrable on any host OS.
/// Returns the resolved preset name.
pub fn set_platform(&mut self, p: Platform) -> String {
let want = LookTheme::for_platform(p).name;
let idx = LookTheme::preset_names().iter().position(|n| *n == want).unwrap_or(self.look_preset);
self.set_look_preset(idx)
}
/// Cycle Mac ⇄ Windows ⇄ Neutral (the toolbar toggle / robot `platform:cycle`).
pub fn cycle_platform(&mut self) -> String {
let next = self.active_platform().cycle();
self.set_platform(next)
}
/// Apply one robot-drive [`DemoCommand`](control::DemoCommand) to the live demo
/// — switch the active tab and/or palette and/or look preset, fire a command id,
/// and/or select a syschart node, in that fixed order. Unknown tab/palette/look
/// names are recorded in `last_command` as `err:<what>:<name>` so a state-reading
/// driver can see the rejection (the demo never panics on a bad command). Exposed
/// `pub` so the inject-assert test drives the exact same code path the live
/// control channel runs, without touching files. Mirrors the viz's `apply_command`.
pub fn apply_command(&mut self, cmd: &DemoCommand) {
if let Some(title) = &cmd.tab {
if self.set_active_tab(title) {
self.last_command = Some("cmd.tab".to_string());
} else {
self.last_command = Some("err:tab".to_string());
}
}
if let Some(name) = &cmd.palette {
match self.set_palette_named(name) {
Some(_) => self.last_command = Some("cmd.palette".to_string()),
None => self.last_command = Some("err:palette".to_string()),
}
}
if let Some(name) = &cmd.look {
match self.set_look_named(name) {
Some(_) => self.last_command = Some("cmd.look".to_string()),
None => self.last_command = Some("err:look".to_string()),
}
}
if let Some(name) = &cmd.platform {
// `"cycle"` advances Mac ⇄ Windows ⇄ Neutral; otherwise resolve a
// platform label (fuzzy). Unknown names are recorded, never panic.
if name.eq_ignore_ascii_case("cycle") {
self.cycle_platform();
self.last_command = Some("cmd.platform".to_string());
} else if let Some(p) = Platform::from_label(name) {
self.set_platform(p);
self.last_command = Some("cmd.platform".to_string());
} else {
self.last_command = Some("err:platform".to_string());
}
}
if let Some(id) = &cmd.command {
// `run_command` takes a `&'static str` (its match arms); resolve the
// incoming string to the known set so the live channel can fire them.
match KNOWN_COMMANDS.iter().find(|&&c| c == id) {
Some(&known) => self.run_command(known),
None => self.last_command = Some("err:command".to_string()),
}
}
if let Some(node) = &cmd.select_node {
// Make syschart active so the selection is visible, then forward it.
self.set_active_tab("syschart");
if self.select_syschart_node(node).is_some() {
self.last_command = Some("cmd.select_node".to_string());
} else {
self.last_command = Some("err:select_node".to_string());
}
}
}
/// Robot-UI drive-half: poll the control channel (`$FACETT_DEMO_CMD`) once and
/// apply any pending [`DemoCommand`]. The command file is consumed (removed) by
/// [`control::take_pending`] so each command fires exactly once; the effect then
/// shows up in the next [`dump_state`](Self::dump_state). A malformed payload is
/// recorded in `last_command` as `err:parse`, never a panic. Called per frame by
/// the binary. Returns `true` if a command was applied.
pub fn poll_control_channel(&mut self) -> bool {
match control::take_pending() {
Some(Ok(cmd)) => {
self.apply_command(&cmd);
true
}
Some(Err(_e)) => {
self.last_command = Some("err:parse".to_string());
true
}
None => false,
}
}
/// Write the whole demo's [`state_json`](Self::state_json) to
/// `$FACETT_DEMO_STATE` so an external driver sees what the demo shows (every
/// control + tab + folded-in component state). The read-half of the channel.
pub fn dump_state(&self) {
let _ = control::dump_state(&self.state_json());
}
/// Draw the **whole demo UI** — the toolbar (palette picker + glow / raven /
/// commands / legend) and the deck of every component — into `ui`. This is the
/// one render path shared by the eframe binary and the robot-UI test, so the
/// robot drives exactly what a human sees. When the `syschart` tab is active it
/// also draws a small **node-select toolbar** (one selectable per node id) so
/// the chart's selection is robot-addressable by label.
/// Pump the live [`Sim`](sim::Sim) ONE frame: advance its throttle and, on a tick,
/// push the fresh self-contained population into the in-deck facets IN PLACE — the
/// warehouse gains rows, the icebergview gains a snapshot (time-travel accrues),
/// and the `Sverige` population tab re-takes the ticking [`PopGraph`] so its linked
/// panes animate. Returns `true` on a frame where a tick fired (the caller asks for
/// a repaint so the gallery keeps breathing). Split out of [`draw_app`](Self::draw_app)
/// so the inject-assert test drives the exact same wiring headlessly.
pub fn pump_sim(&mut self) -> bool {
if !self.sim.maybe_tick() {
return false;
}
self.feed_facets_from_sim();
true
}
/// Push the sim's current data into the warehouse / icebergview / Sverige facets
/// (the shared body of [`pump_sim`](Self::pump_sim) + the initial seed). Each
/// `facet_mut` downcast is a no-op if that facet isn't present, so this never
/// panics if a tab was removed.
pub fn feed_facets_from_sim(&mut self) {
let table = self.sim.table().clone();
if let Some(wh) = self.deck.facet_mut::<WarehouseTableView>("warehouse") {
wh.set_table(sim::TABLE, table);
// Focus the LIVE table so the growing population is what the user sees
// (the seeded `local()` demo tables stay available in the left picker).
wh.select(sim::TABLE);
}
let snaps = self.sim.iceberg_snapshots();
if let Some(ice) = self.deck.facet_mut::<IcebergView>("icebergview") {
ice.set_snapshots(snaps);
}
let pop = self.sim.pop().clone();
if let Some(tab) = self.deck.facet_mut::<pop_tab::PopGraphTab>(pop_tab::TITLE) {
tab.set_sample(pop);
}
}
/// The demo's control strip — an optional title + fps readout, the look/native/palette
/// pickers, the deck-fx + region buttons, and (when the syschart tab is active) the
/// label-addressable node-select toolbar. Split out of [`draw_app`](Self::draw_app) so
/// the responsive fold (#30) can hide it behind the `⚙` drawer on a phone-class viewport
/// (canvas-first) while it stays permanently shown on tablet/desktop. On tablet/desktop
/// `heading == true` reproduces the original toolbar exactly (title + fps lead the first
/// row); on the phone drawer the title/fps already live in the compact header, so it is
/// passed `false`.
fn draw_control_strip(&mut self, perf_ms: f32, perf_fps: f32, heading: bool, ui: &mut egui::Ui) {
let perf_col = if perf_ms <= 16.6 {
egui::Color32::from_rgb(120, 220, 140)
} else {
egui::Color32::from_rgb(230, 190, 90)
};
ui.horizontal_wrapped(|ui| {
if heading {
ui.heading("facett-demo");
// The build stamp (`v<ver> (<git hash>)`) — the SAME string
// `facett-demo --version` prints, so the gallery names WHICH build is
// running (the nordisk LAW). Weak, next to the title; full line on hover.
ui.weak(format!("v{} ({})", env!("CARGO_PKG_VERSION"), env!("FACETT_GIT_HASH")))
.on_hover_text(version_line());
ui.separator();
ui.colored_label(perf_col, format!("{perf_ms:.1} ms · {perf_fps:.0} fps"));
ui.separator();
}
// The look-&-feel preset switcher (Windows/macOS/Device, light+dark).
ui.label("look:");
let names = LookTheme::preset_names();
for (i, name) in names.iter().enumerate() {
// COH-1: drive the selector's own colours from the PUBLISHED facett
// palette, not whatever raw egui `Visuals` happen to be installed —
// `selectable_label` paints from `Visuals` and would render with
// mismatched/garbled colours across preset switches.
if themed_preset_label(ui, self.look_preset == i, name).clicked() {
self.set_look_preset(i);
}
}
ui.separator();
// The PLATFORM-ADAPTIVE native-feel toggle (Mac ⇄ Windows ⇄ Neutral):
// jumps to that platform's preset so its native cues (Mica/vibrancy
// material, snappy/spring motion curve, reveal-highlight vs rubber-band,
// focus ring vs rectangle, traffic-lights vs caption side) are
// demonstrable on ANY host OS. Auto-detected at startup; overridden here.
ui.label("native:");
let active_platform = self.active_platform();
for p in [Platform::Mac, Platform::Windows, Platform::Neutral] {
let label = match p {
Platform::Mac => "macOS",
Platform::Windows => "Windows",
Platform::Neutral => "Neutral",
};
if themed_preset_label(ui, active_platform == p, label).clicked() {
self.set_platform(p);
}
}
});
// The legacy per-facet palette deck (kept; the look bridge already re-themed the
// components — this exercises the old palette path too). ITS OWN ROW: the picker is
// a `horizontal_wrapped` of its own, and nested at the tail of the row above it
// wrapped into a one-item-wide COLUMN whenever that row nearly filled the width —
// measured in the pages bundle at 1600 px (2026-10-01): the strip 1 680 px tall and
// every tab's canvas pushed below the window.
self.deck.palette_picker(ui);
ui.horizontal_wrapped(|ui| {
let glow = self.deck.fx().glow;
if ui.selectable_label(glow, "glow").on_hover_text("Bloom the active facet").clicked() {
self.run_command("fx.glow");
}
if ui.button("send raven").clicked() {
self.run_command("raven.send");
}
if ui.button("commands").clicked() {
self.menu.open();
}
if ui.selectable_label(self.show_about, "legend").clicked() {
self.run_command("about.toggle");
}
// 🌍 Region — opens the region-picker modal (clip a region out of the
// cached planet via osm-katana and reload the OSM 2D/3D tabs).
self.region_picker.button(ui);
});
// When the syschart tab is active, expose a label-addressable node-select
// toolbar so the robot can drive (and assert) the chart's selection.
// `active_title()` — an O(1) field read. This used to be
// `self.deck.state_json()["active"] == "syschart"`, which built the WHOLE
// app's observable state EVERY FRAME — all ~27 facets' `state_json()` AND
// their `caps()` — to compare one string. Those are not cheap getters:
// `MapView::state_json` derives its layer breakdown from the geometry,
// `GeoMap::bbox` walks every point, and the graph facets run `schema()`,
// `edge_indices()` and betweenness CENTRALITY. A live `perf record` of the
// running demo on the OSM 2D tab put roughly a third of the frame in this
// one line and the malloc / memcmp / BTreeMap<String, Value> traffic behind
// it — paid on every tab, whichever tab you are looking at.
if self.deck.active_title() == Some("syschart") {
ui.horizontal_wrapped(|ui| {
ui.label("select node:");
let ids: Vec<&'static str> = data::SUBSYSTEMS.iter().map(|(l, _)| *l).collect();
let selected = self
.deck
.facet_mut::<facett::syschart::SystemChart>("syschart")
.and_then(|c| c.selected().map(|s| s.to_string()));
for id in ids {
let is_sel = selected.as_deref() == Some(id);
// Prefix the control label so it's unambiguous in the AccessKit
// tree (the chart canvas paints the bare node name too).
if ui.selectable_label(is_sel, format!("node:{id}")).clicked() {
self.select_syschart_node(id);
}
}
});
}
// ── The ACTIVE demo's SOURCE link(s) — per demo, not a startup list ───────
// Each tab names its own source right here (derived from the
// LEGEND's crate credits via `source_links`); the About popup no longer
// opens at start with the aggregated roster.
if let Some(active) = self.deck.active_title().map(str::to_owned) {
let links = source_links(&active);
if !links.is_empty() {
ui.horizontal_wrapped(|ui| {
ui.weak("source:");
for (dir, url) in links {
ui.hyperlink_to(dir, url);
}
});
}
}
}
pub fn draw_app(&mut self, ui: &mut egui::Ui) {
let ctx = ui.ctx().clone();
// Frame-budget meter: fold this frame's wall-time into the smoothed readout
// BEFORE the work, so the toolbar/`state_json` report the real cost of the
// demo's own per-frame path (the number the lag fix has to move).
self.perf.tick(ctx.input(|i| i.stable_dt));
self.menu.handle_hotkeys(&ctx);
// ── The live, ticking simulation: pump one frame. On a tick the growing
// self-contained population is pushed into the warehouse / icebergview /
// Sverige facets (data flows, snapshots accrue, the pop graph animates), and
// we request a repaint so the gallery keeps breathing between ticks. ─────────
if self.pump_sim() {
ctx.request_repaint();
} else {
// Even off-tick, keep the frame clock running so the next tick lands on
// time (the throttle counts repaint frames).
ctx.request_repaint_after(std::time::Duration::from_millis(16));
}
// ── AUTO-DEMO / attract mode (#82) ────────────────────────────────────────────
// Detect any REAL user interaction this frame from egui's input (pointer move /
// press / scroll / key / text / touch) — NO wall-clock (`Date::now`), so it is
// wasm-safe and shared by native + web. A programmatic tour tab-switch emits no
// input event, so the auto-demo's own advances never read as interaction. Then
// tick the idle/dwell clock by the real frame `dt` (`stable_dt`, the same source
// the perf meter uses): after `IDLE_ARM_SECS` idle it arms and auto-advances the
// deck through the showcase tour, looping forever; a human cancels it instantly.
// Gated on `attract_enabled` (the live binary turns it on for BOTH native + wasm)
// so a headless robot drive of `draw_app` stays deterministic.
if self.attract_enabled {
let interacted = ctx.input(|i| {
i.pointer.is_moving()
|| i.pointer.any_down()
|| i.smooth_scroll_delta != egui::Vec2::ZERO
|| i.events.iter().any(|e| {
matches!(
e,
egui::Event::PointerMoved(_)
| egui::Event::PointerButton { .. }
| egui::Event::Key { .. }
| egui::Event::Text(_)
| egui::Event::MouseWheel { .. }
| egui::Event::Touch { .. }
| egui::Event::Paste(_)
)
})
});
let dt = ctx.input(|i| i.stable_dt);
if self.attract_step(dt, interacted).is_some() {
// A tab just switched under auto-demo — keep the frames coming so the
// loop stays on cadence even if the sim is off-tick.
ctx.request_repaint();
}
}
// Drive the NEW unified look & feel: install the active preset's full
// `egui::Style` + publish its derived legacy palette so every component
// re-themes coherently (COH-1). This is the demo's reason to exist as the
// reference for `look::Theme` — see `.nornir/look-and-feel.md`.
self.apply_look(&ctx);
// Frame-time / FPS readout — the demo's smoothness, measured. Green ≤ 16.6 ms
// (a smooth 60 fps), amber otherwise, so a regression back into per-frame
// stutter is visible at a glance.
let (perf_ms, perf_fps) = (self.perf.ms, self.perf.fps());
// ── Responsive control-strip FOLD (#30) ──────────────────────────────────────
// On a PHONE-class (narrow) viewport the full control strip (look / native /
// palette pickers + the fx + region buttons) wraps into many rows and shoves the
// demo CANVAS below the phone screen fold — the deployed wasm demo's bug. Classify
// the viewport width through the SHARED scene machinery (facett_app::scene::Device,
// the same classification `scene::render` applies to a Scene's panes) and, when
// it is phone-class, FOLD the whole strip behind a `⚙` drawer so the canvas gets
// the screen (canvas-first; the drawer defaults CLOSED). On tablet/desktop the
// strip renders EXACTLY as before (title + fps lead the first row) — the phone
// fold is a strict addition, leaving the wide-viewport toolbar byte-for-byte
// unchanged (so the robot deck-drive keeps hitting the same widgets).
let vp = ui.max_rect();
let device = facett_app::scene::Device::from_width(ui.available_width());
let phone = device == facett_app::scene::Device::Phone;
let controls_folded = phone && !self.controls_open;
// ── navgraph (dev mode) ───────────────────────────────────────────────────────
// The demo DECLARES its navigation order once (`demo_nav_order`), so the spine
// (app → controls / deck → tabs / pane) numbers `1`, `1.1`, `1.2.1.3` with no
// `~` or `?`. `Ctrl+Shift+G` cycles the overlay, `Ctrl+G 1.2 Enter` focuses.
if !navgraph::has_order(&ctx) {
navgraph::set_order(&ctx, demo_nav_order());
}
let nav_app = navgraph::begin(ui, "facett-demo", NavKind::App, "facett-demo");
let nav_controls = navgraph::begin(ui, "controls", NavKind::Group, "controls");
let controls_top = ui.cursor().top();
if phone {
// The compact phone header: title + live fps + the `⚙` drawer toggle.
let perf_col = if perf_ms <= 16.6 {
egui::Color32::from_rgb(120, 220, 140)
} else {
egui::Color32::from_rgb(230, 190, 90)
};
ui.horizontal_wrapped(|ui| {
ui.heading("facett-demo");
ui.separator();
ui.colored_label(perf_col, format!("{perf_ms:.1} ms · {perf_fps:.0} fps"));
ui.separator();
let (glyph, hint) = if self.controls_open {
("⚙ ✕", "hide controls — give the canvas the screen")
} else {
("⚙", "show controls (look · native · palette · fx · region)")
};
if ui.selectable_label(self.controls_open, glyph).on_hover_text(hint).clicked() {
self.controls_open = !self.controls_open;
}
});
// Only when the drawer is open (default CLOSED ⇒ canvas-first).
if self.controls_open {
self.draw_control_strip(perf_ms, perf_fps, false, ui);
}
} else {
// Tablet/desktop: the full strip, title + fps leading — the original toolbar.
self.draw_control_strip(perf_ms, perf_fps, true, ui);
}
let controls_rect = egui::Rect::from_min_max(egui::pos2(vp.left(), controls_top), egui::pos2(vp.right(), ui.cursor().top()));
navgraph::end(ui, nav_controls, controls_rect);
ui.separator();
// Record the responsive shell facts + how much of the viewport HEIGHT the canvas
// (the deck below) gets — the DATA a headless test reads to prove the phone fold.
let canvas_top = ui.cursor().top();
let h = vp.height().max(1.0);
self.shell = ShellReport {
device: device.tag(),
controls_folded,
canvas_top_frac: ((canvas_top - vp.top()) / h).clamp(0.0, 1.0),
canvas_height_frac: ((vp.bottom() - canvas_top) / h).clamp(0.0, 1.0),
};
// ── Responsive MAIN-MENU collapse ────────────────────────────────────────────
// The deck's tab bar is the demo's MAIN menu — one selectable per facet (60+).
// On a phone-class viewport it wraps into many rows and buries the facett graph
// below the fold (the owner's bug). Drive the SAME device classification into the
// deck so, on a phone width, the tab bar collapses to a `≡` hamburger + drawer
// (canvas-first); tablet/desktop keep the inline wrapped bar unchanged.
self.deck.set_menu_collapsed(phone);
let nav_deck = navgraph::begin(ui, "deck", NavKind::Surface, "deck");
self.deck.ui(ui);
let deck_rect = egui::Rect::from_min_max(egui::pos2(vp.left(), canvas_top), egui::pos2(vp.right(), ui.min_rect().bottom()));
navgraph::end(ui, nav_deck, deck_rect);
navgraph::end(ui, nav_app, vp);
// ── AUTO-DEMO indicator (#82) ─────────────────────────────────────────────────
// A subtle, non-interactive "auto-demo · move to take control" chip floating at
// the bottom-centre while the kiosk loop runs, so a human knows why the tabs are
// advancing and how to stop them. Drawn from the PUBLISHED facett palette so it
// re-themes with the deck; hidden the instant the auto-demo is cancelled.
if self.attract_enabled && self.attract.running() {
let tab = self.attract.current_tab().unwrap_or("");
egui::Area::new(egui::Id::new("facett_attract_indicator"))
.anchor(egui::Align2::CENTER_BOTTOM, egui::vec2(0.0, -20.0))
.interactable(false)
.order(egui::Order::Tooltip)
.show(&ctx, |ui| {
let pal = facett::theme(ui);
egui::Frame::new()
.fill(pal.panel_bg.gamma_multiply(0.92))
.stroke(egui::Stroke::new(1.0_f32, pal.accent.gamma_multiply(0.55)))
.corner_radius(egui::CornerRadius::same(9))
.inner_margin(egui::Margin::symmetric(12, 6))
.show(ui, |ui| {
ui.horizontal(|ui| {
ui.colored_label(pal.accent.gamma_multiply(0.9), "●");
ui.colored_label(
pal.text.gamma_multiply(0.85),
format!("auto-demo · move to take control · {tab}"),
);
});
});
});
}
// ── The About / DIAGNOSTICS popup (the `legend` toggle) ───────────────────
// A real floating window (the former `show_about` flag had no rendered panel).
// Shows the WHICH-build stamp (version + git hash — the nordisk LAW), a
// SOURCE LINK per facet-component, and a compact error-code summary
// pointing at the Errors + Manual tabs. Gated on `show_about`.
if self.show_about {
let mut open = self.show_about;
egui::Window::new("facett-demo · About / Diagnostics")
.open(&mut open)
.collapsible(true)
.resizable(true)
.default_width(560.0)
.show(&ctx, |ui| {
use facett::core::errcode;
// The build stamp — git hash (which build is running).
ui.strong(version_line());
ui.weak(format!("git hash: {}", env!("FACETT_GIT_HASH")));
ui.separator();
ui.strong(format!(
"facett UI error-code registry — {} codes across {} components",
errcode::registry().len(),
errcode::components().len()
));
ui.label("Open the “Errors” tab for the full pink-code matrix slide, and “Manual” for the in-app manual.");
ui.separator();
// A SOURCE LINK per facet-component (first code per slug).
// The primary source surface is the per-demo `source:` row on
// each tab (`source_links`); this aggregated roster is the
// opt-in diagnostics view, no longer shown at start.
ui.strong("Source per facet-component:");
egui::ScrollArea::vertical().max_height(240.0).show(ui, |ui| {
for comp in errcode::components() {
if let Some(e) = errcode::registry().iter().find(|e| e.component == comp) {
ui.horizontal(|ui| {
ui.monospace(format!("facet-{comp}"));
ui.hyperlink_to(
format!("{}/{}", e.crate_dir, e.src_file),
e.source_url(),
);
});
}
}
});
});
self.show_about = open;
}
// The region-picker modal (polls the worker + lists presets). When a region
// finishes loading, swap its geometry into the OSM tabs in place.
self.region_picker.ui(&ctx);
self.apply_loaded_region();
// The command-palette overlay (drawn last). Route any invocation through the
// same command sink the toolbar uses.
let commands = std::mem::take(&mut self.commands);
if let Some(id) = self.menu.ui(&ctx, &commands) {
self.run_command(&id);
}
self.commands = commands;
}
}
/// **The demo's declared navigation order** (navgraph phase 1). The spine is declared
/// here; the group row and the tab strip declare themselves (their draw order is the
/// group order and the deck's facet order inside the active group); the active pane
/// takes the deck's wildcard slot, because its key changes with the tab. Since the
/// two-level bar (2026-10-01) the group row is `1.2.1`, the tabs `1.2.2`, the pane `1.2.3`.
/// Fields INSIDE a pane belong to its component and stay `~` until that component
/// declares them (design §6, phase 3).
pub fn demo_nav_order() -> NavOrder {
NavOrder::new()
.roots(["facett-demo"])
.children("facett-demo", ["controls", "deck"])
.children("facett-demo.deck", ["groups", "tabs", navgraph::WILDCARD])
}
/// Paint one **look-preset selector button** whose colours come from the
/// **published facett palette** ([`facett::theme`]), not from the raw egui
/// `Visuals`.
///
/// `ui.selectable_label` samples `Visuals` (selection fill, widget fg) — but the
/// look presets install their own complete `Style`, and after a preset switch the
/// derived palette (COH-1, `.nornir/look-and-feel.md`) is the authoritative colour
/// source. Painting the selector from `Visuals` made these very buttons render
/// with mismatched/garbled colours across windows/macos/device × light/dark.
///
/// We allocate a button-sized rect and paint it ourselves from the palette:
/// - **selected** → `accent` fill with the panel ground as the text colour (the
/// accent is a saturated role, so the dark panel reads legibly on it);
/// - **unselected / hover** → the panel ground with `text` (hover lifts toward
/// `accent`).
///
/// Text colour is chosen for the better WCAG contrast against the actual fill
/// (`facett::look::contrast_ratio`), so the label stays legible on every preset.
fn themed_preset_label(ui: &mut egui::Ui, selected: bool, text: &str) -> egui::Response {
let pal = facett::theme(ui);
let galley =
ui.painter().layout_no_wrap(text.to_owned(), egui::FontId::proportional(14.0), pal.text);
let pad = egui::vec2(8.0, 3.0);
let desired = galley.size() + pad * 2.0;
let (rect, resp) = ui.allocate_exact_size(desired, egui::Sense::click());
if ui.is_rect_visible(rect) {
let hovered = resp.hovered();
let (fill, fg) = preset_label_colours(&pal, selected, hovered);
let rounding = egui::CornerRadius::same(4);
ui.painter().rect_filled(rect, rounding, fill);
if selected || hovered {
ui.painter().rect_stroke(
rect,
rounding,
egui::Stroke::new(1.0_f32, pal.accent),
egui::StrokeKind::Inside,
);
}
let text_pos = rect.center() - galley.size() * 0.5;
ui.painter().galley(text_pos, galley, fg);
}
resp
}
/// Choose the (fill, text) colours for a preset-selector button from the
/// **published facett palette** — the pure, testable core of
/// [`themed_preset_label`]. Selected → `accent` fill; hovered → a tint toward
/// `accent`; else the panel ground. The text colour is whichever of
/// `{text, panel_bg}` reads with the better WCAG contrast against the chosen fill,
/// so the label stays legible on every preset (windows/macos/device × light/dark).
fn preset_label_colours(
pal: &facett::Theme,
selected: bool,
hovered: bool,
) -> (egui::Color32, egui::Color32) {
use facett::look::contrast_ratio;
let fill = if selected {
pal.accent
} else if hovered {
blend(pal.panel_bg, pal.accent, 0.22)
} else {
pal.panel_bg
};
let fg = if contrast_ratio(fill, pal.text) >= contrast_ratio(fill, pal.panel_bg) {
pal.text
} else {
pal.panel_bg
};
(fill, fg)
}
/// Linear-ish blend between two opaque colours (`t` in 0..1 toward `b`). Cheap
/// sRGB-space lerp — good enough for a hover tint.
fn blend(a: egui::Color32, b: egui::Color32, t: f32) -> egui::Color32 {
let lerp = |x: u8, y: u8| (x as f32 + (y as f32 - x as f32) * t).round() as u8;
egui::Color32::from_rgb(lerp(a.r(), b.r()), lerp(a.g(), b.g()), lerp(a.b(), b.b()))
}
/// Build a detail [`Card`] for a vessel — one field per [`FieldValue`] kind so the
/// card (and the form built over it) exercise every widget.
fn vessel_card(v: &Vessel) -> Card {
Card::new(format!("Vessel {}", v.name))
.with_id(format!("vessel-{}", v.id))
.field(CardField::new("Name", FieldValue::Text(v.name.clone())))
.field(CardField::new("Id", FieldValue::Number(v.id as f64)))
.field(CardField::new(
"Status",
FieldValue::Enum { state: v.status.to_string(), options: data::STATUSES.iter().map(|s| s.to_string()).collect() },
))
.field(CardField::new("Captain", FieldValue::User(v.crew.to_string())))
.field(CardField::new("Opened", FieldValue::Date("2021-01-01".to_string())))
.field(CardField::new(
"Tags",
FieldValue::Tags(vec![v.region.to_string(), if v.active { "active" } else { "idle" }.to_string()]),
))
.field(CardField::new(
"Chart",
FieldValue::Link { label: "open dashboard".into(), url: "https://example.invalid/fleet".into() },
))
.field(CardField::new("Mission", FieldValue::Progress(v.progress as f32)))
}
/// The command set for the menu (facett-menu). Fake but plausible app commands.
fn demo_commands() -> Vec<Command> {
vec![
Command::new("palette.cycle", "Cycle palette", "View").shortcut("Ctrl+P").keywords(["theme", "colour"]),
Command::new("fx.glow", "Toggle glow", "View").shortcut("Ctrl+G"),
Command::new("raven.send", "Send raven", "View").keywords(["hugin", "perch"]),
Command::new("about.toggle", "Toggle about / legend", "Help").shortcut("F1"),
Command::new("vessel.new", "New vessel", "Fleet").shortcut("Ctrl+N"),
Command::new("vessel.alert", "Raise alert", "Fleet").keywords(["dispatch"]),
Command::new("export.csv", "Export grid as CSV", "Data").shortcut("Ctrl+E"),
]
}
#[cfg(test)]
mod tests {
use super::*;
/// **Every demo names its source** — each LEGEND entry must yield ≥1 source
/// link out of its own description credits (`source_links`). A new tab whose
/// description forgets its `facett-xxx` credit goes RED here, because a tab
/// with no credit is a tab with no `source:` row on screen. Red-proven: a
/// description stripped of its credit ("service graph (Scene, circular)")
/// yields the empty vec this asserts against.
#[test]
fn every_legend_entry_yields_a_source_link() {
for (key, desc) in LEGEND {
let links = source_links(key);
assert!(
!links.is_empty(),
"LEGEND entry {key:?} credits no facett crate in its description — \
the per-demo `source:` row would render empty. Description: {desc:?}"
);
for (dir, url) in &links {
assert_eq!(
url.as_str(),
format!("https://gunnar.rs/nordisk/facett/blob/main/{dir}"),
"source link for {key:?} must point into facett's tree on gunnar.rs"
);
}
}
// The extraction itself, proven against the failure modes it guards:
assert!(source_links_in("service graph (Scene, circular)").is_empty(), "no credit → no link");
assert_eq!(
source_links_in("via facett_graphview::MetroView over facett-core"),
vec![
("facett-graphview".to_string(), "https://gunnar.rs/nordisk/facett/blob/main/facett-graphview".to_string()),
("facett-core".to_string(), "https://gunnar.rs/nordisk/facett/blob/main/facett-core".to_string()),
],
"module paths credit their crate; underscores normalise to the dashed dir"
);
assert!(source_links("no-such-tab").is_empty(), "unknown key → empty, never a panic");
}
/// **The credited dirs are REAL** — every crate dir a LEGEND credit derives must
/// exist in this workspace, so no `source:` hyperlink can 404 on gunnar.rs. This
/// is the check that catches a typo'd credit ("facett-graphvew") or a crate
/// rename that forgot the LEGEND. Native-only: reads the workspace fs.
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn every_source_link_names_an_existing_crate_dir() {
let ws = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap().to_owned();
for (key, _) in LEGEND {
for (dir, _) in source_links(key) {
assert!(
ws.join(&dir).join("Cargo.toml").is_file(),
"LEGEND entry {key:?} credits {dir:?}, but {dir}/Cargo.toml does not exist \
in the facett workspace — the source link would 404"
);
}
}
}
/// **RED-when-broken — the native `$FACETT_DEMO_TAB` deep-link.** Asserts the
/// APPLIED state (`deck.active_title()`), never just the `bool` return: a
/// `set_active_tab` that answered `true` and switched nothing would pass a
/// return-value check and still leave the screenshotter on the wrong tab.
///
/// Both misses are pinned too, because "fall back to whatever was open" is the
/// whole safety property: an unknown title and a blank value must leave the
/// active tab EXACTLY where it was. If `apply_initial_tab` ever grew a "default
/// to the first tab" branch, or stopped trimming, the unchanged-assertions here
/// go red.
#[test]
fn initial_tab_deep_link_switches_only_on_a_real_title() {
let mut demo = DemoState::new();
let start = demo.deck.active_title().map(str::to_owned);
// A miss must not move the deck.
assert!(!demo.apply_initial_tab(None), "None is not a tab request");
assert_eq!(demo.deck.active_title().map(str::to_owned), start, "None must not switch tabs");
assert!(!demo.apply_initial_tab(Some(" ")), "blank must be treated as unset");
assert_eq!(demo.deck.active_title().map(str::to_owned), start, "blank must not switch tabs");
assert!(
!demo.apply_initial_tab(Some("no such tab, surely")),
"an unknown title must report false"
);
assert_eq!(
demo.deck.active_title().map(str::to_owned),
start,
"an unknown title must leave the active tab untouched"
);
// A hit must actually land, and the surrounding whitespace must be tolerated
// (an env var set from a shell script routinely carries a stray space).
assert!(demo.apply_initial_tab(Some(" GFX v2 ")), "the GFX v2 tab exists");
assert_eq!(
demo.deck.active_title(),
Some("GFX v2"),
"the deep-link must leave GFX v2 ACTIVE, not merely report success"
);
assert_ne!(
demo.deck.active_title().map(str::to_owned),
start,
"the test is vacuous unless GFX v2 differs from the start tab"
);
}
/// **RED-when-broken — the deep-link must SURVIVE the kiosk tour.** This is the
/// bug the feature was born with and the reason `pin()` exists: without it the
/// tour arms after `IDLE_ARM_SECS` (5 s) and swaps the requested tab for
/// `TOUR[0]`, so every screenshot of a deep-linked tab came back showing
/// `System Map` instead. Observed live on oden before the fix.
///
/// Drives the REAL shared path (`attract_step`, the same call `draw_app` makes
/// every frame) with `interacted = false` for a simulated minute — far past both
/// the arm and dwell windows — and asserts the APPLIED tab, not a flag.
#[test]
fn a_deep_linked_tab_survives_the_auto_demo_tour() {
let mut demo = DemoState::new();
assert!(demo.apply_initial_tab(Some("GFX v2")), "precondition: the deep-link lands");
assert!(demo.attract.pinned(), "a landed deep-link must pin the tour off");
// 60 simulated seconds of total idleness: 12 arm windows, 12 dwell windows.
for _ in 0..60 {
assert_eq!(
demo.attract_step(1.0, false),
None,
"a pinned tour must never switch tabs, however long the demo idles"
);
}
assert_eq!(
demo.deck.active_title(),
Some("GFX v2"),
"the deep-linked tab must still be showing after the tour's arm+dwell windows"
);
// CONTROL — the same idling on a deck that was NOT deep-linked DOES get taken
// over. Without this arm the test above could pass on a demo where the tour is
// simply broken, and would prove nothing about `pin`.
let mut untouched = DemoState::new();
let before = untouched.deck.active_title().map(str::to_owned);
let mut switched = false;
for _ in 0..60 {
switched |= untouched.attract_step(1.0, false).is_some();
}
assert!(switched, "control: an un-pinned tour MUST take over when left idle");
assert_ne!(
untouched.deck.active_title().map(str::to_owned),
before,
"control: the un-pinned deck must have been moved off its start tab"
);
}
/// **The two-level bar** (owner 2026-10-01): every deck tab is named by exactly one
/// group in `TAB_GROUPS` (none falls through to the catch-all by being forgotten),
/// every name in the list is a real tab on native (a renamed tab fails here, BY
/// NAME), the graph tabs sit in "Grafer", and the deck opens on the Grafer group.
#[test]
fn every_deck_tab_is_named_by_exactly_one_group() {
let demo = DemoState::new();
let titles = demo.deck.titles();
for t in &titles {
let n = TAB_GROUPS.iter().filter(|(_, ts)| ts.contains(t)).count();
assert_eq!(n, 1, "deck tab `{t}` is named by {n} groups in TAB_GROUPS (want exactly 1)");
}
#[cfg(not(target_arch = "wasm32"))]
for (g, ts) in TAB_GROUPS {
for t in ts.iter() {
assert!(titles.contains(t), "group `{g}` names `{t}`, which is not a deck tab");
}
}
let names: Vec<&str> = demo.deck.groups().iter().map(|g| g.name.as_str()).collect();
assert_eq!(names, TAB_GROUPS.iter().map(|(n, _)| *n).collect::<Vec<_>>(), "no implicit rest group");
assert_eq!(demo.deck.active_group_name(), Some("Grafer"), "the deck opens on the graphs");
for t in ["Graphs", "lineage", "decograph", "graphpan", "graph3d", "falkor", "World coderoom", "Sverige"] {
let g = demo.deck.groups().iter().find(|g| g.members.iter().any(|&i| titles[i] == t)).unwrap();
assert_eq!(g.name, "Grafer", "`{t}` is a graph tab");
}
let menu = &demo.state_json()["deck"]["menu"];
assert_eq!(menu["group"], "Grafer");
assert_eq!(menu["groups"].as_array().unwrap().len(), TAB_GROUPS.len());
}
/// **Size + generator** (owner 2026-10-01): every graph tab whose component takes a
/// generated model carries the control, opens on its own demo data, and rebuilds
/// through the deck's JSON seam.
#[test]
fn the_graph_tabs_carry_the_size_and_generator_control() {
let mut demo = DemoState::new();
let state = demo.state_json();
for (t, max) in GEN_TABS {
let g = &state["deck"]["facets"][*t]["gen"];
assert_eq!(g["source"], "demo", "`{t}` has the control, opens on its own data, publishes it");
assert_eq!(g["max"], *max, "`{t}` offers up to its ceiling");
assert_eq!(g["compact"], false);
}
let tab = demo
.deck
.facet_mut::<graphgen::GenTab<facett_graphpan::GraphPanView>>("graphpan")
.expect("graphpan is a GenTab over GraphPanView");
assert_eq!(tab.control().source, graphgen::Source::Demo);
assert!(demo.deck.update_json_for("graphpan", r#"{"gen":{"source":"grid","size":1000,"seed":3}}"#));
let s = &demo.state_json()["deck"]["facets"]["graphpan"];
assert_eq!(s["gen"]["nodes"], 1000);
assert_eq!(s["gen"]["builds"], 1);
// The deep link (`?gen=` / `$FACETT_DEMO_GEN`) drives the ACTIVE tab.
assert!(demo.set_active_tab("decograph"));
assert!(!demo.apply_gen_link(Some("tree:many")), "a malformed size is refused");
assert!(demo.apply_gen_link(Some("tree:100:2")));
let g = &demo.state_json()["deck"]["facets"]["decograph"]["gen"];
assert_eq!((g["source"].as_str(), g["nodes"].as_u64(), g["seed"].as_u64()), (Some("tree"), Some(100), Some(2)));
// The Universe tab takes the same link, up to 1M.
assert!(demo.set_active_tab(universe_tab::TITLE));
assert!(demo.apply_gen_link(Some("grid:1000")));
let u = &demo.state_json()["deck"]["facets"][universe_tab::TITLE];
assert_eq!(u["nodes"], 1000);
assert_eq!(u["gen"]["max"], universe_tab::MAX_NODES);
}
/// The four tabs lane E capped at 1 000 (lane G lifted them to 100k, 2026-10-02) take
/// a generated model through their own seam: falkor a Cypher-shaped answer on its
/// runner, the World coderoom a crate graph through its data seam, graph a scene,
/// graphview a kernel model.
#[test]
fn the_four_former_fixture_tabs_take_a_generated_model_through_their_own_seam() {
let mut demo = DemoState::new();
let msg = r#"{"gen":{"source":"sbm","size":1000,"seed":3}}"#;
for t in ["falkor", "World coderoom", "graph", "graphview"] {
assert!(demo.deck.update_json_for(t, msg), "{t} takes the gen message");
let s = &demo.state_json()["deck"]["facets"][t];
assert_eq!(s["gen"]["nodes"], 1000, "{t}: {}", s["gen"]);
assert_eq!(s["gen"]["builds"], 1);
assert_eq!(s["gen"]["max"], 100_000);
}
let s = demo.state_json();
let f = &s["deck"]["facets"]["falkor"];
assert_eq!((f["status"].as_str(), f["node_count"].as_u64()), (Some("ok"), Some(1000)), "the browser ran the generated answer");
assert!(f["query"].as_str().unwrap().starts_with("MATCH (n)-[r]->(m)"));
let c = &s["deck"]["facets"]["World coderoom"];
assert_eq!(c["crate_count"], 1000);
assert_eq!(c["source"], "generated (facett-demo::graphgen)");
assert!(c["breaking_edges"].as_u64().unwrap() > 0, "some compat edges break");
assert_eq!(s["deck"]["facets"]["graph"]["nodes"], 1000);
assert_eq!(s["deck"]["facets"]["graphview"]["nodes"], 1000);
// 1M is above every one of these ceilings: clamped to 100k, never built at 1M.
assert!(demo.deck.update_json_for("graph", r#"{"gen":{"source":"grid","size":1000000}}"#));
assert_eq!(demo.state_json()["deck"]["facets"]["graph"]["gen"]["nodes"], 100_000);
}
/// The generated coderoom's what-breaks set is the SEAM's answer: exactly the
/// consumers whose compat edge to the target breaks.
#[test]
fn the_generated_coderoom_answers_what_breaks_from_its_seam() {
use facett_coderoom_view::CoderoomDataSeam;
let gg = graphgen::generate(graphgen::Source::Gen(facett_graphview::generate::GenKind::ScaleFree), 500, 9).unwrap();
let seam = gen_build::GenCoderoomData::from_generated(&gg);
let edges = seam.compat_edges();
let hit = edges.iter().find(|e| e.compat.is_breaking()).expect("a breaking edge");
let wb = seam.what_breaks(&hit.to).expect("the producer breaks someone");
let want: std::collections::BTreeSet<String> =
edges.iter().filter(|e| e.to == hit.to && e.compat.is_breaking()).map(|e| e.from.clone()).collect();
assert_eq!(wb.impacted_ids(), want);
assert!(edges.iter().filter(|e| !e.compat.is_breaking()).all(|e| !wb.impacted_ids().contains(&e.from) || want.contains(&e.from)));
}
/// LAW (1+6): the demo state is inspectable, and **every** component is
/// present + populated with the fake data. We assert real counts/values, not
/// "didn't panic".
#[test]
fn demo_state_has_every_component_populated() {
let demo = DemoState::new();
let state = demo.state_json();
// The fake dataset is the expected size.
assert_eq!(demo.fleet.len(), data::FLEET);
assert_eq!(state["fleet_len"], data::FLEET);
// Every legend entry is present.
let legend = state["legend"].as_array().unwrap();
assert_eq!(legend.len(), LEGEND.len());
// The deck folded in one state_json per Facet component — assert each by
// its tab title (this is the "every component is rendered" check).
let facets = state["deck"]["facets"].as_object().expect("deck facets map");
// The roster count now lives at module scope (`super::EXPECTED_FACET_COUNT`)
// so `tests/mega_matrix.rs`'s coverage gate derives from the SAME number
// instead of retyping it — see that const's docs.
let expected_titles = [
"graph", "graphview", "Graphs", "Universe", "metro", "Velo L1", "Sverige", "graph3d", "graph3d-given", "rabbit3d", "avocado3d", "depgraph", "pipeline", "synthetic-sdp", "SDP", "brokkr", "runa", "timeline", "plandag", "grid", "table", "card", "form",
"releaseview",
"pop", "plot", "boxplot", "violin", "heatmap", "treemap", "candles", "map", "syschart", "geomap",
"OSM 2D", "OSM 3D", "icebergview", "docview", "log", "stat", "warehouse",
"tabbar", "decograph", "falkor", "matrix", "jobs", "zone", "Jobs", "graphpan", "security", "helix", "dna", "timehelix", "nav", "shell", "overlay", "widgets", "Layers", "primitives",
"console", "Open File", "video", "Git", "warehousedeck", "codevault", "System Map", "scene",
// The 7 flow/workspace widgets reached through the umbrella.
"Bygglov", "Support", "Frister", "Notifications", "Anteckning", "Search", "Attachments",
// The error-code showcase (0b12255): the `Errors` tab over
// `facett_core::errcode::REGISTRY` and the `Manual` tab beside it. These
// two shipped without being added here, which is what left the count
// assertion red at 74-vs-72.
"Errors", "Manual",
// The GFX v2 renderer-track showcase (items 4 / 6 / 8).
"GFX v2",
// The 2026-08-20 roster completion — the workspace components the
// gallery never wired (11 wasm-clean + the 2 native-only decks).
"cicd", "brokkrview", "lineage", "Fleet",
"holger — registry & artifact browser",
"Ingest", "Sign in", "Binary analysis", "World coderoom", "Claude", "About",
"Deploy key", "Build Thing",
// Falsett (facett-flow), 2026-10-04.
"Falsett",
];
for title in expected_titles {
assert!(facets.contains_key(title), "deck is missing the `{title}` facet");
}
// TASK #49: the jera (edda job-engine) surface must read as ONE thing to the
// user — its menu/tab/pane label is "Jobs", never the engine name "jera".
// Red-when-broken: if any user-facing display string reverts to "jera"/"Jera"
// (a deck tab title or a LEGEND/menu key), this fails.
assert!(facets.contains_key("Jobs"), "the jera engine's central pane is labelled `Jobs`");
assert!(
!facets.contains_key("jera") && !facets.contains_key("Jera"),
"no user-facing `jera` tab title — the jera engine name must stay in code only",
);
let legend_keys: Vec<&str> =
state["legend"].as_array().unwrap().iter().map(|v| v.as_str().unwrap()).collect();
assert!(legend_keys.contains(&"Jobs"), "the menu/legend labels the jera surface `Jobs`");
assert!(
!legend_keys.iter().any(|k| *k == "jera" || *k == "Jera"),
"no user-facing `jera` menu/legend label — the jera engine name must stay in code only",
);
// 64 Facet components in the deck (the full gallery incl. the shared zone/jera
// widgets, the `Layers` map-layer toggles, the `codevault` Wreck-It-Ralph
// Code Vault, the 🫧 fluid `System Map`, the `scene` declarative shell, the
// `falkor` FalkorDB graph-DB browser, and the `runa` forge-pipeline DAG).
// A bare count mismatch says "74 vs 72" and nothing else, which is why this
// sat red: the number moved and no one could see WHICH components moved it.
// Name them.
let unlisted: Vec<&str> =
facets.keys().map(String::as_str).filter(|k| !expected_titles.contains(k)).collect();
assert_eq!(
facets.len(),
EXPECTED_FACET_COUNT,
"expected {EXPECTED_FACET_COUNT} Facet components in the deck, found {}. \
Present but not in `expected_titles`: {unlisted:?}. If you added a component, add its \
tab title to that list AND bump the count — the list is what makes this assertion say \
something when it breaks.",
facets.len()
);
// 42 Facet components in the deck (nav + shell + overlay + widgets — the
// facett-app framework tabs — on top of avocado3d + the jobview adoption +
// batch 3a's graphpan + batch 2's tabbar/decograph/matrix + the security view).
// metro: the subway-line test-coverage view — a real two-line map (one fully
// lit/green chain, one with an unlit emitter → red). The proof it drew real
// lines + lit/unlit stations (not an empty shell): the line/green counts, the
// per-line station counts, and the red line's named unlit stop — all in
// state_json. The "green" verdict is honestly false here (the demo map seeds a
// deliberate coverage gap), which is data, NOT an error atom.
// graph3d: the cloud is no longer 140 depth-1 stars. Under `cargo test` the
// budget is GRAPH3D_DEFAULT_NODES = 2000 (the running demo uses 100000, and
// $FACETT_GRAPH3D_NODES overrides both), but the structural properties are the
// same at every size — that is the point of the generator. The big-cloud
// proofs live where they belong: facett-graph3d's `dataset_volume.rs` and
// `cpu_paint_budget.rs` run at 100000.
let g3d = &facets["graph3d"];
let g3d_nodes = g3d["nodes"].as_u64().unwrap();
assert!(
g3d_nodes > 1_000,
"the graph3d cloud carries real DATA, got {g3d_nodes} (it used to be 140 stars)"
);
assert!(
g3d["edges"].as_u64().unwrap() > g3d_nodes,
"…wired with more edges than nodes — chains braided by rungs, not a star forest"
);
// graph3d-given: the HOST-placed twin. The node count proves nothing on its
// own (the force layout would draw a cloud too) — what makes this red when
// the placement is ignored is `given_positions` covering every node, the
// `given` layout being the one on screen, and NO fallback.
let given = &facets["graph3d-given"];
assert_eq!(given["layout"], "given", "the graph3d-given tab is drawn from the host placement");
assert_eq!(
given["given_positions"].as_u64(),
Some(demo_graph3d_given::NODES as u64),
"every traffic sensor is placed by the demo (the host), not by a layout"
);
assert_eq!(given["nodes"].as_u64(), Some(demo_graph3d_given::NODES as u64));
assert!(given["layout_fallback"].is_null(), "a complete placement never degrades to the grid lattice");
assert!(given["error_code"].is_null(), "…and never raises facet-graph3d-5");
let metro = &facets["metro"];
assert_eq!(metro["lines"].as_u64(), Some(2), "metro draws the two-line demo map");
assert_eq!(metro["green_lines"].as_u64(), Some(1), "one of the two lines is fully lit/green");
assert_eq!(metro["green"], false, "the demo map seeds a deliberate unlit-emitter gap");
let metro_lines = metro["line"].as_array().unwrap();
assert_eq!(metro_lines.len(), 2);
assert!(metro_lines[0]["green"].as_bool().unwrap(), "line A (bench) is green");
assert!(metro_lines[0]["station_count"].as_u64().unwrap() >= 4, "line A has its gating stations");
assert!(!metro_lines[1]["green"].as_bool().unwrap(), "line B (docs) is red");
assert_eq!(metro_lines[1]["unlit"].as_array().unwrap().len(), 1, "line B names its one unlit emitter");
// Sverige: THE population showcase — three linked graphs off the knut-popsim
// 11M-person model, with the honest scale + LOD split in state_json. The proof
// it's a real, populated tab (not an empty shell): the analytic 11M stats, the
// three views each with a visible<total LOD split, and the active filter sets.
let sv = &facets["Sverige"];
assert_eq!(sv["component"], "PopGraphTab");
assert!(sv["source"].as_str().unwrap().contains("knut-popsim"), "Sverige is driven by knut-popsim");
assert_eq!(sv["n_persons"].as_u64(), Some(pop_model::CANONICAL_PERSONS), "opens on the canonical 11M");
assert!(sv["stats"]["total_nodes"].as_u64().unwrap() > 50_000_000, "11M people → tens of millions of nodes (analytic)");
let sv_views = sv["views"].as_array().unwrap();
assert_eq!(sv_views.len(), 3, "three linked population views");
let full = sv_views.iter().find(|v| v["view"] == "full").unwrap();
assert!(full["visible_nodes"].as_u64().unwrap() <= pop_tab::RENDER_CAP as u64, "Full view honours the LOD render cap");
assert!(full["total_nodes"].as_u64().unwrap() > full["visible_nodes"].as_u64().unwrap(), "rendering a sample of the whole population");
assert!(sv_views.iter().any(|v| v["view"] == "kommun" && v["visible_nodes"] == 14), "kommun view aggregates to one node per kommun");
assert!(sv["active_labels"].as_array().unwrap().len() == 12, "all 12 labels active by default");
// Velo L1: the L1 vello beauty lane made visible — the SAME scene as L0 vs
// L0+L1. The proof it's a real, observable tab: the 2-category auto-select
// decision + the L0 instance counts ride in state_json (the L1 render
// itself needs the `l1-vello` feature + a GPU, asserted in its own test).
let velo = &facets["Velo L1"];
assert!(velo["render_path"].as_str().unwrap().contains("L1 vello overlay"), "Velo L1 routes L0 + L1 vello");
assert_eq!(velo["category"], "CPU", "demo's Velo L1 tab decides CPU (cpu_only probe)");
assert_eq!(velo["nodes"].as_u64(), Some(velo_l1_tab::NODES as u64));
assert!(velo["l0_quads"].as_u64().unwrap() > velo_l1_tab::NODES as u64, "Velo L1 lowered real L0 SDF quads");
assert!(velo["l0_lines"].as_u64().unwrap() >= velo_l1_tab::NODES as u64, "Velo L1 lowered real L0 SDF edge lines");
// l1_feature mirrors the build; l1_available is false until a GPU render.
assert_eq!(velo["l1_feature"], cfg!(feature = "l1-vello"));
// graphview: the CONSOLIDATED graph engine tab — a real layered mesh routed
// through the shared `facett_core::render` L0 Canvas. The proof it drew the
// mesh through the kernel (not an empty shell): the lowered L0 instance
// counts + a non-blank lit-pixel oracle, both in state_json.
let gv = &facets["graphview"];
assert_eq!(gv["engine"], "facett-graphview");
assert!(gv["render_path"].as_str().unwrap().contains("L0 Canvas"), "graphview routes through the L0 kernel");
assert_eq!(gv["nodes"].as_u64(), Some((graphview_tab::LAYERS * graphview_tab::PER_LAYER) as u64));
assert!(gv["edges"].as_u64().unwrap() > gv["nodes"].as_u64().unwrap(), "graphview has real fan-in/fan-out edges");
assert_eq!(gv["l0_quads"].as_u64(), gv["nodes"].as_u64(), "one L0 marker quad per node");
assert!(gv["l0_lines"].as_u64().unwrap() > 0, "edges lowered to L0 lines");
assert_eq!(gv["navigable"], true, "graphview drives the pan/zoom model");
// synthetic-sdp: ~100 fun-named stages, a valid acyclic DAG with ≥1 source
// and ≥1 sink, animated — the wave's status mix is in its state_json.
let sdp = &facets["synthetic-sdp"];
assert_eq!(sdp["node_count"].as_u64(), Some(synthetic_sdp::STAGES as u64), "synthetic-sdp has ~100 stages");
assert!(sdp["edge_count"].as_u64().unwrap() > synthetic_sdp::STAGES as u64, "synthetic-sdp has real fan-in/fan-out edges");
assert_eq!(sdp["acyclic"], true, "the synthetic SDP DAG is acyclic");
assert!(sdp["source_count"].as_u64().unwrap() >= 1 && sdp["sink_count"].as_u64().unwrap() >= 1);
assert_eq!(sdp["animated"], true, "the synthetic SDP demo animates");
// timeline: the net-new swimlane Gantt — real lanes of time-positioned bars,
// with a phase-segmented task lane (the per-stage task timeline). The proof
// it drew real bars (not an empty shell): the bar count + the segment flag, as
// data; every bar carries a finite start/width fraction over the window.
let timeline = &facets["timeline"];
assert!(timeline["bar_count"].as_u64().unwrap() >= 6, "the timeline laid out executor/job/task bars");
assert!(timeline["lane_count"].as_u64().unwrap() >= 4, "stacked swimlanes");
assert_eq!(timeline["has_segments"], true, "the task lane carries phase segments (the per-stage task timeline)");
// plandag: the net-new metric-bearing operator DAG — the Spark SQL plan /
// Query Profile graph. The proof it's a real, populated tab: every node
// carries a metric body + a cost-heat fraction (as DATA), it advertises the
// AQE initial/final toggle, and a hottest node exists for the active metric.
let plandag = &facets["plandag"];
assert!(plandag["node_count"].as_u64().unwrap() >= 4, "the plan DAG drew operator nodes");
assert_eq!(plandag["has_aqe"], true, "the demo plan has an AQE initial/final pair");
assert_eq!(plandag["metric"], "time", "opens on the Time cost-heat");
let pnodes = plandag["nodes"].as_array().unwrap();
assert!(pnodes.iter().all(|n| n["metrics"]["time_ms"].is_number() && n["heat"].is_number()), "every node carries a metric body + cost-heat as data");
assert!(pnodes.iter().any(|n| (n["heat"].as_f64().unwrap() - 1.0).abs() < 1e-6), "a hottest (heat=1.0) operator exists");
// OSM 2D: a real slippy-map facet — points + ways from the loaded source,
// navigable (zoom-to-cursor + inertial pan), with a camera in state_json.
let osm2d = &facets["OSM 2D"];
assert!(osm2d["points"].as_u64().unwrap() > 0, "the OSM 2D tab loaded points");
assert_eq!(osm2d["navigable"], true, "OSM 2D drives the pan/zoom/fit model");
assert!(osm2d["zoom"].as_f64().unwrap() > 0.0, "OSM 2D camera zoom reported");
// OSM 3D: a real extruded, lit 3D city — triangles + camera pose.
let osm3d = &facets["OSM 3D"];
assert!(osm3d["triangles"].as_u64().unwrap() > 0, "the OSM 3D tab extruded a mesh");
assert!(osm3d["camera"]["distance"].as_f64().unwrap() > 0.0, "OSM 3D camera pose reported");
// rabbit3d: the extruded 3D logo carries a real mesh (vert/tri counts) +
// camera, exposed via state_json (LAW6) — not an empty placeholder.
let rabbit = &facets["rabbit3d"];
assert!(rabbit["triangles"].as_u64().unwrap() > 50, "the 3D rabbit logo is a real extruded mesh");
assert!(rabbit["vertices"].as_u64().unwrap() > 100, "the 3D rabbit logo has real vertices");
assert!(rabbit["spin"].as_bool().unwrap(), "the logo auto-rotates");
// Spot-check that facets carry the fake data, not empty shells.
// grid: 48 rows of fleet.
let grid = &facets["grid"];
assert_eq!(grid["rows"].as_u64().or(grid["nrows"].as_u64()), Some(data::FLEET as u64));
// table: 48 rows too (same batch).
assert_eq!(facets["table"]["rows"], data::FLEET as u64);
// plot: 4 region series.
assert_eq!(facets["plot"]["series"].as_array().unwrap().len(), data::REGIONS.len());
// pop: 4 progress rows, > 0 done.
let pop = &facets["pop"];
assert!(pop["done"].as_u64().unwrap() > 0, "pop has progress");
// The menu carries its commands.
assert_eq!(state["menu"]["count"].as_u64().or(Some(demo.commands.len() as u64)), Some(demo.commands.len() as u64));
assert_eq!(demo.commands.len(), 7);
// Caps map is present (D4 compliance surface) and every facet is themeable.
let caps = state["deck"]["caps"].as_object().expect("deck caps map");
// ONE writer for the gallery size: `caps` and `facets` are the same set seen
// two ways, so they share `EXPECTED_FACET_COUNT` rather than each spelling a
// literal. They were two literals until 2026-08-01, and BOTH went stale
// together when the Errors/Manual tabs landed — a second copy that only ever
// duplicated the first one's bug.
assert_eq!(
caps.len(),
EXPECTED_FACET_COUNT,
"every deck facet advertises caps — {} facets but {} caps entries",
facets.len(),
caps.len()
);
// graphpan advertises navigable (the bare pan/drag + scroll-zoom canvas).
assert_eq!(caps["graphpan"]["navigable"], true, "graphpan advertises navigable (pan/zoom canvas)");
// decograph advertises the navigable capability (pan/drag + scroll-zoom + fit).
assert_eq!(caps["decograph"]["navigable"], true, "decograph advertises navigable (pan/zoom DAG)");
// falkor: the FalkorDB graph-DB browser opens on a real laid-out constellation
// (7 nodes / 7 edges) via a STUBBED in-memory runner — no live redis — and is
// navigable + selectable (pan/zoom + click a node).
let falkor = &facets["falkor"];
assert_eq!(falkor["component"], "FalkorBrowserTab");
assert_eq!(falkor["node_count"], 7, "falkor opens on the demo constellation");
assert_eq!(falkor["edge_count"], 7);
assert_eq!(falkor["status"], "ok", "the stubbed runner answered with no redis");
assert_eq!(falkor["runner"], "in-memory CannedRunner (no redis)");
assert_eq!(caps["falkor"]["navigable"], true, "falkor advertises navigable (pan/zoom)");
assert_eq!(caps["falkor"]["selectable"], true, "falkor advertises selectable (click a node)");
// metro advertises themeable + selectable (a line is drill-in selectable).
assert_eq!(caps["metro"]["selectable"], true, "metro advertises selectable (line drill-in)");
// The two OSM tabs advertise the navigable capability (the new pan/zoom/fit
// cap) — the discovery contract's machine-readable proof they're map views.
assert_eq!(caps["OSM 2D"]["navigable"], true, "OSM 2D advertises navigable");
assert_eq!(caps["OSM 3D"]["navigable"], true, "OSM 3D advertises navigable");
// The graphview tab advertises navigable too (the consolidated engine pans/zooms).
assert_eq!(caps["graphview"]["navigable"], true, "graphview advertises navigable");
// The three later components carry their fake data, not empty shells.
assert_eq!(facets["syschart"]["nodes"].as_array().unwrap().len(), data::SUBSYSTEMS.len());
// geomap: REAL Sigtuna OSM points over the REAL Sigtuna vector basemap,
// with density hotspots binned from the points. The proof the real basemap
// layer is fed + rendered (thousands of ways, not the 5-loop rabbit).
let geomap = &facets["geomap"];
assert!(geomap["points"].as_u64().unwrap() > 1_000, "geomap loaded the real Sigtuna point cloud");
assert!(geomap["ways"].as_u64().unwrap() >= 100, "geomap basemap = the real Sigtuna ways");
// icebergview: now fed by the LIVE in-memory `Sim` — seeded with the tick-0
// population snapshot (the picker GROWS by one per sim tick at runtime), and it
// browses the self-contained `population` table (no skade / catalog).
let ice = &facets["icebergview"];
assert_eq!(ice["snapshots"].as_array().unwrap().len(), 1, "icebergview seeded with the sim's tick-0 snapshot");
assert_eq!(ice["table"], sim::TABLE, "icebergview browses the live population table");
// warehouse: focused on the LIVE population table the sim feeds (the seeded
// demo tables stay in the left picker).
assert_eq!(facets["warehouse"]["selected"], sim::TABLE, "warehouse shows the live population table");
assert!(facets["warehouse"]["preview"]["rows"].as_u64().unwrap() > 0, "the live population table has rows");
// docview: the bundled sample manual parsed to vector SVG pages with a TOC.
// Each page rasterizes on demand (resvg) — the proof the manual is fed +
// parsed (not an empty shell) is the page count + the TOC entries.
let docview = &facets["docview"];
assert_eq!(docview["page_count"].as_u64(), Some(4), "the sample manual has 4 pages");
assert_eq!(docview["toc"].as_array().unwrap().len(), 4, "the sample manual TOC has 4 chapters");
assert_eq!(docview["current_page"], 0, "docview starts on page 0");
assert_eq!(caps["docview"]["scalable"], true, "docview advertises scalable (zoom)");
// map: REAL Sigtuna OSM points over the REAL Sigtuna vector basemap, fed
// through the ways layer (.with_ways + .with_gpu) via Way::from_lonlat. The
// synthetic znippy-rabbit basemap is now only the last-resort fallback when
// no OSM geometry loaded. The proof the real ways layer is fed + rendered.
let map = &facets["map"];
assert!(map["points"].as_u64().unwrap() > 1_000, "map loaded the real Sigtuna point cloud");
assert!(map["ways"].as_u64().unwrap() >= 100, "map basemap = the real Sigtuna ways (not the 5-loop rabbit)");
// security: the supply-chain report view renders a real BLOCK verdict with
// its findings AS DATA (not pixels) — the proof it's a populated pane, not an
// empty shell: the verdict, the worst severity, the KEV tally, and the
// worst-first finding rows (the Critical KEV CVE first) all in state_json.
let sec = &facets["security"];
assert_eq!(sec["verdict"], "BLOCK", "the security view seeds a Block report");
assert_eq!(sec["max_severity"], "CRIT", "worst finding is Critical");
assert_eq!(sec["kev_count"], 1, "one KEV-listed finding");
assert_eq!(sec["findings_total"], 4, "four seeded findings");
let sec_findings = sec["findings"].as_array().unwrap();
assert_eq!(sec_findings.len(), 4, "all four findings ride into state_json as data");
assert_eq!(sec_findings[0]["id"], "CVE-2024-31337", "worst-first: the Critical KEV CVE leads");
assert_eq!(sec_findings[0]["severity"], "CRIT");
assert_eq!(sec_findings[0]["kev"], true);
let sec_ranks: Vec<u64> = sec_findings.iter().map(|f| f["severity_rank"].as_u64().unwrap()).collect();
assert!(sec_ranks.windows(2).all(|w| w[0] >= w[1]), "findings sorted worst-first: {sec_ranks:?}");
}
/// LAW (inject-assert): driving the look-&-feel switcher to every preset sets
/// the demo's `state_json["look"]` to that preset AND `apply` publishes the
/// matching legacy palette name into the context — the COH-1 bridge proven from
/// the demo (the reference app for `look::Theme`). Real input (each preset) →
/// real asserted output (state + published palette), not "didn't panic".
#[test]
fn look_switcher_drives_every_preset_and_publishes_coherent_palette() {
let mut demo = DemoState::new();
let n = LookTheme::PRESETS.len();
assert_eq!(n, 9, "Windows L/D + macOS L/D + Device + Skade Vinter + Neutral + Nordisk L/D");
for i in 0..n {
let name = demo.set_look_preset(i);
// The observable demo state reflects the active preset.
let s = demo.state_json();
assert_eq!(s["look"]["preset"], name, "state_json carries the active preset");
assert_eq!(s["look"]["preset_index"].as_u64(), Some(i as u64));
// `apply` publishes the derived legacy palette into a real context, so
// every existing component follows it (the COH-1 bridge the work order
// requires). We read it back the way components do (`facett::theme(ui)`).
let ctx = egui::Context::default();
demo.apply_look(&ctx);
let mut got = String::new();
ctx.run_ui(egui::RawInput::default(), |ui| {
egui::CentralPanel::default().show(ui, |ui| {
got = facett::theme(ui).name.to_string();
});
}).textures_delta.clear();
assert_eq!(got, name, "the published palette name matches the active preset");
}
// Clamping: an out-of-range index pins to the LAST preset (whatever it is).
let last = demo.set_look_preset(999);
assert_eq!(demo.look_preset, n - 1, "out-of-range clamps to the last preset");
let explicit_last = demo.set_look_preset(n - 1);
assert_eq!(last, explicit_last, "clamp returns the last preset's name");
}
/// Render one full `draw_app` frame on a real context and read back the
/// EFFECTIVE painted colours: the egui `Visuals` window/panel fill + the
/// published legacy palette a component would consume (`facett::theme(ui)`).
/// Pure helper so the regression test can compare two presets frame-to-frame.
fn draw_frame_colours(demo: &mut DemoState) -> (egui::Color32, egui::Color32, String) {
let ctx = egui::Context::default();
let mut palette_name = String::new();
ctx.run_ui(egui::RawInput::default(), |ui| {
egui::CentralPanel::default().show(ui, |ui| {
demo.draw_app(ui);
// Read the palette the way every custom-painted facet does.
palette_name = facett::theme(ui).name.to_string();
});
}).textures_delta.clear();
let v = ctx.global_style().visuals.clone();
(v.window_fill, v.override_text_color.unwrap_or(v.widgets.inactive.fg_stroke.color), palette_name)
}
/// REGRESSION (inject-assert; fails on the bug, passes on the fix): switching
/// the look preset through a FULL `draw_app` frame must change the EFFECTIVE
/// painted colour — both the egui `Visuals` (window fill) AND the published
/// legacy palette a component consumes — not just the size/metrics. The bug:
/// the demo's per-frame deck `palette_picker` auto-pinned a fixed legacy
/// palette on the first frame and `deck.ui` then re-imposed it via `set_theme`
/// (`set_visuals`), clobbering the LookTheme colours each frame while spacing
/// survived — so size changed but colour didn't. We drive two clearly-distinct
/// presets (Windows-light vs Windows-dark) and assert their painted colours
/// differ after the whole frame.
#[test]
fn look_preset_switch_changes_painted_colour_through_full_frame() {
let wl = LookTheme::preset_names().iter().position(|n| *n == "windows-light").expect("windows-light preset");
let wd = LookTheme::preset_names().iter().position(|n| *n == "windows-dark").expect("windows-dark preset");
let mut demo = DemoState::new();
demo.set_look_preset(wl);
let (fill_light, text_light, pal_light) = draw_frame_colours(&mut demo);
demo.set_look_preset(wd);
let (fill_dark, text_dark, pal_dark) = draw_frame_colours(&mut demo);
// (a) The egui Visuals (the window/panel fill the host actually paints)
// must differ between a light and a dark preset.
assert_ne!(
fill_light, fill_dark,
"look preset switch must change the egui window fill through a full frame \
(light={fill_light:?} dark={fill_dark:?}) — something is clobbering the palette after apply()"
);
assert_ne!(text_light, text_dark, "text colour must differ between light and dark presets");
// (b) The published legacy palette a component consumes must follow the
// active preset (not a pinned fixed palette from the deck switcher).
assert_eq!(pal_light, "windows-light", "component consumes the light preset's palette after a full frame");
assert_eq!(pal_dark, "windows-dark", "component consumes the dark preset's palette after a full frame");
// Light preset is light, dark preset is dark — a coarse sanity gate that
// the fill colours weren't just swapped to two arbitrary equal-luma values.
let luma = |c: egui::Color32| 0.299 * c.r() as f32 + 0.587 * c.g() as f32 + 0.114 * c.b() as f32;
assert!(luma(fill_light) > luma(fill_dark), "light preset paints a lighter ground than the dark preset");
}
#[test]
fn deck_cycles_through_every_palette() {
let mut demo = DemoState::new();
// The deck can pin each Theme::ALL palette without panic; cycle wraps.
let n = facett::Theme::ALL.len();
let mut seen = Vec::new();
for _ in 0..n {
seen.push(demo.deck.cycle_palette());
}
assert_eq!(seen, (0..n).collect::<Vec<_>>());
assert_eq!(demo.deck.cycle_palette(), 0, "wraps");
}
/// BUG (preset selector colours): the look-preset selector buttons used
/// `ui.selectable_label`, which paints from the raw egui `Visuals` — so after a
/// preset's `apply` installs a new `Style`, the buttons rendered with garbled
/// colours. The fix drives their colours from the PUBLISHED facett palette
/// (COH-1). This pins the colour-choice core: across EVERY preset
/// (windows/macos/device × light/dark) the selected button fills with the
/// palette `accent`, the unselected button fills with the panel ground, and the
/// chosen text colour stays WCAG-legible (≥ 3:1) on its fill.
#[test]
fn preset_selector_colours_follow_published_palette_and_are_legible() {
use facett::look::contrast_ratio;
for ctor in LookTheme::PRESETS {
let look = ctor();
let pal = look.to_legacy_palette();
let (sel_fill, sel_fg) = super::preset_label_colours(&pal, true, false);
let (unsel_fill, unsel_fg) = super::preset_label_colours(&pal, false, false);
// Selected button draws from the palette accent, unselected from panel.
assert_eq!(sel_fill, pal.accent, "{}: selected fill is the palette accent", pal.name);
assert_eq!(unsel_fill, pal.panel_bg, "{}: unselected fill is the panel ground", pal.name);
// Both states must keep their label legible (UI text ≥ 3:1 WCAG 2.2).
assert!(
contrast_ratio(sel_fill, sel_fg) >= 3.0,
"{}: selected label contrast {:.2} is below 3:1",
pal.name,
contrast_ratio(sel_fill, sel_fg)
);
assert!(
contrast_ratio(unsel_fill, unsel_fg) >= 3.0,
"{}: unselected label contrast {:.2} is below 3:1",
pal.name,
contrast_ratio(unsel_fill, unsel_fg)
);
// Selected vs unselected are visibly distinct (the whole point).
assert_ne!(sel_fill, unsel_fill, "{}: selected and unselected fills differ", pal.name);
}
}
}