use facett_core::{Facet, FacetCaps, Theme, theme};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum StationKind {
Start,
Emitter,
PassThrough,
Grpc,
Terminus,
UiClient,
CliClient,
}
impl StationKind {
pub fn as_str(self) -> &'static str {
match self {
StationKind::Start => "start",
StationKind::Emitter => "emitter",
StationKind::PassThrough => "passthrough",
StationKind::Grpc => "grpc",
StationKind::Terminus => "terminus",
StationKind::UiClient => "ui_client",
StationKind::CliClient => "cli_client",
}
}
pub fn parse(s: &str) -> Self {
match s {
"start" => StationKind::Start,
"emitter" => StationKind::Emitter,
"grpc" => StationKind::Grpc,
"terminus" => StationKind::Terminus,
"ui_client" => StationKind::UiClient,
"cli_client" => StationKind::CliClient,
_ => StationKind::PassThrough,
}
}
pub fn marker(self) -> Option<&'static str> {
match self {
StationKind::UiClient => Some("UI"),
StationKind::CliClient => Some("CLI"),
_ => None,
}
}
pub fn is_client_arm(self) -> bool {
matches!(self, StationKind::UiClient | StationKind::CliClient)
}
pub fn glyph(self) -> &'static str {
match self {
StationKind::Start => "▶",
StationKind::Emitter => "●",
StationKind::PassThrough => "·",
StationKind::Grpc => "◆",
StationKind::Terminus => "■",
StationKind::UiClient => "▲",
StationKind::CliClient => "◇",
}
}
pub fn gates(self) -> bool {
!matches!(self, StationKind::PassThrough | StationKind::UiClient | StationKind::CliClient)
}
pub fn is_station(self) -> bool {
!matches!(self, StationKind::PassThrough)
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct MetroBranch {
pub id: String,
pub label: String,
pub kind: StationKind,
pub lit: bool,
}
impl MetroBranch {
pub fn new(id: impl Into<String>, label: impl Into<String>, kind: StationKind, lit: bool) -> Self {
Self { id: id.into(), label: label.into(), kind, lit }
}
pub fn marker(&self) -> Option<&'static str> {
self.kind.marker()
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct MetroStation {
pub id: String,
pub label: String,
pub kind: StationKind,
pub lit: bool,
pub branches: Vec<MetroBranch>,
}
impl MetroStation {
pub fn new(id: impl Into<String>, label: impl Into<String>, kind: StationKind, lit: bool) -> Self {
Self {
id: id.into(),
label: label.into(),
kind,
lit: if kind == StationKind::PassThrough { true } else { lit },
branches: Vec::new(),
}
}
pub fn with_branches(
id: impl Into<String>,
label: impl Into<String>,
kind: StationKind,
lit: bool,
branches: Vec<MetroBranch>,
) -> Self {
let mut s = Self::new(id, label, kind, lit);
s.branches = branches;
s
}
pub fn has_branches(&self) -> bool {
!self.branches.is_empty()
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct MetroLine {
pub id: String,
pub label: String,
pub stations: Vec<MetroStation>,
}
impl MetroLine {
pub fn new(id: impl Into<String>, label: impl Into<String>, stations: Vec<MetroStation>) -> Self {
Self { id: id.into(), label: label.into(), stations }
}
pub fn is_green(&self) -> bool {
self.stations.iter().filter(|s| s.kind.gates()).all(|s| s.lit)
}
pub fn unlit(&self) -> Vec<&MetroStation> {
self.stations.iter().filter(|s| s.kind.gates() && !s.lit).collect()
}
pub fn station_count(&self) -> usize {
self.stations.iter().filter(|s| s.kind.is_station()).count()
}
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct MetroMap {
pub lines: Vec<MetroLine>,
}
impl MetroMap {
pub fn new(lines: Vec<MetroLine>) -> Self {
Self { lines }
}
pub fn is_green(&self) -> bool {
self.lines.iter().all(|l| l.is_green())
}
pub fn green_count(&self) -> usize {
self.lines.iter().filter(|l| l.is_green()).count()
}
}
const ROW_H: f32 = 64.0;
const LINE_LEFT: f32 = 150.0;
const STOP_GAP: f32 = 96.0;
const STATION_R: f32 = 9.0;
const TICK_R: f32 = 3.0;
const LINE_W: f32 = 6.0;
pub struct MetroView {
title: String,
map: MetroMap,
selected: Option<String>,
}
impl MetroView {
pub fn new(title: impl Into<String>) -> Self {
Self { title: title.into(), map: MetroMap::default(), selected: None }
}
pub fn set_map(&mut self, map: MetroMap) {
self.map = map;
}
pub fn set_title(&mut self, title: impl Into<String>) {
self.title = title.into();
}
pub fn map(&self) -> &MetroMap {
&self.map
}
pub fn select(&mut self, id: Option<String>) {
self.selected = id;
}
pub fn local() -> Self {
let mut v = Self::new("Metro");
v.set_map(demo_map());
v
}
pub fn remote() -> Self {
let mut v = Self::local();
v.title = "Metro (remote)".into();
v
}
fn stop_color(th: &Theme, line_green: bool, lit: bool) -> egui::Color32 {
if !lit {
egui::Color32::from_rgb(224, 90, 90)
} else if line_green {
th.point } else {
th.accent }
}
fn line_color(th: &Theme, line_green: bool) -> egui::Color32 {
if line_green { th.point } else { th.text_dim }
}
fn arm_color(th: &Theme, kind: StationKind, lit: bool) -> egui::Color32 {
let base = match kind {
StationKind::CliClient => egui::Color32::from_rgb(214, 160, 70), _ => th.accent, };
if lit {
base
} else {
egui::Color32::from_rgba_unmultiplied(base.r(), base.g(), base.b(), 120)
}
}
}
const ARM_RISE: f32 = 30.0;
const ARM_RUN: f32 = 34.0;
const ARM_R: f32 = 7.0;
pub fn demo_map() -> MetroMap {
let line_a = MetroLine::new(
"bench_run→Bench.Submit",
"Bench Run",
vec![
MetroStation::new("ui::bench_button", "Run", StationKind::Start, true),
MetroStation::new("bench::collect", "collect", StationKind::Emitter, true),
MetroStation::new("bench::normalize", "normalize", StationKind::PassThrough, true),
MetroStation::new("bench::submit", "submit", StationKind::Emitter, true),
MetroStation::with_branches(
"grpc::Bench.Submit",
"Bench.Submit",
StationKind::Grpc,
true,
vec![
MetroBranch::new("caller::ui::Bench.Submit", "viz", StationKind::UiClient, true),
MetroBranch::new("caller::cli::Bench.Submit", "nornir", StationKind::CliClient, true),
],
),
MetroStation::new("warehouse::bench_runs", "bench_runs", StationKind::Terminus, true),
],
);
let line_b = MetroLine::new(
"docs_export→Docs.Render",
"Docs Export",
vec![
MetroStation::new("ui::docs_button", "Export", StationKind::Start, true),
MetroStation::new("docs::gather", "gather", StationKind::Emitter, true),
MetroStation::new("docs::render_svg", "render_svg", StationKind::Emitter, false), MetroStation::with_branches(
"grpc::Docs.Render",
"Docs.Render",
StationKind::Grpc,
true,
vec![MetroBranch::new("caller::cli::Docs.Render", "nornir-mcp", StationKind::CliClient, true)],
),
MetroStation::new("warehouse::doc_exports", "doc_exports", StationKind::Terminus, true),
],
);
MetroMap::new(vec![line_a, line_b])
}
impl Facet for MetroView {
fn title(&self) -> &str {
&self.title
}
fn ui(&mut self, ui: &mut egui::Ui) {
let th = theme(ui);
ui.horizontal_wrapped(|ui| {
let total = self.map.lines.len();
let green = self.map.green_count();
ui.label(egui::RichText::new(format!("{total} line(s)")).color(th.text).strong());
ui.separator();
ui.label(
egui::RichText::new(format!("✓ {green} green"))
.color(if green == total && total > 0 { th.point } else { th.text_dim }),
);
ui.label(
egui::RichText::new(format!("✗ {} red", total - green))
.color(if green < total { egui::Color32::from_rgb(224, 90, 90) } else { th.text_dim }),
);
});
ui.separator();
if self.map.lines.is_empty() {
ui.label(egui::RichText::new("no lines").color(th.text_dim));
#[cfg(feature = "testmatrix")]
facett_core::testmatrix::emit(
"facett-graphview::MetroView::ui",
"ui_render",
true,
"lines=0 drew=empty_hint",
);
return;
}
let n = self.map.lines.len();
let (rect, _resp) = ui.allocate_exact_size(
egui::vec2(ui.available_width().max(640.0), (ROW_H + ARM_RISE) * n as f32 + 16.0),
egui::Sense::hover(),
);
let painter = ui.painter_at(rect);
for (li, line) in self.map.lines.iter().enumerate() {
let green = line.is_green();
let y = rect.top() + (ROW_H + ARM_RISE) * li as f32 + ARM_RISE + ROW_H * 0.5;
let line_col = MetroView::line_color(&th, green);
painter.text(
egui::pos2(rect.left() + 8.0, y),
egui::Align2::LEFT_CENTER,
&line.label,
egui::FontId::proportional(14.0),
if green { th.point } else { th.text },
);
let x0 = rect.left() + LINE_LEFT;
let stops = &line.stations;
if !stops.is_empty() {
let x_last = x0 + STOP_GAP * (stops.len() as f32 - 1.0);
painter.line_segment(
[egui::pos2(x0, y), egui::pos2(x_last.max(x0), y)],
egui::Stroke::new(LINE_W, line_col),
);
}
for (si, st) in stops.iter().enumerate() {
let x = x0 + STOP_GAP * si as f32;
let center = egui::pos2(x, y);
if st.kind == StationKind::PassThrough {
painter.circle_filled(center, TICK_R, th.text_dim);
} else {
let fill = MetroView::stop_color(&th, green, st.lit);
if st.lit {
painter.circle_filled(center, STATION_R, fill);
} else {
painter.circle_stroke(center, STATION_R, egui::Stroke::new(2.5, fill));
}
let glyph = st.kind.glyph();
if matches!(st.kind, StationKind::Start | StationKind::Grpc | StationKind::Terminus) {
painter.text(
egui::pos2(x, y - STATION_R - 11.0),
egui::Align2::CENTER_CENTER,
glyph,
egui::FontId::proportional(12.0),
th.text,
);
}
}
painter.text(
egui::pos2(x, y + STATION_R + 9.0),
egui::Align2::CENTER_CENTER,
&st.label,
egui::FontId::proportional(10.0),
th.text_dim,
);
if st.has_branches() {
let m = st.branches.len();
for (bi, br) in st.branches.iter().enumerate() {
let frac = if m > 1 { bi as f32 / (m as f32 - 1.0) - 0.5 } else { 0.0 };
let ex = x + frac * (ARM_RUN * (m as f32).min(2.0));
let ey = y - ARM_RISE;
let col = MetroView::arm_color(&th, br.kind, br.lit);
painter.line_segment(
[center, egui::pos2(ex, ey)],
egui::Stroke::new(3.0, col),
);
let ep = egui::pos2(ex, ey);
if br.lit {
painter.circle_filled(ep, ARM_R, col);
} else {
painter.circle_stroke(ep, ARM_R, egui::Stroke::new(2.0, col));
}
painter.text(
egui::pos2(ex, ey - ARM_R - 8.0),
egui::Align2::CENTER_CENTER,
format!("{} {}", br.kind.glyph(), br.marker().unwrap_or("")),
egui::FontId::proportional(10.0),
col,
);
}
}
}
}
#[cfg(feature = "testmatrix")]
facett_core::testmatrix::emit(
"facett-graphview::MetroView::ui",
"ui_render",
!self.map.lines.is_empty(),
&format!("lines={} green={}", self.map.lines.len(), self.map.green_count()),
);
}
fn state_json(&self) -> serde_json::Value {
serde_json::json!({
"title": self.title,
"lines": self.map.lines.len(),
"green_lines": self.map.green_count(),
"green": self.map.is_green(),
"selected": self.selected,
"line": self.map.lines.iter().map(|l| serde_json::json!({
"id": l.id,
"label": l.label,
"green": l.is_green(),
"station_count": l.station_count(),
"unlit": l.unlit().iter().map(|s| s.label.clone()).collect::<Vec<_>>(),
"stations": l.stations.iter().map(|s| serde_json::json!({
"id": s.id,
"label": s.label,
"kind": s.kind.as_str(),
"lit": s.lit,
"branches": s.branches.iter().map(|b| serde_json::json!({
"id": b.id,
"label": b.label,
"kind": b.kind.as_str(),
"marker": b.marker(),
"lit": b.lit,
})).collect::<Vec<_>>(),
})).collect::<Vec<_>>(),
})).collect::<Vec<_>>(),
})
}
fn selection_json(&self) -> serde_json::Value {
match &self.selected {
Some(id) => serde_json::json!({ "line": id }),
None => serde_json::Value::Null,
}
}
fn caps(&self) -> FacetCaps {
FacetCaps::NONE.themeable().resizable().selectable()
}
fn as_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
Some(self)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn green_only_when_every_gating_station_lit() {
let line = MetroLine::new(
"l",
"L",
vec![
MetroStation::new("s", "start", StationKind::Start, true),
MetroStation::new("e", "emit", StationKind::Emitter, true),
MetroStation::new("p", "pass", StationKind::PassThrough, false), MetroStation::new("g", "grpc", StationKind::Grpc, true),
MetroStation::new("t", "wh", StationKind::Terminus, true),
],
);
assert!(line.is_green(), "all gating stops lit + a pass-through doesn't gate");
assert!(line.unlit().is_empty());
}
#[test]
fn one_unlit_emitter_makes_line_red() {
let line = MetroLine::new(
"l",
"L",
vec![
MetroStation::new("s", "start", StationKind::Start, true),
MetroStation::new("e", "emit", StationKind::Emitter, false),
],
);
assert!(!line.is_green());
assert_eq!(line.unlit().len(), 1);
assert_eq!(line.unlit()[0].label, "emit");
}
#[test]
fn passthrough_constructed_lit_and_never_gates() {
let p = MetroStation::new("p", "p", StationKind::PassThrough, false);
assert!(p.lit, "pass-through is forced lit (nothing to light)");
assert!(!p.kind.gates());
assert!(!p.kind.is_station());
}
#[test]
fn kind_parse_roundtrip() {
for k in [
StationKind::UiClient,
StationKind::CliClient,
StationKind::Start,
StationKind::Emitter,
StationKind::PassThrough,
StationKind::Grpc,
StationKind::Terminus,
] {
assert_eq!(StationKind::parse(k.as_str()), k);
}
assert_eq!(StationKind::parse("bogus"), StationKind::PassThrough);
}
#[test]
fn map_green_iff_all_lines_green() {
let m = demo_map();
assert_eq!(m.lines.len(), 2);
assert!(!m.is_green(), "line B has an unlit emitter");
assert_eq!(m.green_count(), 1);
}
#[test]
fn local_builds_two_line_demo() {
let v = MetroView::local();
assert_eq!(v.map().lines.len(), 2);
assert!(v.map().lines[0].is_green());
assert!(!v.map().lines[1].is_green());
}
#[test]
fn set_title_rekeys_view_and_keeps_the_map() {
let mut v = MetroView::local();
v.set_title("metro");
assert_eq!(Facet::title(&v), "metro", "title() reflects the new key");
let sj = v.state_json();
assert_eq!(sj["title"], "metro", "state_json carries the new title");
assert_eq!(sj["lines"].as_u64(), Some(2), "the demo map survived the re-title");
assert_eq!(sj["green_lines"].as_u64(), Some(1), "one green / one red line still");
}
#[test]
fn caller_arms_never_gate_the_trunk() {
assert!(!StationKind::UiClient.gates());
assert!(!StationKind::CliClient.gates());
assert!(StationKind::UiClient.is_client_arm() && StationKind::CliClient.is_client_arm());
let line = MetroLine::new(
"l",
"L",
vec![
MetroStation::new("s", "start", StationKind::Start, true),
MetroStation::with_branches(
"g",
"grpc",
StationKind::Grpc,
true,
vec![
MetroBranch::new("ui", "viz", StationKind::UiClient, false), MetroBranch::new("cli", "nornir", StationKind::CliClient, true),
],
),
MetroStation::new("t", "wh", StationKind::Terminus, true),
],
);
assert!(line.is_green(), "an unlit caller arm does not gate the green trunk");
assert!(line.unlit().is_empty(), "arms are not counted as unlit gating stops");
}
#[test]
fn client_kinds_carry_the_right_marker() {
assert_eq!(StationKind::UiClient.marker(), Some("UI"));
assert_eq!(StationKind::CliClient.marker(), Some("CLI"));
assert_eq!(StationKind::Grpc.marker(), None);
let b = MetroBranch::new("x", "viz", StationKind::UiClient, true);
assert_eq!(b.marker(), Some("UI"));
}
#[test]
fn state_json_carries_the_branch_topology_and_markers() {
let v = MetroView::local(); let sj = v.state_json();
let lines = sj["line"].as_array().unwrap();
let line_a = &lines[0];
let grpc_a = line_a["stations"]
.as_array()
.unwrap()
.iter()
.find(|s| s["kind"] == "grpc")
.expect("line A has a grpc station");
let arms_a = grpc_a["branches"].as_array().unwrap();
assert_eq!(arms_a.len(), 2, "the verb called by BOTH renders two arms");
let markers_a: Vec<&str> = arms_a.iter().map(|b| b["marker"].as_str().unwrap()).collect();
assert!(markers_a.contains(&"UI") && markers_a.contains(&"CLI"), "UI+CLI markers: {markers_a:?}");
assert!(arms_a.iter().all(|b| b["lit"].as_bool().unwrap()), "both arms lit");
assert_eq!(arms_a.iter().find(|b| b["marker"] == "UI").unwrap()["kind"], "ui_client");
assert_eq!(arms_a.iter().find(|b| b["marker"] == "CLI").unwrap()["kind"], "cli_client");
let line_b = &lines[1];
let grpc_b = line_b["stations"]
.as_array()
.unwrap()
.iter()
.find(|s| s["kind"] == "grpc")
.expect("line B has a grpc station");
let arms_b = grpc_b["branches"].as_array().unwrap();
assert_eq!(arms_b.len(), 1, "the CLI-only verb renders a single arm");
assert_eq!(arms_b[0]["marker"], "CLI");
assert_eq!(arms_b[0]["kind"], "cli_client");
let start_a = line_a["stations"].as_array().unwrap().iter().find(|s| s["kind"] == "start").unwrap();
assert_eq!(start_a["branches"].as_array().unwrap().len(), 0, "linear stop has no forks");
}
#[test]
fn empty_map_is_the_empty_state() {
let v = MetroView::new("Metro");
let sj = v.state_json();
assert_eq!(sj["lines"].as_u64(), Some(0), "no lines on an empty map");
assert_eq!(sj["line"].as_array().unwrap().len(), 0);
assert!(v.map().lines.is_empty());
}
}