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}