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

nordisk/holger

mannequin/src/gui.rs

//! Mannequin GUI — the holger test-dummy system map, drawn through **facett**
//! components (the nordisk house pattern) instead of raw egui:
//!
//! - the PKI / OIDC / Nexus system map → a **`facett_syschart::SystemChart`**
//!   (clickable peer nodes + edges, a per-node event-count **badge**, and a
//!   click-select → inline detail panel — the component invented for this GUI);
//! - the selected system's event log → a **`facett_table::Table`** (timestamp /
//!   direction / summary rows);
//! - the PKI issued-cert list → a small tree/list, with the **Regenerate**
//!   action kept as a live button.
//!
//! Everything stays live: each frame we re-read the shared [`EventLog`] +
//! [`PkiService`] and refresh the chart badges + the table rows, so the map
//! updates as the daemon serves requests.
//!
//! ## The shell is a [`Scene`] — because this pane really is master/detail
//!
//! The map and the log were two things stacked in one `ScrollArea` with a
//! `ui.separator()` between them: the operator could not resize either, could not
//! collapse the map to read a long log, and could not blow the log up to read it.
//! But the relationship was master/detail all along — you pick a system ON the map
//! and the panel below is the detail OF that pick.
//!
//! So the shell is declared as one [`facett_app::scene::Scene`] (L5 — the same
//! shared pane host holger-server-ui's shell adopted) and rendered by [`scene::render`]:
//!
//! ```text
//! mannequin-shell ── Split(V, 0.55, DRAGGABLE)
//! ├─ map_pane    (Foldable 🗺) slot:map    — the SystemChart (the MASTER)
//! └─ detail_pane (Foldable 📜) slot:detail — the selection's log + PKI controls
//! ```
//!
//! That buys a real draggable divider and — via facett-app's `Chrome::Auto` — a
//! window title bar on each pane carrying fold · minimise · maximise. Nothing about
//! the two bodies changed: the same `SystemChart` and the same `Table` are drawn,
//! through a [`SceneHost`] instead of inline.

use eframe::egui;
use facett_app::scene::{self, Axis, NavState, Pane, Scene, SceneHost};
use facett_core::look::Theme as LookTheme; // the unified facett look & feel (one preset/frame)
use facett_core::Facet;
use facett_syschart::{SysEdge, SysNode, SystemChart};
use facett_table::Table;
use serde_json::{json, Value};

use crate::event_log::{Direction, EventLog, SystemKind};
use crate::PkiService;
use std::sync::Arc;

/// The root pane's unique name — the DRAGGABLE vertical master/detail split. The
/// `Router` reports it as the scene route and a robot drags its divider by the rect
/// the render publishes under `separators`.
pub const SHELL_PANE: &str = "mannequin-shell";

/// The MASTER pane: the PKI / OIDC / Nexus system map.
pub const MAP_PANE: &str = "map_pane";

/// The DETAIL pane: the selected system's event log (+ the PKI controls).
pub const DETAIL_PANE: &str = "detail_pane";

/// Build the mannequin shell as a declarative [`Scene`]: a draggable vertical
/// master/detail split of the system map over the selection's detail. Both panes are
/// foldable, and both wear facett-app's default window chrome (fold · minimise ·
/// maximise) — the root stays bare, as it must.
///
/// Pure and window-free, so a test can build the exact shipped shell and assert its
/// shape without a display.
pub fn mannequin_scene() -> Scene {
    let map = Pane::slot(MAP_PANE, "map")
        .title("System map")
        .foldable_icon(true, "🗺");
    let detail = Pane::slot(DETAIL_PANE, "detail")
        .title("Detail")
        .foldable_icon(true, "📜");
    Scene::new(
        "mannequin",
        Pane::split(SHELL_PANE, Axis::Vertical, 0.55, vec![map, detail]).title("Holger Mannequin"),
    )
}

/// Stable node ids for the three mock systems (used for chart selection + the
/// badge/detail refresh).
const ID_PKI: &str = "pki";
const ID_OIDC: &str = "oidc";
const ID_NEXUS: &str = "nexus";

fn kind_id(kind: &SystemKind) -> &'static str {
    match kind {
        SystemKind::Pki => ID_PKI,
        SystemKind::Oidc => ID_OIDC,
        SystemKind::Nexus => ID_NEXUS,
    }
}

