bob/menu_mux.rs
1//! The EDM menu mux of a BOY `.opi`: a drop-down whose items each stand
2//! for a set of values, one per target PV (often a `loc://` PV that stands
3//! in for a macro). Picking item `i` writes `values{k}[i]` to `target{k}`.
4//!
5//! An `.opi` menu mux becomes a combo with a `<menu_mux>` property,
6//! bobview's own, that keeps the sets:
7//!
8//! ```xml
9//! <menu_mux>
10//! <index>false</index> <!-- true: pv_name is the mux's own, and gets i -->
11//! <initial>1</initial> <!-- the item whose values are written at load -->
12//! <set>
13//! <target>loc://$(DID)ll<VString></target>
14//! <values><value>RATE</value><value>LOG</value></values>
15//! </set>
16//! </menu_mux>
17//! ```
18//!
19//! Without its own `pv_name` the combo is on the first target, and shows
20//! the item whose value that target holds.
21
22use crate::{Element, Widget};
23
24/// The most sets a menu mux has, as in BOY (`target0`…`target7`); any more
25/// in a `<menu_mux>` are ignored.
26pub const MAX_SETS: usize = 8;
27
28/// One target and the value each item writes to it.
29#[derive(Clone, Debug, PartialEq)]
30pub struct MuxSet {
31 /// The target PV, macros unexpanded.
32 pub target: String,
33 pub values: Vec<String>,
34}
35
36/// A combo's menu mux sets.
37#[derive(Clone, Debug, Default, PartialEq)]
38pub struct MenuMux {
39 /// The combo's `pv_name` is the mux's own, not its first target: a pick
40 /// writes the item's index to it, and its value is the index shown.
41 pub index: bool,
42 /// The item whose values are written when the display opens.
43 pub initial: Option<usize>,
44 pub sets: Vec<MuxSet>,
45}
46
47impl MenuMux {
48 /// The item for a PV value: its index, or where it is among the first
49 /// set's values (the first target's value is `text`).
50 pub fn current(&self, index: Option<usize>, text: &str) -> Option<usize> {
51 if self.index {
52 return index;
53 }
54 let values = &self.sets.first()?.values;
55 values.iter().position(|v| v == text)
56 }
57
58 /// The `(target, value)` writes for picking item `i`.
59 pub fn writes(&self, i: usize) -> Vec<(&str, &str)> {
60 self.sets
61 .iter()
62 .filter_map(|s| Some((s.target.as_str(), s.values.get(i)?.as_str())))
63 .collect()
64 }
65}
66
67impl Widget {
68 /// The widget's `<menu_mux>`, if it has one.
69 pub fn menu_mux(&self) -> Option<MenuMux> {
70 let el = self.get("menu_mux")?;
71 let sets = el
72 .children
73 .iter()
74 .filter(|c| c.name == "set")
75 .take(MAX_SETS)
76 .map(|s| MuxSet {
77 target: s.child_text("target").unwrap_or("").trim().to_string(),
78 values: s
79 .child("values")
80 .map(|v| v.children.iter().map(|c| c.text.clone()).collect())
81 .unwrap_or_default(),
82 })
83 .collect();
84 Some(MenuMux {
85 index: el.child_text("index").map(str::trim) == Some("true"),
86 initial: el.child_text("initial").and_then(|t| t.trim().parse().ok()),
87 sets,
88 })
89 }
90}
91
92/// The `<menu_mux>` for a BOY menu mux `el`, from its `pv_name`,
93/// `num_sets`, `target0`…`target7`, `values0`…`values7` and `initial`.
94/// Sets with no target are left out, as there is nothing to write.
95pub(crate) fn from_opi(el: &Element) -> Element {
96 let text = |name: &str| el.child_text(name).map(str::trim).unwrap_or("");
97 let num_sets = text("num_sets").parse().unwrap_or(1usize).min(MAX_SETS);
98 let mut out = Element::new("menu_mux").with_child(Element::with_text(
99 "index",
100 (!text("pv_name").is_empty()).to_string(),
101 ));
102 if !text("initial").is_empty() {
103 out.children
104 .push(Element::with_text("initial", text("initial")));
105 }
106 for k in 0..num_sets {
107 let target = text(&format!("target{k}"));
108 if target.is_empty() {
109 continue;
110 }
111 let mut values = Element::new("values");
112 if let Some(v) = el.child(&format!("values{k}")) {
113 values.children = v
114 .children
115 .iter()
116 .map(|s| Element::with_text("value", s.text.clone()))
117 .collect();
118 }
119 out.children.push(
120 Element::new("set")
121 .with_child(Element::with_text("target", target))
122 .with_child(values),
123 );
124 }
125 out
126}