Skip to main content

bob/
model.rs

1use crate::{Element, Macros};
2
3/// An RGBA colour, optionally with its Phoebus name.
4#[derive(Clone, Debug, PartialEq)]
5pub struct Color {
6    pub name: Option<String>,
7    pub r: u8,
8    pub g: u8,
9    pub b: u8,
10    pub a: u8,
11}
12
13impl Color {
14    pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
15        Self::rgba_const(r, g, b, 255)
16    }
17
18    const fn rgba_const(r: u8, g: u8, b: u8, a: u8) -> Self {
19        Self {
20            name: None,
21            r,
22            g,
23            b,
24            a,
25        }
26    }
27
28    /// Read `<color name=".." red=".." green=".." blue=".." alpha=".."/>`.
29    pub fn from_element(el: &Element) -> Self {
30        let c = |k: &str, d: u8| {
31            el.attr(k)
32                .and_then(|v| v.trim().parse::<f64>().ok())
33                .map(|v| v.clamp(0.0, 255.0) as u8)
34                .unwrap_or(d)
35        };
36        let name = el.attr("name").map(str::to_string);
37        // A named colour without RGB values is looked up in the default palette.
38        if el.attr("red").is_none()
39            && let Some(n) = &name
40        {
41            return named_color(n);
42        }
43        Self {
44            name,
45            r: c("red", 0),
46            g: c("green", 0),
47            b: c("blue", 0),
48            a: c("alpha", 255),
49        }
50    }
51
52    pub fn rgba(&self) -> [u8; 4] {
53        [self.r, self.g, self.b, self.a]
54    }
55}
56
57/// Colours from the default Phoebus `color.def`.
58pub fn named_color(name: &str) -> Color {
59    let (r, g, b, a) = match name {
60        "OK" | "On" => (0, 255, 0, 255),
61        "MINOR" => (255, 128, 0, 255),
62        "MAJOR" | "STOP" => (255, 0, 0, 255),
63        "INVALID" => (255, 0, 255, 255),
64        "DISCONNECTED" => (200, 0, 200, 200),
65        "ActiveText" => (255, 255, 0, 255),
66        "Background" => (255, 255, 255, 255),
67        "Read_Background" => (240, 240, 240, 255),
68        "Write_Background" => (128, 255, 255, 255),
69        "Button_Background" => (210, 210, 210, 255),
70        "Header_Background" => (77, 77, 77, 255),
71        "Header_ForeGround" => (255, 255, 255, 255),
72        "Attention" => (255, 160, 0, 255),
73        "Off" => (60, 100, 60, 255),
74        "Grid" => (128, 128, 128, 255),
75        "Transparent" => (255, 255, 255, 0),
76        _ => (0, 0, 0, 255), // "Text" and anything unknown
77    };
78    Color {
79        name: Some(name.to_string()),
80        ..Color::rgba_const(r, g, b, a)
81    }
82}
83
84#[derive(Clone, Copy, Debug, PartialEq, Eq)]
85pub enum FontStyle {
86    Regular,
87    Bold,
88    Italic,
89    BoldItalic,
90}
91
92#[derive(Clone, Debug, PartialEq)]
93pub struct Font {
94    pub name: Option<String>,
95    pub family: String,
96    pub style: FontStyle,
97    pub size: f32,
98}
99
100impl Default for Font {
101    /// The Phoebus "Default" font.
102    fn default() -> Self {
103        Self {
104            name: Some("Default".into()),
105            family: "Liberation Sans".into(),
106            style: FontStyle::Regular,
107            size: 14.0,
108        }
109    }
110}
111
112impl Font {
113    pub fn from_element(el: &Element) -> Self {
114        let name = el.attr("name").map(str::to_string);
115        if el.attr("size").is_none()
116            && let Some(n) = &name
117        {
118            return named_font(n);
119        }
120        Self {
121            name,
122            family: el.attr("family").unwrap_or("Liberation Sans").to_string(),
123            style: match el.attr("style").unwrap_or("REGULAR") {
124                "BOLD" => FontStyle::Bold,
125                "ITALIC" => FontStyle::Italic,
126                "BOLD_ITALIC" => FontStyle::BoldItalic,
127                _ => FontStyle::Regular,
128            },
129            size: el.attr("size").and_then(|s| s.parse().ok()).unwrap_or(14.0),
130        }
131    }
132
133    pub fn is_bold(&self) -> bool {
134        matches!(self.style, FontStyle::Bold | FontStyle::BoldItalic)
135    }
136
137    pub fn is_italic(&self) -> bool {
138        matches!(self.style, FontStyle::Italic | FontStyle::BoldItalic)
139    }
140}
141
142/// Fonts from the default Phoebus `font.def`.
143fn named_font(name: &str) -> Font {
144    let (style, size) = match name {
145        "Default Bold" => (FontStyle::Bold, 14.0),
146        "Header 1" => (FontStyle::Bold, 22.0),
147        "Header 2" => (FontStyle::Bold, 18.0),
148        "Header 3" => (FontStyle::Bold, 16.0),
149        "Comment" => (FontStyle::Italic, 14.0),
150        "Fine Print" => (FontStyle::Regular, 12.0),
151        _ => (FontStyle::Regular, 14.0),
152    };
153    Font {
154        name: Some(name.to_string()),
155        style,
156        size,
157        ..Font::default()
158    }
159}
160
161/// A widget action, e.g. from an action button.
162#[derive(Clone, Debug, PartialEq)]
163pub enum Action {
164    OpenDisplay {
165        file: String,
166        macros: Macros,
167        /// "replace", "tab" or "window".
168        target: String,
169        description: String,
170    },
171    WritePv {
172        pv_name: String,
173        value: String,
174        description: String,
175    },
176    OpenWebpage {
177        url: String,
178        description: String,
179    },
180    /// Run a script: `file` names it, and an embedded one (`EmbeddedJs`,
181    /// `EmbeddedPy`) has its source in `text`.
182    Execute {
183        file: String,
184        text: String,
185        description: String,
186    },
187    /// An external command, run in `directory` (the display's own when empty).
188    Command {
189        command: String,
190        directory: String,
191        description: String,
192    },
193    /// Close the display, as BOY's `ScriptUtil.closeAssociatedOPI` scripts
194    /// (EDM "Exit" buttons) do: type `close_display`, bobview's own.
195    CloseDisplay {
196        description: String,
197    },
198    /// Open a trend of `pv_names`, as BOY scripts that open a Data Browser
199    /// do: type `trend`, bobview's own, with a `<pv_name>` for each PV.
200    Trend {
201        pv_names: Vec<String>,
202        description: String,
203    },
204    /// Anything else (other scripts, …): shown but does nothing.
205    Unsupported {
206        kind: String,
207        description: String,
208    },
209}
210
211impl Action {
212    pub fn from_element(el: &Element) -> Self {
213        let text = |n: &str| el.child_text(n).unwrap_or("").to_string();
214        let description = text("description");
215        match el.attr("type").unwrap_or("") {
216            "open_display" => Action::OpenDisplay {
217                file: text("file"),
218                macros: el
219                    .child("macros")
220                    .map(Macros::from_element)
221                    .unwrap_or_default(),
222                target: el.child_text("target").unwrap_or("replace").to_string(),
223                description,
224            },
225            "write_pv" => Action::WritePv {
226                pv_name: text("pv_name"),
227                value: text("value"),
228                description,
229            },
230            "open_webpage" => Action::OpenWebpage {
231                url: text("url"),
232                description,
233            },
234            "execute" => {
235                let script = el.child("script");
236                Action::Execute {
237                    file: script
238                        .and_then(|s| s.attr("file"))
239                        .unwrap_or("")
240                        .to_string(),
241                    text: script
242                        .and_then(|s| s.child_text("text"))
243                        .unwrap_or("")
244                        .to_string(),
245                    description,
246                }
247            }
248            "command" => Action::Command {
249                command: text("command"),
250                directory: text("command_directory"),
251                description,
252            },
253            "close_display" => Action::CloseDisplay { description },
254            "trend" => Action::Trend {
255                pv_names: el
256                    .children
257                    .iter()
258                    .filter(|c| c.name == "pv_name")
259                    .map(|c| c.text.clone())
260                    .collect(),
261                description,
262            },
263            other => Action::Unsupported {
264                kind: other.to_string(),
265                description,
266            },
267        }
268    }
269
270    /// What Phoebus calls the action: its description, or without one its
271    /// kind's default (Phoebus' `Messages.Action*`).
272    pub fn label(&self) -> &str {
273        let description = self.description();
274        if !description.is_empty() {
275            return description;
276        }
277        match self {
278            Action::OpenDisplay { .. } => "Open Display",
279            Action::WritePv { .. } => "Write PV",
280            Action::OpenWebpage { .. } => "Open Web Page",
281            Action::Execute { .. } => "Execute Script",
282            Action::Command { .. } => "Execute Command",
283            Action::CloseDisplay { .. } => "Close Display",
284            Action::Trend { .. } => "Open Data Browser",
285            Action::Unsupported { kind, .. } if kind == "open_file" => "Open File",
286            Action::Unsupported { kind, .. } => kind,
287        }
288    }
289
290    pub fn description(&self) -> &str {
291        match self {
292            Action::OpenDisplay { description, .. }
293            | Action::WritePv { description, .. }
294            | Action::OpenWebpage { description, .. }
295            | Action::Execute { description, .. }
296            | Action::Command { description, .. }
297            | Action::CloseDisplay { description }
298            | Action::Trend { description, .. }
299            | Action::Unsupported { description, .. } => description,
300        }
301    }
302}
303
304/// A rule: sets property `prop_id` from the first expression that holds.
305#[derive(Clone, Debug, PartialEq)]
306pub struct Rule {
307    pub name: String,
308    pub prop_id: String,
309    /// When true the expression's value is the property value (`out_exp`).
310    pub out_exp: bool,
311    pub exprs: Vec<RuleExpr>,
312    pub pv_names: Vec<String>,
313    /// Whether to wait for every PV to connect before evaluating. BOY
314    /// evaluates regardless, reading a disconnected PV as INVALID, and an
315    /// `.opi` rule carries `check_connections="false"` to say so.
316    pub check_connections: bool,
317}
318
319#[derive(Clone, Debug, PartialEq)]
320pub struct RuleExpr {
321    pub condition: String,
322    /// The `<value>` element: text, or a nested `<color>`/`<font>`; for an
323    /// output expression rule, the `<expression>` (or BOY's `<value>`).
324    pub value: Element,
325}
326
327impl Rule {
328    pub fn from_element(el: &Element) -> Self {
329        Self {
330            name: el.attr("name").unwrap_or("").to_string(),
331            prop_id: el.attr("prop_id").unwrap_or("").to_string(),
332            out_exp: el.attr("out_exp") == Some("true"),
333            check_connections: el.attr("check_connections") != Some("false"),
334            exprs: el
335                .children
336                .iter()
337                .filter(|c| c.name == "exp")
338                .map(|e| RuleExpr {
339                    condition: e.attr("bool_exp").unwrap_or("false").to_string(),
340                    value: e
341                        .child("expression")
342                        .or_else(|| e.child("value"))
343                        .cloned()
344                        .unwrap_or_default(),
345                })
346                .collect(),
347            pv_names: el
348                .children
349                .iter()
350                .filter(|c| c.name == "pv_name")
351                .map(|c| c.text.clone())
352                .collect(),
353        }
354    }
355}