fn id_kind(id: &str) -> Option<SystemKind> {
    match id {
        ID_PKI => Some(SystemKind::Pki),
        ID_OIDC => Some(SystemKind::Oidc),
        ID_NEXUS => Some(SystemKind::Nexus),
        _ => None,
    }
}

/// The EXACT AccessKit label `facett_syschart::SystemChart` registers on a node's
/// clickable button — `"{label} · {badge} events"` (see `facett-syschart` FC-4).
/// The robot addresses the REAL on-map node by this string, and `state_json`
/// publishes it per node, so the drive target and the emit stay one truth.
pub fn node_a11y_label(kind: &SystemKind, badge: u64) -> String {
    format!("{} · {} events", kind.label(), badge)
}

pub struct MannequinGui {
    event_log: EventLog,
    pki: Arc<PkiService>,
    /// The system map (facett component). Rebuilt-in-place each frame: badges +
    /// detail text are refreshed from the live event log.
    chart: SystemChart,
    /// Active look preset — an index into [`LookTheme::PRESETS`]. The GUI applies
    /// it every frame; `apply` installs the full egui Style AND publishes the
    /// derived legacy palette so the facett SystemChart + Table re-theme
    /// coherently (COH-1). The mannequin used to ship egui's bare default.
    look_preset: usize,
    /// The declarative shell (the master/detail pane tree) — the DATA the renderer
    /// walks. Constant, so the persisted [`NavState`] stays valid frame-to-frame.
    scene: Scene,
    /// The scene's runtime state: per-pane fold + minimise, the maximised pane, and the
    /// LIVE split ratio a divider drag moves.
    nav: NavState,
    /// The last [`scene::render`]'s observable facts (per-pane rects + `alloc`, chrome
    /// button rects, divider handle rects) — folded into [`state_json`](Self::state_json)
    /// so a robot asserts GEOMETRY, not pixels.
    last_render: Value,
}

/// The default look preset for the mannequin GUI: the running-OS-matched preset's
/// index into [`LookTheme::PRESETS`]. Linux/unknown → windows-dark (facett's
/// documented default). Pure → unit-testable with no window.
pub fn default_look_preset() -> usize {
    let want = LookTheme::from_os(egui::os::OperatingSystem::from_target_os()).name;
    LookTheme::preset_names()
        .iter()
        .position(|n| *n == want)
        .unwrap_or(1)
}

/// Build the mannequin system map as a bare facett [`SystemChart`] — the three
/// mock systems (PKI / OIDC / Nexus) as peer nodes, fully wired with edges. This
/// is the **single source of truth** for the chart's topology: the live GUI
/// ([`MannequinGui::new`]) builds from it AND the headless robot-UI matrix cell
/// renders this exact chart offscreen to assert its `state_json`. Badges start at
/// the supplied counts so a test can inject real numbers and read them back.
pub fn build_system_chart(badges: [(SystemKind, u64); 3]) -> SystemChart {
    let by_kind = |k: &SystemKind| {
        badges
            .iter()
            .find(|(b, _)| b == k)
            .map(|(_, n)| *n)
            .unwrap_or(0)
    };
    let nodes = vec![
        SysNode::new(
            ID_PKI,
            SystemKind::Pki.label(),
            egui::Color32::from_rgb(120, 200, 255),
            (0.12, 0.15),
        )
        .badge(by_kind(&SystemKind::Pki)),
        SysNode::new(
            ID_OIDC,
            SystemKind::Oidc.label(),
            egui::Color32::from_rgb(200, 160, 255),
            (0.88, 0.15),
        )
        .badge(by_kind(&SystemKind::Oidc)),
        SysNode::new(
            ID_NEXUS,
            SystemKind::Nexus.label(),
            egui::Color32::from_rgb(160, 255, 180),
            (0.5, 0.9),
        )
        .badge(by_kind(&SystemKind::Nexus)),
    ];
    let edges = vec![
        SysEdge::new(ID_PKI, ID_OIDC),
        SysEdge::new(ID_PKI, ID_NEXUS),
        SysEdge::new(ID_OIDC, ID_NEXUS),
    ];
    SystemChart::new("Holger Mannequin — System Map", nodes, edges).with_canvas_height(320.0)
}

