egui_any/
lib.rs

1use std::{fmt::Display, hash::Hash};
2
3use egui::{Id, Response, Ui};
4use egui_probe::{EguiProbe, Style};
5use hashbrown::HashMap;
6
7/// Top-level descriptio of a value.
8#[derive(Clone, Debug, Default, EguiProbe)]
9pub enum Desc {
10    /// A boolean value.
11    #[default]
12    Bool,
13
14    /// An integer value.
15    Int { min: Option<i64>, max: Option<i64> },
16
17    /// A floating-point value.
18    Float { min: Option<f64>, max: Option<f64> },
19
20    /// A string value.
21    String {
22        variants: Option<Vec<String>>,
23    },
24
25    /// A list of values.
26    List {
27        // The description of the values.
28        elem_desc: Option<Box<Desc>>,
29    },
30
31    /// A map of key-value pairs.
32    Map {
33        // The description of the values.
34        value_desc: Option<Box<Desc>>,
35    },
36}
37
38impl Desc {
39    pub fn default_value(&self) -> Value {
40        match *self {
41            Desc::Bool => Value::Bool(false),
42            Desc::Int { min, .. } => Value::Int(min.unwrap_or(0)),
43            Desc::Float { min, .. } => Value::Float(min.unwrap_or(0.0)),
44            Desc::String { ref variants } => {
45                variants.as_ref().and_then(|v| v.first()).map_or_else(
46                    || Value::String(String::new()),
47                    |s| Value::String(s.clone()),   
48                )
49            }
50            Desc::List { .. } => Value::List(Vec::new()),
51            Desc::Map { .. } => Value::Map(HashMap::new()),
52        }
53    }
54}
55
56impl Desc {
57    pub fn kind(&self) -> &str {
58        match self {
59            Desc::Bool => "bool",
60            Desc::Int { .. } => "int",
61            Desc::Float { .. } => "float",
62            Desc::String { .. } => "string",
63            Desc::List { .. } => "list",
64            Desc::Map { .. } => "map",
65        }
66    }
67}
68
69/// Top-level value.
70#[derive(Clone)]
71pub enum Value {
72    Bool(bool),
73    Int(i64),
74    Float(f64),
75    String(String),
76    List(Vec<Value>),
77    Map(HashMap<String, Value>),
78}
79
80impl Value {
81    pub fn kind(&self) -> &str {
82        match self {
83            Value::Bool(_) => "bool",
84            Value::Int(_) => "int",
85            Value::Float(_) => "float",
86            Value::String(_) => "string",
87            Value::List(_) => "list",
88            Value::Map(_) => "map",
89        }
90    }
91
92    fn has_inner(&self) -> bool {
93        match self {
94            Value::List(elems) => !elems.is_empty(),
95            Value::Map(values) => !values.is_empty(),
96            _ => false,
97        }
98    
99    }
100}
101
102pub struct ValueProbe<'a> {
103    desc: Option<&'a Desc>,
104    mydesc: Desc,
105    myid: Id,
106    value: &'a mut Value,
107    id_source: Id,
108}
109
110impl<'a> ValueProbe<'a> {
111    pub fn new(desc: Option<&'a Desc>, value: &'a mut Value, id_source: impl Hash) -> Self {
112        ValueProbe {
113            desc,
114            mydesc: Desc::Bool,
115            myid: Id::NULL,
116            value,
117            id_source: Id::new(id_source),
118        }
119    }
120}
121
122impl EguiProbe for ValueProbe<'_> {
123    fn probe(&mut self, ui: &mut Ui, style: &Style) -> Response {
124        match self.desc {
125            None => {
126                let id = ui.make_persistent_id(self.id_source);
127                self.mydesc = ui
128                    .ctx()
129                    .data(|d| d.get_temp::<Desc>(id))
130                    .unwrap_or_default();
131                let r = self.mydesc.probe(ui, style);
132                ui.ctx()
133                    .data_mut(|d| d.insert_temp(id, self.mydesc.clone()));
134                r
135            }
136            Some(Desc::Bool) => match self.value {
137                Value::Bool(value) => value.probe(ui, style),
138                _ => {
139                    ui.horizontal(|ui| {
140                        ui.strong(format!(
141                            "Expected boolean, but is {} instead",
142                            self.value.kind()
143                        ));
144                        if ui.small_button("Reset to false").clicked() {
145                            *self.value = Value::Bool(false);
146                        }
147                        ui.strong("?");
148                    })
149                    .response
150                }
151            },
152            Some(&Desc::Int { min, max }) => {
153                let reset_to = match (min, max) {
154                    (None, None) => 0,
155                    (Some(min), None) => min.max(0),
156                    (None, Some(max)) => max.min(0),
157                    (Some(min), Some(max)) if min <= max => 0i64.clamp(min, max),
158                    (Some(min), Some(max)) => {
159                        return invalid_range(ui, min, max);
160                    }
161                };
162
163                match self.value {
164                    Value::Int(value) => match (min, max) {
165                        (None, None) => value.probe(ui, style),
166                        (Some(min), None) => {
167                            egui_probe::customize::probe_range(min.., value).probe(ui, style)
168                        }
169                        (None, Some(max)) => {
170                            egui_probe::customize::probe_range(..=max, value).probe(ui, style)
171                        }
172                        (Some(min), Some(max)) => {
173                            egui_probe::customize::probe_range(min..=max, value).probe(ui, style)
174                        }
175                    },
176                    Value::Float(value) => {
177                        let f = *value as i64;
178                        let x = match (min, max) {
179                            (None, None) => f,
180                            (Some(min), None) => min.max(f),
181                            (None, Some(max)) => max.min(f),
182                            (Some(min), Some(max)) => f.clamp(min, max),
183                        };
184
185                        ui.horizontal(|ui| {
186                            ui.strong(format!(
187                                "Expected integer, but is {} instead",
188                                self.value.kind()
189                            ));
190
191                            if ui.small_button(format!("Convert to {x}")).clicked() {
192                                *self.value = Value::Int(x);
193                            }
194
195                            ui.strong("?");
196                        })
197                        .response
198                    }
199                    _ => {
200                        ui.horizontal(|ui| {
201                            ui.strong(format!(
202                                "Expected integer, but is {} instead",
203                                self.value.kind()
204                            ));
205                            if ui.small_button(format!("Reset to {reset_to}")).clicked() {
206                                *self.value = Value::Int(reset_to);
207                            }
208                            ui.strong("?");
209                        })
210                        .response
211                    }
212                }
213            }
214            Some(&Desc::Float { min, max }) => {
215                let reset_to = match (min, max) {
216                    (None, None) => 0.0,
217                    (Some(min), None) => min.max(0.0),
218                    (None, Some(max)) => max.min(0.0),
219                    (Some(min), Some(max)) if min <= max => 0f64.clamp(min, max),
220                    (Some(min), Some(max)) => {
221                        return invalid_range(ui, min, max);
222                    }
223                };
224
225                match self.value {
226                    Value::Float(value) => match (min, max) {
227                        (None, None) => value.probe(ui, style),
228                        (Some(min), None) => {
229                            egui_probe::customize::probe_range(min.., value).probe(ui, style)
230                        }
231                        (None, Some(max)) => {
232                            egui_probe::customize::probe_range(..=max, value).probe(ui, style)
233                        }
234                        (Some(min), Some(max)) => {
235                            egui_probe::customize::probe_range(min..=max, value).probe(ui, style)
236                        }
237                    },
238                    Value::Int(value) => {
239                        let f = *value as f64;
240                        let x = match (min, max) {
241                            (None, None) => f,
242                            (Some(min), None) => min.max(f),
243                            (None, Some(max)) => max.min(f),
244                            (Some(min), Some(max)) => f.clamp(min, max),
245                        };
246
247                        ui.horizontal(|ui| {
248                            ui.strong(format!(
249                                "Expected integer, but is {} instead",
250                                self.value.kind()
251                            ));
252
253                            if ui.small_button(format!("Convert to {x:0.1}")).clicked() {
254                                *self.value = Value::Float(x);
255                            }
256
257                            ui.strong("?");
258                        })
259                        .response
260                    }
261                    _ => {
262                        ui.horizontal(|ui| {
263                            ui.strong(format!(
264                                "Expected integer, but is {} instead",
265                                self.value.kind()
266                            ));
267                            if ui.small_button(format!("Reset to {reset_to}")).clicked() {
268                                *self.value = Value::Float(reset_to);
269                            }
270                            ui.strong("?");
271                        })
272                        .response
273                    }
274                }
275            }
276            Some(&Desc::String { ref variants }) => match self.value {
277                Value::String(value) => {
278                    match variants {
279                        None => value.probe(ui, style),
280                        Some(variants) => {
281                            let cbox = egui::ComboBox::from_id_source(self.id_source).selected_text(&**value);
282                            
283                            cbox.show_ui(ui, |ui| {
284                                for variant in variants.iter() {
285                                    if ui.selectable_label(value == variant, variant).clicked() {
286                                        *value = variant.clone();
287                                    }
288                                }
289                            }).response
290                        }
291                    }
292
293                }
294                Value::Bool(value) if variants.is_none() => {
295                    let (r, s) = convert_to_string(ui, value, "bool");
296                    if let Some(s) = s {
297                        *self.value = Value::String(s);
298                    }
299                    r
300                }
301                Value::Int(value) if variants.is_none() => {
302                    let (r, s) = convert_to_string(ui, value, "int");
303                    if let Some(s) = s {
304                        *self.value = Value::String(s);
305                    }
306                    r
307                }
308                Value::Float(value) if variants.is_none() => {
309                    let (r, s) = convert_to_string(ui, value, "float");
310                    if let Some(s) = s {
311                        *self.value = Value::String(s);
312                    }
313                    r
314                }
315                _  if variants.is_none() => {
316                    ui.horizontal(|ui| {
317                        ui.strong(format!(
318                            "Expected string, but is {} instead",
319                            self.value.kind()
320                        ));
321                        if ui.small_button("Reset to empty string").clicked() {
322                            *self.value = Value::String(String::new());
323                        }
324                        ui.strong("?");
325                    })
326                    .response
327                }
328                _  => {
329                    ui.horizontal(|ui| {
330                        ui.strong(format!(
331                            "Expected string, but is {} instead",
332                            self.value.kind()
333                        ));
334                        if ui.small_button("Reset to default value").clicked() {
335                            *self.value = Value::String( variants.as_ref().unwrap().first().map_or(String::new(), |s| s.clone()) );
336                        }
337                        ui.strong("?");
338                    })
339                    .response
340                }
341            },
342            Some(&Desc::List { elem_desc: ref elem }) => match self.value {
343                Value::List(elems) => {
344                    match elem {
345                        None => {
346                            self.myid = ui.make_persistent_id(self.id_source.with("List"));
347                            self.mydesc = ui
348                                .ctx()
349                                .data(|d| d.get_temp::<Desc>(self.myid))
350                                .unwrap_or_default();
351        
352                            let r = ui.horizontal(|ui| {
353                                self.mydesc.probe(ui, style);
354
355                                let r = ui.small_button(style.add_button_text());
356                                if r.clicked() {
357                                    elems.push(self.mydesc.default_value());
358                                }
359                            }).response;
360
361                            ui.ctx().data_mut(|d| d.insert_temp(self.myid, self.mydesc.clone()));
362                            r
363                        }
364                        Some(elem) => {
365                            ui.horizontal(|ui| {
366                                ui.weak(elem.kind());
367
368                                let r = ui.small_button(style.add_button_text());
369                                if r.clicked() {
370                                    elems.push(elem.default_value());
371                                }
372                            }).response
373                        }
374                    }
375                }
376                _ => {
377                    ui.horizontal(|ui| {
378                        ui.strong(format!(
379                            "Expected list, but is {} instead",
380                            self.value.kind()
381                        ));
382                        if ui.small_button("Reset to empty list").clicked() {
383                            *self.value = Value::List(Vec::new());
384                        }
385                        ui.strong("?");
386                    })
387                    .response
388                }
389            },
390            Some(&Desc::Map { value_desc: ref value }) => match self.value {
391                Value::Map(values) => {
392                    #[derive(Clone)]
393                    struct NewKey(String);
394                    
395                    self.myid = ui.make_persistent_id(self.id_source.with("Map"));
396
397                    let mut new_key = ui.ctx().data(|d| d.get_temp::<NewKey>(self.myid)).unwrap_or(NewKey(String::new()));
398
399                    let r = match value {
400                        None => {
401                            self.mydesc = ui
402                                .ctx()
403                                .data(|d| d.get_temp::<Desc>(self.myid))
404                                .unwrap_or_default();
405
406                            let r = ui.horizontal(|ui| {
407                                self.mydesc.probe(ui, style);
408
409                                ui.text_edit_singleline(&mut new_key.0);
410
411                                let r = ui.small_button(style.add_button_text());
412                                if r.clicked() {
413                                    values.insert(std::mem::take(&mut new_key.0), self.mydesc.default_value());
414                                }
415                            }).response;
416
417                            ui.ctx().data_mut(|d| d.insert_temp(self.myid, self.mydesc.clone()));
418                            r
419                        }
420                        Some(elem) => {
421                            ui.horizontal(|ui| {
422                                ui.weak(elem.kind());
423
424                                ui.text_edit_singleline(&mut new_key.0);
425
426                                let r = ui.small_button(style.add_button_text());
427                                if r.clicked() {
428                                    values.insert(std::mem::take(&mut new_key.0), elem.default_value());
429                                }
430                            }).response
431                        }
432                    };
433
434                    ui.ctx().data_mut(|d| d.insert_temp(self.myid, new_key));
435                    r
436                }
437                _ => {
438                    ui.horizontal(|ui| {
439                        ui.strong(format!(
440                            "Expected list, but is {} instead",
441                            self.value.kind()
442                        ));
443                        if ui.small_button("Reset to empty map").clicked() {
444                            *self.value = Value::Map(HashMap::new());
445                        }
446                        ui.strong("?");
447                    })
448                    .response
449                }
450            },
451        }
452    }
453
454    fn has_inner(&mut self) -> bool {
455        match self.desc {
456            None => true,
457            Some(Desc::Bool) => false,
458            Some(Desc::Int { .. }) => false,
459            Some(Desc::Float { .. }) => false,
460            Some(Desc::String { .. }) => false,
461            Some(Desc::List { elem_desc }) => elem_desc.is_none() || self.value.has_inner(),
462            Some(Desc::Map { value_desc }) => value_desc.is_none() || self.value.has_inner(),
463
464        }
465    }
466
467    fn iterate_inner(&mut self, ui: &mut Ui, f: &mut dyn FnMut(&str, &mut Ui, &mut dyn EguiProbe)) {
468        match self.desc {
469            None => {
470                let mut probe = ValueProbe::new(Some(&self.mydesc), self.value, self.id_source);
471                f("value", ui, &mut probe);
472            }
473            Some(Desc::Bool) => {}
474            Some(Desc::Int { .. }) => {}
475            Some(Desc::Float { .. }) => {}
476            Some(Desc::String { .. }) => {}
477            Some(Desc::List { elem_desc: elem }) => {
478                let elem = match elem {
479                    None => {
480                        if self.mydesc.has_inner() {
481                            self.mydesc.iterate_inner(ui, f);
482                        }
483                        ui.ctx().data_mut(|d| d.insert_temp(self.myid, self.mydesc.clone()));
484
485                        &self.mydesc
486                    },
487                    Some(elem) => &**elem,
488                };
489
490                match self.value {
491                    Value::List(elems) => {
492                        let id = self.id_source.with("List");
493
494                        let mut idx = 0;
495                        elems.retain_mut(|value| {
496                            let mut probe = ValueProbe::new(Some(elem), value, id.with(idx));
497                            let mut item = DeleteMe {
498                                value: &mut probe,
499                                delete: false,
500                            };
501                            f(&format!("[{idx}]"), ui, &mut item);
502                            idx += 1;
503                            !item.delete
504                        });
505                    }
506                    _ => {}
507                }
508            }
509            Some(Desc::Map { value_desc: value }) => {
510                let desc = match value {
511                    None => {
512                        if self.mydesc.has_inner() {
513                            self.mydesc.iterate_inner(ui, f);
514                        }
515                        ui.ctx().data_mut(|d| d.insert_temp(self.myid, self.mydesc.clone()));
516
517                        &self.mydesc
518                    },
519                    Some(value) => &**value,
520                };
521
522                match self.value {
523                    Value::Map(values) => {
524                        let id: Id = self.id_source.with("List");
525
526                        let mut idx = 0;
527                        values.retain(|key, value| {
528                            let mut probe = ValueProbe::new(Some(desc), value, id.with(idx));
529                            let mut item = DeleteMe {
530                                value: &mut probe,
531                                delete: false,
532                            };
533                            f(key, ui, &mut item);
534                            idx += 1;
535                            !item.delete
536                        });
537                    }
538                    _ => {}
539                }
540            }
541        }
542    }
543}
544
545fn invalid_range<T: Display>(ui: &mut Ui, min: T, max: T) -> Response {
546    ui.strong(format!(
547        "Invalid range. `min = {}` must be not greater than `max = {}`.",
548        min, max
549    ))
550}
551
552fn convert_to_string<T: ToString>(
553    ui: &mut Ui,
554    value: &T,
555    kind: &str,
556) -> (Response, Option<String>) {
557    let mut convert = false;
558    let s = value.to_string();
559
560    let r = ui
561        .horizontal(|ui| {
562            ui.strong(format!("Expected string, but is {} instead", kind));
563            if ui.small_button(format!("Convert to {s:?}")).clicked() {
564                convert = true;
565            }
566            ui.strong("?");
567        })
568        .response;
569
570    (r, if convert { Some(s) } else { None })
571}
572
573/// Modifier to add a delete button to an item probe UI.
574pub struct DeleteMe<'a, T> {
575    pub value: &'a mut T,
576    pub delete: bool,
577}
578
579impl<T> EguiProbe for DeleteMe<'_, T>
580where
581    T: EguiProbe,
582{
583    fn probe(&mut self, ui: &mut egui::Ui, style: &Style) -> egui::Response {
584        ui.horizontal(|ui| {
585            self.value.probe(ui, style);
586            ui.add_space(ui.spacing().item_spacing.x);
587            if ui.small_button(style.remove_button_text()).clicked() {
588                self.delete = true;
589            };
590        })
591        .response
592    }
593
594    fn has_inner(&mut self) -> bool {
595        self.value.has_inner() && !self.delete
596    }
597
598    fn iterate_inner(&mut self, ui: &mut Ui, f: &mut dyn FnMut(&str, &mut Ui, &mut dyn EguiProbe)) {
599        self.value.iterate_inner(ui, f);
600    }
601}