cog_task/action/core/
instruction.rs

1use crate::action::{Action, ActionSignal, Props, StatefulAction, INFINITE, VISUAL};
2use crate::comm::{QWriter, Signal, SignalId};
3use crate::gui::{center_x, header_body_controls, style_ui, text::button1, Style};
4use crate::resource::{
5    parse_text, IoManager, OptionalPath, OptionalString, ResourceAddr, ResourceManager,
6    ResourceValue,
7};
8use crate::server::{AsyncSignal, Config, State, SyncSignal};
9use crate::util::f64_with_precision;
10use eframe::egui;
11use eframe::egui::{CursorIcon, ScrollArea};
12use egui_extras::{Size, StripBuilder};
13use eyre::{eyre, Result};
14use regex::Regex;
15use serde::{Deserialize, Serialize};
16use serde_cbor::Value;
17use std::collections::{BTreeMap, BTreeSet};
18
19#[derive(Debug, Deserialize, Serialize)]
20#[serde(deny_unknown_fields)]
21pub struct Instruction {
22    #[serde(default)]
23    text: OptionalString,
24    #[serde(default)]
25    src: OptionalPath,
26    #[serde(default)]
27    header: String,
28    #[serde(default)]
29    params: BTreeMap<String, String>,
30    #[serde(default)]
31    in_mapping: BTreeMap<SignalId, String>,
32    #[serde(default = "defaults::persistent")]
33    #[serde(rename = "static")]
34    persistent: bool,
35}
36
37stateful!(Instruction {
38    text: String,
39    header: String,
40    params: BTreeMap<String, String>,
41    persistent: bool,
42    in_mapping: BTreeMap<SignalId, String>,
43});
44
45mod defaults {
46    #[inline(always)]
47    pub fn persistent() -> bool {
48        false
49    }
50}
51
52impl Action for Instruction {
53    fn init(self) -> Result<Box<dyn Action>>
54    where
55        Self: 'static + Sized,
56    {
57        match (self.text.is_some(), self.src.is_some()) {
58            (false, false) => Err(eyre!("`text` and `src` cannot both be empty.")),
59            (true, true) => Err(eyre!("Only one of `text` and `src` should be set.")),
60            _ => Ok(Box::new(self)),
61        }
62    }
63
64    #[inline]
65    fn in_signals(&self) -> BTreeSet<SignalId> {
66        self.in_mapping.keys().cloned().collect()
67    }
68
69    #[inline(always)]
70    fn resources(&self, _config: &Config) -> Vec<ResourceAddr> {
71        if let OptionalPath::Some(src) = &self.src {
72            vec![ResourceAddr::Text(src.clone())]
73        } else {
74            vec![]
75        }
76    }
77
78    fn stateful(
79        &self,
80        _io: &IoManager,
81        res: &ResourceManager,
82        _config: &Config,
83        _sync_writer: &QWriter<SyncSignal>,
84        _async_writer: &QWriter<AsyncSignal>,
85    ) -> Result<Box<dyn StatefulAction>> {
86        let text = if let OptionalPath::Some(src) = &self.src {
87            match res.fetch(&ResourceAddr::Text(src.clone()))? {
88                ResourceValue::Text(text) => (*text).clone(),
89                _ => return Err(eyre!("Resource address and value types don't match.")),
90            }
91        } else if let OptionalString::Some(text) = &self.text {
92            text.clone()
93        } else {
94            "".to_owned()
95        };
96
97        let mut params = self.params.clone();
98        let re = Regex::new(r"\$\{([[:alpha:]][[:word:]]*)\}").unwrap();
99        for caps in re.captures_iter(&text) {
100            params
101                .entry(caps[1].to_owned())
102                .or_insert_with(|| "<UNSET>".to_owned());
103        }
104
105        for (_, v) in self.in_mapping.iter() {
106            if !params.contains_key(v) {
107                return Err(eyre!("Undefined parameter `{v}` in `in_mapping`."));
108            }
109        }
110
111        Ok(Box::new(StatefulInstruction {
112            done: false,
113            text,
114            header: self.header.clone(),
115            params,
116            persistent: self.persistent,
117            in_mapping: self.in_mapping.clone(),
118        }))
119    }
120}
121
122impl StatefulAction for StatefulInstruction {
123    impl_stateful!();
124
125    #[inline(always)]
126    fn props(&self) -> Props {
127        if self.persistent {
128            INFINITE | VISUAL
129        } else {
130            VISUAL
131        }
132        .into()
133    }
134
135    fn start(
136        &mut self,
137        sync_writer: &mut QWriter<SyncSignal>,
138        _async_writer: &mut QWriter<AsyncSignal>,
139        state: &State,
140    ) -> Result<Signal> {
141        for (id, key) in self.in_mapping.iter() {
142            if let Some(entry) = self.params.get_mut(key) {
143                if let Some(value) = state.get(id) {
144                    *entry = match value {
145                        Value::Bool(v) => v.to_string(),
146                        Value::Integer(v) => v.to_string(),
147                        Value::Float(v) => format!("{}", f64_with_precision(*v as f32, 4)),
148                        Value::Text(v) => v.to_string(),
149                        Value::Null => "<UNSET>".to_owned(),
150                        _ => "<INVALID>".to_owned(),
151                    };
152                }
153            }
154        }
155
156        sync_writer.push(SyncSignal::Repaint);
157        Ok(Signal::none())
158    }
159
160    fn update(
161        &mut self,
162        signal: &ActionSignal,
163        sync_writer: &mut QWriter<SyncSignal>,
164        _async_writer: &mut QWriter<AsyncSignal>,
165        state: &State,
166    ) -> Result<Signal> {
167        let mut changed = false;
168        if let ActionSignal::StateChanged(_, signal) = signal {
169            for id in signal {
170                if let Some(key) = self.in_mapping.get(id) {
171                    if let Some(entry) = self.params.get_mut(key) {
172                        *entry = match state.get(id).unwrap() {
173                            Value::Bool(v) => v.to_string(),
174                            Value::Integer(v) => v.to_string(),
175                            Value::Float(v) => format!("{}", f64_with_precision(*v as f32, 4)),
176                            Value::Text(v) => v.to_string(),
177                            Value::Null => "<UNSET>".to_owned(),
178                            _ => "<INVALID>".to_owned(),
179                        };
180                    }
181                    changed = true;
182                }
183            }
184        }
185
186        if changed {
187            sync_writer.push(SyncSignal::Repaint);
188        }
189        Ok(Signal::none())
190    }
191
192    fn show(
193        &mut self,
194        ui: &mut egui::Ui,
195        sync_writer: &mut QWriter<SyncSignal>,
196        _async_writer: &mut QWriter<AsyncSignal>,
197        _state: &State,
198    ) -> Result<()> {
199        let mut text = self.text.clone();
200
201        for (k, v) in self.params.iter() {
202            text = Regex::new(&format!(r"\$\{{{k}}}"))
203                .unwrap()
204                .replace_all(&text, v)
205                .to_string();
206        }
207
208        header_body_controls(ui, |strip| {
209            strip.cell(|ui| {
210                ui.centered_and_justified(|ui| ui.heading(&self.header));
211            });
212            strip.empty();
213            strip.strip(|builder| {
214                builder
215                    .size(Size::remainder())
216                    .size(Size::exact(1520.0))
217                    .size(Size::remainder())
218                    .horizontal(|mut strip| {
219                        strip.empty();
220                        strip.cell(|ui| {
221                            ScrollArea::vertical().show(ui, |ui| {
222                                ui.centered_and_justified(|ui| {
223                                    let _ = parse_text(ui, &text);
224                                });
225                            });
226                        });
227                        strip.empty();
228                    });
229            });
230            strip.empty();
231            strip.strip(|builder| {
232                if !self.persistent {
233                    self.show_controls(builder, sync_writer);
234                }
235            });
236        });
237
238        if self.persistent {
239            ui.output().cursor_icon = CursorIcon::None;
240        }
241
242        Ok(())
243    }
244}
245
246impl StatefulInstruction {
247    fn show_controls(&mut self, builder: StripBuilder, sync_writer: &mut QWriter<SyncSignal>) {
248        enum Interaction {
249            None,
250            Next,
251        }
252
253        let mut interaction = Interaction::None;
254
255        center_x(builder, 200.0, |ui| {
256            ui.horizontal_centered(|ui| {
257                style_ui(ui, Style::SubmitButton);
258                if ui.button(button1("Next")).clicked() {
259                    interaction = Interaction::Next;
260                }
261            });
262        });
263
264        match interaction {
265            Interaction::None => {}
266            Interaction::Next => {
267                self.done = true;
268                sync_writer.push(SyncSignal::UpdateGraph);
269            }
270        }
271    }
272
273    #[allow(dead_code)]
274    fn debug(&self) -> Vec<(&str, String)> {
275        <dyn StatefulAction>::debug(self)
276            .into_iter()
277            .chain([("persistent", format!("{:?}", self.persistent))])
278            .collect()
279    }
280}