egui/data/
output.rs

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