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

nordisk/facett

facett-about/src/lib.rs

//! # facett-about — the shared **About card** widget
//!
//! The ONE robot-testable egui `Window` that renders a
//! [`nornir_appshell::Identity`], so every suite app's *About* surface (korp ·
//! holger · dwarves · nornir · ordning) is **one widget, not a per-app twin**
//! (LAW #5). It ports korp's original `draw_about_window` **verbatim**,
//! swapping the hard-coded `WORDMARK`/`TAGLINE`/`SUBTITLE`/`FRAMEWORK[]` consts
//! for [`Identity`] accessors, so korp's *and* holger's current layouts both
//! reproduce with **no visible change**.
//!
//! ## Why it exists (LAW #5 — DRY)
//! `korp/src/about.rs` and `holger/server/ui/src/about.rs` carried the SAME egui
//! body — `Window(..).collapsible(false).resizable(false).anchor(CENTER_CENTER)
//! .open(..).show( vertical_centered( Image.max_width(..).alt_text(..);
//! heading(wordmark); ...; Close ))` + the identical dismiss teardown — differing
//! only in DATA: korp `max_width = 560` + an italic raven flavour line; holger
//! `max_width = 360` + a `v{version}` line + the `FRAMEWORK[]` bullet list. That
//! is a data-parameterised widget masquerading as two copies. This leaf owns the
//! one body; the [`Identity`] carries the data.
//!
//! ## The reproduction map (no visible change)
//! Every field of [`Identity`] paints where the two apps painted it:
//! | Identity field | painted as | korp | holger |
//! |---|---|---|---|
//! | `logo_png` (→ [`logo_texture`]) | `Image.max_width(logo_max_width).alt_text(logo_alt)` | ✓ (560) | ✓ (360, LFS-tolerant `Option`) |
//! | `display_name` | `heading` | ✓ | ✓ |
//! | `version` (if non-empty) | `v{version}` monospace-strong | — | ✓ |
//! | `server_version` (if non-empty) | `server v{server_version}` monospace-weak | ✓ (connected korp-server / embedded engine) | — |
//! | `tagline` | `label` | ✓ | ✓ |
//! | `subtitle` (if non-empty) | `label` | ✓ (SUBTITLE) | — |
//! | `engines` (if non-empty) | italic-weak flavour lines | ✓ (the raven line) | — |
//! | `framework` (if non-empty) | "Built on …" weak header + `· line` small | — | ✓ (FRAMEWORK[]) |
//! | `links` (if non-empty) | `hyperlink_to(label, url)` | — | — (nornir/dwarves) |
//!
//! ## The GPU rows (every app, no per-app wiring)
//!
//! Below the identity, the card paints **what the app is actually rendering on** —
//! adapter name, `backend · device class · N adapter(s) enumerated`, and an honest
//! *GPU lane* line. Two facts made this necessary, and neither was visible from any
//! UI in the suite:
//!
//! * a host can silently be on **llvmpipe** — a CPU software rasteriser that is
//!   itself a Vulkan ICD, so "Vulkan" proves nothing (LAW #4 cares about the *lane*,
//!   not the API string). The row says `SOFTWARE (CPU rasteriser)`, in the error
//!   colour, and it says it even when the adapter reports itself `DiscreteGpu`,
//!   because the class comes from the selection's software flag, never from
//!   `device_type` alone;
//! * a GPU lane can be **installed and never called** — `renderer_installed()` is
//!   true, and every frame still goes through `painter.circle_filled`.
//!
//! The lane line is therefore MEASURED, from `facett_core::render::lane`'s frame
//! counters, and reports `unknown` rather than guessing when an installed lane does
//! not count its frames. The whole story is folded into [`state_json`] under `"gpu"`
//! so a robot asserts it. Rows come from `GpuStatus::rows()` — the ONE writer the UI
//! and the assertion share.
//!
//! korp maps its one italic raven line onto `engines`; holger maps `FRAMEWORK[]`
//! onto `framework`. Each app KEEPS only `let id = Identity{..}` + the toggle bool
//! + the `draw_about_window(..)` call — the const-blobs and the egui body delete.
//!
//! ## The API
//! * [`logo_texture`] — LFS-tolerant PNG→texture (holger's `Option`; `None` when
//!   the bytes are absent, empty, or an un-pulled git-lfs pointer — never a panic).
//! * [`draw_about_window`] — the verbatim korp/holger body, parameterised by
//!   `Identity`, `Option<&TextureHandle>`, and `logo_max_width`. Returns
//!   **`true` iff a dismiss happened this frame** (korp/holger's exact contract).
//! * [`AboutCard`] — the ergonomic form: caches the decoded logo + `logo_max_width`
//!   once, then `AboutCard::show(ctx, &mut open, &id)` renders each frame.
//! * [`gpu_rows`] — the GPU rows on their own, so a diagnostics pane or status bar
//!   can mount the same four lines instead of re-deriving them.
//! * [`state_json`] — [`Identity::state_json`] plus a `"gpu"` fold (adapter · backend
//!   · device class · adapter count · software · lane · lane_frames · lanes · rows),
//!   so a caller need not reach into `nornir_appshell` or `facett_core` for it.
//! * `*_with_gpu` siblings ([`draw_about_window_with_gpu`], [`state_json_with_gpu`],
//!   [`AboutCard::with_gpu`]) take an explicit [`GpuStatus`] instead of the live
//!   process globals — additive, so every existing call site is unchanged and gets
//!   the live probe.

