pub trait Panel {
fn title(&self) -> &str;
fn ui(&mut self, ui: &mut egui::Ui);
fn state_json(&self) -> serde_json::Value;
fn update_json(&mut self, msg_json: &str);
fn severity(&self) -> crate::Severity {
crate::Severity::Info
}
fn component(&self) -> &'static str {
""
}
}
impl<T: crate::Facet + ?Sized> Panel for T {
fn title(&self) -> &str {
crate::Facet::title(self)
}
fn ui(&mut self, ui: &mut egui::Ui) {
crate::Facet::ui(self, ui)
}
fn state_json(&self) -> serde_json::Value {
crate::Facet::state_json(self)
}
fn update_json(&mut self, msg_json: &str) {
crate::Facet::update_json(self, msg_json)
}
fn severity(&self) -> crate::Severity {
crate::Facet::severity(self)
}
fn component(&self) -> &'static str {
crate::Facet::component(self)
}
}
pub fn drive<'a>(panel: &mut dyn Panel, msgs: impl IntoIterator<Item = &'a str>) -> Vec<serde_json::Value> {
let mut snapshots = Vec::new();
for m in msgs {
panel.update_json(m);
snapshots.push(panel.state_json());
}
snapshots
}
pub fn snapshot<'a>(panel: &mut dyn Panel, msgs: impl IntoIterator<Item = &'a str>) -> serde_json::Value {
drive(panel, msgs);
panel.state_json()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Facet;
use egui::Ui;
struct Static {
title: String,
items: usize,
}
impl Facet for Static {
fn title(&self) -> &str {
&self.title
}
fn ui(&mut self, _ui: &mut Ui) {}
fn state_json(&self) -> serde_json::Value {
serde_json::json!({ "items": self.items })
}
}
struct Counter {
count: i64,
last: Option<String>,
}
impl Facet for Counter {
fn title(&self) -> &str {
"counter"
}
fn ui(&mut self, _ui: &mut Ui) {}
fn state_json(&self) -> serde_json::Value {
serde_json::json!({ "count": self.count, "last": self.last })
}
fn update_json(&mut self, msg_json: &str) {
let Ok(v) = serde_json::from_str::<serde_json::Value>(msg_json) else {
return;
};
if let Some(n) = v.get("add").and_then(|n| n.as_i64()) {
self.count += n;
}
if let Some(s) = v.get("mark").and_then(|s| s.as_str()) {
self.last = Some(s.to_string());
}
}
}
#[test]
fn blanket_impl_makes_a_facet_usable_as_box_dyn_panel() {
let mut p: Box<dyn Panel> = Box::new(Static { title: "s".into(), items: 3 });
assert_eq!(Panel::title(&*p), "s");
assert_eq!(p.state_json()["items"], 3);
p.update_json(r#"{"anything":true}"#);
assert_eq!(p.state_json()["items"], 3, "defaulted update_json is a no-op");
}
#[test]
fn override_update_json_is_routed_through_panel() {
let mut c = Counter { count: 0, last: None };
Panel::update_json(&mut c, r#"{"add": 5}"#);
assert_eq!(Panel::state_json(&c)["count"], 5, "Panel routed into the Facet override");
}
#[test]
fn headless_driver_applies_msgs_and_state_json_transitions() {
let mut c = Counter { count: 0, last: None };
let snaps = drive(
&mut c as &mut dyn Panel,
[r#"{"add": 1}"#, r#"{"add": 2}"#, r#"{"mark": "done"}"#, r#"{"add": -3}"#],
);
assert_eq!(snaps.len(), 4);
assert_eq!(snaps[0]["count"], 1);
assert_eq!(snaps[1]["count"], 3);
assert_eq!(snaps[2]["count"], 3, "the mark message doesn't change count");
assert_eq!(snaps[2]["last"], "done");
assert_eq!(snaps[3]["count"], 0, "1+2-3 = 0");
}
#[test]
fn structural_severity_rides_the_facet_panel_seam() {
use crate::Severity;
struct Loader {
failed: bool,
}
impl Facet for Loader {
fn title(&self) -> &str {
"loader"
}
fn ui(&mut self, _ui: &mut Ui) {}
fn state_json(&self) -> serde_json::Value {
serde_json::json!({ "failed": self.failed })
}
fn update_json(&mut self, msg_json: &str) {
if let Ok(v) = serde_json::from_str::<serde_json::Value>(msg_json) {
if let Some(b) = v.get("failed").and_then(|b| b.as_bool()) {
self.failed = b;
}
}
}
fn severity(&self) -> Severity {
crate::worst_severity([if self.failed {
Severity::Error
} else {
Severity::Info
}])
}
}
let mut p: Box<dyn Panel> = Box::new(Loader { failed: false });
assert_eq!(Panel::severity(&*p), Severity::Info, "clean pane is green");
assert!(!Panel::severity(&*p).is_error());
p.update_json(r#"{"failed": true}"#);
assert_eq!(
Panel::severity(&*p),
Severity::Error,
"a driven load failure is RED through the Panel seam (the gate signal)"
);
assert!(Panel::severity(&*p).is_error(), "this is what FAILS the Robot-UI gate");
let d: Box<dyn Panel> = Box::new(Static { title: "s".into(), items: 1 });
assert_eq!(Panel::severity(&*d), Severity::Info, "defaulted facet is green");
}
#[test]
fn snapshot_returns_terminal_state() {
let mut c = Counter { count: 10, last: None };
let final_state = snapshot(&mut c as &mut dyn Panel, [r#"{"add": 4}"#, r#"{"add": 6}"#]);
assert_eq!(final_state["count"], 20);
}
#[test]
fn box_dyn_panel_deck_is_heterogeneous() {
let mut deck: Vec<Box<dyn Panel>> = vec![
Box::new(Static { title: "a".into(), items: 1 }),
Box::new(Counter { count: 7, last: None }),
];
let titles: Vec<String> = deck.iter().map(|p| p.title().to_string()).collect();
assert_eq!(titles, vec!["a".to_string(), "counter".to_string()]);
deck[1].update_json(r#"{"add": 3}"#);
assert_eq!(deck[1].state_json()["count"], 10);
assert_eq!(deck[0].state_json()["items"], 1);
}
}