nornir viz — the Urðr-Threads master shell as a DECLARATIVE scene — design
Class:
design· Function axis:f0(the master shell as data) ·f1the shell builder ·f2NavState + render wiring ·f3the SceneHost that DRIVES the panes ·f4responsive + drawer ·f5observability + tests.Strict: this file states only the shape we run. The scene lives in
src/viz/shell_scene.rs; the render wiring + host live insrc/viz/app.rs.
f0 — the master shell as DATA
nornir's egui shell (UrdrThreadsApp) is a real master/detail app: a top-bar tab strip, a right "Time machine" inspector, a bottom action-log, and a central active pane. Its master/detail LAYOUT is now described DECLARATIVELY as a [facett_app::scene::Scene] — a menubar + a tree of foldable/draggable/drawer panes
- nav routes — and rendered by
facett_app::scene::renderthrough facett-app's OWN primitives (Router / TabView / facett-menu). The scene reinvents nothing: it DRIVES the existing panes through this app'sSceneHostimpl.
What moved onto the scene (was hand-wired panels):
- the right
SidePanel::right("detail")→ thedetailslot (a foldable pane on the DRAGGABLE master/detail split); - the bottom
TopBottomPanel::bottom("action_log")→ theaction_logslot (a foldable.drawer()that auto-collapses on a phone-class width); - the central
CentralPaneltab dispatch → theactive_paneslot (rendered bydraw_active_pane, preserving the S3cTab::id()AccessKitid_salt).
What deliberately STAYS facett_ui::TabBar (the migration brief permits keeping it as the menubar host): the 28-tab top strip + the workspace/live/🎨-palette pickers and the ℹ About / 🐞 action-log toggles — the stateful chrome that has no clean menu mapping and that the whole existing robot suite drives by label. The scene ADDS an in-window View / Go / Help menubar (facett-menu) with shortcut hints + routes.
f1 — the shell builder (shell_scene.rs)
nornir_shell_scene() (src/viz/shell_scene.rs:80) builds the shell with the Rust builder; the identical value round-trips byte-stable through RON.
root ─ Split(H, 0.74, DRAGGABLE) [phone → Tabs under 760pt]
├─ workspace ─ Split(V, 0.82, DRAGGABLE)
│ ├─ panes (Fixed) slot:active_pane — the ACTIVE tab's pane (28 Tab slots)
│ └─ action_log (Foldable 🐞, drawer@760) slot:action_log — the action trail
└─ detail (Foldable 🔍) slot:detail — the "Time machine" inspector
menubar: View { Toggle Time machine (Ctrl+I), Toggle Action trail (Ctrl+L), Reload (Ctrl+R) }
Go { Home (Ctrl+1), Release (Ctrl+2), Test (Ctrl+3), Warehouse (Ctrl+4) }
Help { About (F1) }
routes: Ctrl+I → Toggle(detail); button focus-detail → Focus(detail); go.release → Show(panes)
Pane::splitis DRAGGABLE by default: a real drag handle re-apportions the live ratio, kept inNavState.split_ratio(observable). The workspace|detail divider seeds 0.74 (≈ the old 360pt inspector); the panes|log divider seeds 0.82 (≈ the old 160pt bottom panel).- The 28 [
Tab] panes are the host-filled slots (tab_slot_ids(),src/viz/shell_scene.rs:65) the centralactive_paneslot delegates to — so every tab is aContent::Slot(Tab::id())filled byimpl SceneHost(per the brief).
f2 — NavState + render wiring (app.rs)
UrdrThreadsApp holds one nav: NavState (the live fold / split-ratio / route state) seeded in the constructor from nornir_shell_scene(). Each frame, inside the central egui::CentralPanel (src/viz/app.rs:3232), draw_ui:
- rebuilds the (cheap, 28-pane) scene;
- syncs the action-log drawer fold to the top-bar 🐞 toggle (
set_fold(ACTION_LOG_SLOT, !show_action_log)); - takes
navOUT (std::mem::replace) soselfis free to be theSceneHost, then callsscene::render(&scene, &mut nav, ui, self); - flows a scene-side drawer toggle back to
show_action_log, restoresnav, and caches the returned renderValueinlast_scene_render.
f3 — the SceneHost that DRIVES the panes
impl facett_app::scene::SceneHost for UrdrThreadsApp (src/viz/app.rs:3295) maps each slot id to a REAL widget and returns its state:
active_pane→draw_active_pane(src/viz/app.rs:3354): the former central dispatch verbatim (per-frame active-jobs poll + onboarding/timeline guards + thepush_id(Tab::id())render root + theif self.tab == …dispatch). It delegates through everyTab::id()slot:slot("Release")renders the Release pane, etc.detail→draw_detail_body(src/viz/app.rs:3742): the "Time machine" inspector (workspace freshness + the selected release's git-checkout commands + gate status + published versions).action_log→draw_action_log_body(src/viz/app.rs:3799): the palette-coloured tail of the shared$NORNIR_VIZ_ACTIONLOGfile.command(id)drives menu actions:refresh→ reload + refresh tables;about→ toggle the About window;go:<TabId>→ switch the active tab.
f4 — responsive + drawer
scene::render picks a Device from the available width. The root master/detail split carries .phone(Layout::Tabs, 760) → under 760pt it collapses to Tabs; the action_log pane carries .drawer(760) → it auto-folds crossing INTO phone-class and restores above it (the desktop-rail → phone-drawer pattern). Both share the PHONE_BREAKPOINT (760pt).
f5 — observability (LAW #6) + tests
state_json()["scene"] (folded in at src/viz/app.rs:1334 via scene_state(), src/viz/app.rs:3840) carries the pane tree, the foldable / drawer / draggable-split sets, the 28 host tab slots, the live folded + split_ratio maps, the menu/route counts, and the last scene::render facts (device, per-pane rects, per-slot state, live ratios + drag-handle separators). So a robot asserts the whole shell without pixels.
Tests
-
src/viz/shell_scene.rsunit tests (all green;cargo test -p nornir --features viz shell_scene):shell_scene_is_valid_with_unique_names—Scene::validate(addressability).shell_scene_ron_round_trips_byte_stable— RED-when-broken: RON parse ∘ serialize is a byte-stable fixed point.the_detail_and_log_panes_are_foldable.folding_the_detail_pane_is_observable— RED-when-broken: a fold round-trips throughNavState.the_master_detail_splits_are_draggable_with_live_ratios— RED-when-broken: both splits resizable, seeded ratios observable.dragging_the_master_detail_divider_reapportions_the_live_ratio— RED-when-broken: the live ratio mutates (the same state a handle-drag writes).the_action_log_is_a_drawer_at_the_phone_breakpoint.the_root_split_collapses_to_tabs_on_a_phone_width.every_tab_is_a_host_slot_id,the_menubar_carries_actions_shortcuts_and_routes.
-
tests/robot_scene.rs(nornir-robotui / egui_kittest, real AccessKit):the_declarative_shell_renders_and_is_observable— one paint throughscene::render; asserts the draggable splits, the drawer, the 28 tab slots, the live root ratio, and thatrender.scene == "urdr-threads".a_tab_click_switches_and_the_active_pane_slot_renders_it— RED-when-broken: a real click on the 🚀 Release header switchesself.tabAND the scene'sactive_paneslot renders it (render.slots.active_pane.tab == "Release").