use egui::{Context, TextureHandle};
pub use nornir_appshell::Identity;

/// The GPU facts the card paints — re-exported so a caller drives the whole About
/// surface through `facett_about::` alone (the [`state_json`] precedent).
pub use facett_core::render::lane::{GpuLaneUse, GpuStatus};

/// korp's original About logo width — the default when a caller does not override
/// it. holger passes `360.0`.
pub const DEFAULT_LOGO_MAX_WIDTH: f32 = 560.0;

/// Decode an [`Identity`]'s embedded logo PNG into an egui texture, **LFS-tolerant**.
///
/// Returns `None` — never a panic — when the identity has no logo, the bytes are
/// empty, or the bytes don't decode (a checkout where the git-lfs blob was not
/// pulled: the compiled-in bytes are the pointer text, not a PNG). The card then
/// shows its text identity without the hero image. Mirrors holger's original
/// graceful `image::load_from_memory(..).ok()`.
pub fn logo_texture(ctx: &Context, id: &Identity) -> Option<TextureHandle> {
    let bytes = id.logo_png.as_ref()?;
    if bytes.is_empty() {
        return None;
    }
    let img = image::load_from_memory(bytes).ok()?.to_rgba8();
    let (w, h) = img.dimensions();
    let color = egui::ColorImage::from_rgba_unmultiplied([w as usize, h as usize], img.as_raw());
    Some(ctx.load_texture(
        format!("facett_about_logo_{}", id.name),
        color,
        egui::TextureOptions::LINEAR,
    ))
}

/// The headless **oracle** for the About card: [`Identity::state_json`] (wordmark ·
/// version · logo_alt · has_logo · framework · …) **plus a `"gpu"` fold** — the
/// adapter, its backend and device class, how many adapters were enumerated, the
/// software flag, the measured lane use, and the painted rows verbatim.
///
/// `open` reports whether the window is showing. The identity keys are untouched, so
/// an existing About robot keeps passing; the GPU half is what lets one assert "this
/// app is on the 4090 and its map lane is really painting" without pixels. The GPU
/// facts are read from the process globals a wgpu host records at startup — use
/// [`state_json_with_gpu`] to assert a specific status.
pub fn state_json(id: &Identity, open: bool) -> serde_json::Value {
    state_json_with_gpu(id, open, &GpuStatus::probe())
}

