Skip to main content

agg_gui/widget/
tree.rs

1use super::*;
2
3/// Walk the subtree rooted at `widget` and return the path (list of child
4/// indices) to the deepest widget that passes `hit_test` at `local_pos`.
5///
6/// `local_pos` is expressed in `widget`'s coordinate space (not including
7/// `widget.bounds().x/y` — the caller has already accounted for that).
8///
9/// Returns `Some(vec![])` if `widget` itself is hit but no child is.
10/// Returns `None` if nothing is hit.
11pub fn hit_test_subtree(widget: &dyn Widget, local_pos: Point) -> Option<Vec<usize>> {
12    if !widget.is_visible() || !widget.hit_test(local_pos) {
13        return None;
14    }
15    // Let overlays (e.g. a floating scrollbar) claim the pointer before any
16    // child that happens to cover the same pixels.
17    if widget.claims_pointer_exclusively(local_pos) {
18        return Some(vec![]);
19    }
20    // Check children in reverse order (last drawn = topmost = highest priority).
21    for (i, child) in widget.children().iter().enumerate().rev() {
22        let child_local = Point::new(
23            local_pos.x - child.bounds().x,
24            local_pos.y - child.bounds().y,
25        );
26        if let Some(mut sub_path) = hit_test_subtree(child.as_ref(), child_local) {
27            sub_path.insert(0, i);
28            return Some(sub_path);
29        }
30    }
31    Some(vec![]) // hit this widget, no child claimed it
32}
33
34/// Return the path to the topmost active modal subtree, ignoring normal
35/// hit-testing bounds. Modal overlays paint at app level, so their event
36/// routing must also bypass regular child clipping/window hit regions.
37pub fn active_modal_path(widget: &dyn Widget) -> Option<Vec<usize>> {
38    if !widget.is_visible() {
39        return None;
40    }
41    for (i, child) in widget.children().iter().enumerate().rev() {
42        if let Some(mut sub_path) = active_modal_path(child.as_ref()) {
43            sub_path.insert(0, i);
44            return Some(sub_path);
45        }
46    }
47    if widget.has_active_modal() {
48        Some(vec![])
49    } else {
50        None
51    }
52}
53
54/// Return the topmost widget whose app-level overlay contains `local_pos`.
55///
56/// This intentionally ignores ancestor `hit_test` bounds while descending:
57/// global overlays such as ComboBox popups are painted outside their normal
58/// parent clip/bounds, so their event routing must escape those bounds too.
59pub fn global_overlay_hit_path(widget: &dyn Widget, local_pos: Point) -> Option<Vec<usize>> {
60    if !widget.is_visible() {
61        return None;
62    }
63    for (i, child) in widget.children().iter().enumerate().rev() {
64        let child_local = Point::new(
65            local_pos.x - child.bounds().x,
66            local_pos.y - child.bounds().y,
67        );
68        if let Some(mut sub_path) = global_overlay_hit_path(child.as_ref(), child_local) {
69            sub_path.insert(0, i);
70            return Some(sub_path);
71        }
72    }
73    if widget.hit_test_global_overlay(local_pos) {
74        Some(vec![])
75    } else {
76        None
77    }
78}
79
80/// Dispatch `event` through a path (list of child indices from the root).
81/// The event bubbles leaf → root; returns `Consumed` if any widget consumed it.
82///
83/// `pos_in_root` is the event position in the root widget's coordinate space.
84/// The function translates it down through each level of the path.
85pub fn dispatch_event(
86    root: &mut Box<dyn Widget>,
87    path: &[usize],
88    event: &Event,
89    pos_in_root: Point,
90) -> EventResult {
91    if path.is_empty() {
92        let before = crate::animation::invalidation_epoch();
93        let result = root.on_event(event);
94        if result == EventResult::Consumed || before != crate::animation::invalidation_epoch() {
95            root.mark_dirty();
96        }
97        return result;
98    }
99    let idx = path[0];
100    // Path can become stale between when it was captured (hit-test or
101    // previous-frame hovered/focus) and when it is dispatched — e.g. a
102    // CollapsingHeader collapsed since then and dropped its child.  Rather
103    // than panic, just stop descending and deliver the event at this level.
104    if idx >= root.children().len() {
105        return root.on_event(event);
106    }
107    let child_bounds = root.children()[idx].bounds();
108    let child_pos = Point::new(
109        pos_in_root.x - child_bounds.x,
110        pos_in_root.y - child_bounds.y,
111    );
112    let translated_event = translate_event(event, child_pos);
113
114    let before_child = crate::animation::invalidation_epoch();
115    let child_result = dispatch_event(
116        &mut root.children_mut()[idx],
117        &path[1..],
118        &translated_event,
119        child_pos,
120    );
121    if child_result == EventResult::Consumed {
122        root.mark_dirty();
123        return EventResult::Consumed;
124    }
125    if before_child != crate::animation::invalidation_epoch() {
126        root.mark_dirty();
127    }
128    // Bubble: deliver to this widget too (with original pos_in_root coords).
129    let before_self = crate::animation::invalidation_epoch();
130    let result = root.on_event(event);
131    if result == EventResult::Consumed || before_self != crate::animation::invalidation_epoch() {
132        root.mark_dirty();
133    }
134    result
135}
136
137/// Give visible widgets a chance to handle a key ignored by the focused path.
138///
139/// Traverses in reverse paint order so topmost windows/menu bars win.
140pub fn dispatch_unconsumed_key(
141    widget: &mut dyn Widget,
142    key: &Key,
143    modifiers: Modifiers,
144) -> EventResult {
145    if !widget.is_visible() {
146        return EventResult::Ignored;
147    }
148    for child in widget.children_mut().iter_mut().rev() {
149        if dispatch_unconsumed_key(child.as_mut(), key, modifiers) == EventResult::Consumed {
150            widget.mark_dirty();
151            return EventResult::Consumed;
152        }
153    }
154    let before = crate::animation::invalidation_epoch();
155    let result = widget.on_unconsumed_key(key, modifiers);
156    if result == EventResult::Consumed || before != crate::animation::invalidation_epoch() {
157        widget.mark_dirty();
158    }
159    result
160}
161
162/// Produce a version of `event` with mouse positions replaced by `new_pos`.
163/// Non-mouse events (key, focus) are returned unchanged.
164fn translate_event(event: &Event, new_pos: Point) -> Event {
165    match event {
166        Event::MouseMove { .. } => Event::MouseMove { pos: new_pos },
167        Event::MouseDown {
168            button, modifiers, ..
169        } => Event::MouseDown {
170            pos: new_pos,
171            button: *button,
172            modifiers: *modifiers,
173        },
174        Event::MouseUp {
175            button, modifiers, ..
176        } => Event::MouseUp {
177            pos: new_pos,
178            button: *button,
179            modifiers: *modifiers,
180        },
181        Event::MouseWheel {
182            delta_y,
183            delta_x,
184            modifiers,
185            ..
186        } => Event::MouseWheel {
187            pos: new_pos,
188            delta_y: *delta_y,
189            delta_x: *delta_x,
190            modifiers: *modifiers,
191        },
192        other => other.clone(),
193    }
194}
195
196// ---------------------------------------------------------------------------
197// Inspector support
198// ---------------------------------------------------------------------------
199
200/// Flat snapshot of one widget for the inspector panel.
201#[derive(Clone)]
202pub struct InspectorNode {
203    pub type_name: &'static str,
204    /// Absolute screen bounds (Y-up), accumulated as the tree is walked.
205    pub screen_bounds: Rect,
206    pub depth: usize,
207    /// Type-specific display properties from [`Widget::properties`].
208    pub properties: Vec<(&'static str, String)>,
209}
210
211// ── Global mouse-world-pos (for nested drags that can't use widget-
212//    local coords because ancestor layout shifts under them each frame) ─────
213
214thread_local! {
215    static CURRENT_MOUSE_WORLD: std::cell::Cell<Option<Point>> =
216        std::cell::Cell::new(None);
217    static CURRENT_VIEWPORT: std::cell::Cell<Size> =
218        std::cell::Cell::new(Size::new(1.0, 1.0));
219}
220
221/// Record the current mouse cursor position in app-level (world / Y-up
222/// logical) coordinates.  Called by `App`'s mouse entry points.
223pub fn set_current_mouse_world(p: Point) {
224    CURRENT_MOUSE_WORLD.with(|c| c.set(Some(p)));
225}
226
227/// Retrieve the latest world-space mouse position.  Widgets doing a
228/// drag gesture that needs invariance against ancestor-layout shifts
229/// (e.g. a nested `Resize` inside an auto-sized `Window`, where the
230/// window grows/shrinks as the user drags and moves the widget's
231/// ancestor frame) should prefer this over the widget-local `pos`
232/// carried in `Event::Mouse*`.
233pub fn current_mouse_world() -> Option<Point> {
234    CURRENT_MOUSE_WORLD.with(|c| c.get())
235}
236
237/// Record the current app-level viewport in logical Y-up coordinates.
238pub fn set_current_viewport(s: Size) {
239    CURRENT_VIEWPORT.with(|c| c.set(s));
240}
241
242/// Retrieve the latest app-level viewport in logical coordinates.
243pub fn current_viewport() -> Size {
244    CURRENT_VIEWPORT.with(|c| c.get())
245}
246
247/// Depth-first search the subtree rooted at `widget` for one whose
248/// [`Widget::id`] matches `id`.  Returns the first match in paint order,
249/// including `widget` itself.  Used primarily by tests to locate a
250/// specific `Window` by its title without knowing the tree shape.
251pub fn find_widget_by_id<'a>(widget: &'a dyn Widget, id: &str) -> Option<&'a dyn Widget> {
252    if widget.id() == Some(id) {
253        return Some(widget);
254    }
255    for child in widget.children() {
256        if let Some(found) = find_widget_by_id(child.as_ref(), id) {
257            return Some(found);
258        }
259    }
260    None
261}
262
263/// Mutable counterpart to [`find_widget_by_id`].  Required when a test
264/// needs to poke at a sub-widget's mutable state (e.g. calling a
265/// `ScrollView::set_scroll_offset`) after finding it by id.
266pub fn find_widget_by_id_mut<'a>(
267    widget: &'a mut dyn Widget,
268    id: &str,
269) -> Option<&'a mut dyn Widget> {
270    if widget.id() == Some(id) {
271        return Some(widget);
272    }
273    for child in widget.children_mut().iter_mut() {
274        if let Some(found) = find_widget_by_id_mut(child.as_mut(), id) {
275            return Some(found);
276        }
277    }
278    None
279}
280
281/// Depth-first search for a widget by its [`Widget::type_name`].  Returns
282/// the first match in paint order.  Used by tests that want to assert on
283/// a specific widget kind inside an opaque content subtree (e.g.
284/// "find the ScrollView inside this window").
285pub fn find_widget_by_type<'a>(widget: &'a dyn Widget, type_name: &str) -> Option<&'a dyn Widget> {
286    if widget.type_name() == type_name {
287        return Some(widget);
288    }
289    for child in widget.children() {
290        if let Some(found) = find_widget_by_type(child.as_ref(), type_name) {
291            return Some(found);
292        }
293    }
294    None
295}
296
297/// Walk the subtree rooted at `widget` and collect an `InspectorNode` per
298/// widget in DFS paint order (root first).
299///
300/// `screen_origin` is the accumulated parent offset in screen Y-up coords.
301pub fn collect_inspector_nodes(
302    widget: &dyn Widget,
303    depth: usize,
304    screen_origin: Point,
305    out: &mut Vec<InspectorNode>,
306) {
307    // Invisible widgets (and their entire subtrees) are excluded from the
308    // inspector — they are not part of the live rendered scene.
309    if !widget.is_visible() {
310        return;
311    }
312    // Utility widgets opt out of the inspector entirely.
313    if !widget.show_in_inspector() {
314        return;
315    }
316
317    let b = widget.bounds();
318    let abs = Rect::new(
319        screen_origin.x + b.x,
320        screen_origin.y + b.y,
321        b.width,
322        b.height,
323    );
324    // Build the properties vec — include the universal `backbuffer` flag
325    // first (so every widget shows it in a consistent location), then the
326    // widget-specific properties.
327    let mut props = vec![(
328        "backbuffer",
329        if widget.has_backbuffer() {
330            "true".to_string()
331        } else {
332            "false".to_string()
333        },
334    )];
335    props.extend(widget.properties());
336    out.push(InspectorNode {
337        type_name: widget.type_name(),
338        screen_bounds: abs,
339        depth,
340        properties: props,
341    });
342
343    // Widgets that are part of the inspector infrastructure opt out of child
344    // recursion to prevent the inspector from growing its own node list every
345    // frame (exponential growth).  Their sub-trees are still visible in the
346    // inspector on the next frame through the normal layout snapshot.
347    if !widget.contributes_children_to_inspector() {
348        return;
349    }
350
351    let child_origin = Point::new(abs.x, abs.y);
352    for child in widget.children() {
353        collect_inspector_nodes(child.as_ref(), depth + 1, child_origin, out);
354    }
355}