Skip to main content

eguidev/
ui_ext.rs

1//! Helper extensions for recording common widgets with explicit ids.
2
3use 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/// Options for selected-aware buttons.
14#[derive(Debug, Clone, Copy, Default)]
15pub struct ButtonOptions {
16    /// Whether the button is in a selected/toggled state.
17    pub selected: bool,
18}
19
20/// Options for third-state checkboxes.
21#[derive(Debug, Clone, Copy, Default)]
22pub struct CheckboxOptions {
23    /// Whether the checkbox should render as indeterminate.
24    pub indeterminate: bool,
25}
26
27/// Options for text-edit widgets.
28#[derive(Debug, Clone, Copy, Default)]
29pub struct TextEditOptions {
30    /// Whether the edit is multiline.
31    pub multiline: bool,
32    /// Whether the edit masks its contents.
33    pub password: bool,
34}
35
36/// Options for progress bars.
37#[derive(Debug, Clone, Default)]
38pub struct ProgressBarOptions {
39    /// Optional overlay text rendered inside the bar.
40    pub text: Option<String>,
41    /// Whether to show a percentage when `text` is absent.
42    pub show_percentage: bool,
43}
44
45/// Helper extensions for recording common widgets with explicit ids.
46///
47/// Prefer these for standard widgets; they auto-populate role/type/value/label metadata.
48pub trait DevUiExt {
49    /// Add a button with an explicit id.
50    fn dev_button(
51        &mut self,
52        id: impl Into<String>,
53        text: impl Into<egui::WidgetText>,
54    ) -> egui::Response;
55
56    /// Add a selected-aware button with explicit metadata.
57    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    /// Add a link with an explicit id.
65    fn dev_link(
66        &mut self,
67        id: impl Into<String>,
68        text: impl Into<egui::WidgetText>,
69    ) -> egui::Response;
70
71    /// Add a hyperlink with an explicit id, using its URL as label and value metadata.
72    fn dev_hyperlink(&mut self, id: impl Into<String>, url: impl ToString) -> egui::Response;
73
74    /// Add a hyperlink with an explicit id, label, and URL metadata.
75    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    /// Add an image with an explicit id and developer-provided description.
83    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    /// Add a label with an explicit id.
91    fn dev_label(
92        &mut self,
93        id: impl Into<String>,
94        text: impl Into<egui::WidgetText>,
95    ) -> egui::Response;
96
97    /// Add a checkbox with an explicit id.
98    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    /// Add an indeterminate-aware checkbox with explicit metadata.
106    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    /// Add a text edit with an explicit id.
115    fn dev_text_edit(&mut self, id: impl Into<String>, text: &mut String) -> egui::Response;
116
117    /// Add a text edit with explicit mode metadata.
118    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    /// Add a slider with an explicit id.
126    fn dev_slider(
127        &mut self,
128        id: impl Into<String>,
129        value: &mut f32,
130        range: RangeInclusive<f32>,
131    ) -> egui::Response;
132
133    /// Add a combo box with an explicit id.
134    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    /// Add a float drag value with an explicit id.
143    fn dev_drag_value(&mut self, id: impl Into<String>, value: &mut f32) -> egui::Response;
144
145    /// Add a float drag value with an explicit constrained range.
146    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    /// Add an integer drag value with an explicit id.
154    fn dev_drag_value_i32(&mut self, id: impl Into<String>, value: &mut i32) -> egui::Response;
155
156    /// Add an integer drag value with an explicit constrained range.
157    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    /// Add a multiline text edit with an explicit id.
165    fn dev_text_edit_multiline(
166        &mut self,
167        id: impl Into<String>,
168        text: &mut String,
169    ) -> egui::Response;
170
171    /// Add a toggle value with an explicit id.
172    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    /// Add a radio value with an explicit id.
180    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    /// Add a selectable value with an explicit id.
189    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    /// Add a separator with an explicit id.
198    fn dev_separator(&mut self, id: impl Into<String>) -> egui::Response;
199
200    /// Add a spinner with an explicit id.
201    fn dev_spinner(&mut self, id: impl Into<String>) -> egui::Response;
202
203    /// Add a progress bar with an explicit id.
204    fn dev_progress_bar(&mut self, id: impl Into<String>, progress: f32) -> egui::Response;
205
206    /// Add a progress bar with explicit overlay text options.
207    fn dev_progress_bar_with(
208        &mut self,
209        id: impl Into<String>,
210        progress: f32,
211        options: ProgressBarOptions,
212    ) -> egui::Response;
213
214    /// Add a color-edit button with an explicit id.
215    fn dev_color_edit(
216        &mut self,
217        id: impl Into<String>,
218        color: &mut egui::Color32,
219    ) -> egui::Response;
220
221    /// Add a menu button with an explicit id.
222    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    /// Add a collapsing header with an explicit id, bound to app state.
230    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
826/// Helper extensions for recording scroll areas with explicit ids.
827pub trait DevScrollAreaExt {
828    /// Show a scroll area and record it with DevMCP metadata.
829    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    /// Show a scroll area with viewport access and record it with DevMCP metadata.
837    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
871/// Consume a queued widget value override for a custom instrumented widget.
872pub 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
970/// Record a standard widget with consistent metadata, visibility, and active context lookup.
971fn 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
1017/// Parse a CSS-style `#RRGGBB` or `#RRGGBBAA` color literal.
1018pub 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}