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

nordisk/nornir

.nornir/top-bar-component.md

The top-bar TabBar component (D3b)

Design + guide for factoring the viz top-bar (workspace picker + the ~16 tab buttons in 2 rows + 🎨 palette picker + ℹ About + 🐞 log + live toggle) out of the bespoke inline chrome in app.rs into a reusable, themed nav/toolbar component, facett_ui::TabBar. Closes THE_BIG_PLAN D3b.

Companion to .nornir/theming-and-facett-migration.md (C8/D3 — the app-wide palette + the facett_ui widget module). Laws honoured: #1 inject-assert tests · #2 this doc · #3 facett-style component + theme · #6 "see what the user sees" as data · CLI parity (the --theme/$NORNIR_VIZ_THEME parity is kept).

TL;DR

The component API (src/viz/facett_ui.rs)

pub struct TabItem { pub id: String, pub label: String }
//  id    = stable identifier (the tab enum's debug name) → state_json + click match
//  label = full header text the user reads, e.g. "🔗 Dep Graph"

pub struct BarControl { pub key: String, pub kind: String, pub value: String }
//  BarControl::picker(key, value)  — a dropdown reporting its selection
//  BarControl::toggle(key, on)     — a boolean reporting "on"/"off"

pub struct TabBar<'a> { /* … */ }
impl<'a> TabBar<'a> {
    fn new(id_salt, tabs: &[TabItem], active: &str) -> Self;
    fn split_at(self, split: usize) -> Self;          // 2-row split (viz uses 8)
    fn with_control(self, c: BarControl) -> Self;      // declare a hosted picker/toggle
    fn state_json(&self) -> serde_json::Value;         // readable-data dump (LAW #6)
    fn show(&self, ui, theme: &Theme,
            left:  impl FnMut(&mut Ui),                 // host paints its pickers/actions
            right: impl FnMut(&mut Ui)) -> Option<String>; // → id of the newly-clicked tab
}

How the viz wires it (UrdrThreadsApp::draw_ui)

The bespoke control row + the two selectable_value tab rows are gone. In their place:

  1. Build Tab::tab_items() (id = format!("{tab:?}"), label = Tab::label()) and active_id = format!("{:?}", self.tab).
  2. Slot closures only accumulate intents into locals (chosen workspace, chosen palette, reload/sync/toggle flags, the live checkbox bool). They do not touch &mut self — that would fight the bar over the borrow. The one &mut a slot needs (self.ws_panel.draw_controls) is captured as a disjoint field borrow, with the server handle precomputed before the panel.
  3. After the panel closes, the intents are applied to self (the same calls the old inline chrome made: switch_workspace, reload, set_palette, ensure_server_info, the toggles, and self.tab = … for a clicked tab).

Behaviour is unchanged: same tabs, same labels (so robot/AccessKit click_label("🚀 Release") still resolves), same switching, same palette/About/live actions, same action-log + ui.tab trace events.

state_json["top_bar"]

UrdrThreadsApp::top_bar_state() builds the same TabBar the render builds and returns its state_json(), so the dump can't drift from what's painted:

"top_bar": {
  "tabs": [ { "id": "Timeline", "label": "🧵 Timeline", "active": false }, … ],
  "active": "Test",
  "split": 8,
  "controls": [
    { "key": "workspace",  "kind": "picker", "value": "nornir" },
    { "key": "live",       "kind": "toggle", "value": "off" },
    { "key": "about",      "kind": "toggle", "value": "off" },
    { "key": "action_log", "kind": "toggle", "value": "off" },
    { "key": "palette",    "kind": "picker", "value": "default" }
  ]
}

CLI parity (kept)

The 🎨 palette picker is one of the bar's controls, but the initial palette still resolves headlessly via C8's Theme::resolve_initial($NORNIR_VIZ_THEME → --theme → default) — tests/cli_parity.rs is untouched and still green. The bar reports the active palette in top_bar.controls[palette].value, so a headless/agent run can read which palette is showing without a screen.

facett re-home (the egui-version blocker)

Like the rest of facett_ui.rs, TabBar is vendored in the viz rather than imported from facett, because the viz pins egui 0.33 (the highest egui-snarl 0.9, the Funnel node-graph, supports) while facett is on egui 0.34, and egui's Ui/Color32/Painter types do not interoperate across versions. When egui-snarl ships on 0.34 (or the Funnel leaves snarl), this collapses to a thin re-export:

pub use facett::TabBar; // once egui 0.33 ↔ 0.34 align

It is already Theme-driven and slot-generic, so the move is a re-home, not a rewrite (a doc note to that effect lives on the TabBar doc comment).

Tests (LAW #1 — inject + assert)