facett-graphnav/examples/vg_watch.rs
//! # WATCH V.G (the GRAPH NAVIGATOR) drive itself on a real window
//!
//! The **visible sibling** of the headless self-driving robot test
//! `facett-graphnav/tests/robot.rs::robot_self_drives_vg_visibly_moving`: the SAME
//! scripted exploration — query → select → layout → focus → cycle → similar — but
//! run through `nornir-robotui`'s live WATCH backend (feature `watch`) so a human
//! WATCHES the robot steer a cursor across V.G, click each control, and see the
//! navigator react: the Person result set lights up, the inspector populates, the
//! nodes relocate on a layout switch, the camera zooms to the selection, an amber
//! cycle ring lights a triangle, and the teal similarity rings appear.
//!
//! One `Observable` + one `draw(ui, app, targets)` closure + one `Scenario` — the
//! 10-line adoption pattern from
//! `../../nornir/crates/nornir-robotui/.nornir/robotui-watch-mode.md`. The headless
//! matrix/robot path in `tests/robot.rs` is untouched; this is purely additive.
//!
//! ## Run it
//!
//! ```sh
//! DISPLAY=:0 cargo run -p facett-graphnav --example vg_watch
//! # slower / hold the window open at the end:
//! DISPLAY=:0 ROBOTUI_WATCH_DELAY_MS=1600 ROBOTUI_WATCH_HOLD=1 \
//! cargo run -p facett-graphnav --example vg_watch
//! # print the final observable state + per-step verdicts:
//! DISPLAY=:0 ROBOTUI_WATCH_DEBUG=1 cargo run -p facett-graphnav --example vg_watch
//! ```
use facett_core::Facet;
use facett_graphnav::{GraphNav, LayoutKind, Msg};
use nornir_robotui::{
run_scenario_mode, Action, Assert, Observable, RunMode, Scenario, Step, Targets, WatchConfig,
};
use serde_json::{json, Value};
/// The live V.G navigator wrapped as an [`Observable`]. `state_json` forwards the
/// navigator's own introspection blob unchanged, so the SAME JSON pointers the
/// headless robot test reads (`/query/result_nodes`, `/inspector/caption`,
/// `/canvas/layout`, `/camera/zoom`, `/cycles/highlighted`, `/similarity/k`)
/// resolve here too.
struct WatchVg(GraphNav);
impl Observable for WatchVg {
fn state_json(&self) -> Value {
// Disambiguate: both `Facet` and `Observable` expose `state_json`.
Facet::state_json(&self.0)
}
}
/// ONE closure, BOTH modes. The top control row records a compact button per
/// scenario step (the WATCH driver resolves an `Action`'s label to a recorded rect
/// and steers the cursor there); each button drives the navigator through its
/// public `update(Msg)` Elm surface — the exact `Msg`s the headless self-driving
/// scenario feeds. Below the row the LIVE V.G surface is drawn (`Facet::ui`), so
/// the query result, inspector, relocating nodes, camera, cycle ring and
/// similarity rings are all on screen to watch react.
fn draw(ui: &mut egui::Ui, app: &mut WatchVg, t: &mut Targets) {
ui.horizontal_wrapped(|ui| {
if t.record("Run Person query", ui.button("Run Person query")).clicked() {
app.0.update(Msg::SetQuery("g.V().hasLabel('Person')".into()));
app.0.update(Msg::RunQuery);
}
if t.record("Select bridge (Carol)", ui.button("Select bridge (Carol)")).clicked() {
app.0.update(Msg::SelectNode("p3".into()));
}
if t.record("Radial layout", ui.button("Radial layout")).clicked() {
app.0.update(Msg::SetLayout(LayoutKind::Radial));
}
if t.record("Focus selection", ui.button("Focus selection")).clicked() {
app.0.update(Msg::ZoomToSelection);
}
if t.record("Highlight cycle", ui.button("Highlight cycle")).clicked() {
app.0.update(Msg::FitView);
app.0.update(Msg::HighlightCycle(Some(0)));
}
if t.record("Similar nodes", ui.button("Similar nodes")).clicked() {
app.0.update(Msg::FindSimilar(5));
}
});
ui.separator();
// The LIVE navigator — the surface the robot's control row drives.
app.0.ui(ui);
}
/// The V.G self-driving WATCH scenario — the visible twin of the headless
/// `robot_self_drives_vg_visibly_moving`: query → select → layout → focus →
/// cycle → similar, asserting the observable state after each driven step.
fn vg_scenario() -> Scenario {
Scenario::new("robot-self-drives-vg-watch")
.step(
Step::new("run the Person query — six Person nodes light up")
.action(Action::ClickButton("Run Person query".into()))
.assert(Assert::State { pointer: "/query/result_nodes".into(), expected: json!(6) }),
)
.step(
Step::new("select the bridge node — the inspector shows Carol")
.action(Action::ClickButton("Select bridge (Carol)".into()))
.assert(Assert::State {
pointer: "/inspector/caption".into(),
expected: json!("Carol"),
}),
)
.step(
Step::new("switch to a radial layout — the nodes visibly relocate")
.action(Action::ClickButton("Radial layout".into()))
.assert(Assert::State {
pointer: "/canvas/layout".into(),
expected: json!("radial"),
}),
)
.step(
Step::new("focus the selection — the camera zooms in to ~2.6×")
.action(Action::ClickButton("Focus selection".into()))
// f32 2.6 serialises to 2.5999999…; StateContains proves the zoom
// snapped to FOCUS_ZOOM without pinning the float tail.
.assert(Assert::StateContains {
pointer: "/camera/zoom".into(),
needle: "2.59".into(),
}),
)
.step(
Step::new("highlight the first directed cycle — the amber ring lights a triangle")
.action(Action::ClickButton("Highlight cycle".into()))
.assert(Assert::State { pointer: "/cycles/highlighted".into(), expected: json!(0) }),
)
.step(
Step::new("find nodes similar to the selection — the teal similarity rings appear")
.action(Action::ClickButton("Similar nodes".into()))
.assert(Assert::State { pointer: "/similarity/k".into(), expected: json!(5) }),
)
}
fn main() -> anyhow::Result<()> {
let cfg = WatchConfig::from_env().with_title("facett — V.G graph navigator (watch)");
// This IS the watch entrypoint, so drive the visible backend directly (still
// honours ROBOTUI_WATCH_DELAY_MS / _LINGER_MS / _HOLD / _DEBUG via WatchConfig).
let _ = RunMode::Headless; // (documents the other backend the headless test uses)
run_scenario_mode(WatchVg(GraphNav::demo()), draw, &vg_scenario(), "facett", RunMode::Watch, cfg)?;
Ok(())
}