facet-wrapped/src/devmode.rs
//! **Dev mode for a wrapped component** — the navigation graph (navgraph phase 1) over
//! the one component a canvas mounts, so a Streamlit host in dev mode shows the numbers
//! too.
//!
//! A shipped bundle is a release build: the dev-id gate is closed and there is no
//! environment to open it with. [`request`] opens it at runtime (the loader calls it
//! when the host passes `dev=True`), and [`draw`] wraps the component in the graph's
//! spine: `facet-wrapped` (the app, `1`) → the mounted component (the pane, `1.1`) →
//! the component's own atoms (`1.1.~n` until the component declares them, phase 3).
//!
//! Nothing here draws the component differently: [`draw`] calls `Facet::ui` exactly
//! once, and the overlay is painted by the navgraph on the top layer.
use std::sync::Mutex;
use facett_core::Facet;
use facett_core::navgraph::{self, NavKind, NavMode, NavOrder};
/// The app node every wrapped canvas hangs its component under.
pub const APP: &str = "facet-wrapped";
/// The overlay mode a host asked for, applied to the canvas's context on its first
/// frame (the context does not exist yet when the host asks).
static PENDING_MODE: Mutex<Option<NavMode>> = Mutex::new(None);
/// **Turn dev mode on for this page** with overlay mode `nav` (`labels`, `next`, `all`;
/// anything else reads as `labels`). Returns whether the dev gate is now open — `false`
/// only when something already decided it closed, which a host should report rather
/// than pretend.
pub fn request(nav: &str) -> bool {
let open = facett_core::devid::try_force_gate(true);
if open && let Ok(mut m) = PENDING_MODE.lock() {
*m = Some(NavMode::parse(nav).filter(|m| *m != NavMode::Off).unwrap_or(NavMode::Labels));
}
open
}
/// The declared order of a wrapped canvas: the app, then the one component `name`.
pub fn order(name: &str) -> NavOrder {
NavOrder::new().roots([APP]).children(APP, [name])
}
/// **Draw `facet` as the app's one pane.** Outside dev mode this is `facet.ui(ui)` and
/// nothing else.
pub fn draw(ui: &mut egui::Ui, name: &str, facet: &mut dyn Facet) {
if let Some(mode) = PENDING_MODE.lock().ok().and_then(|mut m| m.take()) {
navgraph::set_mode(ui.ctx(), mode);
}
if !navgraph::collecting(ui.ctx()) {
facet.ui(ui);
return;
}
// Re-declared only when the mounted component changes (a live data swap may
// change it); `set_order` is a no-op for an equal order.
let declared = navgraph::with(ui.ctx(), |g| g.order().get(APP).is_some_and(|k| k.len() == 1 && k[0] == name));
if !declared {
navgraph::set_order(ui.ctx(), order(name));
}
let rect = ui.max_rect();
let app = navgraph::begin(ui, APP, NavKind::App, APP);
let pane = navgraph::begin(ui, name, NavKind::Pane, facet.title());
facet.ui(ui);
navgraph::end(ui, pane, rect);
navgraph::end(ui, app, rect);
}
/// The last finished frame's navigation graph (`navgraph::NavGraph::to_json`).
///
/// Stamped with `frame` (egui's cumulative pass number), so a driver that samples this
/// several times can prove it read the graph of DIFFERENT frames, not one frozen copy
/// (`examples/gallery_probe.rs --dev` asserts the numbers on every sample).
pub fn graph_json(ctx: &egui::Context) -> serde_json::Value {
let mut j = navgraph::to_json(ctx);
if let Some(o) = j.as_object_mut() {
o.insert("frame".into(), ctx.cumulative_pass_nr().into());
}
j
}
#[cfg(test)]
mod tests {
use super::*;
/// A mounted component numbers `1` → `1.1`, declared, and its atoms hang under it.
#[test]
#[allow(unexpected_cfgs)]
#[cfg(not(feature = "group-only"))]
fn a_wrapped_component_is_pane_one_one_of_a_declared_app() {
assert!(request("labels"), "the dev gate opens at runtime");
let reg = crate::Registry::with_builtins();
let mut facet = reg.build("graph2d").expect("graph2d builds");
let ctx = egui::Context::default();
// Two frames: the first installs the plugin, the second collects.
for _ in 0..3 {
ctx.run_ui(egui::RawInput::default(), |ui| {
draw(ui, "graph2d", facet.as_mut());
}).textures_delta.clear();
}
assert_eq!(navgraph::mode(&ctx), NavMode::Labels);
navgraph::with(&ctx, |g| {
assert_eq!(g.find_id(APP).map(|n| n.number.as_str()), Some("1"));
assert_eq!(g.find_id("facet-wrapped.graph2d").map(|n| n.number.as_str()), Some("1.1"));
let spine_bad = g.nodes().iter().filter(|n| n.depth <= 2 && !n.step.is_declared()).count();
assert_eq!(spine_bad, 0, "the wrapped spine is declared");
});
let j = graph_json(&ctx);
assert_eq!(j["mode"], "labels");
assert!(j["count"].as_u64().unwrap() >= 2);
assert!(j["frame"].as_u64().is_some_and(|f| f >= 2), "the graph names its frame: {}", j["frame"]);
}
}