Skip to main content

egui/data/
output.rs

1//! All the data egui returns to the backend at the end of each frame.
2
3use core::ops::Range;
4
5use epaint::text::CharIndex;
6
7use crate::{OrderedViewportIdMap, RepaintCause, ViewportOutput, WidgetType};
8
9/// What egui emits each frame from [`crate::Context::run_ui`].
10///
11/// The backend should use this.
12#[derive(Clone, Default)]
13pub struct FullOutput {
14    /// Non-rendering related output.
15    pub platform_output: PlatformOutput,
16
17    /// Texture changes since last frame (including the font texture).
18    ///
19    /// The backend needs to apply [`crate::TexturesDelta::push`] _before_ painting,
20    /// and free any texture in [`crate::TexturesDelta::free`] _after_ painting.
21    ///
22    /// It is assumed that all egui viewports share the same painter and texture namespace.
23    pub textures_delta: epaint::textures::TexturesDelta,
24
25    /// What to paint.
26    ///
27    /// You can use [`crate::Context::tessellate`] to turn this into triangles.
28    pub shapes: Vec<epaint::ClippedShape>,
29
30    /// The number of physical pixels per logical ui point, for the viewport that was updated.
31    ///
32    /// You can pass this to [`crate::Context::tessellate`] together with [`Self::shapes`].
33    pub pixels_per_point: f32,
34
35    /// All the active viewports, including the root.
36    ///
37    /// It is up to the integration to spawn a native window for each viewport,
38    /// and to close any window that no longer has a viewport in this map.
39    pub viewport_output: OrderedViewportIdMap<ViewportOutput>,
40}
41
42impl FullOutput {
43    /// Add on new output.
44    pub fn append(&mut self, newer: Self) {
45        use std::collections::btree_map::Entry;
46
47        let Self {
48            platform_output,
49            textures_delta,
50            shapes,
51            pixels_per_point,
52            viewport_output,
53        } = newer;
54
55        self.platform_output.append(platform_output);
56        self.textures_delta.append(textures_delta);
57        self.shapes = shapes; // Only paint the latest
58        self.pixels_per_point = pixels_per_point; // Use latest
59
60        for (id, new_viewport) in viewport_output {
61            match self.viewport_output.entry(id) {
62                Entry::Vacant(entry) => {
63                    entry.insert(new_viewport);
64                }
65                Entry::Occupied(mut entry) => {
66                    entry.get_mut().append(new_viewport);
67                }
68            }
69        }
70    }
71
72    /// [`epaint::textures::TexturesDelta`] will panic when dropped with still unapplied deltas,
73    /// this is a helper to clear the deltas.
74    pub fn drop_without_applying_deltas(mut self) {
75        self.textures_delta.clear();
76    }
77}
78
79/// What egui emits from [`crate::Context::run_logic`], i.e. from a tick where no ui was shown.
80///
81/// There is nothing to paint, but the app may still have asked the integration to do things,
82/// e.g. to show a hidden window again with [`crate::ViewportCommand::Focus`].
83#[derive(Clone, Default)]
84pub struct LogicOutput {
85    /// Non-rendering related output.
86    pub platform_output: PlatformOutput,
87
88    /// The commands sent with [`crate::Context::send_viewport_cmd`] and friends.
89    ///
90    /// Note that this contains no information about which viewports exist:
91    /// the integration should leave its viewports as they are.
92    pub viewport_commands: OrderedViewportIdMap<Vec<crate::ViewportCommand>>,
93}
94
95/// Information about text being edited.
96///
97/// Useful for IME.
98#[derive(Copy, Clone, Debug, PartialEq, Eq)]
99#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
100pub struct IMEOutput {
101    /// IME's purpose.
102    pub purpose: crate::IMEPurpose,
103
104    /// Where the [`crate::TextEdit`] is located on screen.
105    pub rect: crate::Rect,
106
107    /// Where the primary cursor is.
108    ///
109    /// This is a very thin rectangle.
110    pub cursor_rect: crate::Rect,
111
112    /// Whether any ongoing IME composition should be interrupted.
113    pub should_interrupt_composition: bool,
114}
115
116/// Commands that the egui integration should execute at the end of a frame.
117///
118/// Commands that are specific to a viewport should be put in [`crate::ViewportCommand`] instead.
119#[derive(Clone, Debug, PartialEq, Eq)]
120#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
121pub enum OutputCommand {
122    /// Put this text to the system clipboard.
123    ///
124    /// This is often a response to [`crate::Event::Copy`] or [`crate::Event::Cut`].
125    CopyText(String),
126
127    /// Put this image to the system clipboard.
128    CopyImage(crate::ColorImage),
129
130    /// Open this url in a browser.
131    OpenUrl(OpenUrl),
132}
133
134/// The non-rendering part of what egui emits each frame.
135///
136/// You can access (and modify) this with [`crate::Context::output`].
137///
138/// The backend should use this.
139#[derive(Default, Clone, PartialEq)]
140#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
141pub struct PlatformOutput {
142    /// Commands that the egui integration should execute at the end of a frame.
143    pub commands: Vec<OutputCommand>,
144
145    /// Set the cursor to this icon.
146    pub cursor_icon: CursorIcon,
147
148    /// If set, the integration should display this RGBA image as the OS
149    /// cursor (via e.g. `winit::window::CustomCursor`) instead of the
150    /// standard `cursor_icon`. Set per frame; integrations that don't
151    /// support custom cursors fall back to `cursor_icon`.
152    ///
153    /// Skipped from serde because the bitmap is ephemeral and shouldn't
154    /// roundtrip through persisted state.
155    #[cfg_attr(feature = "serde", serde(skip))]
156    pub cursor_image: Option<CustomCursorImage>,
157
158    /// Events that may be useful to e.g. a screen reader.
159    pub events: Vec<OutputEvent>,
160
161    /// Is there a mutable [`TextEdit`](crate::TextEdit) under the cursor?
162    /// Use by `eframe` web to show/hide mobile keyboard and IME agent.
163    pub mutable_text_under_cursor: bool,
164
165    /// This is set if, and only if, the user is currently editing text.
166    ///
167    /// Useful for IME.
168    ///
169    /// This field should only be set by the widget that currently owns IME
170    /// events (see [`crate::Memory::owns_ime_events`]).
171    pub ime: Option<IMEOutput>,
172
173    /// The difference in the widget tree since last frame.
174    ///
175    /// NOTE: this needs to be per-viewport.
176    pub accesskit_update: Option<accesskit::TreeUpdate>,
177
178    /// How many ui passes is this the sum of?
179    ///
180    /// See [`crate::Context::request_discard`] for details.
181    ///
182    /// This is incremented at the END of each frame,
183    /// so this will be `0` for the first pass.
184    pub num_completed_passes: usize,
185
186    /// Was [`crate::Context::request_discard`] called during the latest pass?
187    ///
188    /// If so, what was the reason(s) for it?
189    ///
190    /// If empty, there was never any calls.
191    #[cfg_attr(feature = "serde", serde(skip))]
192    pub request_discard_reasons: Vec<RepaintCause>,
193}
194
195impl PlatformOutput {
196    /// This can be used by a text-to-speech system to describe the events (if any).
197    pub fn events_description(&self) -> String {
198        // only describe last event:
199        if let Some(event) = self.events.iter().next_back() {
200            match event {
201                OutputEvent::Clicked(widget_info)
202                | OutputEvent::DoubleClicked(widget_info)
203                | OutputEvent::TripleClicked(widget_info)
204                | OutputEvent::FocusGained(widget_info)
205                | OutputEvent::TextSelectionChanged(widget_info)
206                | OutputEvent::ValueChanged(widget_info) => {
207                    return widget_info.description();
208                }
209            }
210        }
211        Default::default()
212    }
213
214    /// Add on new output.
215    pub fn append(&mut self, newer: Self) {
216        let Self {
217            mut commands,
218            cursor_icon,
219            cursor_image,
220            mut events,
221            mutable_text_under_cursor,
222            ime,
223            accesskit_update,
224            num_completed_passes,
225            mut request_discard_reasons,
226        } = newer;
227
228        self.commands.append(&mut commands);
229        self.cursor_icon = cursor_icon;
230        self.cursor_image = cursor_image;
231        self.events.append(&mut events);
232        self.mutable_text_under_cursor = mutable_text_under_cursor;
233        self.ime = ime.or(self.ime);
234        self.num_completed_passes += num_completed_passes;
235        self.request_discard_reasons
236            .append(&mut request_discard_reasons);
237
238        // egui produces a complete AccessKit tree for each frame, so overwrite rather than append:
239        self.accesskit_update = accesskit_update;
240    }
241
242    /// Take everything ephemeral (everything except `cursor_icon` and
243    /// `cursor_image` currently)
244    pub fn take(&mut self) -> Self {
245        let taken = core::mem::take(self);
246        self.cursor_icon = taken.cursor_icon; // sticky between frames
247        self.cursor_image = taken.cursor_image.clone(); // sticky between frames
248        taken
249    }
250
251    /// Was [`crate::Context::request_discard`] called?
252    pub fn requested_discard(&self) -> bool {
253        !self.request_discard_reasons.is_empty()
254    }
255}
256
257/// What URL to open, and how.
258///
259/// Use with [`crate::Context::open_url`].
260#[derive(Clone, Debug, PartialEq, Eq)]
261#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
262pub struct OpenUrl {
263    pub url: String,
264
265    /// If `true`, open the url in a new tab.
266    /// If `false` open it in the same tab.
267    /// Only matters when in a web browser.
268    pub new_tab: bool,
269}
270
271impl OpenUrl {
272    #[expect(clippy::needless_pass_by_value)]
273    pub fn same_tab(url: impl ToString) -> Self {
274        Self {
275            url: url.to_string(),
276            new_tab: false,
277        }
278    }
279
280    #[expect(clippy::needless_pass_by_value)]
281    pub fn new_tab(url: impl ToString) -> Self {
282        Self {
283            url: url.to_string(),
284            new_tab: true,
285        }
286    }
287}
288
289/// Types of attention to request from a user when a native window is not in focus.
290///
291/// See [winit's documentation][user_attention_type] for platform-specific meaning of the attention types.
292///
293/// [user_attention_type]: https://docs.rs/winit/latest/winit/window/enum.UserAttentionType.html
294#[derive(Clone, Copy, Debug, PartialEq, Eq)]
295#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
296pub enum UserAttentionType {
297    /// Request an elevated amount of animations and flair for the window and the task bar or dock icon.
298    Critical,
299
300    /// Request a standard amount of attention-grabbing actions.
301    Informational,
302
303    /// Reset the attention request and interrupt related animations and flashes.
304    Reset,
305}
306
307/// A bitmap cursor pushed to the integration via [`PlatformOutput::cursor_image`].
308///
309/// The integration is expected to upload this to the OS as a real cursor
310/// (so the image is not clipped by the egui window — what `egui::Painter`
311/// drawn cursors suffer from). Backends that don't support it should fall
312/// back to [`PlatformOutput::cursor_icon`].
313///
314/// `rgba` is straight (non-premultiplied) RGBA — same encoding as
315/// `winit::window::CustomCursor::from_rgba`. The buffer length must be
316/// exactly `size[0] * size[1] * 4` bytes. `size` and `hotspot` use
317/// `u16` to match winit's native types and avoid a lossy cast in the
318/// integration layer.
319///
320/// `Arc<[u8]>` is used so integrations can dedupe / cache by pointer
321/// identity (`Arc::ptr_eq`) and avoid re-uploading the same bitmap to
322/// the OS every frame.
323#[derive(Clone, PartialEq, Eq)]
324pub struct CustomCursorImage {
325    pub rgba: std::sync::Arc<[u8]>,
326    pub size: [u16; 2],
327    pub hotspot: [u16; 2],
328}
329
330impl core::fmt::Debug for CustomCursorImage {
331    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
332        f.debug_struct("CustomCursorImage")
333            .field("size", &self.size)
334            .field("hotspot", &self.hotspot)
335            .field("rgba_len", &self.rgba.len())
336            .finish()
337    }
338}
339
340/// A mouse cursor icon.
341///
342/// egui emits a [`CursorIcon`] in [`PlatformOutput`] each frame as a request to the integration.
343///
344/// Loosely based on <https://developer.mozilla.org/en-US/docs/Web/CSS/cursor>.
345#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
346#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
347pub enum CursorIcon {
348    /// Normal cursor icon, whatever that is.
349    #[default]
350    Default,
351
352    /// Show no cursor
353    None,
354
355    // ------------------------------------
356    // Links and status:
357    /// A context menu is available
358    ContextMenu,
359
360    /// Question mark
361    Help,
362
363    /// Pointing hand, used for e.g. web links
364    PointingHand,
365
366    /// Shows that processing is being done, but that the program is still interactive.
367    Progress,
368
369    /// Not yet ready, try later.
370    Wait,
371
372    // ------------------------------------
373    // Selection:
374    /// Hover a cell in a table
375    Cell,
376
377    /// For precision work
378    Crosshair,
379
380    /// Text caret, e.g. "Click here to edit text"
381    Text,
382
383    /// Vertical text caret, e.g. "Click here to edit vertical text"
384    VerticalText,
385
386    // ------------------------------------
387    // Drag-and-drop:
388    /// Indicated an alias, e.g. a shortcut
389    Alias,
390
391    /// Indicate that a copy will be made
392    Copy,
393
394    /// Omnidirectional move icon (e.g. arrows in all cardinal directions)
395    Move,
396
397    /// Can't drop here
398    NoDrop,
399
400    /// Forbidden
401    NotAllowed,
402
403    /// The thing you are hovering can be grabbed
404    Grab,
405
406    /// You are grabbing the thing you are hovering
407    Grabbing,
408
409    // ------------------------------------
410    /// Something can be scrolled in any direction (panned).
411    AllScroll,
412
413    // ------------------------------------
414    // Resizing in two directions:
415    /// Horizontal resize `-` to make something wider or more narrow (left to/from right)
416    ResizeHorizontal,
417
418    /// Diagonal resize `/` (right-up to/from left-down)
419    ResizeNeSw,
420
421    /// Diagonal resize `\` (left-up to/from right-down)
422    ResizeNwSe,
423
424    /// Vertical resize `|` (up-down or down-up)
425    ResizeVertical,
426
427    // ------------------------------------
428    // Resizing in one direction:
429    /// Resize something rightwards (e.g. when dragging the right-most edge of something)
430    ResizeEast,
431
432    /// Resize something down and right (e.g. when dragging the bottom-right corner of something)
433    ResizeSouthEast,
434
435    /// Resize something downwards (e.g. when dragging the bottom edge of something)
436    ResizeSouth,
437
438    /// Resize something down and left (e.g. when dragging the bottom-left corner of something)
439    ResizeSouthWest,
440
441    /// Resize something leftwards (e.g. when dragging the left edge of something)
442    ResizeWest,
443
444    /// Resize something up and left (e.g. when dragging the top-left corner of something)
445    ResizeNorthWest,
446
447    /// Resize something up (e.g. when dragging the top edge of something)
448    ResizeNorth,
449
450    /// Resize something up and right (e.g. when dragging the top-right corner of something)
451    ResizeNorthEast,
452
453    // ------------------------------------
454    /// Resize a column
455    ResizeColumn,
456
457    /// Resize a row
458    ResizeRow,
459
460    // ------------------------------------
461    // Zooming:
462    /// Enhance!
463    ZoomIn,
464
465    /// Let's get a better overview
466    ZoomOut,
467}
468
469impl CursorIcon {
470    pub const ALL: [Self; 35] = [
471        Self::Default,
472        Self::None,
473        Self::ContextMenu,
474        Self::Help,
475        Self::PointingHand,
476        Self::Progress,
477        Self::Wait,
478        Self::Cell,
479        Self::Crosshair,
480        Self::Text,
481        Self::VerticalText,
482        Self::Alias,
483        Self::Copy,
484        Self::Move,
485        Self::NoDrop,
486        Self::NotAllowed,
487        Self::Grab,
488        Self::Grabbing,
489        Self::AllScroll,
490        Self::ResizeHorizontal,
491        Self::ResizeNeSw,
492        Self::ResizeNwSe,
493        Self::ResizeVertical,
494        Self::ResizeEast,
495        Self::ResizeSouthEast,
496        Self::ResizeSouth,
497        Self::ResizeSouthWest,
498        Self::ResizeWest,
499        Self::ResizeNorthWest,
500        Self::ResizeNorth,
501        Self::ResizeNorthEast,
502        Self::ResizeColumn,
503        Self::ResizeRow,
504        Self::ZoomIn,
505        Self::ZoomOut,
506    ];
507}
508
509/// Things that happened during this frame that the integration may be interested in.
510///
511/// In particular, these events may be useful for accessibility, i.e. for screen readers.
512#[derive(Clone, PartialEq)]
513#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
514pub enum OutputEvent {
515    /// A widget was clicked.
516    Clicked(WidgetInfo),
517
518    /// A widget was double-clicked.
519    DoubleClicked(WidgetInfo),
520
521    /// A widget was triple-clicked.
522    TripleClicked(WidgetInfo),
523
524    /// A widget gained keyboard focus (by tab key).
525    FocusGained(WidgetInfo),
526
527    /// Text selection was updated.
528    TextSelectionChanged(WidgetInfo),
529
530    /// A widget's value changed.
531    ValueChanged(WidgetInfo),
532}
533
534impl OutputEvent {
535    pub fn widget_info(&self) -> &WidgetInfo {
536        match self {
537            Self::Clicked(info)
538            | Self::DoubleClicked(info)
539            | Self::TripleClicked(info)
540            | Self::FocusGained(info)
541            | Self::TextSelectionChanged(info)
542            | Self::ValueChanged(info) => info,
543        }
544    }
545}
546
547impl core::fmt::Debug for OutputEvent {
548    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
549        match self {
550            Self::Clicked(wi) => write!(f, "Clicked({wi:?})"),
551            Self::DoubleClicked(wi) => write!(f, "DoubleClicked({wi:?})"),
552            Self::TripleClicked(wi) => write!(f, "TripleClicked({wi:?})"),
553            Self::FocusGained(wi) => write!(f, "FocusGained({wi:?})"),
554            Self::TextSelectionChanged(wi) => write!(f, "TextSelectionChanged({wi:?})"),
555            Self::ValueChanged(wi) => write!(f, "ValueChanged({wi:?})"),
556        }
557    }
558}
559
560/// Describes a widget such as a [`crate::Button`] or a [`crate::TextEdit`].
561#[derive(Clone, PartialEq)]
562#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
563pub struct WidgetInfo {
564    /// The type of widget this is.
565    pub typ: WidgetType,
566
567    /// Whether the widget is enabled.
568    pub enabled: bool,
569
570    /// The text on labels, buttons, checkboxes etc.
571    pub label: Option<String>,
572
573    /// The contents of some editable text (for [`TextEdit`](crate::TextEdit) fields).
574    pub current_text_value: Option<String>,
575
576    /// The previous text value.
577    pub prev_text_value: Option<String>,
578
579    /// The current value of checkboxes and radio buttons.
580    pub selected: Option<bool>,
581
582    /// The current value of sliders etc.
583    pub value: Option<f64>,
584
585    /// Selected range of characters in [`Self::current_text_value`].
586    ///
587    /// The range is `start..end` in *character* offsets (not bytes), with `end` exclusive.
588    pub text_selection: Option<Range<CharIndex>>,
589
590    /// The hint text for text edit fields.
591    pub hint_text: Option<String>,
592}
593
594impl core::fmt::Debug for WidgetInfo {
595    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
596        let Self {
597            typ,
598            enabled,
599            label,
600            current_text_value: text_value,
601            prev_text_value,
602            selected,
603            value,
604            text_selection,
605            hint_text,
606        } = self;
607
608        let mut s = f.debug_struct("WidgetInfo");
609
610        s.field("typ", typ);
611
612        if !enabled {
613            s.field("enabled", enabled);
614        }
615
616        if let Some(label) = label {
617            s.field("label", label);
618        }
619        if let Some(text_value) = text_value {
620            s.field("text_value", text_value);
621        }
622        if let Some(prev_text_value) = prev_text_value {
623            s.field("prev_text_value", prev_text_value);
624        }
625        if let Some(selected) = selected {
626            s.field("selected", selected);
627        }
628        if let Some(value) = value {
629            s.field("value", value);
630        }
631        if let Some(text_selection) = text_selection {
632            s.field("text_selection", text_selection);
633        }
634        if let Some(hint_text) = hint_text {
635            s.field("hint_text", hint_text);
636        }
637
638        s.finish()
639    }
640}
641
642impl WidgetInfo {
643    pub fn new(typ: WidgetType) -> Self {
644        Self {
645            typ,
646            enabled: true,
647            label: None,
648            current_text_value: None,
649            prev_text_value: None,
650            selected: None,
651            value: None,
652            text_selection: None,
653            hint_text: None,
654        }
655    }
656
657    #[expect(clippy::needless_pass_by_value)]
658    pub fn labeled(typ: WidgetType, enabled: bool, label: impl ToString) -> Self {
659        Self {
660            enabled,
661            label: Some(label.to_string()),
662            ..Self::new(typ)
663        }
664    }
665
666    /// checkboxes, radio-buttons etc
667    #[expect(clippy::needless_pass_by_value)]
668    pub fn selected(typ: WidgetType, enabled: bool, selected: bool, label: impl ToString) -> Self {
669        Self {
670            enabled,
671            label: Some(label.to_string()),
672            selected: Some(selected),
673            ..Self::new(typ)
674        }
675    }
676
677    pub fn drag_value(enabled: bool, value: f64) -> Self {
678        Self {
679            enabled,
680            value: Some(value),
681            ..Self::new(WidgetType::DragValue)
682        }
683    }
684
685    #[expect(clippy::needless_pass_by_value)]
686    pub fn slider(enabled: bool, value: f64, label: impl ToString) -> Self {
687        let label = label.to_string();
688        Self {
689            enabled,
690            label: if label.is_empty() { None } else { Some(label) },
691            value: Some(value),
692            ..Self::new(WidgetType::Slider)
693        }
694    }
695
696    #[expect(clippy::needless_pass_by_value)]
697    pub fn text_edit(
698        enabled: bool,
699        prev_text_value: impl ToString,
700        text_value: impl ToString,
701        hint_text: impl ToString,
702    ) -> Self {
703        let text_value = text_value.to_string();
704        let prev_text_value = prev_text_value.to_string();
705        let hint_text = hint_text.to_string();
706        let prev_text_value = if text_value == prev_text_value {
707            None
708        } else {
709            Some(prev_text_value)
710        };
711        Self {
712            enabled,
713            current_text_value: Some(text_value),
714            prev_text_value,
715            hint_text: Some(hint_text),
716            ..Self::new(WidgetType::TextEdit)
717        }
718    }
719
720    #[expect(clippy::needless_pass_by_value)]
721    pub fn text_selection_changed(
722        enabled: bool,
723        text_selection: Range<CharIndex>,
724        current_text_value: impl ToString,
725    ) -> Self {
726        Self {
727            enabled,
728            text_selection: Some(text_selection),
729            current_text_value: Some(current_text_value.to_string()),
730            ..Self::new(WidgetType::TextEdit)
731        }
732    }
733
734    /// This can be used by a text-to-speech system to describe the widget.
735    pub fn description(&self) -> String {
736        let Self {
737            typ,
738            enabled,
739            label,
740            current_text_value: text_value,
741            prev_text_value: _,
742            selected,
743            value,
744            text_selection: _,
745            hint_text: _,
746        } = self;
747
748        // TODO(emilk): localization
749        let widget_type = match typ {
750            WidgetType::Link => "link",
751            WidgetType::TextEdit => "text edit",
752            WidgetType::Button => "button",
753            WidgetType::Checkbox => "checkbox",
754            WidgetType::RadioButton => "radio",
755            WidgetType::RadioGroup => "radio group",
756            WidgetType::SelectableLabel => "selectable",
757            WidgetType::ComboBox => "combo",
758            WidgetType::Slider => "slider",
759            WidgetType::DragValue => "drag value",
760            WidgetType::ColorButton => "color button",
761            WidgetType::Image => "image",
762            WidgetType::CollapsingHeader => "collapsing header",
763            WidgetType::Panel => "panel",
764            WidgetType::ProgressIndicator => "progress indicator",
765            WidgetType::Window => "window",
766            WidgetType::ScrollBar => "scroll bar",
767            WidgetType::ResizeHandle => "resize handle",
768            WidgetType::Label | WidgetType::Other => "",
769        };
770
771        let mut description = widget_type.to_owned();
772
773        if let Some(selected) = selected {
774            if *typ == WidgetType::Checkbox {
775                let state = if *selected { "checked" } else { "unchecked" };
776                description = format!("{state} {description}");
777            } else {
778                description += if *selected { "selected" } else { "" };
779            }
780        }
781
782        if let Some(label) = label {
783            description = format!("{label}: {description}");
784        }
785
786        if typ == &WidgetType::TextEdit {
787            let text = if let Some(text_value) = text_value {
788                if text_value.is_empty() {
789                    "blank".into()
790                } else {
791                    text_value.clone()
792                }
793            } else {
794                "blank".into()
795            };
796            description = format!("{text}: {description}");
797        }
798
799        if let Some(value) = value {
800            description += " ";
801            description += &value.to_string();
802        }
803
804        if !enabled {
805            description += ": disabled";
806        }
807        description.trim().to_owned()
808    }
809}