/// [`state_json`] against an explicit [`GpuStatus`] — the form a test uses, so the
/// oracle can be asserted for a 4090, for llvmpipe and for a GPU-less process inside
/// one test binary (the live probe reads process globals and is therefore whatever
/// this process happens to have brought up).
pub fn state_json_with_gpu(id: &Identity, open: bool, gpu: &GpuStatus) -> serde_json::Value {
    let mut js = id.state_json(open);
    if let Some(obj) = js.as_object_mut() {
        obj.insert("gpu".to_owned(), gpu.to_json());
    }
    js
}

/// Paint the **GPU rows** — the answer to "what is this app actually rendering on":
/// adapter name, backend · device class · adapters enumerated, the honest lane line,
/// **why an idle lane is idle**, and a recorded bring-up failure's code + remedy when
/// there was one.
///
/// Public because the About card is not the only place worth telling: a diagnostics
/// pane or status bar mounts the SAME rows rather than re-deriving them (LAW #5). The
/// row STRINGS come from [`GpuStatus::rows`], the one writer a robot also reads, so
/// the assertion and the pixels cannot drift.
///
/// Colour carries the warning a sentence cannot: a software rasteriser is painted in
/// the error colour (a 4090 box quietly on llvmpipe is the whole point of these rows),
/// an installed-but-unused or unknown lane in the warn colour.
pub fn gpu_rows(ui: &mut egui::Ui, gpu: &GpuStatus) {
    let (warn, err, weak, normal) = {
        let v = ui.visuals();
        (v.warn_fg_color, v.error_fg_color, v.weak_text_color(), v.text_color())
    };
    ui.label(
        egui::RichText::new(gpu.adapter_line())
            .monospace()
            .color(if gpu.is_software() { err } else { normal }),
    );
    ui.label(egui::RichText::new(gpu.detail_line()).monospace().small().color(
        if gpu.is_software() { err } else { weak },
    ));
    ui.label(egui::RichText::new(gpu.lane_line()).monospace().small().color(match gpu.lane {
        GpuLaneUse::InUse { .. } => weak,
        GpuLaneUse::NotInstalled => weak,
        GpuLaneUse::InstalledUnused | GpuLaneUse::Unknown { .. } => warn,
    }));
    // WHY an idle lane is idle. Without this the previous row is ambiguous by
    // construction: "installed but UNUSED … drew 0 frames" is equally true of a lane that
    // correctly stood aside and of one that is broken. MEASURED 2026-08-02 — korp printed
    // exactly that on an RTX 4090 with nothing wrong (the map pane had not been painted:
    // modal chooser, landing tab is Tables), and it read as a fault to both the owner and an
    // agent with tools. The reason reached `state_json` first and this row did not exist,
    // which is the same defect one layer out: the robot could see it and the human could
    // not.
    if let Some(line) = gpu.idle_reason_line() {
        ui.label(egui::RichText::new(line).monospace().small().color(weak));
    }
    if let Some(line) = gpu.unavailable_line() {
        ui.label(egui::RichText::new(line).monospace().small().color(err));
    }
}

/// Draw the **About** modal window into `ctx` — the ONE render body korp and holger
/// (and the robot test) share.
///
/// * `open` gates visibility; it is set to `false` when the user dismisses the
///   window (its own ✕ or the Close button).
/// * `id` is the app identity ([`Identity`]) — every field paints per the crate-doc
///   reproduction map.
/// * `logo` is the decoded hero texture (from [`logo_texture`]); shown with the
///   stable `id.logo_alt` accessible name so the logo is observable to a robot.
///   `None` (an un-pulled LFS blob, or an app with no logo) draws the text identity
///   without the hero image — the exact holger behaviour.
/// * `logo_max_width` caps the logo width (korp `560.0`, holger `360.0`).
///
/// Returns **`true` iff a dismiss happened this frame** (korp/holger's exact
/// contract, so their call sites port unchanged). Draws nothing and returns
/// `false` when `*open` is already `false`.
pub fn draw_about_window(
    ctx: &Context,
    open: &mut bool,
    id: &Identity,
    logo: Option<&TextureHandle>,
    logo_max_width: f32,
) -> bool {
    draw_about_window_with_gpu(ctx, open, id, logo, logo_max_width, &GpuStatus::probe())
}

