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}