impl MannequinGui {
    pub fn new(event_log: EventLog, pki: Arc<PkiService>) -> Self {
        let chart = build_system_chart([
            (SystemKind::Pki, 0),
            (SystemKind::Oidc, 0),
            (SystemKind::Nexus, 0),
        ]);
        let scene = mannequin_scene();
        debug_assert!(
            scene.validate().is_ok(),
            "scene names must be unique: {:?}",
            scene.validate()
        );
        let nav = NavState::new(&scene);
        Self {
            event_log,
            pki,
            chart,
            look_preset: default_look_preset(),
            scene,
            nav,
            last_render: Value::Null,
        }
    }

    /// The active look preset's [`LookTheme`].
    pub fn look_theme(&self) -> LookTheme {
        let presets = LookTheme::PRESETS;
        presets[self.look_preset.min(presets.len() - 1)]()
    }

    /// Switch the active look preset (clamped); 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
    }

    /// Remote-observable state (LAW 6): **every on-screen VALUE an operator sees**,
    /// as data, so a robot asserts the value — never the pixels. The drivable half
    /// exists too (`set_look_preset` / `select_system` / `regenerate_pki` /
    /// `toggle_fold` …), so a robot can drive each primary function and assert the
    /// result IN CODE.
    ///
    /// The roster, matched 1:1 against what the shipped shell paints:
    /// * `look_preset` / `look_name` — the chrome bar's look switcher's active pick;
    /// * `selected_system` — the map's one selection (drives the detail pane);
    /// * `events.{pki,oidc,nexus}` — the per-node badge numbers on the map;
    /// * `pki_issued` — the PKI issued-cert count;
    /// * `chart_nodes[]` — every map node's `{id,label,badge,selected}` PLUS the
    ///   exact AccessKit label (`a11y_label`) the facett `SystemChart` registers for
    ///   it, so a robot can click the REAL on-map node (no proxy);
    /// * `detail{}` — what the detail pane renders for the current selection, and its
    ///   EMPTY / ERROR states as booleans (`empty`, `pki_certs_empty`, `activity_*`),
    ///   so an empty pane is an asserted value, never an eyeball (LAW 2 / 4);
    /// * `scene{}` — the pane-chrome geometry + fold/minimise/maximise/split state.
    pub fn state_json(&self) -> serde_json::Value {
        let names = LookTheme::preset_names();
        serde_json::json!({
            "look_preset": self.look_preset,
            "look_name": names.get(self.look_preset).map(|s| s.to_string()).unwrap_or_default(),
            "preset_count": names.len(),
            "selected_system": self.chart.selected(),
            // ── live daemon-facing atoms a robot can drive + assert (LAW 6) ──────────
            // Per-system event counts (a Regenerate logs one, so a robot proves the
            // action landed) and the PKI issued-cert count.
            "events": {
                "pki": self.event_log.count_for(&SystemKind::Pki),
                "oidc": self.event_log.count_for(&SystemKind::Oidc),
                "nexus": self.event_log.count_for(&SystemKind::Nexus),
            },
            "pki_issued": self.pki.list_issued().len(),
            // ── the map's node roster + the EXACT AccessKit label per node ──────────
            // (what `facett_syschart` paints and registers), so a robot addresses the
            // REAL on-map node instead of a proxy control.
            "chart_nodes": self.chart_nodes_json(),
            // ── the detail pane as data, including its empty/error states ───────────
            "detail": self.detail_json(),
            // ── the declarative shell as observable data (the Observe step) ─────────
            "scene": {
                "name": self.scene.name,
                "pane_names": self.scene.pane_names(),
                // facett-app's own adoption audit — the one blob naming every affordance
                // this shell ships (chrome roster · draggable dividers · foldables).
                "affordances": self.scene.affordances(),
                "draggable_splits": self.scene.draggable_splits(),
                "chrome": self.scene.chrome_panes(),
                "foldable": self.scene.foldable_panes(),
                "folded": self.nav.folded,
                "minimized": self.nav.minimized,
                "maximized": self.nav.maximized,
                "last_chrome": self.nav.last_chrome,
                "split_ratio": self.nav.split_ratio,
                // The last render's facts: per-pane `rect` + `alloc` + `area`, each
                // chrome control's button rect, and the divider handle rects.
                "render": self.last_render,
            },
        })
    }

    /// The map's node roster as observable data — one entry per painted node with
    /// the EXACT AccessKit label `facett_syschart` registers for it
    /// (`"{label} · {badge} events"`), so a robot can address the REAL on-map node.
    /// This IS the map's LAW-6 populated-state emit: `badge` is the node's live
    /// event count, `selected` marks the one the detail pane follows.
    fn chart_nodes_json(&self) -> Value {
        let sel = self.chart.selected();
        let node = |kind: SystemKind| {
            let id = kind_id(&kind);
            let badge = self.event_log.count_for(&kind) as u64;
            json!({
                "id": id,
                "label": kind.label(),
                "badge": badge,
                "selected": sel == Some(id),
                // The label facett_syschart attaches to the node's AccessKit button.
                "a11y_label": node_a11y_label(&kind, badge),
            })
        };
        json!([
            node(SystemKind::Pki),
            node(SystemKind::Oidc),
            node(SystemKind::Nexus)
        ])
    }

    /// The detail pane as observable data — what it renders for the current
    /// selection AND every empty/error state it can show, each as a boolean value a
    /// robot asserts (LAW 2 / 4): `empty` (the "(select a system on the map)"
    /// placeholder), `pki_certs_empty` (the "(no child certs issued yet)" line), and
    /// `activity_empty` (the per-system "No activity yet" state).
    fn detail_json(&self) -> Value {
        let sel = self.chart.selected().map(|s| s.to_string());
        let sel_kind = sel.as_deref().and_then(id_kind);
        let event_count = sel_kind
            .as_ref()
            .map(|k| self.event_log.count_for(k))
            .unwrap_or(0);
        let issued = self.pki.list_issued();
        json!({
            "selected": sel,
            // The "(select a system on the map)" placeholder is showing.
            "empty": sel.is_none(),
            // The selected system's real event count (what the detail Table lists).
            "event_count": event_count,
            // The PKI issued-cert list the detail pane draws, and whether it is the
            // "(no child certs issued yet)" empty line.
            "pki_certs": issued,
            "pki_certs_empty": issued.is_empty(),
            // Per-system "No activity yet" empty state (the chart's inline detail).
            "activity_empty": {
                "pki": self.event_log.count_for(&SystemKind::Pki) == 0,
                "oidc": self.event_log.count_for(&SystemKind::Oidc) == 0,
                "nexus": self.event_log.count_for(&SystemKind::Nexus) == 0,
            },
        })
    }

    /// Robot/test hook: fold or unfold a scene pane by name (the same toggle the chrome
    /// bar's ⏷ control runs). Returns the new folded state.
    pub fn toggle_fold(&mut self, pane: &str) -> bool {
        self.nav.toggle_fold(pane)
    }

    /// Robot/test hook: **minimise** a pane — collapse it to its chrome title bar and
    /// hand its space back to its sibling. Returns the new minimised state.
    pub fn toggle_minimize(&mut self, pane: &str) -> bool {
        self.nav.toggle_minimize(pane)
    }

    /// Robot/test hook: **maximise** a pane — give it the whole shell, shrinking its
    /// sibling to zero area. At most one pane holds it. Returns the new state.
    pub fn toggle_maximize(&mut self, pane: &str) -> bool {
        self.nav.toggle_maximize(pane)
    }

    /// Robot/test hook: **restore** a pane (drop its minimise AND its maximise). The
    /// split ratio was never overwritten, so the layout returns exactly as it was.
    pub fn restore_pane(&mut self, pane: &str) -> bool {
        self.nav.restore(pane)
    }

    /// Robot/test hook: the live first-child (map) fraction of the master/detail split —
    /// what a divider drag moves.
    pub fn split_ratio(&self) -> Option<&Vec<f32>> {
        self.nav.split_ratios(SHELL_PANE)
    }

    /// Robot/test hook: **select a system on the map** (the headless analogue of clicking
    /// a chart node) — the ONE selection the detail pane renders. Re-selecting the same
    /// node deselects, per the chart's documented toggle. Returns the new selection.
    pub fn select_system(&mut self, id: &str) -> Option<String> {
        self.chart.select(id);
        self.chart.selected().map(|s| s.to_string())
    }

    /// Robot/test hook: the ONE selected system id (`"pki"`/`"oidc"`/`"nexus"`), or
    /// `None` when the map has nothing selected. The read half of [`select_system`].
    pub fn selected_system(&self) -> Option<String> {
        self.chart.selected().map(|s| s.to_string())
    }

    /// Robot/test hook: **regenerate the PKI CA** (the headless analogue of clicking the
    /// detail pane's 🔄 Regenerate button). On success it logs the same outbound PKI
    /// event the live button does, so the event count a robot asserts moves. Returns
    /// whether the regenerate succeeded.
    pub fn regenerate_pki(&mut self) -> bool {
        match self.pki.regenerate() {
            Ok(()) => {
                self.event_log.log(
                    SystemKind::Pki,
                    Direction::Outbound,
                    "CA regenerated (new keypair)",
                    None,
                );
                true
            }
            Err(e) => {
                tracing::error!("PKI regenerate failed: {}", e);
                false
            }
        }
    }

    /// Robot/test hook: the PKI issued-cert common-names (what the detail pane's cert
    /// list renders).
    pub fn issued_certs(&self) -> Vec<String> {
        self.pki.list_issued()
    }

    /// Robot/test hook: the live event count for one system (the badge number).
    pub fn event_count(&self, kind: &SystemKind) -> usize {
        self.event_log.count_for(kind)
    }

    /// Apply the active preset to `ctx` — installs the full egui Style and
    /// publishes the derived legacy palette so the facett SystemChart + Table
    /// follow (COH-1).
    pub fn apply_look(&self, ctx: &egui::Context) {
        self.look_theme().apply(ctx);
    }

    pub fn run(self) -> anyhow::Result<()> {
        let options = eframe::NativeOptions {
            // LAW #4 — WebGPU, never OpenGL. Named explicitly rather than left to
            // `Renderer::default()`: the default follows whichever backend feature the
            // manifest happens to carry, which is exactly how this GUI shipped on glow
            // without the word appearing anywhere.
            renderer: eframe::Renderer::Wgpu,
            viewport: egui::ViewportBuilder::default()
                .with_inner_size([900.0, 700.0])
                .with_title("Holger Mannequin — System Map"),
            ..Default::default()
        };

        eframe::run_native(
            "Holger Mannequin",
            options,
            Box::new(|_cc| Ok(Box::new(self))),
        )
        .map_err(|e| anyhow::anyhow!("eframe error: {}", e))
    }

    /// Refresh every chart node's badge (event count) + its inline detail text
    /// (the most recent log lines) from the live event log. Runs each frame so
    /// the map tracks the daemon.
    fn refresh_chart(&mut self) {
        for kind in [SystemKind::Pki, SystemKind::Oidc, SystemKind::Nexus] {
            let id = kind_id(&kind);
            self.chart
                .set_badge(id, self.event_log.count_for(&kind) as u64);
            self.chart
                .set_detail(id, recent_log_text(&self.event_log, &kind, 12));
        }
    }
}

