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

nordisk/dwarves

dwarves-gui/src/build_thing_scene.rs

//! **⚒ Build Thing, as a SCENE.**
//!
//! The shell has been the constellation's facett-scene reference for a while — draggable
//! master/detail root, drawer rail, foldable panes. This tab's BODY was not: it was one
//! `Slot` holding one imperative flow of thirteen `draw_*` calls, so minimise, maximise
//! and drag operated on the whole tab and could do **nothing inside it**. A readout that
//! grew — a step log, a dependency picture, a guest framebuffer — pushed whatever it
//! shared the flow with off the bottom, and there was no divider to correct it with.
//!
//! That is not a style complaint. It is why the boot console could not be made bigger:
//! there was nothing to drag.
//!
//! ```text
//! build-thing ── Split(H, 0.55, DRAGGABLE)
//! ├─ build-thing-graph   (Foldable ⚒)  WHICH ROOT (the recipe search), then THE
//! │                                    BUILD GRAPH: a node per variant, a node per
//! │                                    feature on it (a feature with children is a
//! │                                    subgraph, folded until clicked), containment
//! │                                    edges, CONSUMPTION edges (`--build X`, double
//! │                                    stroke) — the same DAG runa will run. The
//! │                                    variant/column strips stay under it as the
//! │                                    accessible index (every existing click survives)
//! └─ side ─── Split(V, 0.50, DRAGGABLE)
//!    ├─ build-thing-menu    (Foldable ☰)  THE SELECTED NODE'S MENU: its own settings
//!    │                                    (feature → checkbox, val → strip, setting →
//!    │                                    field), each with its PATH and the flag it
//!    │                                    equals (law C), the verbs the typ bears, ▶
//!    └─ build-thing-console (Foldable 🖥)  the step log and the guest's framebuffer
//! ```
//!
//! # Three leaves — a GRAPH, not a list (owner, 2026-08-26: "byt")
//!
//! Five leaves (recipes · matrix · settings · kernel · console) became four when the
//! kernel leaf and its runtime-presence overlay were retired (gap #6: the kernel is the
//! `kärna` feature of a variant, not a region), and four became three on the owner's
//! ruling that the settings renderer is a GRAPH, not an indented list: the tree IS a
//! graph — node = variant or feature, a feature with children a SUBGRAPH collapsed by
//! default and unfolded on click (which is what removes the "booting/complex" feel),
//! edge = containment or CONSUMPTION — and each node has a MENU: its own settings plus
//! the verbs from the typ. The kernel kconfig picture is simply the unfolded subgraph
//! under `kärna`, same renderer, deeper, wearing the honesty label. The renderer is the
//! one dwarves already has ([`crate::dag_graph`], over facett-graphview's
//! `DecoratedGraphView`) — extended with the consumption stroke, fold and selection —
//! not a third graph widget.
//!
//! Nothing resolved → an empty graph, no `kärna`. A library → its variants, none with
//! `kärna`. gunnar → `kärna` on the installer with the edge to `tunnr/tunnr-kernel`.
//! tunnr → `kärna` as the kernel variant's own subgraph.
//!
//! **Nothing here knows what a repo is.** The tree is a shape; every word in it comes
//! from [`Forge`](crate::build_scene::Forge), the trait the scene was already injected
//! with. Pick `gunnar` and the graph, the menu and the run surface all re-resolve from
//! that one selection — the same way the roster itself is discovered rather than listed.
//!
//! # THIS TREE IS MOUNTED — and for a long time it was not, while these docs implied it
//!
//! Measured 2026-08-26, and kept because a claim that was false for months should not
//! vanish the day it becomes true: **[`body`] had no callers.** `tabbed.rs` could mount a
//! tab body in exactly two shapes — `TabPanel::Whole` (one slot) or `TabPanel::Split`
//! (the `<slug>-main` | `<slug>-inspector` pair `body_split` declares) — and the ⚒ tab
//! was mounted `Whole`. What closed it is a THIRD mount shape — `tabbed::LeafPanel` /
//! `TabPanel::Leaves`, whose subtree for a slug is `tabbed::body_leaves`, which for ⚒ is
//! literally [`body`]. `BuildScene` implements it, `lib.rs` mounts the tab as
//! `Box<dyn LeafPanel>`, and `TabbedView::new` ASSERTS that a slug with a declared
//! leaf-body is mounted as one.
//!
//! The proof that it renders is at the integration layer: `tests/robot_five_leaf_body.rs`
//! (its name from the five-leaf days, kept so the lane that named it still finds it)
//! addresses all three leaves by name in the SHIPPED shell, drags both dividers with a
//! real pointer, and folds each leaf. `BuildScene::ui` still draws the same regions
//! imperatively, in the same order, for anything that boxes the pane whole.

use facett_app::scene::{Axis, Layout, Pane};

/// The tab id the shell folds this pane's state under. Must stay `slug("Build Thing")`.
pub const TAB: &str = "build-thing";

/// The three leaves, in the order they read on screen. `pub` because the shell binds a
/// panel to each by name and `tests/robot_scene.rs` addresses them the same way — a leaf
/// nothing can name is a leaf a robot cannot drive.
///
/// The build GRAPH — which root (the recipe search), the variants and their features
/// as nodes, containment and consumption as edges, the accessible strips beneath.
pub const GRAPH: &str = "build-thing-graph";
/// The selected node's MENU — its settings with their paths and flags, the verbs, ▶.
pub const MENU: &str = "build-thing-menu";
/// The step log and the guest's framebuffer.
pub const CONSOLE: &str = "build-thing-console";

