src/viz/metro_tab.rs
//! 🚇 **Metro tab** — the dedicated viz pane that renders nornir's button → gRPC →
//! warehouse coverage CHAINS through the reusable facett component
//! [`facett_graphview::MetroView`] (a [`facett_core::Facet`], imported here as
//! [`facett_jobview::Facet`] — the SAME `facett-core` trait both crates re-export).
//!
//! This pane owns the `MetroView` and pushes a fresh [`facett_graphview::MetroMap`]
//! into it each refresh (the push model). The map is produced by the PURE
//! [`super::metro_feed::build_metro_map`] over the live workspace's warehouse facts
//! + coverage; the app feeds it from the 🏛 Architecture tab's loaded data via
//! [`super::arch_tab::ArchTabState::facett_metro_map`] (one loader, no second scan).
//!
//! [`state_json`](Facet::state_json) surfaces the whole drawn map (lines, per-station
//! {label, kind, lit}, per-line + overall green) so the headless test matrix asserts
//! exactly what the line lit up WITHOUT a pixel — LAW 6.
use facett_graphview::{MetroMap, MetroView};
use facett_jobview::Facet; // == facett_core::Facet (both crates re-export the 0.1 trait)
use super::facett_theme::Theme;
/// 🚇 The Metro tab pane state — a thin host around the facett [`MetroView`].
pub struct MetroTabState {
view: MetroView,
/// Cached so a workspace switch re-feeds (the app pushes a fresh map on change).
fed: bool,
/// Lines in the last-fed map — 0 ⇒ the warehouse has no coverage chains yet
/// (the LAW-2 empty board), so the shared `repo_pane` empty-state shows the
/// "⏳ in route for populate…" / "not scanned yet" reason instead of a blank.
line_count: usize,
/// The active workspace (app pushes it via `set_workspace_name`) — the key the
/// shared `repo_pane` jobs lookup needs to know if a populate is in route.
workspace: String,
}
impl Default for MetroTabState {
fn default() -> Self {
Self {
view: MetroView::new("🚇 Metro"),
fed: false,
line_count: 0,
workspace: String::new(),
}
}
}
impl MetroTabState {
/// **Discovery-contract `local()`** — the no-arg ctor the test matrix enumerates.
/// Starts empty; the app feeds the real workspace map via [`Self::set_map`]
/// (mirrors `facett-jobview`'s `JobList::default` host on the 🧬 Nornir tab).
pub fn local() -> Self {
Self::default()
}
/// **Discovery-contract `remote()`** — same surface; the thin/server-fed variant.
pub fn remote() -> Self {
Self::default()
}
/// Replace the drawn map (the push model — called each refresh from the app's
/// live workspace data). Marks the pane fed so a "no data yet" hint clears, and
/// records the line count so an EMPTY map (no coverage chains) shows the shared
/// LAW-2 empty-state instead of a blank board.
pub fn set_map(&mut self, map: MetroMap) {
self.line_count = map.lines.len();
self.view.set_map(map);
self.fed = true;
}
/// The active workspace, so the shared `repo_pane` empty-state can tell whether
/// a populate is in route. The app pushes it each refresh alongside `set_map`.
pub fn set_workspace_name(&mut self, workspace: String) {
self.workspace = workspace;
}
/// Whether a real map has been fed yet (else the pane shows a load hint).
pub fn is_fed(&self) -> bool {
self.fed
}
/// The pane's LAW-2 empty-state: a fed-but-line-less map is the warehouse
/// having no coverage chains yet — "⏳ in route for populate…" when a job is
/// active for this workspace, else "not scanned yet". `Populated` once lines
/// exist (or before the first feed, where the load hint already shows).
fn empty_state(&self) -> super::repo_pane::EmptyState {
let empty = self.fed && self.line_count == 0;
super::repo_pane::classify_empty(empty, &self.workspace, "")
}
/// The active facett palette (C8) — the Facet reads the ambient egui theme, so
/// this is a no-op seam kept for parity with the other tabs' `set_palette`.
pub fn set_palette(&mut self, _t: Theme) {}
/// Draw the pane: a header + the facett [`MetroView`] (scoped to the stable
/// `Metro` pane id so the AccessKit `id_salt` matches the surfaces registry).
pub fn draw(&mut self, ui: &mut egui::Ui) {
ui.push_id("Metro", |ui| {
ui.horizontal(|ui| {
ui.heading("🚇 Metro");
ui.label(
egui::RichText::new(
"button → emitters → gRPC → warehouse table, lit by the latest coverage run",
)
.weak(),
);
});
if !self.fed {
ui.label(
egui::RichText::new(
"loading the workspace's coverage chains… (open 🏛 Architecture once to warm the warehouse facts)",
)
.weak(),
);
} else if self.line_count == 0 {
// Fed but no chains. The fuel is the code room's symbol / call rows
// (they ride the 🏛 Architecture payload): either the room has no
// UI-button → handler chain for this workspace, or that payload
// could not be read — the Architecture tab's state says which.
ui.label(
egui::RichText::new(
"no button → gRPC chain in the code room's rows for this workspace (🏛 Architecture shows whether its board could be read)",
)
.weak(),
);
}
ui.separator();
Facet::ui(&mut self.view, ui);
});
// EMIT-DOCTRINE (§selftest): a metro draw is a per-frame render — ok ⇔ the
// pane was fed a REAL map carrying at least one coverage chain; a fed-but-
// empty board (warehouse has no chains yet) or an un-fed pane is RED.
#[cfg(feature = "testmatrix")]
crate::selftest::emit(
"viz/metro (egui draw)",
"metro_fed_lit",
self.fed && self.line_count > 0,
&format!("{} line(s), fed={}", self.line_count, self.fed),
);
}
/// The drawn map as DATA (LAW 6) — delegates to the facett `MetroView`'s
/// `state_json` (lines, per-station {label, kind, lit}, per-line + overall green).
pub fn state_json(&self) -> serde_json::Value {
let mut v = Facet::state_json(&self.view);
if let Some(obj) = v.as_object_mut() {
obj.insert("fed".into(), serde_json::Value::Bool(self.fed));
// LAW 2 empty-state tag (populated / in_route / not_scanned) so a
// headless drive asserts a fed-but-empty metro names its reason.
obj.insert(
"empty_state".into(),
serde_json::Value::String(self.empty_state().id().to_string()),
);
// fed from the code room's rows: `code_room` with lines, `no_chains`
// when the rows hold no button → handler chain (or could not be read)
if self.fed {
let state = if self.line_count == 0 { "no_chains" } else { "code_room" };
obj.insert("state".into(), serde_json::Value::String(state.into()));
}
}
v
}
}
#[cfg(test)]
mod tests {
use super::*;
use facett_graphview::{MetroLine, MetroStation, StationKind};
/// inject-assert: feeding a real green line lights state_json up (green=true) and
/// the pane reports fed.
#[test]
fn fed_map_surfaces_in_state_json() {
let mut tab = MetroTabState::local();
assert!(!tab.is_fed());
assert_eq!(tab.state_json()["fed"], serde_json::json!(false));
let line = MetroLine::new(
"b→Svc.Verb",
"b",
vec![
MetroStation::new("ui::b", "b", StationKind::Start, true),
MetroStation::new("svc::emit", "emit", StationKind::Emitter, true),
MetroStation::new("grpc::Svc.Verb", "Svc.Verb", StationKind::Grpc, true),
MetroStation::new("wh::t", "t", StationKind::Terminus, true),
],
);
tab.set_map(MetroMap::new(vec![line]));
assert!(tab.is_fed());
let js = tab.state_json();
assert_eq!(js["fed"], serde_json::json!(true));
assert_eq!(js["lines"], serde_json::json!(1));
assert_eq!(js["green"], serde_json::json!(true));
}
}