agg_gui/widget/tree.rs
1//! Widget-tree traversal helpers: dirty marking and hit-test path lookup.
2//!
3//! [`mark_subtree_dirty`] is called by the frame loop after off-event-path
4//! async work finishes (font load, image decode). The hit-test family —
5//! [`hit_test_subtree`], [`active_modal_path`], [`global_overlay_hit_path`]
6//! — returns a `Vec<usize>` path of child indices identifying the deepest
7//! widget that claims a position; the App event router uses that path to
8//! dispatch mouse events to the right subtree.
9//!
10//! # Coordinate system
11//!
12//! All positions in this module are **logical Y-up**, origin at the
13//! bottom-left, expressed in the receiving widget's own local space (the
14//! caller has already subtracted ancestor `bounds().x/y`). The App
15//! pre-converts platform Y-down input coordinates via `App::flip_y`
16//! before calling in — do not flip Y here.
17
18use super::*;
19
20/// Map a point from a parent's local space into the local space of its child
21/// at index `idx`, honouring the parent's optional
22/// [`Widget::child_transform`](crate::widget::Widget::child_transform).
23///
24/// The child transform (if any) is inverted first — it maps child→parent, so
25/// its inverse maps the incoming parent-local point back into child space —
26/// and only then is the child's `bounds()` offset removed. Bounds are thus
27/// interpreted *inside* the transform, matching how [`paint_subtree`] applies
28/// the transform to the whole child group before translating per child.
29fn child_local_pos(parent: &dyn Widget, child_bounds: Rect, pos_in_parent: Point) -> Point {
30 let mut x = pos_in_parent.x;
31 let mut y = pos_in_parent.y;
32 if let Some(t) = parent.child_transform() {
33 t.inverse_transform(&mut x, &mut y);
34 }
35 Point::new(x - child_bounds.x, y - child_bounds.y)
36}
37
38/// Recursively call `mark_dirty` on `widget` and every visible
39/// descendant. Used by the host frame loop after an async data
40/// source (image fetch + decode, font load, etc.) finishes outside
41/// the normal event-dispatch path that would otherwise mark widgets
42/// dirty as the event bubbles. Called explicitly at the top of the
43/// frame so the user-visible "freshly-decoded data lands in stale
44/// FBO contents" bug never opens a one-frame race window.
45pub fn mark_subtree_dirty(widget: &mut dyn Widget) {
46 widget.mark_dirty();
47 for child in widget.children_mut().iter_mut() {
48 mark_subtree_dirty(child.as_mut());
49 }
50}
51
52/// Walk the subtree rooted at `widget` and return the path (list of child
53/// indices) to the deepest widget that passes `hit_test` at `local_pos`.
54///
55/// `local_pos` is expressed in `widget`'s coordinate space (not including
56/// `widget.bounds().x/y` — the caller has already accounted for that).
57///
58/// Returns `Some(vec![])` if `widget` itself is hit but no child is.
59/// Returns `None` if nothing is hit.
60pub fn hit_test_subtree(widget: &dyn Widget, local_pos: Point) -> Option<Vec<usize>> {
61 if !widget.is_visible() || !widget.hit_test(local_pos) {
62 return None;
63 }
64 // Let overlays (e.g. a floating scrollbar) claim the pointer before any
65 // child that happens to cover the same pixels, and let a disabled scope
66 // swallow the pointer so clicks never reach its (non-interactive) children.
67 if widget.claims_pointer_exclusively(local_pos) || widget.blocks_child_interaction() {
68 return Some(vec![]);
69 }
70 // Check children in reverse order (last drawn = topmost = highest priority).
71 for (i, child) in widget.children().iter().enumerate().rev() {
72 let child_local = child_local_pos(widget, child.bounds(), local_pos);
73 if let Some(mut sub_path) = hit_test_subtree(child.as_ref(), child_local) {
74 sub_path.insert(0, i);
75 return Some(sub_path);
76 }
77 }
78 Some(vec![]) // hit this widget, no child claimed it
79}
80
81/// Return the path to the topmost active modal subtree, ignoring normal
82/// hit-testing bounds. Modal overlays paint at app level, so their event
83/// routing must also bypass regular child clipping/window hit regions.
84pub fn active_modal_path(widget: &dyn Widget) -> Option<Vec<usize>> {
85 if !widget.is_visible() {
86 return None;
87 }
88 for (i, child) in widget.children().iter().enumerate().rev() {
89 if let Some(mut sub_path) = active_modal_path(child.as_ref()) {
90 sub_path.insert(0, i);
91 return Some(sub_path);
92 }
93 }
94 if widget.has_active_modal() {
95 Some(vec![])
96 } else {
97 None
98 }
99}
100
101/// Return the topmost widget whose app-level overlay contains `local_pos`.
102///
103/// This intentionally ignores ancestor `hit_test` bounds while descending:
104/// global overlays such as ComboBox popups are painted outside their normal
105/// parent clip/bounds, so their event routing must escape those bounds too.
106pub fn global_overlay_hit_path(widget: &dyn Widget, local_pos: Point) -> Option<Vec<usize>> {
107 if !widget.is_visible() {
108 return None;
109 }
110 for (i, child) in widget.children().iter().enumerate().rev() {
111 let child_local = child_local_pos(widget, child.bounds(), local_pos);
112 if let Some(mut sub_path) = global_overlay_hit_path(child.as_ref(), child_local) {
113 sub_path.insert(0, i);
114 return Some(sub_path);
115 }
116 }
117 if widget.hit_test_global_overlay(local_pos) {
118 Some(vec![])
119 } else {
120 None
121 }
122}
123
124/// Centralized event delivery: call the widget's `on_event` and apply the
125/// framework's automatic-invalidation policy in ONE place, so no dispatch
126/// path can silently forget it. A [`EventResult::Consumed`] result schedules
127/// a repaint via [`crate::animation::request_draw`];
128/// [`EventResult::ConsumedQuiet`] and [`EventResult::Ignored`] do not.
129///
130/// This single hook makes "consume ⇒ repaint" the default, fixing the
131/// recurring bug class where a widget mutates paint-affecting state on an
132/// event but forgets to request a draw, leaving part of itself stale.
133fn deliver(widget: &mut dyn Widget, event: &Event) -> EventResult {
134 auto_request_draw(widget.on_event(event))
135}
136
137/// Apply the auto-invalidation policy to an already-computed [`EventResult`].
138/// Shared by [`deliver`] (for `on_event`) and the unconsumed-key path (for
139/// `on_unconsumed_key`) so every delivery route honours the same rule.
140fn auto_request_draw(result: EventResult) -> EventResult {
141 if result.requests_redraw() {
142 crate::animation::request_draw();
143 }
144 result
145}
146
147/// Dispatch `event` through a path (list of child indices from the root).
148/// The event bubbles leaf → root; returns a consuming result if any widget
149/// consumed it (preserving the `Consumed` vs `ConsumedQuiet` distinction).
150///
151/// `pos_in_root` is the event position in the root widget's coordinate space.
152/// The function translates it down through each level of the path.
153pub fn dispatch_event(
154 root: &mut Box<dyn Widget>,
155 path: &[usize],
156 event: &Event,
157 pos_in_root: Point,
158) -> EventResult {
159 if path.is_empty() {
160 let before = crate::animation::invalidation_epoch();
161 let result = deliver(root.as_mut(), event);
162 if result.requests_redraw() || before != crate::animation::invalidation_epoch() {
163 root.mark_dirty();
164 }
165 return result;
166 }
167 let idx = path[0];
168 // Path can become stale between when it was captured (hit-test or
169 // previous-frame hovered/focus) and when it is dispatched — e.g. a
170 // CollapsingHeader collapsed since then and dropped its child. Rather
171 // than panic, just stop descending and deliver the event at this level.
172 if idx >= root.children().len() {
173 return deliver(root.as_mut(), event);
174 }
175 let child_bounds = root.children()[idx].bounds();
176 let child_pos = child_local_pos(root.as_ref(), child_bounds, pos_in_root);
177 let translated_event = translate_event(event, child_pos);
178
179 let before_child = crate::animation::invalidation_epoch();
180 let child_result = dispatch_event(
181 &mut root.children_mut()[idx],
182 &path[1..],
183 &translated_event,
184 child_pos,
185 );
186 // A child (or descendant) that requested a draw bumped the epoch —
187 // invalidate our own cache so the retained backbuffer re-rasters. A
188 // quiet consume bumps nothing, so it correctly leaves the cache alone.
189 if before_child != crate::animation::invalidation_epoch() {
190 root.mark_dirty();
191 }
192 if child_result.is_consumed() {
193 return child_result;
194 }
195 // Bubble: deliver to this widget too (with original pos_in_root coords).
196 let before_self = crate::animation::invalidation_epoch();
197 let result = deliver(root.as_mut(), event);
198 if result.requests_redraw() || before_self != crate::animation::invalidation_epoch() {
199 root.mark_dirty();
200 }
201 result
202}
203
204/// Variant of [`dispatch_event`] that accepts `&mut dyn Widget` as the
205/// root. Useful when a parent owns a sub-tree by concrete type (e.g.
206/// `Window`'s `title_bar: WindowTitleBar`) and wants to route an event
207/// into it via the framework's standard hit-test + bubble dispatch,
208/// instead of running coordinate hit-tests inline.
209pub fn dispatch_event_dyn(
210 root: &mut dyn Widget,
211 path: &[usize],
212 event: &Event,
213 pos_in_root: Point,
214) -> EventResult {
215 if path.is_empty() {
216 let before = crate::animation::invalidation_epoch();
217 let result = deliver(root, event);
218 if result.requests_redraw() || before != crate::animation::invalidation_epoch() {
219 root.mark_dirty();
220 }
221 return result;
222 }
223 let idx = path[0];
224 if idx >= root.children().len() {
225 return deliver(root, event);
226 }
227 let child_bounds = root.children()[idx].bounds();
228 let child_pos = child_local_pos(root, child_bounds, pos_in_root);
229 let translated_event = translate_event(event, child_pos);
230
231 let before_child = crate::animation::invalidation_epoch();
232 // After the first hop we're inside the Vec<Box<dyn Widget>>, so we
233 // can fall back to the regular Box-based dispatcher.
234 let child_result = dispatch_event(
235 &mut root.children_mut()[idx],
236 &path[1..],
237 &translated_event,
238 child_pos,
239 );
240 if before_child != crate::animation::invalidation_epoch() {
241 root.mark_dirty();
242 }
243 if child_result.is_consumed() {
244 return child_result;
245 }
246 let before_self = crate::animation::invalidation_epoch();
247 let result = deliver(root, event);
248 if result.requests_redraw() || before_self != crate::animation::invalidation_epoch() {
249 root.mark_dirty();
250 }
251 result
252}
253
254/// Give visible widgets a chance to handle a key ignored by the focused path.
255///
256/// Traverses in reverse paint order so topmost windows/menu bars win.
257pub fn dispatch_unconsumed_key(
258 widget: &mut dyn Widget,
259 key: &Key,
260 modifiers: Modifiers,
261) -> EventResult {
262 if !widget.is_visible() {
263 return EventResult::Ignored;
264 }
265 let mut consumed = None;
266 for child in widget.children_mut().iter_mut().rev() {
267 let r = dispatch_unconsumed_key(child.as_mut(), key, modifiers);
268 if r.is_consumed() {
269 consumed = Some(r);
270 break;
271 }
272 }
273 if let Some(r) = consumed {
274 widget.mark_dirty();
275 return r;
276 }
277 let before = crate::animation::invalidation_epoch();
278 let result = auto_request_draw(widget.on_unconsumed_key(key, modifiers));
279 if result.requests_redraw() || before != crate::animation::invalidation_epoch() {
280 widget.mark_dirty();
281 }
282 result
283}
284
285/// Produce a version of `event` with mouse positions replaced by `new_pos`.
286/// Non-mouse events (key, focus) are returned unchanged.
287fn translate_event(event: &Event, new_pos: Point) -> Event {
288 match event {
289 Event::MouseMove { .. } => Event::MouseMove { pos: new_pos },
290 Event::MouseDown {
291 button, modifiers, ..
292 } => Event::MouseDown {
293 pos: new_pos,
294 button: *button,
295 modifiers: *modifiers,
296 },
297 Event::MouseUp {
298 button, modifiers, ..
299 } => Event::MouseUp {
300 pos: new_pos,
301 button: *button,
302 modifiers: *modifiers,
303 },
304 Event::MouseWheel {
305 delta_y,
306 delta_x,
307 modifiers,
308 ..
309 } => Event::MouseWheel {
310 pos: new_pos,
311 delta_y: *delta_y,
312 delta_x: *delta_x,
313 modifiers: *modifiers,
314 },
315 Event::FileDropped { paths, .. } => Event::FileDropped {
316 pos: new_pos,
317 paths: paths.clone(),
318 },
319 other => other.clone(),
320 }
321}
322
323/// Offer `event` to every visible widget in the subtree, depth-first with
324/// the same topmost-last-child priority as [`hit_test_subtree`], stopping
325/// at the first consumer. Positions are translated into each widget's
326/// local space on the way down, exactly like [`dispatch_event`].
327///
328/// This exists for events that must not be lost when the widget under
329/// their position ignores them — file drops foremost: native shells can't
330/// always produce an accurate drop position (winit's Windows backend
331/// discards the OLE drop point and emits no CursorMoved during the drag),
332/// so the position may point at chrome while the only interested handler
333/// (a canvas) sits in a sibling subtree the bubble path never reaches.
334pub fn dispatch_event_broadcast(
335 root: &mut Box<dyn Widget>,
336 event: &Event,
337 pos_in_root: Point,
338) -> EventResult {
339 if !root.is_visible() {
340 return EventResult::Ignored;
341 }
342 for i in (0..root.children().len()).rev() {
343 let child_bounds = root.children()[i].bounds();
344 let child_pos = child_local_pos(root.as_ref(), child_bounds, pos_in_root);
345 let translated = translate_event(event, child_pos);
346 let r = dispatch_event_broadcast(&mut root.children_mut()[i], &translated, child_pos);
347 if r.is_consumed() {
348 root.mark_dirty();
349 return r;
350 }
351 }
352 let result = deliver(root.as_mut(), event);
353 if result.is_consumed() {
354 root.mark_dirty();
355 }
356 result
357}