/// [`draw_about_window`] against an explicit [`GpuStatus`] instead of the live
/// process globals — the form a test drives (and a host that already holds its own
/// decision). Additive: every existing call site keeps the five-argument form and
/// gets the live probe.
pub fn draw_about_window_with_gpu(
    ctx: &Context,
    open: &mut bool,
    id: &Identity,
    logo: Option<&TextureHandle>,
    logo_max_width: f32,
    gpu: &GpuStatus,
) -> bool {
    if !*open {
        return false;
    }
    let mut window_open = true;
    let mut close_clicked = false;
    egui::Window::new(format!("About {}", id.display_name))
        .collapsible(false)
        .resizable(false)
        .anchor(egui::Align2::CENTER_CENTER, egui::vec2(0.0, 0.0))
        .open(&mut window_open)
        .show(ctx, |ui| {
            ui.vertical_centered(|ui| {
                if let Some(logo) = logo {
                    ui.add(
                        egui::Image::from_texture(logo)
                            .max_width(logo_max_width)
                            .alt_text(id.logo_alt.clone()),
                    );
                }
                ui.add_space(10.0);
                ui.heading(id.display_name.as_str());
                // holger's `v{VERSION}` monospace line (skipped when blank — korp).
                // This is the UI / crate version.
                if !id.version.is_empty() {
                    ui.label(
                        egui::RichText::new(format!("v{}", id.version))
                            .monospace()
                            .strong(),
                    );
                }
                // The connected SERVER / engine version — a weak sibling of the UI
                // version line (korp thin: korp-server's `Health` version; korp fat:
                // the embedded engine crate version). Skipped when blank/unknown, so
                // the card shows only `v{version}` until the backend version is known.
                if !id.server_version.is_empty() {
                    ui.label(
                        egui::RichText::new(format!("server v{}", id.server_version))
                            .monospace()
                            .weak(),
                    );
                }
                if !id.tagline.is_empty() {
                    ui.label(id.tagline.as_str());
                }
                // korp's SUBTITLE (holger has none → blank → skipped).
                if !id.subtitle.is_empty() {
                    ui.label(id.subtitle.as_str());
                }
                // korp's italic raven flavour line(s) — mapped onto `engines`.
                if !id.engines.is_empty() {
                    ui.add_space(6.0);
                    for line in &id.engines {
                        ui.label(egui::RichText::new(line).italics().weak());
                    }
                }
                // holger's FRAMEWORK[] — the shared-framework bullet list.
                if !id.framework.is_empty() {
                    ui.add_space(8.0);
                    ui.label(
                        egui::RichText::new("Built on the shared nordisk framework:").weak(),
                    );
                    for line in &id.framework {
                        ui.label(egui::RichText::new(format!("· {line}")).small());
                    }
                }
                // WHAT THIS APP IS ACTUALLY RENDERING ON. Not decoration: the two
                // facts these rows carry were both invisible from every UI in the
                // suite — that a host had silently fallen back to llvmpipe, and that
                // a GPU lane can be installed and never called. Painted for every
                // app, from the process globals, with no per-app wiring.
                ui.add_space(8.0);
                ui.separator();
                gpu_rows(ui, gpu);
                // Extra (label, url) links (nornir's endpoint/workspace, a docs
                // pointer, …) — none for korp/holger, so the list is empty there.
                if !id.links.is_empty() {
                    ui.add_space(6.0);
                    for (label, url) in &id.links {
                        ui.hyperlink_to(label.as_str(), url.as_str());
                    }
                }
                ui.add_space(10.0);
                if ui.button("Close").clicked() {
                    close_clicked = true;
                }
            });
        });
    // The window's own ✕ flips `window_open`; the Close button flips `close_clicked`.
    let dismissed = !window_open || close_clicked;
    if dismissed {
        *open = false;
    }
    dismissed
}