/// The recent event log for one system as plain text (the chart's inline detail
/// panel renders this; it's also the headless-observable detail).
fn recent_log_text(event_log: &EventLog, kind: &SystemKind, max: usize) -> String {
    let entries = event_log.entries_for(kind);
    if entries.is_empty() {
        return "No activity yet".to_string();
    }
    entries
        .iter()
        .rev()
        .take(max)
        .map(|e| {
            let ts = e.timestamp.format("%H:%M:%S");
            let arrow = match e.direction {
                Direction::Inbound => "→",
                Direction::Outbound => "←",
            };
            format!("{ts} {arrow} {}", e.summary)
        })
        .collect::<Vec<_>>()
        .join("\n")
}

/// Build a facett `Table` of one system's recent events (timestamp/dir/summary).
fn event_table(event_log: &EventLog, kind: &SystemKind) -> Table {
    let mut t = Table::new(
        format!("{} — events", kind.label()),
        vec!["time".into(), "dir".into(), "summary".into()],
    );
    for e in event_log.entries_for(kind).iter().rev().take(50) {
        let dir = match e.direction {
            Direction::Inbound => "in",
            Direction::Outbound => "out",
        };
        t.push_row(vec![
            e.timestamp.format("%H:%M:%S").to_string(),
            dir.into(),
            e.summary.clone(),
        ]);
    }
    t
}

