1use crate::{Element, Macros};
2
3#[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 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 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
57pub 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), };
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 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
142fn 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#[derive(Clone, Debug, PartialEq)]
163pub enum Action {
164 OpenDisplay {
165 file: String,
166 macros: Macros,
167 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 Execute {
183 file: String,
184 text: String,
185 description: String,
186 },
187 Command {
189 command: String,
190 directory: String,
191 description: String,
192 },
193 CloseDisplay {
196 description: String,
197 },
198 Trend {
201 pv_names: Vec<String>,
202 description: String,
203 },
204 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 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#[derive(Clone, Debug, PartialEq)]
306pub struct Rule {
307 pub name: String,
308 pub prop_id: String,
309 pub out_exp: bool,
311 pub exprs: Vec<RuleExpr>,
312 pub pv_names: Vec<String>,
313 pub check_connections: bool,
317}
318
319#[derive(Clone, Debug, PartialEq)]
320pub struct RuleExpr {
321 pub condition: String,
322 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}