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