/// The slot host: fills each `Content::Slot` with the SAME widget the shell drew
/// inline before — the `map` slot is the facett `SystemChart`, the `detail` slot the
/// selection's PKI controls + event-log `Table`. Nothing is forked; the scene only
/// decides WHERE each goes and how much room it gets.
struct Host<'a> {
    chart: &'a mut SystemChart,
    event_log: &'a EventLog,
    pki: &'a Arc<PkiService>,
}

impl SceneHost for Host<'_> {
    fn slot(&mut self, id: &str, ui: &mut egui::Ui) -> Value {
        match id {
            // ── MASTER: the system map. Scrolls inside its own pane, so shrinking the
            // pane with the divider never clips the chart away.
            "map" => {
                egui::ScrollArea::vertical()
                    .id_salt("mannequin-map-scroll")
                    .show(ui, |ui| {
                        self.chart.ui(ui);
                    });
                json!({ "selected": self.chart.selected() })
            }
            // ── DETAIL: whatever the map's selection points at.
            "detail" => {
                let Some(kind) = self.chart.selected().and_then(id_kind) else {
                    ui.weak("(select a system on the map)");
                    return json!({ "selected": Value::Null, "events": 0 });
                };
                let mut regenerated = false;
                egui::ScrollArea::vertical()
                    .id_salt("mannequin-detail-scroll")
                    .show(ui, |ui| {
                        if kind == SystemKind::Pki {
                            ui.horizontal(|ui| {
                                ui.label("🔐 CA: Holger Mannequin CA");
                                if ui.button("🔄 Regenerate").clicked() {
                                    if let Err(e) = self.pki.regenerate() {
                                        tracing::error!("PKI regenerate failed: {}", e);
                                    } else {
                                        regenerated = true;
                                    }
                                }
                            });
                            // Issued-cert tree/list.
                            let issued = self.pki.list_issued();
                            if issued.is_empty() {
                                ui.weak("  (no child certs issued yet)");
                            } else {
                                for cn in &issued {
                                    ui.label(format!("  └─ {cn}"));
                                }
                            }
                            ui.separator();
                        }

                        // The event log as a facett Table.
                        let mut table = event_table(self.event_log, &kind);
                        table.ui(ui);
                    });
                if regenerated {
                    self.event_log.log(
                        SystemKind::Pki,
                        Direction::Outbound,
                        "CA regenerated (new keypair)",
                        None,
                    );
                }
                json!({
                    "selected": kind_id(&kind),
                    "events": self.event_log.entries_for(&kind).len(),
                })
            }
            other => {
                ui.weak(format!("no widget for slot: {other}"));
                Value::Null
            }
        }
    }
}

