use serde::{Deserialize, Serialize};
pub type Key = String;
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum ControlKind {
Text {
hint: String,
},
Toggle,
Choice {
options: Vec<Key>,
},
Number {
min: f64,
max: f64,
},
Readout,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Control {
pub id: Key,
pub label: String,
pub kind: ControlKind,
pub value: Option<String>,
}
impl Control {
pub fn readout(id: impl Into<Key>, label: impl Into<String>, value: impl Into<String>) -> Control {
Control {
id: id.into(),
label: label.into(),
kind: ControlKind::Readout,
value: Some(value.into()),
}
}
pub fn toggle(id: impl Into<Key>, label: impl Into<String>, on: bool) -> Control {
Control {
id: id.into(),
label: label.into(),
kind: ControlKind::Toggle,
value: Some(on.to_string()),
}
}
pub fn text(id: impl Into<Key>, label: impl Into<String>, hint: impl Into<String>, value: &str) -> Control {
Control {
id: id.into(),
label: label.into(),
kind: ControlKind::Text { hint: hint.into() },
value: (!value.is_empty()).then(|| value.to_string()),
}
}
}
pub trait Interface: crate::Facet {
fn options(&self) -> Vec<Key> {
Vec::new()
}
fn select(&mut self, _key: &str) -> bool {
false
}
fn selected(&mut self) -> Option<&mut dyn Interface> {
None
}
fn controls(&self) -> Vec<Control> {
Vec::new()
}
}
pub trait Face {
fn label(&self) -> &str;
fn controls(&self) -> Vec<Control> {
Vec::new()
}
}
pub trait Infrastructure {
fn label(&self) -> &str;
fn faces(&self) -> Vec<(&'static str, &dyn Face)> {
Vec::new()
}
}
pub trait App {
fn infrastructure(&self) -> &dyn Infrastructure;
fn interface(&mut self) -> &mut dyn Interface;
}
pub fn selected_path(root: &mut dyn Interface) -> Vec<Key> {
let mut path = Vec::new();
let mut node: &mut dyn Interface = root;
loop {
let has_child = node.selected().is_some();
if !has_child {
return path;
}
node = node.selected().expect("checked");
path.push(crate::Facet::title(node).to_string());
}
}
#[cfg(test)]
mod tests {
use super::*;
struct Node {
title: String,
chosen: usize,
kids: Vec<Node>,
}
impl crate::Facet for Node {
fn title(&self) -> &str {
&self.title
}
fn ui(&mut self, _ui: &mut egui::Ui) {}
fn state_json(&self) -> serde_json::Value {
serde_json::json!({ "title": self.title })
}
}
impl Interface for Node {
fn options(&self) -> Vec<Key> {
self.kids.iter().map(|k| k.title.clone()).collect()
}
fn select(&mut self, key: &str) -> bool {
match self.kids.iter().position(|k| k.title == key) {
Some(i) => {
self.chosen = i;
true
}
None => false,
}
}
fn selected(&mut self) -> Option<&mut dyn Interface> {
self.kids.get_mut(self.chosen).map(|k| k as &mut dyn Interface)
}
fn controls(&self) -> Vec<Control> {
vec![Control::readout(format!("{}_readout", self.title), &self.title, &self.title)]
}
}
fn tree() -> Node {
Node {
title: "root".into(),
chosen: 0,
kids: vec![
Node { title: "2d".into(), chosen: 0, kids: Vec::new() },
Node { title: "3d".into(), chosen: 0, kids: Vec::new() },
],
}
}
#[test]
fn every_choice_yields_a_different_ui() {
let mut root = tree();
let opts = root.options();
assert!(opts.len() >= 2, "need two options to compare, got {opts:?}");
let mut seen: Vec<(Key, Vec<Key>)> = Vec::new();
for key in &opts {
assert!(root.select(key), "select({key}) refused a key its own options() offered");
let child = root.selected().expect("a selected option must yield a node");
let ids: Vec<Key> = child.controls().into_iter().map(|c| c.id).collect();
assert!(!ids.is_empty(), "option {key} yielded a node with NO controls");
seen.push((key.clone(), ids));
}
for (i, (ka, a)) in seen.iter().enumerate() {
for (kb, b) in seen.iter().skip(i + 1) {
assert_ne!(
a, b,
"options {ka:?} and {kb:?} render the SAME controls — the choice \
changed nothing, which is the bug this guard exists to catch"
);
}
}
}
#[test]
fn selecting_an_unoffered_key_is_refused_not_ignored() {
let mut root = tree();
assert!(!root.select("no-such-mode"), "an unknown key must return false");
assert_eq!(crate::Facet::title(root.selected().expect("still selected")), "2d");
}
#[test]
fn a_leaf_needs_no_overrides_and_reports_no_choices() {
struct Leaf;
impl crate::Facet for Leaf {
fn title(&self) -> &str {
"leaf"
}
fn ui(&mut self, _ui: &mut egui::Ui) {}
fn state_json(&self) -> serde_json::Value {
serde_json::Value::Null
}
}
impl Interface for Leaf {}
let mut leaf = Leaf;
assert!(leaf.options().is_empty());
assert!(leaf.selected().is_none());
assert!(!leaf.select("anything"));
assert!(leaf.controls().is_empty());
}
#[test]
fn the_selected_path_names_the_leaf_the_user_is_looking_at() {
let mut root = tree();
assert!(root.select("3d"));
assert_eq!(selected_path(&mut root), vec!["3d".to_string()]);
assert!(root.select("2d"));
assert_eq!(selected_path(&mut root), vec!["2d".to_string()]);
}
#[test]
fn a_control_with_no_value_is_not_a_control_with_an_empty_one() {
let unset = Control::text("url", "URL", "https://…", "");
let set = Control::text("url", "URL", "https://…", "http://localhost:9000");
assert_eq!(unset.value, None, "an empty string means NO value, not a blank one");
assert_eq!(set.value.as_deref(), Some("http://localhost:9000"));
}
}