Skip to main content

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/// Recursively call `mark_dirty` on `widget` and every visible
21/// descendant.  Used by the host frame loop after an async data
22/// source (image fetch + decode, font load, etc.) finishes outside
23/// the normal event-dispatch path that would otherwise mark widgets
24/// dirty as the event bubbles.  Called explicitly at the top of the
25/// frame so the user-visible "freshly-decoded data lands in stale
26/// FBO contents" bug never opens a one-frame race window.
27pub fn mark_subtree_dirty(widget: &mut dyn Widget) {
28    widget.mark_dirty();
29    for child in widget.children_mut().iter_mut() {
30        mark_subtree_dirty(child.as_mut());
31    }
32}
33
34/// Walk the subtree rooted at `widget` and return the path (list of child
35/// indices) to the deepest widget that passes `hit_test` at `local_pos`.
36///
37/// `local_pos` is expressed in `widget`'s coordinate space (not including
38/// `widget.bounds().x/y` — the caller has already accounted for that).
39///
40/// Returns `Some(vec![])` if `widget` itself is hit but no child is.
41/// Returns `None` if nothing is hit.
42pub fn hit_test_subtree(widget: &dyn Widget, local_pos: Point) -> Option<Vec<usize>> {
43    if !widget.is_visible() || !widget.hit_test(local_pos) {
44        return None;
45    }
46    // Let overlays (e.g. a floating scrollbar) claim the pointer before any
47    // child that happens to cover the same pixels.
48    if widget.claims_pointer_exclusively(local_pos) {
49        return Some(vec![]);
50    }
51    // Check children in reverse order (last drawn = topmost = highest priority).
52    for (i, child) in widget.children().iter().enumerate().rev() {
53        let child_local = Point::new(
54            local_pos.x - child.bounds().x,
55            local_pos.y - child.bounds().y,
56        );
57        if let Some(mut sub_path) = hit_test_subtree(child.as_ref(), child_local) {
58            sub_path.insert(0, i);
59            return Some(sub_path);
60        }
61    }
62    Some(vec![]) // hit this widget, no child claimed it
63}
64
65/// Return the path to the topmost active modal subtree, ignoring normal
66/// hit-testing bounds. Modal overlays paint at app level, so their event
67/// routing must also bypass regular child clipping/window hit regions.
68pub fn active_modal_path(widget: &dyn Widget) -> Option<Vec<usize>> {
69    if !widget.is_visible() {
70        return None;
71    }
72    for (i, child) in widget.children().iter().enumerate().rev() {
73        if let Some(mut sub_path) = active_modal_path(child.as_ref()) {
74            sub_path.insert(0, i);
75            return Some(sub_path);
76        }
77    }
78    if widget.has_active_modal() {
79        Some(vec![])
80    } else {
81        None
82    }
83}
84
85/// Return the topmost widget whose app-level overlay contains `local_pos`.
86///
87/// This intentionally ignores ancestor `hit_test` bounds while descending:
88/// global overlays such as ComboBox popups are painted outside their normal
89/// parent clip/bounds, so their event routing must escape those bounds too.
90pub fn global_overlay_hit_path(widget: &dyn Widget, local_pos: Point) -> Option<Vec<usize>> {
91    if !widget.is_visible() {
92        return None;
93    }
94    for (i, child) in widget.children().iter().enumerate().rev() {
95        let child_local = Point::new(
96            local_pos.x - child.bounds().x,
97            local_pos.y - child.bounds().y,
98        );
99        if let Some(mut sub_path) = global_overlay_hit_path(child.as_ref(), child_local) {
100            sub_path.insert(0, i);
101            return Some(sub_path);
102        }
103    }
104    if widget.hit_test_global_overlay(local_pos) {
105        Some(vec![])
106    } else {
107        None
108    }
109}
110
111/// Dispatch `event` through a path (list of child indices from the root).
112/// The event bubbles leaf → root; returns `Consumed` if any widget consumed it.
113///
114/// `pos_in_root` is the event position in the root widget's coordinate space.
115/// The function translates it down through each level of the path.
116pub fn dispatch_event(
117    root: &mut Box<dyn Widget>,
118    path: &[usize],
119    event: &Event,
120    pos_in_root: Point,
121) -> EventResult {
122    if path.is_empty() {
123        let before = crate::animation::invalidation_epoch();
124        let result = root.on_event(event);
125        if result == EventResult::Consumed || before != crate::animation::invalidation_epoch() {
126            root.mark_dirty();
127        }
128        return result;
129    }
130    let idx = path[0];
131    // Path can become stale between when it was captured (hit-test or
132    // previous-frame hovered/focus) and when it is dispatched — e.g. a
133    // CollapsingHeader collapsed since then and dropped its child.  Rather
134    // than panic, just stop descending and deliver the event at this level.
135    if idx >= root.children().len() {
136        return root.on_event(event);
137    }
138    let child_bounds = root.children()[idx].bounds();
139    let child_pos = Point::new(
140        pos_in_root.x - child_bounds.x,
141        pos_in_root.y - child_bounds.y,
142    );
143    let translated_event = translate_event(event, child_pos);
144
145    let before_child = crate::animation::invalidation_epoch();
146    let child_result = dispatch_event(
147        &mut root.children_mut()[idx],
148        &path[1..],
149        &translated_event,
150        child_pos,
151    );
152    if child_result == EventResult::Consumed {
153        root.mark_dirty();
154        return EventResult::Consumed;
155    }
156    if before_child != crate::animation::invalidation_epoch() {
157        root.mark_dirty();
158    }
159    // Bubble: deliver to this widget too (with original pos_in_root coords).
160    let before_self = crate::animation::invalidation_epoch();
161    let result = root.on_event(event);
162    if result == EventResult::Consumed || before_self != crate::animation::invalidation_epoch() {
163        root.mark_dirty();
164    }
165    result
166}
167
168/// Variant of [`dispatch_event`] that accepts `&mut dyn Widget` as the
169/// root. Useful when a parent owns a sub-tree by concrete type (e.g.
170/// `Window`'s `title_bar: WindowTitleBar`) and wants to route an event
171/// into it via the framework's standard hit-test + bubble dispatch,
172/// instead of running coordinate hit-tests inline.
173pub fn dispatch_event_dyn(
174    root: &mut dyn Widget,
175    path: &[usize],
176    event: &Event,
177    pos_in_root: Point,
178) -> EventResult {
179    if path.is_empty() {
180        let before = crate::animation::invalidation_epoch();
181        let result = root.on_event(event);
182        if result == EventResult::Consumed || before != crate::animation::invalidation_epoch() {
183            root.mark_dirty();
184        }
185        return result;
186    }
187    let idx = path[0];
188    if idx >= root.children().len() {
189        return root.on_event(event);
190    }
191    let child_bounds = root.children()[idx].bounds();
192    let child_pos = Point::new(
193        pos_in_root.x - child_bounds.x,
194        pos_in_root.y - child_bounds.y,
195    );
196    let translated_event = translate_event(event, child_pos);
197
198    let before_child = crate::animation::invalidation_epoch();
199    // After the first hop we're inside the Vec<Box<dyn Widget>>, so we
200    // can fall back to the regular Box-based dispatcher.
201    let child_result = dispatch_event(
202        &mut root.children_mut()[idx],
203        &path[1..],
204        &translated_event,
205        child_pos,
206    );
207    if child_result == EventResult::Consumed {
208        root.mark_dirty();
209        return EventResult::Consumed;
210    }
211    if before_child != crate::animation::invalidation_epoch() {
212        root.mark_dirty();
213    }
214    let before_self = crate::animation::invalidation_epoch();
215    let result = root.on_event(event);
216    if result == EventResult::Consumed || before_self != crate::animation::invalidation_epoch() {
217        root.mark_dirty();
218    }
219    result
220}
221
222/// Give visible widgets a chance to handle a key ignored by the focused path.
223///
224/// Traverses in reverse paint order so topmost windows/menu bars win.
225pub fn dispatch_unconsumed_key(
226    widget: &mut dyn Widget,
227    key: &Key,
228    modifiers: Modifiers,
229) -> EventResult {
230    if !widget.is_visible() {
231        return EventResult::Ignored;
232    }
233    for child in widget.children_mut().iter_mut().rev() {
234        if dispatch_unconsumed_key(child.as_mut(), key, modifiers) == EventResult::Consumed {
235            widget.mark_dirty();
236            return EventResult::Consumed;
237        }
238    }
239    let before = crate::animation::invalidation_epoch();
240    let result = widget.on_unconsumed_key(key, modifiers);
241    if result == EventResult::Consumed || before != crate::animation::invalidation_epoch() {
242        widget.mark_dirty();
243    }
244    result
245}
246
247/// Produce a version of `event` with mouse positions replaced by `new_pos`.
248/// Non-mouse events (key, focus) are returned unchanged.
249fn translate_event(event: &Event, new_pos: Point) -> Event {
250    match event {
251        Event::MouseMove { .. } => Event::MouseMove { pos: new_pos },
252        Event::MouseDown {
253            button, modifiers, ..
254        } => Event::MouseDown {
255            pos: new_pos,
256            button: *button,
257            modifiers: *modifiers,
258        },
259        Event::MouseUp {
260            button, modifiers, ..
261        } => Event::MouseUp {
262            pos: new_pos,
263            button: *button,
264            modifiers: *modifiers,
265        },
266        Event::MouseWheel {
267            delta_y,
268            delta_x,
269            modifiers,
270            ..
271        } => Event::MouseWheel {
272            pos: new_pos,
273            delta_y: *delta_y,
274            delta_x: *delta_x,
275            modifiers: *modifiers,
276        },
277        Event::FileDropped { paths, .. } => Event::FileDropped {
278            pos: new_pos,
279            paths: paths.clone(),
280        },
281        other => other.clone(),
282    }
283}
284
285/// Offer `event` to every visible widget in the subtree, depth-first with
286/// the same topmost-last-child priority as [`hit_test_subtree`], stopping
287/// at the first consumer. Positions are translated into each widget's
288/// local space on the way down, exactly like [`dispatch_event`].
289///
290/// This exists for events that must not be lost when the widget under
291/// their position ignores them — file drops foremost: native shells can't
292/// always produce an accurate drop position (winit's Windows backend
293/// discards the OLE drop point and emits no CursorMoved during the drag),
294/// so the position may point at chrome while the only interested handler
295/// (a canvas) sits in a sibling subtree the bubble path never reaches.
296pub fn dispatch_event_broadcast(
297    root: &mut Box<dyn Widget>,
298    event: &Event,
299    pos_in_root: Point,
300) -> EventResult {
301    if !root.is_visible() {
302        return EventResult::Ignored;
303    }
304    for i in (0..root.children().len()).rev() {
305        let child_bounds = root.children()[i].bounds();
306        let child_pos = Point::new(
307            pos_in_root.x - child_bounds.x,
308            pos_in_root.y - child_bounds.y,
309        );
310        let translated = translate_event(event, child_pos);
311        if dispatch_event_broadcast(&mut root.children_mut()[i], &translated, child_pos)
312            == EventResult::Consumed
313        {
314            root.mark_dirty();
315            return EventResult::Consumed;
316        }
317    }
318    let result = root.on_event(event);
319    if result == EventResult::Consumed {
320        root.mark_dirty();
321    }
322    result
323}