facett-app scene — window chrome — guide
Class:
guide· Function axis:f0what you get for free ·f1the host recipe ·f2driving it headlessly ·f3robot-testing it ·f4opting out ·f5for idiots (the five-minute version). The shape and the why live inscene-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)
- 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. −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.- 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_fixedon purpose. - To make it stop:
.no_chrome()on that pane. That's the whole opt-out. - To test it, assert a rectangle, not a
true.state_jsontells you the exact box every pane was given (/panes/<name>/alloc). If your maximise button really works, the neighbour's box is0wide. If you only assert the boolean, you have tested that a variable can be set, which it can.