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

nordisk/facett

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"]);
    }
}