egui/
pass_state.rs

1use ahash::HashMap;
2
3use crate::{Align, Id, IdMap, LayerId, Rangef, Rect, Vec2, WidgetRects, id::IdSet, style};
4
5#[cfg(debug_assertions)]
6use crate::{Align2, Color32, FontId, NumExt as _, Painter, pos2};
7
8/// Reset at the start of each frame.
9#[derive(Clone, Debug, Default)]
10pub struct TooltipPassState {
11    /// If a tooltip has been shown this frame, where was it?
12    /// This is used to prevent multiple tooltips to cover each other.
13    pub widget_tooltips: IdMap<PerWidgetTooltipState>,
14}
15
16impl TooltipPassState {
17    pub fn clear(&mut self) {
18        let Self { widget_tooltips } = self;
19        widget_tooltips.clear();
20    }
21}
22
23#[derive(Clone, Copy, Debug)]
24pub struct PerWidgetTooltipState {
25    /// Bounding rectangle for all widget and all previous tooltips.
26    pub bounding_rect: Rect,
27
28    /// How many tooltips have been shown for this widget this frame?
29    pub tooltip_count: usize,
30}
31
32#[derive(Clone, Debug, Default)]
33pub struct PerLayerState {
34    /// Is there any open popup (menus, combo-boxes, etc)?
35    ///
36    /// Does NOT include tooltips.
37    pub open_popups: IdSet,
38
39    /// Which widget is showing a tooltip (if any)?
40    ///
41    /// Only one widget per layer may show a tooltip.
42    /// But if a tooltip contains a tooltip, you can show a tooltip on top of a tooltip.
43    pub widget_with_tooltip: Option<Id>,
44}
45
46#[derive(Clone, Debug)]
47pub struct ScrollTarget {
48    // The range that the scroll area should scroll to.
49    pub range: Rangef,
50
51    /// How should we align the rect within the visible area?
52    /// If `align` is [`Align::TOP`] it means "put the top of the rect at the top of the scroll area", etc.
53    /// If `align` is `None`, it'll scroll enough to bring the UI into view.
54    pub align: Option<Align>,
55
56    /// How should the scroll be animated?
57    pub animation: style::ScrollAnimation,
58}
59
60impl ScrollTarget {
61    pub fn new(range: Rangef, align: Option<Align>, animation: style::ScrollAnimation) -> Self {
62        Self {
63            range,
64            align,
65            animation,
66        }
67    }
68}
69
70#[cfg(feature = "accesskit")]
71#[derive(Clone)]
72pub struct AccessKitPassState {
73    pub nodes: IdMap<accesskit::Node>,
74    pub parent_stack: Vec<Id>,
75}
76
77#[cfg(debug_assertions)]
78#[derive(Clone)]
79pub struct DebugRect {
80    pub rect: Rect,
81    pub callstack: String,
82    pub is_clicking: bool,
83}
84
85#[cfg(debug_assertions)]
86impl DebugRect {
87    pub fn paint(self, painter: &Painter) {
88        let Self {
89            rect,
90            callstack,
91            is_clicking,
92        } = self;
93
94        let ctx = painter.ctx();
95
96        // Paint rectangle around widget:
97        {
98            // Print width and height:
99            let text_color = if ctx.style().visuals.dark_mode {
100                Color32::WHITE
101            } else {
102                Color32::BLACK
103            };
104            painter.debug_text(
105                rect.left_center() + 2.0 * Vec2::LEFT,
106                Align2::RIGHT_CENTER,
107                text_color,
108                format!("H: {:.1}", rect.height()),
109            );
110            painter.debug_text(
111                rect.center_top(),
112                Align2::CENTER_BOTTOM,
113                text_color,
114                format!("W: {:.1}", rect.width()),
115            );
116
117            // Paint rect:
118            let rect_fg_color = if is_clicking {
119                Color32::WHITE
120            } else {
121                Color32::LIGHT_BLUE
122            };
123            let rect_bg_color = Color32::BLUE.gamma_multiply(0.5);
124            painter.rect(
125                rect,
126                0.0,
127                rect_bg_color,
128                (1.0, rect_fg_color),
129                crate::StrokeKind::Outside,
130            );
131        }
132
133        if !callstack.is_empty() {
134            let font_id = FontId::monospace(12.0);
135            let text = format!("{callstack}\n\n(click to copy)");
136            let text_color = Color32::WHITE;
137            let galley = painter.layout_no_wrap(text, font_id, text_color);
138
139            // Position the text either under or above:
140            let screen_rect = ctx.screen_rect();
141            let y = if galley.size().y <= rect.top() {
142                // Above
143                rect.top() - galley.size().y - 16.0
144            } else {
145                // Below
146                rect.bottom()
147            };
148
149            let y = y
150                .at_most(screen_rect.bottom() - galley.size().y)
151                .at_least(0.0);
152
153            let x = rect
154                .left()
155                .at_most(screen_rect.right() - galley.size().x)
156                .at_least(0.0);
157            let text_pos = pos2(x, y);
158
159            let text_bg_color = Color32::from_black_alpha(180);
160            let text_rect_stroke_color = if is_clicking {
161                Color32::WHITE
162            } else {
163                text_bg_color
164            };
165            let text_rect = Rect::from_min_size(text_pos, galley.size());
166            painter.rect(
167                text_rect,
168                0.0,
169                text_bg_color,
170                (1.0, text_rect_stroke_color),
171                crate::StrokeKind::Middle,
172            );
173            painter.galley(text_pos, galley, text_color);
174
175            if is_clicking {
176                ctx.copy_text(callstack);
177            }
178        }
179    }
180}
181
182/// State that is collected during a pass, then saved for the next pass,
183/// and then cleared.
184///
185/// (NOTE: we usually run only one pass per frame).
186///
187/// One per viewport.
188#[derive(Clone)]
189pub struct PassState {
190    /// All [`Id`]s that were used this pass.
191    pub used_ids: IdMap<Rect>,
192
193    /// All widgets produced this pass.
194    pub widgets: WidgetRects,
195
196    /// Per-layer state.
197    ///
198    /// Not all layers registers themselves there though.
199    pub layers: HashMap<LayerId, PerLayerState>,
200
201    pub tooltips: TooltipPassState,
202
203    /// Starts off as the `screen_rect`, shrinks as panels are added.
204    /// The [`crate::CentralPanel`] does not change this.
205    pub available_rect: Rect,
206
207    /// Starts off as the `screen_rect`, shrinks as panels are added.
208    /// The [`crate::CentralPanel`] retracts from this.
209    pub unused_rect: Rect,
210
211    /// How much space is used by panels.
212    pub used_by_panels: Rect,
213
214    /// The current scroll area should scroll to this range (horizontal, vertical).
215    pub scroll_target: [Option<ScrollTarget>; 2],
216
217    /// The current scroll area should scroll by this much.
218    ///
219    /// The delta dictates how the _content_ should move.
220    ///
221    /// A positive X-value indicates the content is being moved right,
222    /// as when swiping right on a touch-screen or track-pad with natural scrolling.
223    ///
224    /// A positive Y-value indicates the content is being moved down,
225    /// as when swiping down on a touch-screen or track-pad with natural scrolling.
226    pub scroll_delta: (Vec2, style::ScrollAnimation),
227
228    #[cfg(feature = "accesskit")]
229    pub accesskit_state: Option<AccessKitPassState>,
230
231    /// Highlight these widgets the next pass.
232    pub highlight_next_pass: IdSet,
233
234    #[cfg(debug_assertions)]
235    pub debug_rect: Option<DebugRect>,
236}
237
238impl Default for PassState {
239    fn default() -> Self {
240        Self {
241            used_ids: Default::default(),
242            widgets: Default::default(),
243            layers: Default::default(),
244            tooltips: Default::default(),
245            available_rect: Rect::NAN,
246            unused_rect: Rect::NAN,
247            used_by_panels: Rect::NAN,
248            scroll_target: [None, None],
249            scroll_delta: (Vec2::default(), style::ScrollAnimation::none()),
250            #[cfg(feature = "accesskit")]
251            accesskit_state: None,
252            highlight_next_pass: Default::default(),
253
254            #[cfg(debug_assertions)]
255            debug_rect: None,
256        }
257    }
258}
259
260impl PassState {
261    pub(crate) fn begin_pass(&mut self, screen_rect: Rect) {
262        profiling::function_scope!();
263        let Self {
264            used_ids,
265            widgets,
266            tooltips,
267            layers,
268            available_rect,
269            unused_rect,
270            used_by_panels,
271            scroll_target,
272            scroll_delta,
273            #[cfg(feature = "accesskit")]
274            accesskit_state,
275            highlight_next_pass,
276
277            #[cfg(debug_assertions)]
278            debug_rect,
279        } = self;
280
281        used_ids.clear();
282        widgets.clear();
283        tooltips.clear();
284        layers.clear();
285        *available_rect = screen_rect;
286        *unused_rect = screen_rect;
287        *used_by_panels = Rect::NOTHING;
288        *scroll_target = [None, None];
289        *scroll_delta = Default::default();
290
291        #[cfg(debug_assertions)]
292        {
293            *debug_rect = None;
294        }
295
296        #[cfg(feature = "accesskit")]
297        {
298            *accesskit_state = None;
299        }
300
301        highlight_next_pass.clear();
302    }
303
304    /// How much space is still available after panels has been added.
305    pub(crate) fn available_rect(&self) -> Rect {
306        debug_assert!(
307            self.available_rect.is_finite(),
308            "Called `available_rect()` before `Context::run()`"
309        );
310        self.available_rect
311    }
312
313    /// Shrink `available_rect`.
314    pub(crate) fn allocate_left_panel(&mut self, panel_rect: Rect) {
315        debug_assert!(
316            panel_rect.min.distance(self.available_rect.min) < 0.1,
317            "Mismatching left panel. You must not create a panel from within another panel."
318        );
319        self.available_rect.min.x = panel_rect.max.x;
320        self.unused_rect.min.x = panel_rect.max.x;
321        self.used_by_panels |= panel_rect;
322    }
323
324    /// Shrink `available_rect`.
325    pub(crate) fn allocate_right_panel(&mut self, panel_rect: Rect) {
326        debug_assert!(
327            panel_rect.max.distance(self.available_rect.max) < 0.1,
328            "Mismatching right panel. You must not create a panel from within another panel."
329        );
330        self.available_rect.max.x = panel_rect.min.x;
331        self.unused_rect.max.x = panel_rect.min.x;
332        self.used_by_panels |= panel_rect;
333    }
334
335    /// Shrink `available_rect`.
336    pub(crate) fn allocate_top_panel(&mut self, panel_rect: Rect) {
337        debug_assert!(
338            panel_rect.min.distance(self.available_rect.min) < 0.1,
339            "Mismatching top panel. You must not create a panel from within another panel."
340        );
341        self.available_rect.min.y = panel_rect.max.y;
342        self.unused_rect.min.y = panel_rect.max.y;
343        self.used_by_panels |= panel_rect;
344    }
345
346    /// Shrink `available_rect`.
347    pub(crate) fn allocate_bottom_panel(&mut self, panel_rect: Rect) {
348        debug_assert!(
349            panel_rect.max.distance(self.available_rect.max) < 0.1,
350            "Mismatching bottom panel. You must not create a panel from within another panel."
351        );
352        self.available_rect.max.y = panel_rect.min.y;
353        self.unused_rect.max.y = panel_rect.min.y;
354        self.used_by_panels |= panel_rect;
355    }
356
357    pub(crate) fn allocate_central_panel(&mut self, panel_rect: Rect) {
358        // Note: we do not shrink `available_rect`, because
359        // we allow windows to cover the CentralPanel.
360        self.unused_rect = Rect::NOTHING; // Nothing left unused after this
361        self.used_by_panels |= panel_rect;
362    }
363}