facett-app/tests/navgraph_scene.rs
//! **navgraph phase 2 on the scene container** (`.nornir/navgraph-design.md` §6).
//!
//! korp, dwarves and every other scene-based app draw their tabs and panes through
//! `facett_app::scene::render`. The scene tree is DATA, so the renderer declares the
//! navigation order from it: a children pane declares its kids in authored order, a tab
//! set declares `[tabs, *]` (the strip, then the active tab in the wildcard slot) and the
//! strip's headers in draw order. No host writes a `NavOrder` for its tabs and panes.
//!
//! This proves it on a korp-shaped scene (a rail beside a three-tab workspace):
//! 1. the spine (every scene pane, the strip and its headers) shows no `~` and no `?`;
//! 2. the numbers are the authored order: rail `1.1`, workspace `1.2`, strip `1.2.1`,
//! headers `1.2.1.1..3`, the active tab `1.2.2` whichever tab is active;
//! 3. `Ctrl+G 1.2.1.3 Enter` focuses the third header.
//!
//! ```bash
//! cargo test -p facett-app --test navgraph_scene
//! ```
use egui::{Key, Modifiers};
use egui_kittest::Harness;
use facett_app::scene::{self, Axis, NavState, Pane, Scene, SceneHost};
use facett_core::navgraph::{self, NavKind, NavMode};
use serde_json::Value;
struct Host;
impl SceneHost for Host {
fn slot(&mut self, id: &str, ui: &mut egui::Ui) -> Value {
ui.label(format!("content of {id}"));
Value::Null
}
fn component(&self) -> &'static str {
"korp"
}
}
fn korpish() -> Scene {
Scene::new(
"navgraph-scene",
Pane::split(
"shell",
Axis::Horizontal,
0.25,
vec![
Pane::slot("rail", "facets").title("Facets"),
Pane::tabs(
"work",
0,
vec![
Pane::slot("cases", "cases").title("Cases"),
Pane::slot("map", "map").title("Map"),
Pane::slot("jobs", "jobs").title("Jobs"),
],
),
],
),
)
}
struct St {
scene: Scene,
nav: NavState,
}
fn harness() -> Harness<'static, St> {
facett_core::devid::force_gate(true);
let scene = korpish();
let nav = NavState::new(&scene);
let mut h = Harness::builder().with_size(egui::vec2(1000.0, 700.0)).build_ui_state(
|ui, st: &mut St| {
scene::render(&st.scene, &mut st.nav, ui, &mut Host);
},
St { scene, nav },
);
navgraph::set_mode(&h.ctx, NavMode::Labels);
h.run_steps(3);
h
}
/// Is node `i` inside a host-owned pane (phase 3), below the scene's own spine?
fn in_host_pane(g: &navgraph::NavGraph, i: usize) -> bool {
let mut at = g.nodes()[i].parent;
while let Some(p) = at {
if g.nodes()[p as usize].kind == NavKind::Pane {
return true;
}
at = g.nodes()[p as usize].parent;
}
false
}
fn number(g: &navgraph::NavGraph, id: &str) -> String {
g.find_id(id).unwrap_or_else(|| panic!("no node {id}: {}", ids(g))).number.clone()
}
fn ids(g: &navgraph::NavGraph) -> String {
g.nodes().iter().map(|n| format!("{}={}", n.id, n.number)).collect::<Vec<_>>().join(" ")
}
#[test]
fn the_scene_tree_is_the_declared_order() {
let h = harness();
navgraph::with(&h.ctx, |g| {
assert_eq!(number(g, "shell"), "1");
assert_eq!(number(g, "shell.rail"), "1.1");
assert_eq!(number(g, "shell.work"), "1.2");
assert_eq!(number(g, "shell.work.tabs"), "1.2.1");
assert_eq!(number(g, "shell.work.tabs.cases"), "1.2.1.1");
assert_eq!(number(g, "shell.work.tabs.map"), "1.2.1.2");
assert_eq!(number(g, "shell.work.tabs.jobs"), "1.2.1.3");
assert_eq!(number(g, "shell.work.cases"), "1.2.2", "the active tab takes the wildcard slot");
let spine_bad: Vec<String> = g
.nodes()
.iter()
.enumerate()
.filter(|(i, n)| !n.step.is_declared() && !n.harvested && !in_host_pane(g, *i))
.map(|(_, n)| format!("{} = {}", n.id, n.number))
.collect();
assert!(spine_bad.is_empty(), "the scene spine shows ~ or ?: {spine_bad:?}");
});
}
#[test]
fn switching_tabs_keeps_the_active_pane_number() {
let mut h = harness();
h.state_mut().nav.set_active_tab("work", 2);
h.run_steps(3);
navgraph::with(&h.ctx, |g| {
assert!(g.find_id("shell.work.cases").is_none(), "the inactive tab is not drawn");
assert_eq!(number(g, "shell.work.jobs"), "1.2.2");
});
}
#[test]
fn the_chord_focuses_the_third_tab_header() {
let mut h = harness();
let want = navgraph::with(&h.ctx, |g| g.find_number("1.2.1.3").and_then(|n| n.widget)).expect("a widget");
h.key_press_modifiers(Modifiers::CTRL, Key::G);
h.step();
for c in "1.2.1.3".chars() {
h.key_press(if c == '.' { Key::Period } else { Key::from_name(&c.to_string()).unwrap() });
h.step();
}
h.key_press(Key::Enter);
h.run_steps(3);
assert_eq!(h.ctx.memory(|m| m.focused()), Some(want), "Ctrl+G 1.2.1.3 did not focus the Jobs header");
}