egui/
pass_state.rs

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