egui/data/
output.rs

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