/// Every DECLARED leaf, in reading order — the roster a host binds and a robot walks.
/// Three, unconditionally: their subjects exist whenever the tab does. The kernel is a
/// node of the graph, not a leaf.
pub const LEAVES: [&str; 3] = [GRAPH, MENU, CONSOLE];

/// The ⚒ tab's body.
///
/// Ratios are the *initial* ones — every split here is draggable, and the shell persists
/// what the operator drags them to. The graph gets the wider half because it is the
/// thing being read; the console keeps real height because a guest that boots into a
/// two-line strip is a guest nobody can watch.
#[must_use]
pub fn body() -> Pane {
    let graph = Pane::slot(GRAPH, GRAPH)
        .title("Byggraf")
        .tooltip(
            "Vilken rot (receptsökningen), sedan byggrafen: varianter och deras features som \
             noder — en feature med barn är en delgraf, hopfälld tills du klickar — \
             innehållskanter och KONSUMTIONSKANTER (--build X, dubbelt streck): samma DAG som \
             runa kör",
        )
        .foldable_icon(true, "⚒");

    let menu = Pane::slot(MENU, MENU)
        .title("Meny")
        .tooltip(
            "Den valda nodens egna inställningar — varje rad med sin sökväg och flaggan den \
             är lika med — verben dess typ bär, och ▶",
        )
        .foldable_icon(true, "☰");

    let console = Pane::slot(CONSOLE, CONSOLE)
        .title("Konsol")
        .tooltip("Stegloggen och gästens bildruta")
        .foldable_icon(true, "🖥");

    // The menu and the console share the right side; the divider between them is the
    // one the operator reaches for when a boot starts painting.
    let side = Pane::split(
        format!("{TAB}-side"),
        Axis::Vertical,
        0.50,
        vec![menu, console],
    );

    Pane::split(TAB, Axis::Horizontal, 0.55, vec![graph, side])
        .title("Build Thing")
        .foldable_icon(true, "⚒")
        // Phone-class: three panes side by side is three unreadable panes. The same
        // three leaves become tabs, so nothing is lost and nothing is squeezed.
        .phone(Layout::Tabs { active: 0 }, 760.0)
}

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

    fn walk(p: &Pane, splits: &mut usize, leaves: &mut Vec<String>) {
        if let Layout::Split { resizable, .. } = &p.layout {
            assert!(
                *resizable,
                "the split `{}` is FIXED — a divider the operator cannot drag is the \
                 same as no divider, which is what made the console unresizable",
                p.name
            );
            *splits += 1;
        }
        match &p.content {
            Content::Children(kids) => {
                for k in kids {
                    walk(k, splits, leaves);
                }
            }
            _ => leaves.push(p.name.clone()),
        }
    }

    /// **The console must be DRAGGABLE, which is the defect this module exists for.**
    #[test]
    fn every_divider_inside_the_tab_is_draggable() {
        let (mut splits, mut leaves) = (0, Vec::new());
        walk(&body(), &mut splits, &mut leaves);
        assert_eq!(splits, 2, "two dividers: graph|side, menu|console");
        assert_eq!(leaves.len(), LEAVES.len(), "three leaves, got {leaves:?}");
    }

    /// **The kernel is NOT a leaf of ⚒ — it is a node of the build graph.** Neither is
    /// the matrix or a settings list: both are the graph and the menu now.
    ///
    /// RED WHEN REVERTED: add a `Pane::slot("build-thing-kernel", …)` (or a settings /
    /// matrix leaf) and the walk finds it.
    #[test]
    fn the_kernel_the_matrix_and_the_settings_are_the_graph_and_the_menu_not_leaves() {
        let (mut splits, mut leaves) = (0, Vec::new());
        walk(&body(), &mut splits, &mut leaves);
        for retired in ["kernel", "matrix", "settings", "recipes"] {
            assert!(
                !leaves.iter().any(|l| l.contains(retired)),
                "a `{retired}` LEAF is back in the ⚒ body: {leaves:?}"
            );
        }
        assert_eq!(
            leaves,
            vec![GRAPH.to_string(), MENU.to_string(), CONSOLE.to_string()]
        );
    }

    /// Every leaf is addressable by name. A pane a robot cannot name is a pane no test
    /// can drive and no deep link can reach.
    #[test]
    fn the_three_leaves_are_named_and_distinct() {
        fn names(p: &Pane, out: &mut Vec<String>) {
            out.push(p.name.clone());
            if let Content::Children(kids) = &p.content {
                for k in kids {
                    names(k, out);
                }
            }
        }
        let mut all = Vec::new();
        names(&body(), &mut all);
        for want in LEAVES {
            assert!(
                all.iter().any(|n| n == want),
                "leaf `{want}` is missing from {all:?}"
            );
        }
        let mut sorted = all.clone();
        sorted.sort();
        sorted.dedup();
        assert_eq!(sorted.len(), all.len(), "duplicate pane name in {all:?}");
    }
}