/// The **ergonomic** About card: decodes the logo once (holger's LFS-tolerant
/// `Option`) and remembers `logo_max_width`, so the per-frame call is just
/// [`AboutCard::show`]`(ctx, &mut open, &id)` — the app keeps only a toggle bool.
///
/// Build it once (or when the identity changes) and store it in the app; call
/// [`show`](AboutCard::show) each frame with the same [`Identity`].
pub struct AboutCard {
    /// The decoded hero logo, if any (`None` = no logo / un-pulled LFS blob).
    logo: Option<TextureHandle>,
    /// The logo width cap (korp `560.0`, holger `360.0`).
    logo_max_width: f32,
    /// A pinned GPU status, or `None` to read the live process globals each show —
    /// which is what an app wants, since a lane's frame count changes as it paints.
    gpu: Option<GpuStatus>,
}

impl AboutCard {
    /// Decode `id`'s logo (LFS-tolerant) and build a card with korp's default
    /// [`DEFAULT_LOGO_MAX_WIDTH`]. Chain [`with_max_width`](Self::with_max_width)
    /// for holger's narrower `360.0`.
    pub fn new(ctx: &Context, id: &Identity) -> Self {
        AboutCard {
            logo: logo_texture(ctx, id),
            logo_max_width: DEFAULT_LOGO_MAX_WIDTH,
            gpu: None,
        }
    }

    /// Override the logo width cap (holger passes `360.0`).
    pub fn with_max_width(mut self, logo_max_width: f32) -> Self {
        self.logo_max_width = logo_max_width;
        self
    }

    /// Pin the [`GpuStatus`] the card reports instead of reading the live process
    /// globals each frame (a test, or a host that resolved the adapter itself).
    pub fn with_gpu(mut self, gpu: GpuStatus) -> Self {
        self.gpu = Some(gpu);
        self
    }

    /// `true` iff a logo texture decoded (so the card shows the hero image, not the
    /// text-only fallback) — the robot-observable twin of `Identity::has_logo`
    /// after decode.
    pub fn has_logo_texture(&self) -> bool {
        self.logo.is_some()
    }

    /// Render the About window for `id` this frame. Returns **whether the window is
    /// still open afterwards** (`true` = still showing; `false` = dismissed this
    /// frame or already closed) — the ergonomic inverse of
    /// [`draw_about_window`]'s dismiss flag, matching a `while card.show(..)` toggle.
    pub fn show(&self, ctx: &Context, open: &mut bool, id: &Identity) -> bool {
        let gpu = match &self.gpu {
            Some(g) => g.clone(),
            None => GpuStatus::probe(),
        };
        draw_about_window_with_gpu(ctx, open, id, self.logo.as_ref(), self.logo_max_width, &gpu);
        *open
    }
}

// ── testmatrix emit shim (real row emitter with `--features testmatrix`, a
//    compiled-out no-op otherwise — the facett-confirm idiom) ─────────────────
/// Emit one functional-status row (no-op unless built `--features testmatrix`).
#[cfg(feature = "testmatrix")]
pub fn emit(component: &str, check: &str, ok: bool, detail: &str) {
    nornir_testmatrix::functional_status(component, check, ok, detail);
}
/// No-op shape compiled when `testmatrix` is OFF (the release path).
#[cfg(not(feature = "testmatrix"))]
#[inline(always)]
pub fn emit(_component: &str, _check: &str, _ok: bool, _detail: &str) {}

#[cfg(test)]
mod tests {
    use super::*;

