cog_task/action/core/
question.rs

1use crate::action::{Action, Props, StatefulAction, VISUAL};
2use crate::comm::QWriter;
3use crate::gui::{
4    center_x, header_body_controls, style_ui, text::body, text::button1, text::inactive, Style,
5    TEXT_SIZE_BODY,
6};
7use crate::resource::{parse_text, IoManager, LoggerSignal, ResourceManager};
8use crate::server::{AsyncSignal, Config, State, SyncSignal};
9use crate::util::{f32_with_precision, f64_with_precision};
10use eframe::egui;
11use eframe::egui::{
12    Checkbox, Color32, RadioButton, ScrollArea, Slider, Stroke, TextEdit, Vec2, Widget,
13};
14use egui_extras::StripBuilder;
15use eyre::{eyre, Result};
16use serde::{Deserialize, Serialize};
17use serde_cbor::Value;
18use std::ops::RangeInclusive;
19
20#[derive(Debug, Deserialize, Serialize)]
21#[serde(deny_unknown_fields)]
22pub struct Question {
23    #[serde(default = "defaults::group")]
24    group: String,
25    list: Vec<QItem>,
26}
27
28stateful!(Question {
29    group: String,
30    list: Vec<StatefulQItem>,
31});
32
33mod defaults {
34    #[inline(always)]
35    pub fn group() -> String {
36        "questions".to_owned()
37    }
38
39    #[inline(always)]
40    pub fn lines() -> usize {
41        3
42    }
43
44    #[inline(always)]
45    pub fn columns() -> usize {
46        10
47    }
48
49    #[inline(always)]
50    pub fn precision() -> u8 {
51        3
52    }
53}
54
55impl Action for Question {
56    fn stateful(
57        &self,
58        _io: &IoManager,
59        _res: &ResourceManager,
60        _config: &Config,
61        _sync_writer: &QWriter<SyncSignal>,
62        _async_writer: &QWriter<AsyncSignal>,
63    ) -> Result<Box<dyn StatefulAction>> {
64        if self.group.is_empty() {
65            return Err(eyre!("Question `group` cannot be an empty string"));
66        }
67
68        Ok(Box::new(StatefulQuestion {
69            done: false,
70            group: self.group.clone(),
71            list: self.list.iter().map(|q| q.stateful()).collect(),
72        }))
73    }
74}
75
76impl StatefulAction for StatefulQuestion {
77    impl_stateful!();
78
79    #[inline(always)]
80    fn props(&self) -> Props {
81        VISUAL.into()
82    }
83
84    fn show(
85        &mut self,
86        ui: &mut egui::Ui,
87        sync_writer: &mut QWriter<SyncSignal>,
88        async_writer: &mut QWriter<AsyncSignal>,
89        _state: &State,
90    ) -> Result<()> {
91        header_body_controls(ui, |strip| {
92            strip.empty();
93            strip.empty();
94            strip.strip(|builder| {
95                center_x(builder, 1520.0, |ui| {
96                    ScrollArea::vertical().show(ui, |ui| self.show_items(ui));
97                });
98            });
99            strip.empty();
100            strip.strip(|builder| self.show_controls(builder, sync_writer, async_writer));
101        });
102
103        Ok(())
104    }
105}
106
107impl StatefulQuestion {
108    fn show_items(&mut self, ui: &mut egui::Ui) {
109        ui.scope(|ui| {
110            ui.spacing_mut().item_spacing = Vec2::splat(25.0);
111
112            for (i, question) in self.list.iter_mut().enumerate() {
113                if i > 0 {
114                    ui.separator();
115                }
116
117                ui.vertical(|ui| {
118                    ui.spacing_mut().item_spacing = Vec2::splat(15.0);
119
120                    match question {
121                        StatefulQItem::SingleLine { prompt, .. }
122                        | StatefulQItem::MultiLine { prompt, .. }
123                        | StatefulQItem::SingleChoice { prompt, .. }
124                        | StatefulQItem::MultiChoice { prompt, .. }
125                        | StatefulQItem::Slider { prompt, .. } => {
126                            let _ = parse_text(ui, prompt.as_str());
127                        }
128                    };
129
130                    question.ui(ui);
131                });
132            }
133        });
134    }
135
136    fn show_controls(
137        &mut self,
138        builder: StripBuilder,
139        sync_writer: &mut QWriter<SyncSignal>,
140        async_writer: &mut QWriter<AsyncSignal>,
141    ) {
142        enum Interaction {
143            None,
144            Submit,
145        }
146
147        let mut interaction = Interaction::None;
148
149        center_x(builder, 250.0, |ui| {
150            ui.horizontal_centered(|ui| {
151                style_ui(ui, Style::SubmitButton);
152                if ui.button(button1("Submit")).clicked() {
153                    interaction = Interaction::Submit;
154                }
155            });
156        });
157
158        match interaction {
159            Interaction::None => {}
160            Interaction::Submit => {
161                self.done = true;
162                sync_writer.push(SyncSignal::UpdateGraph);
163                async_writer.push(LoggerSignal::Extend(
164                    self.group.clone(),
165                    self.list.iter().map(|q| q.to_string()).collect(),
166                ));
167            }
168        }
169    }
170}
171
172#[derive(Debug, Deserialize, Serialize)]
173#[serde(deny_unknown_fields)]
174#[serde(rename_all = "snake_case")]
175pub enum QItem {
176    SingleLine {
177        id: String,
178        prompt: String,
179    },
180    MultiLine {
181        id: String,
182        prompt: String,
183        #[serde(default = "defaults::lines")]
184        lines: usize,
185    },
186    SingleChoice {
187        id: String,
188        prompt: String,
189        options: Vec<String>,
190        #[serde(default = "defaults::columns")]
191        columns: usize,
192    },
193    MultiChoice {
194        id: String,
195        prompt: String,
196        options: Vec<String>,
197        #[serde(default = "defaults::columns")]
198        columns: usize,
199    },
200    Slider {
201        id: String,
202        prompt: String,
203        range: (f32, f32),
204        step: f32,
205        #[serde(default = "defaults::precision")]
206        precision: u8,
207    },
208}
209
210impl QItem {
211    fn stateful(&self) -> StatefulQItem {
212        match self {
213            QItem::SingleLine { id, prompt } => StatefulQItem::SingleLine {
214                id: id.clone(),
215                prompt: prompt.clone(),
216                input: String::new(),
217            },
218            QItem::MultiLine { id, prompt, lines } => StatefulQItem::MultiLine {
219                id: id.clone(),
220                prompt: prompt.clone(),
221                lines: *lines,
222                input: String::new(),
223            },
224            QItem::SingleChoice {
225                id,
226                prompt,
227                options,
228                columns,
229            } => StatefulQItem::SingleChoice {
230                id: id.clone(),
231                prompt: prompt.clone(),
232                options: options.clone(),
233                choice: None,
234                columns: *columns,
235            },
236            QItem::MultiChoice {
237                id,
238                prompt,
239                options,
240                columns,
241            } => StatefulQItem::MultiChoice {
242                id: id.clone(),
243                prompt: prompt.clone(),
244                options: options.clone(),
245                choice: vec![false; options.len()],
246                columns: *columns,
247            },
248            QItem::Slider {
249                id,
250                prompt,
251                range,
252                step,
253                precision,
254            } => StatefulQItem::Slider {
255                id: id.clone(),
256                prompt: prompt.clone(),
257                range: (
258                    f32_with_precision(range.0, *precision),
259                    f32_with_precision(range.1, *precision),
260                ),
261                step: *step,
262                choice: range.0,
263                precision: *precision,
264            },
265        }
266    }
267}
268
269#[derive(Debug, Clone)]
270pub enum StatefulQItem {
271    SingleLine {
272        id: String,
273        prompt: String,
274        input: String,
275    },
276    MultiLine {
277        id: String,
278        prompt: String,
279        lines: usize,
280        input: String,
281    },
282    SingleChoice {
283        id: String,
284        prompt: String,
285        options: Vec<String>,
286        choice: Option<usize>,
287        columns: usize,
288    },
289    MultiChoice {
290        id: String,
291        prompt: String,
292        options: Vec<String>,
293        choice: Vec<bool>,
294        columns: usize,
295    },
296    Slider {
297        id: String,
298        prompt: String,
299        range: (f32, f32),
300        step: f32,
301        choice: f32,
302        precision: u8,
303    },
304}
305
306impl StatefulQItem {
307    fn ui(&mut self, ui: &mut egui::Ui) {
308        match self {
309            StatefulQItem::SingleLine { input, .. } => Self::show_single_line(ui, input),
310            StatefulQItem::MultiLine { input, lines, .. } => {
311                Self::show_multi_line(ui, input, *lines)
312            }
313            StatefulQItem::SingleChoice {
314                options,
315                choice,
316                columns,
317                ..
318            } => Self::show_single_choice(ui, options, choice, *columns),
319            StatefulQItem::MultiChoice {
320                options,
321                choice,
322                columns,
323                ..
324            } => Self::show_multi_choice(ui, options, choice, *columns),
325            StatefulQItem::Slider {
326                range,
327                step,
328                choice,
329                precision,
330                ..
331            } => Self::show_slider(ui, *range, *step, choice, *precision),
332        }
333    }
334
335    #[allow(clippy::ptr_arg)]
336    fn show_single_line(ui: &mut egui::Ui, input: &mut String) {
337        ui.vertical_centered_justified(|ui| {
338            TextEdit::singleline(input)
339                .hint_text(inactive("Your answer goes here"))
340                .ui(ui);
341        });
342    }
343
344    #[allow(clippy::ptr_arg)]
345    fn show_multi_line(ui: &mut egui::Ui, input: &mut String, lines: usize) {
346        ui.vertical_centered_justified(|ui| {
347            TextEdit::multiline(input)
348                .hint_text(inactive("Your answer goes here"))
349                .desired_rows(lines)
350                .ui(ui);
351        });
352    }
353
354    fn show_single_choice(
355        ui: &mut egui::Ui,
356        options: &[String],
357        choice: &mut Option<usize>,
358        columns: usize,
359    ) {
360        ui.horizontal_wrapped(|ui| {
361            ui.spacing_mut().item_spacing = Vec2::new(45.0, 15.0);
362            ui.spacing_mut().icon_width = TEXT_SIZE_BODY * 0.75;
363            ui.spacing_mut().icon_width_inner = TEXT_SIZE_BODY * 0.5;
364            ui.spacing_mut().icon_spacing = TEXT_SIZE_BODY * 0.25;
365            ui.visuals_mut().widgets.inactive.fg_stroke = Stroke::new(2.5, Color32::DARK_GRAY);
366            ui.visuals_mut().widgets.hovered.fg_stroke = Stroke::new(2.5, Color32::DARK_GRAY);
367            ui.visuals_mut().widgets.active.fg_stroke = Stroke::new(2.5, Color32::DARK_GRAY);
368            ui.visuals_mut().widgets.noninteractive.fg_stroke = Stroke::new(2.5, Color32::GRAY);
369
370            if columns > 0 {
371                let mut i = 0;
372                ui.vertical_centered_justified(|ui| {
373                    while i < options.len() {
374                        ui.columns(columns, |ui| {
375                            while i < options.len() {
376                                if RadioButton::new(*choice == Some(i), body(options[i].as_str()))
377                                    .ui(&mut ui[i % columns])
378                                    .clicked()
379                                {
380                                    *choice = Some(i);
381                                }
382
383                                i += 1;
384                            }
385                        });
386                    }
387                });
388            } else {
389                ui.horizontal_wrapped(|ui| {
390                    options.iter().enumerate().for_each(|(i, option)| {
391                        if RadioButton::new(*choice == Some(i), body(option.as_str()))
392                            .ui(ui)
393                            .clicked()
394                        {
395                            *choice = Some(i);
396                        }
397                    });
398                });
399            }
400        });
401    }
402
403    fn show_multi_choice(
404        ui: &mut egui::Ui,
405        options: &[String],
406        choice: &mut [bool],
407        columns: usize,
408    ) {
409        ui.scope(|ui| {
410            ui.spacing_mut().item_spacing = Vec2::new(45.0, 15.0);
411            ui.spacing_mut().icon_width = TEXT_SIZE_BODY * 0.75;
412            ui.spacing_mut().icon_width_inner = TEXT_SIZE_BODY * 0.5;
413            ui.spacing_mut().icon_spacing = TEXT_SIZE_BODY * 0.25;
414            ui.visuals_mut().widgets.inactive.fg_stroke = Stroke::new(2.5, Color32::DARK_GRAY);
415            ui.visuals_mut().widgets.hovered.fg_stroke = Stroke::new(2.5, Color32::DARK_GRAY);
416            ui.visuals_mut().widgets.active.fg_stroke = Stroke::new(2.5, Color32::DARK_GRAY);
417            ui.visuals_mut().widgets.noninteractive.fg_stroke = Stroke::new(2.5, Color32::GRAY);
418
419            if columns > 0 {
420                let mut i = 0;
421                ui.vertical_centered_justified(|ui| {
422                    while i < options.len() {
423                        ui.columns(columns, |ui| {
424                            while i < options.len() {
425                                Checkbox::new(&mut choice[i], body(options[i].as_str()))
426                                    .ui(&mut ui[i % columns]);
427
428                                i += 1;
429                            }
430                        });
431                    }
432                });
433            } else {
434                ui.horizontal_wrapped(|ui| {
435                    options.iter().enumerate().for_each(|(i, option)| {
436                        Checkbox::new(&mut choice[i], body(option.as_str())).ui(ui);
437                    });
438                });
439            }
440        });
441    }
442
443    fn show_slider(
444        ui: &mut egui::Ui,
445        range: (f32, f32),
446        step: f32,
447        choice: &mut f32,
448        precision: u8,
449    ) {
450        let range = RangeInclusive::new(
451            f32_with_precision(range.0, precision),
452            f32_with_precision(range.1, precision),
453        );
454
455        ui.horizontal_centered(|ui| {
456            ui.spacing_mut().slider_width = 400.0;
457
458            ui.add_space(560.0);
459            Slider::new(choice, range)
460                .max_decimals(precision as usize)
461                .step_by(step as f64)
462                .clamp_to_range(true)
463                .ui(ui);
464        });
465    }
466}
467
468impl StatefulQItem {
469    fn to_string(&self) -> (String, Value) {
470        let name = match self {
471            StatefulQItem::SingleLine { id, .. }
472            | StatefulQItem::MultiLine { id, .. }
473            | StatefulQItem::SingleChoice { id, .. }
474            | StatefulQItem::MultiChoice { id, .. }
475            | StatefulQItem::Slider { id, .. } => id.to_owned(),
476        };
477
478        let value = match self {
479            StatefulQItem::SingleLine { input, .. } | StatefulQItem::MultiLine { input, .. } => {
480                Value::Text(input.to_owned())
481            }
482            StatefulQItem::SingleChoice {
483                choice, options, ..
484            } => {
485                if let Some(choice) = choice {
486                    Value::Text(options[*choice].to_owned())
487                } else {
488                    Value::Null
489                }
490            }
491            StatefulQItem::MultiChoice {
492                choice, options, ..
493            } => Value::Array(
494                choice
495                    .iter()
496                    .enumerate()
497                    .filter_map(|(i, checked)| {
498                        if *checked {
499                            Some(Value::Text(options[i].to_owned()))
500                        } else {
501                            None
502                        }
503                    })
504                    .collect(),
505            ),
506            StatefulQItem::Slider {
507                choice, precision, ..
508            } => Value::Float(f64_with_precision(*choice, *precision)),
509        };
510
511        (name, value)
512    }
513}