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:?}");
}
}