1use std::ops::RangeInclusive;
4
5use egui::{collapsing_header::CollapsingResponse, scroll_area::ScrollAreaOutput};
6
7use crate::{
8 instrument::{active_inner, capture_layout, container, swallow_panic},
9 types::{RoleState, WidgetRange, WidgetRole, WidgetValue},
10 widget_registry::{WidgetMeta, record_widget},
11};
12
13#[derive(Debug, Clone, Copy, Default)]
15pub struct ButtonOptions {
16 pub selected: bool,
18}
19
20#[derive(Debug, Clone, Copy, Default)]
22pub struct CheckboxOptions {
23 pub indeterminate: bool,
25}
26
27#[derive(Debug, Clone, Copy, Default)]
29pub struct TextEditOptions {
30 pub multiline: bool,
32 pub password: bool,
34}
35
36#[derive(Debug, Clone, Default)]
38pub struct ProgressBarOptions {
39 pub text: Option<String>,
41 pub show_percentage: bool,
43}
44
45pub trait DevUiExt {
49 fn dev_button(
51 &mut self,
52 id: impl Into<String>,
53 text: impl Into<egui::WidgetText>,
54 ) -> egui::Response;
55
56 fn dev_button_with(
58 &mut self,
59 id: impl Into<String>,
60 text: impl Into<egui::WidgetText>,
61 options: ButtonOptions,
62 ) -> egui::Response;
63
64 fn dev_link(
66 &mut self,
67 id: impl Into<String>,
68 text: impl Into<egui::WidgetText>,
69 ) -> egui::Response;
70
71 fn dev_hyperlink(&mut self, id: impl Into<String>, url: impl ToString) -> egui::Response;
73
74 fn dev_hyperlink_to(
76 &mut self,
77 id: impl Into<String>,
78 label: impl Into<egui::WidgetText>,
79 url: impl ToString,
80 ) -> egui::Response;
81
82 fn dev_image<'a>(
84 &mut self,
85 id: impl Into<String>,
86 description: impl Into<String>,
87 source: impl Into<egui::ImageSource<'a>>,
88 ) -> egui::Response;
89
90 fn dev_label(
92 &mut self,
93 id: impl Into<String>,
94 text: impl Into<egui::WidgetText>,
95 ) -> egui::Response;
96
97 fn dev_checkbox(
99 &mut self,
100 id: impl Into<String>,
101 value: &mut bool,
102 text: impl Into<egui::WidgetText>,
103 ) -> egui::Response;
104
105 fn dev_checkbox_with(
107 &mut self,
108 id: impl Into<String>,
109 value: &mut bool,
110 text: impl Into<egui::WidgetText>,
111 options: CheckboxOptions,
112 ) -> egui::Response;
113
114 fn dev_text_edit(&mut self, id: impl Into<String>, text: &mut String) -> egui::Response;
116
117 fn dev_text_edit_with(
119 &mut self,
120 id: impl Into<String>,
121 text: &mut String,
122 options: TextEditOptions,
123 ) -> egui::Response;
124
125 fn dev_slider(
127 &mut self,
128 id: impl Into<String>,
129 value: &mut f32,
130 range: RangeInclusive<f32>,
131 ) -> egui::Response;
132
133 fn dev_combo_box<T: ToString>(
135 &mut self,
136 id: impl Into<String>,
137 label: impl Into<String>,
138 selected: &mut usize,
139 options: &[T],
140 ) -> egui::Response;
141
142 fn dev_drag_value(&mut self, id: impl Into<String>, value: &mut f32) -> egui::Response;
144
145 fn dev_drag_value_range(
147 &mut self,
148 id: impl Into<String>,
149 value: &mut f32,
150 range: RangeInclusive<f32>,
151 ) -> egui::Response;
152
153 fn dev_drag_value_i32(&mut self, id: impl Into<String>, value: &mut i32) -> egui::Response;
155
156 fn dev_drag_value_i32_range(
158 &mut self,
159 id: impl Into<String>,
160 value: &mut i32,
161 range: RangeInclusive<i32>,
162 ) -> egui::Response;
163
164 fn dev_text_edit_multiline(
166 &mut self,
167 id: impl Into<String>,
168 text: &mut String,
169 ) -> egui::Response;
170
171 fn dev_toggle_value(
173 &mut self,
174 id: impl Into<String>,
175 selected: &mut bool,
176 text: impl Into<egui::WidgetText>,
177 ) -> egui::Response;
178
179 fn dev_radio_value<V: PartialEq + Clone>(
181 &mut self,
182 id: impl Into<String>,
183 current: &mut V,
184 alternative: V,
185 text: impl Into<egui::WidgetText>,
186 ) -> egui::Response;
187
188 fn dev_selectable_value<V: PartialEq + Clone>(
190 &mut self,
191 id: impl Into<String>,
192 current: &mut V,
193 alternative: V,
194 text: impl Into<egui::WidgetText>,
195 ) -> egui::Response;
196
197 fn dev_separator(&mut self, id: impl Into<String>) -> egui::Response;
199
200 fn dev_spinner(&mut self, id: impl Into<String>) -> egui::Response;
202
203 fn dev_progress_bar(&mut self, id: impl Into<String>, progress: f32) -> egui::Response;
205
206 fn dev_progress_bar_with(
208 &mut self,
209 id: impl Into<String>,
210 progress: f32,
211 options: ProgressBarOptions,
212 ) -> egui::Response;
213
214 fn dev_color_edit(
216 &mut self,
217 id: impl Into<String>,
218 color: &mut egui::Color32,
219 ) -> egui::Response;
220
221 fn dev_menu_button<R>(
223 &mut self,
224 id: impl Into<String>,
225 text: impl Into<egui::WidgetText>,
226 add_contents: impl FnOnce(&mut egui::Ui) -> R,
227 ) -> egui::InnerResponse<Option<R>>;
228
229 fn dev_collapsing<R>(
231 &mut self,
232 id: impl Into<String>,
233 open: &mut bool,
234 heading: impl Into<egui::WidgetText>,
235 add_contents: impl FnOnce(&mut egui::Ui) -> R,
236 ) -> CollapsingResponse<R>;
237}
238
239impl DevUiExt for egui::Ui {
240 fn dev_button(
241 &mut self,
242 id: impl Into<String>,
243 text: impl Into<egui::WidgetText>,
244 ) -> egui::Response {
245 let id = id.into();
246 let (text, label) = widget_text_parts(text);
247 let response = self.button(text);
248 record_widget_with_layout(
249 self,
250 id,
251 &response,
252 WidgetRole::Button,
253 Some(label),
254 None,
255 None,
256 );
257 response
258 }
259
260 fn dev_button_with(
261 &mut self,
262 id: impl Into<String>,
263 text: impl Into<egui::WidgetText>,
264 options: ButtonOptions,
265 ) -> egui::Response {
266 let id = id.into();
267 let (text, label) = widget_text_parts(text);
268 let response = self.add(egui::Button::new(text).selected(options.selected));
269 record_widget_with_layout(
270 self,
271 id,
272 &response,
273 WidgetRole::Button,
274 Some(label),
275 None,
276 Some(RoleState::Button {
277 selected: options.selected,
278 }),
279 );
280 response
281 }
282
283 fn dev_link(
284 &mut self,
285 id: impl Into<String>,
286 text: impl Into<egui::WidgetText>,
287 ) -> egui::Response {
288 let id = id.into();
289 let (text, label) = widget_text_parts(text);
290 let response = self.link(text);
291 record_widget_with_layout(
292 self,
293 id,
294 &response,
295 WidgetRole::Link,
296 Some(label),
297 None,
298 None,
299 );
300 response
301 }
302
303 fn dev_hyperlink(&mut self, id: impl Into<String>, url: impl ToString) -> egui::Response {
304 let id = id.into();
305 let url = url.to_string();
306 let response = self.hyperlink(url.clone());
307 record_widget_with_layout(
308 self,
309 id,
310 &response,
311 WidgetRole::Link,
312 Some(url.clone()),
313 Some(WidgetValue::Text(url)),
314 None,
315 );
316 response
317 }
318
319 fn dev_hyperlink_to(
320 &mut self,
321 id: impl Into<String>,
322 label: impl Into<egui::WidgetText>,
323 url: impl ToString,
324 ) -> egui::Response {
325 let id = id.into();
326 let (label, text) = widget_text_parts(label);
327 let url = url.to_string();
328 let response = self.hyperlink_to(label, url.clone());
329 record_widget_with_layout(
330 self,
331 id,
332 &response,
333 WidgetRole::Link,
334 Some(text),
335 Some(WidgetValue::Text(url)),
336 None,
337 );
338 response
339 }
340
341 fn dev_image<'a>(
342 &mut self,
343 id: impl Into<String>,
344 description: impl Into<String>,
345 source: impl Into<egui::ImageSource<'a>>,
346 ) -> egui::Response {
347 let id = id.into();
348 let description = description.into();
349 let response = self.image(source);
350 record_widget_with_layout(
351 self,
352 id,
353 &response,
354 WidgetRole::Image,
355 Some(description.clone()),
356 Some(WidgetValue::Text(description)),
357 None,
358 );
359 response
360 }
361
362 fn dev_label(
363 &mut self,
364 id: impl Into<String>,
365 text: impl Into<egui::WidgetText>,
366 ) -> egui::Response {
367 let id = id.into();
368 let (text, label) = widget_text_parts(text);
369 let response = self.label(text);
370 record_widget_with_layout(
371 self,
372 id,
373 &response,
374 WidgetRole::Label,
375 Some(label.clone()),
376 Some(WidgetValue::Text(label)),
377 None,
378 );
379 response
380 }
381
382 fn dev_checkbox(
383 &mut self,
384 id: impl Into<String>,
385 value: &mut bool,
386 text: impl Into<egui::WidgetText>,
387 ) -> egui::Response {
388 let id = id.into();
389 if let Some(updated) = take_bool_override(self, &id) {
390 *value = updated;
391 }
392 let (text, label) = widget_text_parts(text);
393 let response = self.checkbox(value, text);
394 record_widget_with_layout(
395 self,
396 id,
397 &response,
398 WidgetRole::Checkbox,
399 Some(label),
400 Some(WidgetValue::Bool(*value)),
401 None,
402 );
403 response
404 }
405
406 fn dev_checkbox_with(
407 &mut self,
408 id: impl Into<String>,
409 value: &mut bool,
410 text: impl Into<egui::WidgetText>,
411 options: CheckboxOptions,
412 ) -> egui::Response {
413 let id = id.into();
414 if let Some(updated) = take_bool_override(self, &id) {
415 *value = updated;
416 }
417 let (text, label) = widget_text_parts(text);
418 let response =
419 self.add(egui::Checkbox::new(value, text).indeterminate(options.indeterminate));
420 record_widget_with_layout(
421 self,
422 id,
423 &response,
424 WidgetRole::Checkbox,
425 Some(label),
426 Some(WidgetValue::Bool(*value)),
427 Some(RoleState::Checkbox {
428 indeterminate: options.indeterminate,
429 }),
430 );
431 response
432 }
433
434 fn dev_text_edit(&mut self, id: impl Into<String>, text: &mut String) -> egui::Response {
435 self.dev_text_edit_with(id, text, TextEditOptions::default())
436 }
437
438 fn dev_text_edit_with(
439 &mut self,
440 id: impl Into<String>,
441 text: &mut String,
442 options: TextEditOptions,
443 ) -> egui::Response {
444 let id = id.into();
445 if let Some(updated) = take_text_override(self, &id) {
446 *text = updated;
447 }
448 let builder = if options.multiline {
449 egui::TextEdit::multiline(text)
450 } else {
451 egui::TextEdit::singleline(text)
452 }
453 .password(options.password);
454 let response = self.add(builder);
455 record_widget_with_layout(
456 self,
457 id,
458 &response,
459 WidgetRole::TextEdit,
460 None,
461 Some(WidgetValue::Text(text.clone())),
462 Some(RoleState::TextEdit {
463 multiline: options.multiline,
464 password: options.password,
465 }),
466 );
467 response
468 }
469
470 fn dev_slider(
471 &mut self,
472 id: impl Into<String>,
473 value: &mut f32,
474 range: RangeInclusive<f32>,
475 ) -> egui::Response {
476 let id = id.into();
477 let range_meta = widget_range_from_f32(&range);
478 if let Some(updated) = take_float_override(self, &id) {
479 *value = updated;
480 }
481 let response = self.add(egui::Slider::new(value, range));
482 record_widget_with_layout(
483 self,
484 id,
485 &response,
486 WidgetRole::Slider,
487 None,
488 Some(WidgetValue::Float(f64::from(*value))),
489 Some(RoleState::Slider { range: range_meta }),
490 );
491 response
492 }
493
494 fn dev_combo_box<T: ToString>(
495 &mut self,
496 id: impl Into<String>,
497 label: impl Into<String>,
498 selected: &mut usize,
499 options: &[T],
500 ) -> egui::Response {
501 let id = id.into();
502 if let Some(updated) = take_usize_override(self, &id) {
503 *selected = updated;
504 }
505 let label_text = label.into();
506 let option_labels = options.iter().map(ToString::to_string).collect::<Vec<_>>();
507 let len = options.len();
508 if len == 0 || *selected >= len {
509 *selected = 0;
510 }
511 let response = if len == 0 {
512 egui::ComboBox::from_label(label_text.as_str())
513 .selected_text("")
514 .show_ui(self, |_| {})
515 .response
516 } else {
517 let selected_text = options
518 .get(*selected)
519 .map(ToString::to_string)
520 .unwrap_or_default();
521 egui::ComboBox::from_label(label_text.as_str())
522 .selected_text(selected_text)
523 .show_index(self, selected, len, |index| options[index].to_string())
524 };
525 record_widget_with_layout(
526 self,
527 id,
528 &response,
529 WidgetRole::ComboBox,
530 Some(label_text),
531 Some(WidgetValue::Int(*selected as i64)),
532 Some(RoleState::ComboBox {
533 options: option_labels,
534 }),
535 );
536 response
537 }
538
539 fn dev_drag_value(&mut self, id: impl Into<String>, value: &mut f32) -> egui::Response {
540 let id = id.into();
541 if let Some(updated) = take_float_override(self, &id) {
542 *value = updated;
543 }
544 let response = self.add(egui::DragValue::new(value));
545 record_widget_with_layout(
546 self,
547 id,
548 &response,
549 WidgetRole::DragValue,
550 None,
551 Some(WidgetValue::Float(f64::from(*value))),
552 Some(RoleState::DragValue { range: None }),
553 );
554 response
555 }
556
557 fn dev_drag_value_range(
558 &mut self,
559 id: impl Into<String>,
560 value: &mut f32,
561 range: RangeInclusive<f32>,
562 ) -> egui::Response {
563 let id = id.into();
564 let range_meta = widget_range_from_f32(&range);
565 if let Some(updated) = take_float_override(self, &id) {
566 *value = updated;
567 }
568 let response = self.add(egui::DragValue::new(value).range(range));
569 record_widget_with_layout(
570 self,
571 id,
572 &response,
573 WidgetRole::DragValue,
574 None,
575 Some(WidgetValue::Float(f64::from(*value))),
576 Some(RoleState::DragValue {
577 range: Some(range_meta),
578 }),
579 );
580 response
581 }
582
583 fn dev_drag_value_i32(&mut self, id: impl Into<String>, value: &mut i32) -> egui::Response {
584 let id = id.into();
585 if let Some(updated) = take_i32_override(self, &id) {
586 *value = updated;
587 }
588 let response = self.add(egui::DragValue::new(value));
589 record_widget_with_layout(
590 self,
591 id,
592 &response,
593 WidgetRole::DragValue,
594 None,
595 Some(WidgetValue::Int(i64::from(*value))),
596 Some(RoleState::DragValue { range: None }),
597 );
598 response
599 }
600
601 fn dev_drag_value_i32_range(
602 &mut self,
603 id: impl Into<String>,
604 value: &mut i32,
605 range: RangeInclusive<i32>,
606 ) -> egui::Response {
607 let id = id.into();
608 let range_meta = widget_range_from_i32(&range);
609 if let Some(updated) = take_i32_override(self, &id) {
610 *value = updated;
611 }
612 let response = self.add(egui::DragValue::new(value).range(range));
613 record_widget_with_layout(
614 self,
615 id,
616 &response,
617 WidgetRole::DragValue,
618 None,
619 Some(WidgetValue::Int(i64::from(*value))),
620 Some(RoleState::DragValue {
621 range: Some(range_meta),
622 }),
623 );
624 response
625 }
626
627 fn dev_text_edit_multiline(
628 &mut self,
629 id: impl Into<String>,
630 text: &mut String,
631 ) -> egui::Response {
632 self.dev_text_edit_with(
633 id,
634 text,
635 TextEditOptions {
636 multiline: true,
637 password: false,
638 },
639 )
640 }
641
642 fn dev_toggle_value(
643 &mut self,
644 id: impl Into<String>,
645 selected: &mut bool,
646 text: impl Into<egui::WidgetText>,
647 ) -> egui::Response {
648 let id = id.into();
649 if let Some(updated) = take_bool_override(self, &id) {
650 *selected = updated;
651 }
652 let (text, label) = widget_text_parts(text);
653 let response = self.toggle_value(selected, text);
654 record_widget_with_layout(
655 self,
656 id,
657 &response,
658 WidgetRole::Toggle,
659 Some(label),
660 Some(WidgetValue::Bool(*selected)),
661 None,
662 );
663 response
664 }
665
666 fn dev_radio_value<V: PartialEq + Clone>(
667 &mut self,
668 id: impl Into<String>,
669 current: &mut V,
670 alternative: V,
671 text: impl Into<egui::WidgetText>,
672 ) -> egui::Response {
673 record_choice_widget(
674 self,
675 id.into(),
676 current,
677 alternative,
678 text,
679 ChoiceWidgetMeta::new(WidgetRole::Radio),
680 Self::radio_value,
681 )
682 }
683
684 fn dev_selectable_value<V: PartialEq + Clone>(
685 &mut self,
686 id: impl Into<String>,
687 current: &mut V,
688 alternative: V,
689 text: impl Into<egui::WidgetText>,
690 ) -> egui::Response {
691 record_choice_widget(
692 self,
693 id.into(),
694 current,
695 alternative,
696 text,
697 ChoiceWidgetMeta::new(WidgetRole::Selectable),
698 Self::selectable_value,
699 )
700 }
701
702 fn dev_separator(&mut self, id: impl Into<String>) -> egui::Response {
703 let id = id.into();
704 let response = self.separator();
705 record_widget_with_layout(self, id, &response, WidgetRole::Separator, None, None, None);
706 response
707 }
708
709 fn dev_spinner(&mut self, id: impl Into<String>) -> egui::Response {
710 let id = id.into();
711 let response = self.spinner();
712 record_widget_with_layout(self, id, &response, WidgetRole::Spinner, None, None, None);
713 response
714 }
715
716 fn dev_progress_bar(&mut self, id: impl Into<String>, progress: f32) -> egui::Response {
717 self.dev_progress_bar_with(id, progress, ProgressBarOptions::default())
718 }
719
720 fn dev_progress_bar_with(
721 &mut self,
722 id: impl Into<String>,
723 progress: f32,
724 options: ProgressBarOptions,
725 ) -> egui::Response {
726 let id = id.into();
727 let progress = progress.clamp(0.0, 1.0);
728 let mut widget = egui::ProgressBar::new(progress);
729 let label = if let Some(text) = options.text {
730 widget = widget.text(text.clone());
731 Some(text)
732 } else if options.show_percentage {
733 widget = widget.show_percentage();
734 Some(format!("{}%", (progress * 100.0) as usize))
735 } else {
736 None
737 };
738 let response = self.add(widget);
739 record_widget_with_layout(
740 self,
741 id,
742 &response,
743 WidgetRole::ProgressBar,
744 label,
745 Some(WidgetValue::Float(f64::from(progress))),
746 None,
747 );
748 response
749 }
750
751 fn dev_color_edit(
752 &mut self,
753 id: impl Into<String>,
754 color: &mut egui::Color32,
755 ) -> egui::Response {
756 let id = id.into();
757 if let Some(updated) = take_color_override(self, &id) {
758 *color = updated;
759 }
760 let response = self.color_edit_button_srgba(color);
761 record_widget_with_layout(
762 self,
763 id,
764 &response,
765 WidgetRole::ColorPicker,
766 None,
767 Some(WidgetValue::Text(format_color_hex(*color))),
768 None,
769 );
770 response
771 }
772
773 fn dev_menu_button<R>(
774 &mut self,
775 id: impl Into<String>,
776 text: impl Into<egui::WidgetText>,
777 add_contents: impl FnOnce(&mut Self) -> R,
778 ) -> egui::InnerResponse<Option<R>> {
779 let id = id.into();
780 let menu_tag = format!("{id}.menu");
781 let (text, label) = widget_text_parts(text);
782 let output = self.menu_button(text, |ui| container(ui, menu_tag, add_contents));
783 record_widget_with_layout(
784 self,
785 id,
786 &output.response,
787 WidgetRole::MenuButton,
788 Some(label),
789 Some(WidgetValue::Bool(output.inner.is_some())),
790 None,
791 );
792 output
793 }
794
795 fn dev_collapsing<R>(
796 &mut self,
797 id: impl Into<String>,
798 open: &mut bool,
799 heading: impl Into<egui::WidgetText>,
800 add_contents: impl FnOnce(&mut Self) -> R,
801 ) -> CollapsingResponse<R> {
802 let id = id.into();
803 if let Some(updated) = take_bool_override(self, &id) {
804 *open = updated;
805 }
806 let body_tag = format!("{id}.body");
807 let (heading, label) = widget_text_parts(heading);
808 let output = egui::CollapsingHeader::new(heading)
809 .id_salt(id.as_str())
810 .open(Some(*open))
811 .show(self, |ui| container(ui, body_tag, add_contents));
812 *open = !output.fully_closed();
813 record_widget_with_layout(
814 self,
815 id,
816 &output.header_response,
817 WidgetRole::CollapsingHeader,
818 Some(label),
819 Some(WidgetValue::Bool(*open)),
820 None,
821 );
822 output
823 }
824}
825
826pub trait DevScrollAreaExt {
828 fn dev_show<R>(
830 self,
831 ui: &mut egui::Ui,
832 id: impl Into<String>,
833 add_contents: impl FnOnce(&mut egui::Ui) -> R,
834 ) -> ScrollAreaOutput<R>;
835
836 fn dev_show_viewport<R>(
838 self,
839 ui: &mut egui::Ui,
840 id: impl Into<String>,
841 add_contents: impl FnOnce(&mut egui::Ui, egui::Rect) -> R,
842 ) -> ScrollAreaOutput<R>;
843}
844
845impl DevScrollAreaExt for egui::ScrollArea {
846 fn dev_show<R>(
847 self,
848 ui: &mut egui::Ui,
849 id: impl Into<String>,
850 add_contents: impl FnOnce(&mut egui::Ui) -> R,
851 ) -> ScrollAreaOutput<R> {
852 self.dev_show_viewport(ui, id, |ui, _| add_contents(ui))
853 }
854
855 fn dev_show_viewport<R>(
856 self,
857 ui: &mut egui::Ui,
858 id: impl Into<String>,
859 add_contents: impl FnOnce(&mut egui::Ui, egui::Rect) -> R,
860 ) -> ScrollAreaOutput<R> {
861 let id = id.into();
862 let output = self.show_viewport(ui, |ui, rect| {
863 let _guard = super::instrument::begin_container(ui, id.clone());
864 add_contents(ui, rect)
865 });
866 super::instrument::record_scroll_area(ui, id, &output);
867 output
868 }
869}
870
871pub fn take_widget_value_override(ui: &egui::Ui, id: &str) -> Option<WidgetValue> {
873 let inner = active_inner()?;
874 let viewport_id = ui.ctx().viewport_id();
875 inner.take_widget_value_update(viewport_id, id)
876}
877
878fn widget_text_parts(text: impl Into<egui::WidgetText>) -> (egui::WidgetText, String) {
879 let text = text.into();
880 let label = text.text().to_string();
881 (text, label)
882}
883
884fn take_bool_override(ui: &egui::Ui, id: &str) -> Option<bool> {
885 match take_widget_value_override(ui, id) {
886 Some(WidgetValue::Bool(updated)) => Some(updated),
887 _ => None,
888 }
889}
890
891fn take_text_override(ui: &egui::Ui, id: &str) -> Option<String> {
892 match take_widget_value_override(ui, id) {
893 Some(WidgetValue::Text(updated)) => Some(updated),
894 _ => None,
895 }
896}
897
898fn take_color_override(ui: &egui::Ui, id: &str) -> Option<egui::Color32> {
899 match take_widget_value_override(ui, id) {
900 Some(WidgetValue::Text(updated)) => parse_color_hex(&updated),
901 _ => None,
902 }
903}
904
905fn take_float_override(ui: &egui::Ui, id: &str) -> Option<f32> {
906 match take_widget_value_override(ui, id) {
907 Some(WidgetValue::Float(updated)) => Some(updated as f32),
908 Some(WidgetValue::Int(updated)) => Some(updated as f32),
909 _ => None,
910 }
911}
912
913fn take_i32_override(ui: &egui::Ui, id: &str) -> Option<i32> {
914 match take_widget_value_override(ui, id) {
915 Some(WidgetValue::Int(updated)) => i32::try_from(updated).ok(),
916 Some(WidgetValue::Float(updated)) => Some(updated as i32),
917 _ => None,
918 }
919}
920
921fn take_usize_override(ui: &egui::Ui, id: &str) -> Option<usize> {
922 match take_widget_value_override(ui, id) {
923 Some(WidgetValue::Int(updated)) => usize::try_from(updated).ok(),
924 Some(WidgetValue::Float(updated)) => Some(updated as usize),
925 _ => None,
926 }
927}
928
929struct ChoiceWidgetMeta {
930 role: WidgetRole,
931}
932
933impl ChoiceWidgetMeta {
934 fn new(role: WidgetRole) -> Self {
935 Self { role }
936 }
937}
938
939fn record_choice_widget<V>(
940 ui: &mut egui::Ui,
941 id: String,
942 current: &mut V,
943 alternative: V,
944 text: impl Into<egui::WidgetText>,
945 meta: ChoiceWidgetMeta,
946 add_widget: impl FnOnce(&mut egui::Ui, &mut V, V, egui::WidgetText) -> egui::Response,
947) -> egui::Response
948where
949 V: PartialEq + Clone,
950{
951 let (text, label) = widget_text_parts(text);
952 let selected_value = alternative.clone();
953 if take_bool_override(ui, &id).is_some_and(|updated| updated) {
954 *current = alternative.clone();
955 }
956 let response = add_widget(ui, current, alternative, text);
957 let selected = *current == selected_value;
958 record_widget_with_layout(
959 ui,
960 id,
961 &response,
962 meta.role,
963 Some(label),
964 Some(WidgetValue::Bool(selected)),
965 None,
966 );
967 response
968}
969
970fn record_widget_with_layout(
972 ui: &egui::Ui,
973 id: String,
974 response: &egui::Response,
975 role: WidgetRole,
976 label: Option<String>,
977 value: Option<WidgetValue>,
978 role_state: Option<RoleState>,
979) {
980 let Some(inner) = active_inner() else {
981 return;
982 };
983 let visible = ui.is_visible() && ui.is_rect_visible(response.rect);
984 let layout = Some(capture_layout(ui, response));
985 swallow_panic("record_widget_with_layout", || {
986 record_widget(
987 &inner.widgets,
988 id,
989 response,
990 WidgetMeta {
991 role,
992 label,
993 value,
994 layout,
995 role_state,
996 visible,
997 ..Default::default()
998 },
999 );
1000 });
1001}
1002
1003fn widget_range_from_f32(range: &RangeInclusive<f32>) -> WidgetRange {
1004 WidgetRange {
1005 min: f64::from(*range.start()),
1006 max: f64::from(*range.end()),
1007 }
1008}
1009
1010fn widget_range_from_i32(range: &RangeInclusive<i32>) -> WidgetRange {
1011 WidgetRange {
1012 min: f64::from(*range.start()),
1013 max: f64::from(*range.end()),
1014 }
1015}
1016
1017pub fn parse_color_hex(value: &str) -> Option<egui::Color32> {
1019 let hex = value.strip_prefix('#')?;
1020 if hex.len() != 8 {
1021 return None;
1022 }
1023 let bytes = u32::from_str_radix(hex, 16).ok()?;
1024 let r = ((bytes >> 24) & 0xff) as u8;
1025 let g = ((bytes >> 16) & 0xff) as u8;
1026 let b = ((bytes >> 8) & 0xff) as u8;
1027 let a = (bytes & 0xff) as u8;
1028 Some(egui::Color32::from_rgba_unmultiplied(r, g, b, a))
1029}
1030
1031pub fn format_color_hex(color: egui::Color32) -> String {
1032 let [r, g, b, a] = color.to_srgba_unmultiplied();
1033 format!("#{r:02X}{g:02X}{b:02X}{a:02X}")
1034}