    /// A tiny **real** 2×2 PNG, encoded at runtime so the decode path is exercised
    /// with genuine bytes (no committed asset needed).
    fn tiny_png() -> Vec<u8> {
        let img = image::RgbaImage::from_pixel(2, 2, image::Rgba([200, 30, 30, 255]));
        let mut buf = std::io::Cursor::new(Vec::new());
        image::DynamicImage::ImageRgba8(img)
            .write_to(&mut buf, image::ImageFormat::Png)
            .expect("encode tiny PNG");
        buf.into_inner()
    }

    fn holgerish_identity(logo: Option<Vec<u8>>) -> Identity {
        Identity {
            logo_png: logo,
            logo_alt: "test logo — a raven".into(),
            framework: vec!["jera — the shared job manager".into()],
            ..Identity::new("testapp", "TestApp", "9.9.9", "a test tagline")
        }
    }

    /// **RED-when-broken: the logo decode is LFS-tolerant.** Real PNG bytes decode
    /// to `Some` texture; an un-pulled git-lfs pointer (present but not a PNG)
    /// decodes to `None` without a panic; a `None`/empty logo is `None`. If the
    /// decode started panicking on a pointer, or dropped a real PNG, this is red.
    #[test]
    fn logo_texture_is_lfs_tolerant() {
        let ctx = egui::Context::default();

        // Real PNG → Some texture.
        let ok = holgerish_identity(Some(tiny_png()));
        assert!(logo_texture(&ctx, &ok).is_some(), "a real PNG decodes to a texture");

        // Un-pulled LFS pointer (present but not a PNG) → None, no panic.
        let lfs = holgerish_identity(Some(
            b"version https://git-lfs.github.com/spec/v1\noid sha256:deadbeef\n".to_vec(),
        ));
        assert!(logo_texture(&ctx, &lfs).is_none(), "an LFS pointer degrades to None");

        // No logo at all → None.
        let bare = holgerish_identity(None);
        assert!(logo_texture(&ctx, &bare).is_none(), "no logo → None");

        // Empty bytes → None.
        let empty = holgerish_identity(Some(Vec::new()));
        assert!(logo_texture(&ctx, &empty).is_none(), "empty bytes → None");

        emit(
            "facett-about",
            "logo_texture_lfs_tolerant",
            logo_texture(&ctx, &ok).is_some()
                && logo_texture(&ctx, &lfs).is_none()
                && logo_texture(&ctx, &bare).is_none(),
            "real=Some lfs=None none=None",
        );
    }

    /// The `state_json` fold carries the identity oracle **unchanged** — the same
    /// shape korp/holger's About robots read (`wordmark` · `version` · `logo_alt` ·
    /// `has_logo` · `framework`) — and adds `"gpu"` beside it. ADDITIVE: every
    /// identity key must still be byte-identical to `Identity::state_json`, or an
    /// existing app's About robot breaks.
    #[test]
    fn state_json_folds_gpu_beside_an_unchanged_identity() {
        let id = holgerish_identity(Some(tiny_png()));
        let js = state_json(&id, true);
        let base = id.state_json(true);

        for (k, v) in base.as_object().expect("identity object") {
            assert_eq!(js.get(k), Some(v), "identity key {k} must be unchanged by the gpu fold");
        }
        assert_eq!(js["wordmark"], "TestApp");
        assert_eq!(js["version"], "9.9.9");
        assert_eq!(js["has_logo"], serde_json::json!(true));
        assert!(!js["framework"].as_array().unwrap().is_empty());
        // …and exactly one new key.
        assert_eq!(
            js.as_object().expect("object").len(),
            base.as_object().expect("object").len() + 1,
            "the fold adds `gpu` and nothing else"
        );
        assert!(js["gpu"].is_object(), "the gpu fold is present: {}", js["gpu"]);
        // The keys a robot keys off must always exist, whatever this process has.
        for k in ["adapter", "adapter_count", "device_class", "software", "lane", "lane_frames", "rows"] {
            assert!(!js["gpu"][k].is_null() || k == "adapter", "gpu.{k} must be present: {}", js["gpu"]);
        }
    }
}