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#[derive(Clone)]
71pub struct AccessKitPassState {
72    pub nodes: IdMap<accesskit::Node>,
73    pub parent_map: IdMap<Id>,
74}
75
76#[cfg(debug_assertions)]
77#[derive(Clone)]
78pub struct DebugRect {
79    pub rect: Rect,
80    pub callstack: String,
81    pub is_clicking: bool,
82}
83
84#[cfg(debug_assertions)]
85impl DebugRect {
86    pub fn paint(self, painter: &Painter) {
87        let Self {
88            rect,
89            callstack,
90            is_clicking,
91        } = self;
92
93        let ctx = painter.ctx();
94
95        // Paint rectangle around widget:
96        {
97            // Print width and height:
98            let text_color = if ctx.global_style().visuals.dark_mode {
99                Color32::WHITE
100            } else {
101                Color32::BLACK
102            };
103            painter.debug_text(
104                rect.left_center() + 2.0 * Vec2::LEFT,
105                Align2::RIGHT_CENTER,
106                text_color,
107                format!("H: {:.1}", rect.height()),
108            );
109            painter.debug_text(
110                rect.center_top(),
111                Align2::CENTER_BOTTOM,
112                text_color,
113                format!("W: {:.1}", rect.width()),
114            );
115
116            // Paint rect:
117            let rect_fg_color = if is_clicking {
118                Color32::WHITE
119            } else {
120                Color32::LIGHT_BLUE
121            };
122            let rect_bg_color = Color32::BLUE.gamma_multiply(0.5);
123            painter.rect(
124                rect,
125                0.0,
126                rect_bg_color,
127                (1.0, rect_fg_color),
128                crate::StrokeKind::Outside,
129            );
130        }
131
132        if !callstack.is_empty() {
133            let font_id = FontId::monospace(12.0);
134            let text = format!("{callstack}\n\n(click to copy)");
135            let text_color = Color32::WHITE;
136            let galley = painter.layout_no_wrap(text, font_id, text_color);
137
138            // Position the text either under or above:
139            let content_rect = ctx.content_rect();
140            let y = if galley.size().y <= rect.top() {
141                // Above
142                rect.top() - galley.size().y - 16.0
143            } else {
144                // Below
145                rect.bottom()
146            };
147
148            let y = y
149                .at_most(content_rect.bottom() - galley.size().y)
150                .at_least(0.0);
151
152            let x = rect
153                .left()
154                .at_most(content_rect.right() - galley.size().x)
155                .at_least(0.0);
156            let text_pos = pos2(x, y);
157
158            let text_bg_color = Color32::from_black_alpha(180);
159            let text_rect_stroke_color = if is_clicking {
160                Color32::WHITE
161            } else {
162                text_bg_color
163            };
164            let text_rect = Rect::from_min_size(text_pos, galley.size());
165            painter.rect(
166                text_rect,
167                0.0,
168                text_bg_color,
169                (1.0, text_rect_stroke_color),
170                crate::StrokeKind::Middle,
171            );
172            painter.galley(text_pos, galley, text_color);
173
174            if is_clicking {
175                ctx.copy_text(callstack);
176            }
177        }
178    }
179}
180
181/// State that is collected during a pass, then saved for the next pass,
182/// and then cleared.
183///
184/// (NOTE: we usually run only one pass per frame).
185///
186/// One per viewport.
187#[derive(Clone)]
188pub struct PassState {
189    /// All [`Id`]s that were used this pass.
190    pub used_ids: IdMap<Rect>,
191
192    /// All widgets produced this pass.
193    pub widgets: WidgetRects,
194
195    /// Per-layer state.
196    ///
197    /// Not all layers registers themselves there though.
198    pub layers: HashMap<LayerId, PerLayerState>,
199
200    pub tooltips: TooltipPassState,
201
202    /// Starts off as the `screen_rect`, shrinks as panels are added.
203    /// The [`crate::CentralPanel`] does not change this.
204    pub available_rect: Rect,
205
206    /// Starts off as the `screen_rect`, shrinks as panels are added.
207    /// The [`crate::CentralPanel`] retracts from this.
208    pub unused_rect: Rect,
209
210    /// How much space is used by panels.
211    pub used_by_panels: Rect,
212
213    /// The current scroll area should scroll to this range (horizontal, vertical).
214    pub scroll_target: [Option<ScrollTarget>; 2],
215
216    /// The current scroll area should scroll by this much.
217    ///
218    /// The delta dictates how the _content_ should move.
219    ///
220    /// A positive X-value indicates the content is being moved right,
221    /// as when swiping right on a touch-screen or track-pad with natural scrolling.
222    ///
223    /// A positive Y-value indicates the content is being moved down,
224    /// as when swiping down on a touch-screen or track-pad with natural scrolling.
225    pub scroll_delta: (Vec2, style::ScrollAnimation),
226
227    pub accesskit_state: Option<AccessKitPassState>,
228
229    /// Highlight these widgets the next pass.
230    pub highlight_next_pass: IdSet,
231
232    #[cfg(debug_assertions)]
233    pub debug_rect: Option<DebugRect>,
234}
235
236impl Default for PassState {
237    fn default() -> Self {
238        Self {
239            used_ids: Default::default(),
240            widgets: Default::default(),
241            layers: Default::default(),
242            tooltips: Default::default(),
243            available_rect: Rect::NAN,
244            unused_rect: Rect::NAN,
245            used_by_panels: Rect::NAN,
246            scroll_target: [None, None],
247            scroll_delta: (Vec2::default(), style::ScrollAnimation::none()),
248            accesskit_state: None,
249            highlight_next_pass: Default::default(),
250
251            #[cfg(debug_assertions)]
252            debug_rect: None,
253        }
254    }
255}
256
257impl PassState {
258    pub(crate) fn begin_pass(&mut self, content_rect: Rect) {
259        profiling::function_scope!();
260        let Self {
261            used_ids,
262            widgets,
263            tooltips,
264            layers,
265            available_rect,
266            unused_rect,
267            used_by_panels,
268            scroll_target,
269            scroll_delta,
270            accesskit_state,
271            highlight_next_pass,
272
273            #[cfg(debug_assertions)]
274            debug_rect,
275        } = self;
276
277        used_ids.clear();
278        widgets.clear();
279        tooltips.clear();
280        layers.clear();
281        *available_rect = content_rect;
282        *unused_rect = content_rect;
283        *used_by_panels = Rect::NOTHING;
284        *scroll_target = [None, None];
285        *scroll_delta = Default::default();
286
287        #[cfg(debug_assertions)]
288        {
289            *debug_rect = None;
290        }
291
292        *accesskit_state = None;
293
294        highlight_next_pass.clear();
295    }
296
297    /// How much space is still available after panels has been added.
298    pub(crate) fn available_rect(&self) -> Rect {
299        debug_assert!(
300            self.available_rect.is_finite(),
301            "Called `available_rect()` before `Context::run()`"
302        );
303        self.available_rect
304    }
305
306    /// Shrink `available_rect`.
307    pub(crate) fn allocate_left_panel(&mut self, panel_rect: Rect) {
308        debug_assert!(
309            panel_rect.min.distance(self.available_rect.min) < 0.1,
310            "Mismatching left panel. You must not create a panel from within another panel."
311        );
312        self.available_rect.min.x = panel_rect.max.x;
313        self.unused_rect.min.x = panel_rect.max.x;
314        self.used_by_panels |= panel_rect;
315    }
316
317    /// Shrink `available_rect`.
318    pub(crate) fn allocate_right_panel(&mut self, panel_rect: Rect) {
319        debug_assert!(
320            panel_rect.max.distance(self.available_rect.max) < 0.1,
321            "Mismatching right panel. You must not create a panel from within another panel."
322        );
323        self.available_rect.max.x = panel_rect.min.x;
324        self.unused_rect.max.x = panel_rect.min.x;
325        self.used_by_panels |= panel_rect;
326    }
327
328    /// Shrink `available_rect`.
329    pub(crate) fn allocate_top_panel(&mut self, panel_rect: Rect) {
330        debug_assert!(
331            panel_rect.min.distance(self.available_rect.min) < 0.1,
332            "Mismatching top panel. You must not create a panel from within another panel."
333        );
334        self.available_rect.min.y = panel_rect.max.y;
335        self.unused_rect.min.y = panel_rect.max.y;
336        self.used_by_panels |= panel_rect;
337    }
338
339    /// Shrink `available_rect`.
340    pub(crate) fn allocate_bottom_panel(&mut self, panel_rect: Rect) {
341        debug_assert!(
342            panel_rect.max.distance(self.available_rect.max) < 0.1,
343            "Mismatching bottom panel. You must not create a panel from within another panel."
344        );
345        self.available_rect.max.y = panel_rect.min.y;
346        self.unused_rect.max.y = panel_rect.min.y;
347        self.used_by_panels |= panel_rect;
348    }
349
350    pub(crate) fn allocate_central_panel(&mut self, panel_rect: Rect) {
351        // Note: we do not shrink `available_rect`, because
352        // we allow windows to cover the CentralPanel.
353        self.unused_rect = Rect::NOTHING; // Nothing left unused after this
354        self.used_by_panels |= panel_rect;
355    }
356}