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

nordisk/facett

.nornir/scene-chrome-guide.md

facett-app scene — window chrome — guide

Class: guide · Function axis: f0 what you get for free · f1 the host recipe · f2 driving it headlessly · f3 robot-testing it · f4 opting out · f5 for idiots (the five-minute version). The shape and the why live in scene-design.md §f6.


f0 — what you get for FREE (no call at all)

The adoption call is zero lines. If your app already does this:

let scene = my_scene();                     // unchanged
let mut nav = facett_app::scene::NavState::new(&scene);   // unchanged
let state = facett_app::scene::render(&scene, &mut nav, ui, &mut host); // unchanged

then as of this version every pane in your tree except the root already draws a title bar with:

control glyph what it does
fold ⏷ / ⏵ hides the body, keeps the pane's slot (only on a Foldable pane)
minimise − collapses the pane to that bar and hands its space to its siblings
maximise 🗖 the pane takes the whole scene area; every sibling is allotted zero
restore 🗗 replaces minimise/maximise once engaged; returns the EXACT prior layout

and every Layout::Split with two or more children draws a real drag handle with a three-dot grip between each adjacent pair.

Nothing else changed: your RON is byte-identical (the default is skipped on serialize), and a pane you mark .no_chrome() renders exactly as it did before chrome existed.

f1 — the host recipe (what you SHOULD add)

Three optional one-liners, in the order they pay off:

1. Prove the affordances are there. In your shell's test:

let aff = my_scene().affordances();
// {"chrome":[...], "draggable_splits":[...], "foldable":[...]}
assert!(!aff["chrome"].as_array().unwrap().is_empty(), "the shell ships window chrome");

2. Hang a "reset layout" on a menu. NavState::restore_all() clears every minimise and the maximise in one call:

MenuItem::new("view.reset", "Reset layout").action(Action::Command("view:reset".into()))
// …and in SceneHost::command: "view:reset" => { self.nav.restore_all(); }

3. Or drive chrome declaratively, no host code at all — the scene's own routes and menu actions reach it:

MenuItem::new("view.zoom", "Maximize the canvas").action(Action::Maximize("canvas".into()))
Route::on_shortcut("F11", Target::Maximize("canvas".into()))

Give your root a bar too (a Tabs-rooted shell like holger's has no split to drag, so the root bar is where its window controls live):

Pane::tabs(ROOT, 0, leaves).title("holger").chrome_always()

Want a divider in a Tabs-rooted shell? Wrap the tabs in a split — Pane::split is draggable by default, so this is the whole change:

Pane::split("shell", Axis::Horizontal, 0.22, vec![
    Pane::slot("rail", "nav").title("Rail"),
    Pane::tabs(ROOT, 0, leaves).title("holger"),   // your existing tabbed root
])

f2 — driving it headlessly (CLI / test / remote-robot parity)

Every affordance has a headless twin on NavState; the drawn control is only ever a second way in:

nav.toggle_minimize("sidebar");   // -> the new bool
nav.set_minimized("sidebar", true);
nav.is_minimized("sidebar");

nav.set_maximized("canvas");      // at most ONE maximised pane, scene-wide
nav.toggle_maximize("canvas");
nav.is_maximized("canvas");
nav.maximized_pane();             // Option<&str>

nav.restore("canvas");            // un-minimise + drop the maximise
nav.restore_all();                // the "reset layout" one-liner

These work for any pane wherever it sits — inside a Split, inside Tabs, nested. Chrome governs whether the button is drawn, never whether the capability exists.

f3 — robot-testing it (L4)

Read the geometry, not the flag. render's state_json gives you, per pane:

pointer meaning
/panes/<n>/alloc [x,y,w,h] the parent allotted — zero-extent for a sibling hidden by a maximise
/panes/<n>/area the allotted area — exactly 0.0 when hidden
/panes/<n>/content_rect, /content_height what the body occupied — 0.0 when folded or minimised
/panes/<n>/rect what the pane drew — just the bar when collapsed
/panes/<n>/chrome/buttons/{fold,minimize,maximize} each control's clickable rect
/panes/<n>/chrome/bar_height the title bar's height
/nav/minimized, /nav/maximized, /nav/last_chrome the state, and which control last fired

Two ways to click a control, both supported — use whichever your driver has:

// by unique AccessKit label
harness.get_by_label("maximize body").click();
// by published rect (the same addressing the split `separators` use)
let b = &state["panes"]["body"]["chrome"]["buttons"]["maximize"];
robot.click_at(pos2(b[0]+b[2]/2.0, b[1]+b[3]/2.0));

A nornir-robotui scenario step, with a rect for its oracle:

Step::new("maximise the canvas -> the rail is allotted ZERO width")
    .action(Action::ClickLabel("maximize canvas".into()))
    .assert(Assert::State { pointer: "/panes/rail/alloc/2".into(), expected: json!(0.0) })

facett-app/tests/robot_chrome.rs is the worked, running example (17 steps, 17 matrix rows). Do not assert maximized == true and stop there — that assertion passes on a renderer that moved nothing, which is precisely the failure this layer exists to prevent.

f4 — opting out

Pane::slot("canvas", "gl").no_chrome()              // no bar at all
Pane::slot("side", "nav").chrome(Chrome::FoldOnly)  // a bar, fold toggle only
Pane::tabs(ROOT, 0, kids).chrome_always()           // a bar on the ROOT too
Pane::split_fixed("hdr", Axis::Vertical, 0.2, kids) // a deliberately STATIC divider

A Fold::Foldable pane with Chrome::None falls back to the plain CollapsingHeader — pixel-identical to the pre-chrome renderer.

f5 — for idiots (the five-minute version)

  1. You don't have to do anything. Rebuild against the new facett-app. Every pane in your scene now has a little title bar with − and 🗖 on the right of it. Click 🗖 on a pane: it fills the window and its neighbours vanish. Click 🗗: everything comes back exactly where it was.
  2. − is not the same as the fold arrow. The fold arrow hides a pane's contents and leaves the hole where they were. − hides the contents and gives the space to the pane next door. That is the difference between an accordion and a window.
  3. The lines between panes are draggable. They have three dots in the middle. Grab and pull. If a divider isn't draggable, someone wrote split_fixed on purpose.
  4. To make it stop: .no_chrome() on that pane. That's the whole opt-out.
  5. To test it, assert a rectangle, not a true. state_json tells you the exact box every pane was given (/panes/<name>/alloc). If your maximise button really works, the neighbour's box is 0 wide. If you only assert the boolean, you have tested that a variable can be set, which it can.