1use std::{fmt::Display, hash::Hash};
2
3use egui::{Id, Response, Ui};
4use egui_probe::{EguiProbe, Style};
5use hashbrown::HashMap;
6
7#[derive(Clone, Debug, Default, EguiProbe)]
9pub enum Desc {
10 #[default]
12 Bool,
13
14 Int { min: Option<i64>, max: Option<i64> },
16
17 Float { min: Option<f64>, max: Option<f64> },
19
20 String {
22 variants: Option<Vec<String>>,
23 },
24
25 List {
27 elem_desc: Option<Box<Desc>>,
29 },
30
31 Map {
33 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#[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
573pub 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}