Skip to main content

egui/
pass_state.rs

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