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

nordisk/facett

facett-demo/src/flow_tab.rs

//! Showcase host for Falsett. The library facet owns the model; this host only
//! supplies a frame clock and sends the same messages exposed to headless callers.

use facett::Facet;
use facett::core::{Elm, FacetCaps};
use facett::flow::{FlowGraph, Graph, Msg};

pub const TITLE: &str = "Falsett";

pub struct FlowTab {
    flow: FlowGraph,
    fixture: String,
}

impl Default for FlowTab {
    fn default() -> Self {
        Self::new()
    }
}

impl FlowTab {
    pub fn new() -> Self {
        let mut tab = Self {
            flow: FlowGraph::local(),
            fixture: "hub".into(),
        };
        tab.flow.set_compact_state(true);
        tab.flow.set_labels(true);
        tab
    }

    /// Select deterministic synthetic fixtures through the same seam as the UI.
    pub fn set_fixture(&mut self, fixture: &str) -> Result<(), String> {
        let mut graph = match fixture {
            "hub" => Graph::hub_demo(10_000),
            "stages" => Graph::demo(500),
            "layered" => Graph::demo(80),
            _ => return Err("Unknown flow fixture".into()),
        };
        if fixture == "layered" {
            graph.layered(facett_flow::LayeredOpts::default())?;
        }
        let mut flow = FlowGraph::new("showcase-flow", graph)?;
        flow.set_compact_state(true);
        flow.set_labels(true);
        flow.update(Msg::Clustered(fixture == "hub"));
        if fixture == "layered" {
            use facett_flow::attr::{Rates, Status, StatusTable};
            let nodes = flow.state().graph.nodes.len();
            let edges = flow.state().graph.edges.len();
            flow.update(Msg::Status(std::sync::Arc::new(StatusTable {
                nodes: (0..nodes)
                    .map(|i| match i % 7 {
                        0 => Some(Status::Error),
                        1 => Some(Status::Slow),
                        _ => Some(Status::Ok),
                    })
                    .collect(),
                edges: vec![None; edges],
            })));
            flow.update(Msg::Rates(std::sync::Arc::new(Rates {
                edges: (0..edges).map(|i| Some((i % 9) as f32 * 100.0)).collect(),
            })));
        }
        self.flow = flow;
        self.fixture = fixture.into();
        Ok(())
    }

    /// Supplied-data seam, shared by the showcase and its empty-state matrix.
    pub fn from_graph(graph: Graph) -> Result<Self, String> {
        let mut flow = FlowGraph::new("showcase-flow", graph)?;
        flow.set_compact_state(true);
        flow.set_labels(true);
        Ok(Self {
            flow,
            fixture: "supplied".into(),
        })
    }
}

impl Facet for FlowTab {
    fn title(&self) -> &str {
        TITLE
    }

    fn ui(&mut self, ui: &mut egui::Ui) {
        ui.horizontal_wrapped(|ui| {
            ui.label("Synthetic fixture:");
            for (key, label) in [
                ("hub", "Hub"),
                ("stages", "Stages"),
                ("layered", "Layered DAG"),
            ] {
                if ui.selectable_label(self.fixture == key, label).clicked() {
                    let _ = self.set_fixture(key);
                }
            }
            let state = self.flow.state();
            let mut running = state.running;
            let mut clustered = state.clustered;
            let mut reduced = state.reduced_motion;
            let count = state.graph.nodes.len();
            let edges = state.graph.edges.len();
            if ui.checkbox(&mut running, "Animate flow").changed() {
                self.flow.update(Msg::Running(running));
            }
            if ui.checkbox(&mut clustered, "Cluster overview").changed() {
                self.flow.update(Msg::Clustered(clustered));
            }
            if ui.checkbox(&mut reduced, "Reduced motion").changed() {
                self.flow.update(Msg::ReducedMotion(reduced));
            }
            if ui.button("Fit flow").clicked() {
                self.flow.update(Msg::Fit);
            }
            ui.label(format!("{count} nodes · {edges} links"));
        });
        let state = self.flow.state();
        let animate = state.running
            && !state.reduced_motion
            && facett::look::effects_policy(ui).allows_decorative_motion();
        if animate {
            let dt = ui.input(|i| i.stable_dt).clamp(0.0, 0.1);
            self.flow.update(Msg::Tick(f64::from(dt)));
            ui.ctx().request_repaint();
        }
        self.flow.ui(ui);
    }

    fn state_json(&self) -> serde_json::Value {
        let mut state = self.flow.state_json();
        state["fixture"] = serde_json::json!(self.fixture);
        state
    }

    fn update_json(&mut self, json: &str) {
        if let Ok(value) = serde_json::from_str::<serde_json::Value>(json)
            && let Some(fixture) = value.get("fixture").and_then(|v| v.as_str())
        {
            let _ = self.set_fixture(fixture);
            return;
        }
        self.flow.update_json(json);
    }

    fn caps(&self) -> FacetCaps {
        self.flow.caps()
    }

    fn selection_json(&self) -> serde_json::Value {
        self.flow.selection_json()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn showcase_clock_animates_and_respects_pause_and_reduced_motion() {
        let mut flow = FlowTab::from_graph(Graph::hub_demo(100)).unwrap();
        let ctx = egui::Context::default();
        let paint = |flow: &mut FlowTab| {
            let mut output = ctx.run_ui(egui::RawInput::default(), |ui| {
                facett::look::Theme::nordisk_dark().apply(ui.ctx());
                egui::CentralPanel::default().show(ui, |ui| flow.ui(ui));
            });
            assert!(!output.shapes.is_empty());
            output.textures_delta.clear();
        };
        paint(&mut flow);
        let started = flow.state_json()["seconds"].as_f64().unwrap();
        assert!(started > 0.0);
        flow.update_json(r#"{"Running":false}"#);
        paint(&mut flow);
        assert_eq!(flow.state_json()["seconds"], started);
        flow.update_json(r#"{"Running":true}"#);
        flow.update_json(r#"{"ReducedMotion":true}"#);
        paint(&mut flow);
        assert_eq!(flow.state_json()["seconds"], started);
    }

    #[test]
    fn showcase_empty_data_stays_empty() {
        let mut flow = FlowTab::from_graph(Graph {
            nodes: Vec::new(),
            edges: Vec::new(),
        })
        .unwrap();
        let report = facett::harness::headless_render(&mut flow);
        assert!(report.drew());
        assert_eq!(report.state["node_count"].as_u64().unwrap(), 0);
        assert_eq!(report.state["edge_count"].as_u64().unwrap(), 0);
    }

    #[test]
    fn fixture_picker_uses_layered_positions_and_compact_state() {
        let mut tab = FlowTab::new();
        tab.update_json(r#"{"fixture":"layered"}"#);
        let state = tab.state_json();
        assert_eq!(state["fixture"], "layered");
        assert_eq!(state["node_count"], 80);
        assert!(state.get("graph").is_none());
        let graph = &tab.flow.state().graph;
        for edge in &graph.edges {
            let source = graph.nodes.iter().find(|n| n.id == edge.from).unwrap();
            let target = graph.nodes.iter().find(|n| n.id == edge.to).unwrap();
            assert!(source.position[0] < target.position[0]);
        }
        assert!(tab.set_fixture("missing").is_err());
        assert_eq!(tab.state_json()["fixture"], "layered");
        tab.set_fixture("stages").unwrap();
        assert_eq!(tab.state_json()["node_count"], 500);
        tab.set_fixture("hub").unwrap();
        assert_eq!(tab.state_json()["node_count"], 10_000);
        assert_eq!(tab.state_json()["clustered"], true);
    }
}