impl eframe::App for MannequinGui {
    // eframe 0.35: `ui` IS the render entry point and `update` is `#[deprecated]` — and
    // implementing both would draw twice per frame, because eframe calls them in sequence
    // (`native/epi_integration.rs`). We get a `&mut Ui` for the root viewport directly.
    fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
        // Live updates: repaint every 500ms.
        ui.ctx()
            .request_repaint_after(std::time::Duration::from_millis(500));

        // Unified look & feel: install the active preset's full egui Style +
        // publish its derived legacy palette so the facett SystemChart + Table
        // re-theme coherently (COH-1). The mannequin previously used egui default.
        self.apply_look(&ui.ctx().clone());

        // Pull the latest counts/detail into the facett chart before drawing.
        self.refresh_chart();

        // The app chrome (identity + the look switcher) stays OUTSIDE the scene — the
        // scene owns the pane tree, this bar owns the shell-wide controls.
        let mut pick = None;
        egui::Panel::top("mannequin-chrome").show(ui, |ui| {
            ui.horizontal(|ui| {
                ui.heading("Holger Mannequin — System Map");
                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() {
                    if ui.selectable_label(self.look_preset == i, name).clicked() {
                        pick = Some(i);
                    }
                }
            });
        });
        if let Some(i) = pick {
            self.set_look_preset(i);
        }

        // ── The shell, rendered from its declarative Scene. Disjoint field borrows so
        // the host holds the widgets while `render` reads the scene + mutates the nav.
        let Self {
            event_log,
            pki,
            chart,
            scene,
            nav,
            last_render,
            ..
        } = self;
        let mut host = Host {
            chart: &mut *chart,
            event_log: &*event_log,
            pki: &*pki,
        };
        *last_render = scene::render(&*scene, &mut *nav, ui, &mut host);
    }
}