egui/data/
output.rs

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