Skip to main content

agg_gui/widget/
tree.rs

1use super::*;
2
3/// Recursively call `mark_dirty` on `widget` and every visible
4/// descendant.  Used by the host frame loop after an async data
5/// source (image fetch + decode, font load, etc.) finishes outside
6/// the normal event-dispatch path that would otherwise mark widgets
7/// dirty as the event bubbles.  Called explicitly at the top of the
8/// frame so the user-visible "freshly-decoded data lands in stale
9/// FBO contents" bug never opens a one-frame race window.
10pub fn mark_subtree_dirty(widget: &mut dyn Widget) {
11    widget.mark_dirty();
12    for child in widget.children_mut().iter_mut() {
13        mark_subtree_dirty(child.as_mut());
14    }
15}
16
17/// Walk the subtree rooted at `widget` and return the path (list of child
18/// indices) to the deepest widget that passes `hit_test` at `local_pos`.
19///
20/// `local_pos` is expressed in `widget`'s coordinate space (not including
21/// `widget.bounds().x/y` — the caller has already accounted for that).
22///
23/// Returns `Some(vec![])` if `widget` itself is hit but no child is.
24/// Returns `None` if nothing is hit.
25pub fn hit_test_subtree(widget: &dyn Widget, local_pos: Point) -> Option<Vec<usize>> {
26    if !widget.is_visible() || !widget.hit_test(local_pos) {
27        return None;
28    }
29    // Let overlays (e.g. a floating scrollbar) claim the pointer before any
30    // child that happens to cover the same pixels.
31    if widget.claims_pointer_exclusively(local_pos) {
32        return Some(vec![]);
33    }
34    // Check children in reverse order (last drawn = topmost = highest priority).
35    for (i, child) in widget.children().iter().enumerate().rev() {
36        let child_local = Point::new(
37            local_pos.x - child.bounds().x,
38            local_pos.y - child.bounds().y,
39        );
40        if let Some(mut sub_path) = hit_test_subtree(child.as_ref(), child_local) {
41            sub_path.insert(0, i);
42            return Some(sub_path);
43        }
44    }
45    Some(vec![]) // hit this widget, no child claimed it
46}
47
48/// Return the path to the topmost active modal subtree, ignoring normal
49/// hit-testing bounds. Modal overlays paint at app level, so their event
50/// routing must also bypass regular child clipping/window hit regions.
51pub fn active_modal_path(widget: &dyn Widget) -> Option<Vec<usize>> {
52    if !widget.is_visible() {
53        return None;
54    }
55    for (i, child) in widget.children().iter().enumerate().rev() {
56        if let Some(mut sub_path) = active_modal_path(child.as_ref()) {
57            sub_path.insert(0, i);
58            return Some(sub_path);
59        }
60    }
61    if widget.has_active_modal() {
62        Some(vec![])
63    } else {
64        None
65    }
66}
67
68/// Return the topmost widget whose app-level overlay contains `local_pos`.
69///
70/// This intentionally ignores ancestor `hit_test` bounds while descending:
71/// global overlays such as ComboBox popups are painted outside their normal
72/// parent clip/bounds, so their event routing must escape those bounds too.
73pub fn global_overlay_hit_path(widget: &dyn Widget, local_pos: Point) -> Option<Vec<usize>> {
74    if !widget.is_visible() {
75        return None;
76    }
77    for (i, child) in widget.children().iter().enumerate().rev() {
78        let child_local = Point::new(
79            local_pos.x - child.bounds().x,
80            local_pos.y - child.bounds().y,
81        );
82        if let Some(mut sub_path) = global_overlay_hit_path(child.as_ref(), child_local) {
83            sub_path.insert(0, i);
84            return Some(sub_path);
85        }
86    }
87    if widget.hit_test_global_overlay(local_pos) {
88        Some(vec![])
89    } else {
90        None
91    }
92}
93
94/// Dispatch `event` through a path (list of child indices from the root).
95/// The event bubbles leaf → root; returns `Consumed` if any widget consumed it.
96///
97/// `pos_in_root` is the event position in the root widget's coordinate space.
98/// The function translates it down through each level of the path.
99pub fn dispatch_event(
100    root: &mut Box<dyn Widget>,
101    path: &[usize],
102    event: &Event,
103    pos_in_root: Point,
104) -> EventResult {
105    if path.is_empty() {
106        let before = crate::animation::invalidation_epoch();
107        let result = root.on_event(event);
108        if result == EventResult::Consumed || before != crate::animation::invalidation_epoch() {
109            root.mark_dirty();
110        }
111        return result;
112    }
113    let idx = path[0];
114    // Path can become stale between when it was captured (hit-test or
115    // previous-frame hovered/focus) and when it is dispatched — e.g. a
116    // CollapsingHeader collapsed since then and dropped its child.  Rather
117    // than panic, just stop descending and deliver the event at this level.
118    if idx >= root.children().len() {
119        return root.on_event(event);
120    }
121    let child_bounds = root.children()[idx].bounds();
122    let child_pos = Point::new(
123        pos_in_root.x - child_bounds.x,
124        pos_in_root.y - child_bounds.y,
125    );
126    let translated_event = translate_event(event, child_pos);
127
128    let before_child = crate::animation::invalidation_epoch();
129    let child_result = dispatch_event(
130        &mut root.children_mut()[idx],
131        &path[1..],
132        &translated_event,
133        child_pos,
134    );
135    if child_result == EventResult::Consumed {
136        root.mark_dirty();
137        return EventResult::Consumed;
138    }
139    if before_child != crate::animation::invalidation_epoch() {
140        root.mark_dirty();
141    }
142    // Bubble: deliver to this widget too (with original pos_in_root coords).
143    let before_self = crate::animation::invalidation_epoch();
144    let result = root.on_event(event);
145    if result == EventResult::Consumed || before_self != crate::animation::invalidation_epoch() {
146        root.mark_dirty();
147    }
148    result
149}
150
151/// Give visible widgets a chance to handle a key ignored by the focused path.
152///
153/// Traverses in reverse paint order so topmost windows/menu bars win.
154pub fn dispatch_unconsumed_key(
155    widget: &mut dyn Widget,
156    key: &Key,
157    modifiers: Modifiers,
158) -> EventResult {
159    if !widget.is_visible() {
160        return EventResult::Ignored;
161    }
162    for child in widget.children_mut().iter_mut().rev() {
163        if dispatch_unconsumed_key(child.as_mut(), key, modifiers) == EventResult::Consumed {
164            widget.mark_dirty();
165            return EventResult::Consumed;
166        }
167    }
168    let before = crate::animation::invalidation_epoch();
169    let result = widget.on_unconsumed_key(key, modifiers);
170    if result == EventResult::Consumed || before != crate::animation::invalidation_epoch() {
171        widget.mark_dirty();
172    }
173    result
174}
175
176/// Produce a version of `event` with mouse positions replaced by `new_pos`.
177/// Non-mouse events (key, focus) are returned unchanged.
178fn translate_event(event: &Event, new_pos: Point) -> Event {
179    match event {
180        Event::MouseMove { .. } => Event::MouseMove { pos: new_pos },
181        Event::MouseDown {
182            button, modifiers, ..
183        } => Event::MouseDown {
184            pos: new_pos,
185            button: *button,
186            modifiers: *modifiers,
187        },
188        Event::MouseUp {
189            button, modifiers, ..
190        } => Event::MouseUp {
191            pos: new_pos,
192            button: *button,
193            modifiers: *modifiers,
194        },
195        Event::MouseWheel {
196            delta_y,
197            delta_x,
198            modifiers,
199            ..
200        } => Event::MouseWheel {
201            pos: new_pos,
202            delta_y: *delta_y,
203            delta_x: *delta_x,
204            modifiers: *modifiers,
205        },
206        other => other.clone(),
207    }
208}
209
210// ---------------------------------------------------------------------------
211// Inspector support
212// ---------------------------------------------------------------------------
213
214/// Flat snapshot of one widget for the inspector panel.
215#[derive(Clone)]
216pub struct InspectorNode {
217    pub type_name: &'static str,
218    /// Absolute screen bounds (Y-up), accumulated as the tree is walked.
219    pub screen_bounds: Rect,
220    /// Outer margin in logical units (per-side).  Drawn as the orange band
221    /// outside `screen_bounds` in the Chrome F12-style hover overlay.
222    pub margin: crate::layout_props::Insets,
223    /// Inner padding in logical units (per-side) — only nonzero on container
224    /// widgets that override [`Widget::padding`].  Drawn as the green band
225    /// inset from `screen_bounds`.
226    pub padding: crate::layout_props::Insets,
227    /// Horizontal anchor from the widget's `WidgetBase`, if present.
228    pub h_anchor: crate::layout_props::HAnchor,
229    /// Vertical anchor from the widget's `WidgetBase`, if present.
230    pub v_anchor: crate::layout_props::VAnchor,
231    pub depth: usize,
232    /// Path of child indices from the App root to this widget.  Used by the
233    /// inspector's live-editing pipeline to walk back to the live widget and
234    /// apply a reflected edit.  Empty for the root.
235    pub path: Vec<usize>,
236    /// Type-specific display properties from [`Widget::properties`].
237    pub properties: Vec<(&'static str, String)>,
238}
239
240/// Walk a reflected struct's fields and produce `(name, display)` pairs
241/// suitable for the inspector's property pane.  Public so callers can build
242/// the same typed dump for ad-hoc reflectable values (e.g. a debug hover
243/// inspector outside the widget tree).
244#[cfg(feature = "reflect")]
245pub fn reflect_fields(reflected: &dyn bevy_reflect::Reflect) -> Vec<(&'static str, String)> {
246    use bevy_reflect::{ReflectRef, TypeInfo};
247    let mut out = Vec::new();
248    if let ReflectRef::Struct(s) = reflected.reflect_ref() {
249        // The TypeInfo of the struct gives us field NAMES with `'static`
250        // lifetime — required because `InspectorNode::properties` is
251        // `Vec<(&'static str, String)>`.  Falling back to indexed names
252        // ("field_0") for unrepresented info keeps the dump alive even on
253        // tuple structs that don't carry named fields.
254        let names: Vec<&'static str> = if let Some(TypeInfo::Struct(info)) =
255            reflected.get_represented_type_info()
256        {
257            (0..s.field_len())
258                .map(|i| info.field_at(i).map(|f| f.name()).unwrap_or(""))
259                .collect()
260        } else {
261            vec![""; s.field_len()]
262        };
263        for i in 0..s.field_len() {
264            let name = names.get(i).copied().unwrap_or("");
265            if name.is_empty() {
266                continue;
267            }
268            if let Some(field) = s.field_at(i) {
269                out.push((name, format_reflect_value(field)));
270            }
271        }
272    }
273    out
274}
275
276#[cfg(feature = "reflect")]
277fn format_reflect_value(value: &dyn bevy_reflect::PartialReflect) -> String {
278    // Try common primitive types first for clean output, then fall back to
279    // `Debug` via `reflect_short_type_path`.  bevy_reflect's `Debug` impl
280    // for arbitrary reflected values produces verbose "Reflected(..)" style
281    // output — bypass it for the types the inspector sees on a typical frame.
282    if let Some(v) = value.try_downcast_ref::<bool>() {
283        return v.to_string();
284    }
285    if let Some(v) = value.try_downcast_ref::<f64>() {
286        return format!("{v:.3}");
287    }
288    if let Some(v) = value.try_downcast_ref::<f32>() {
289        return format!("{v:.3}");
290    }
291    if let Some(v) = value.try_downcast_ref::<i32>() {
292        return v.to_string();
293    }
294    if let Some(v) = value.try_downcast_ref::<u32>() {
295        return v.to_string();
296    }
297    if let Some(v) = value.try_downcast_ref::<usize>() {
298        return v.to_string();
299    }
300    if let Some(v) = value.try_downcast_ref::<String>() {
301        return format!("\"{v}\"");
302    }
303    if let Some(v) = value.try_downcast_ref::<crate::color::Color>() {
304        return format!(
305            "rgba({:.2}, {:.2}, {:.2}, {:.2})",
306            v.r, v.g, v.b, v.a
307        );
308    }
309    // Generic fallback: `Debug`-print the reflected value.
310    format!("{value:?}")
311}
312
313/// Snapshot pushed to the platform render loop so the host can draw a
314/// Chrome F12-style three-band overlay (margin + bounds + padding) around
315/// the widget the inspector is hovering.
316#[derive(Clone, Copy, Debug, PartialEq)]
317pub struct InspectorOverlay {
318    pub bounds: Rect,
319    pub margin: crate::layout_props::Insets,
320    pub padding: crate::layout_props::Insets,
321}
322
323// ── Global mouse-world-pos (for nested drags that can't use widget-
324//    local coords because ancestor layout shifts under them each frame) ─────
325
326thread_local! {
327    static CURRENT_MOUSE_WORLD: std::cell::Cell<Option<Point>> =
328        std::cell::Cell::new(None);
329    static CURRENT_VIEWPORT: std::cell::Cell<Size> =
330        std::cell::Cell::new(Size::new(1.0, 1.0));
331}
332
333/// Record the current mouse cursor position in app-level (world / Y-up
334/// logical) coordinates.  Called by `App`'s mouse entry points.
335pub fn set_current_mouse_world(p: Point) {
336    CURRENT_MOUSE_WORLD.with(|c| c.set(Some(p)));
337}
338
339/// Retrieve the latest world-space mouse position.  Widgets doing a
340/// drag gesture that needs invariance against ancestor-layout shifts
341/// (e.g. a nested `Resize` inside an auto-sized `Window`, where the
342/// window grows/shrinks as the user drags and moves the widget's
343/// ancestor frame) should prefer this over the widget-local `pos`
344/// carried in `Event::Mouse*`.
345pub fn current_mouse_world() -> Option<Point> {
346    CURRENT_MOUSE_WORLD.with(|c| c.get())
347}
348
349/// Record the current app-level viewport in logical Y-up coordinates.
350pub fn set_current_viewport(s: Size) {
351    CURRENT_VIEWPORT.with(|c| c.set(s));
352}
353
354/// Retrieve the latest app-level viewport in logical coordinates.
355pub fn current_viewport() -> Size {
356    CURRENT_VIEWPORT.with(|c| c.get())
357}
358
359/// Depth-first search the subtree rooted at `widget` for one whose
360/// [`Widget::id`] matches `id`.  Returns the first match in paint order,
361/// including `widget` itself.  Used primarily by tests to locate a
362/// specific `Window` by its title without knowing the tree shape.
363pub fn find_widget_by_id<'a>(widget: &'a dyn Widget, id: &str) -> Option<&'a dyn Widget> {
364    if widget.id() == Some(id) {
365        return Some(widget);
366    }
367    for child in widget.children() {
368        if let Some(found) = find_widget_by_id(child.as_ref(), id) {
369            return Some(found);
370        }
371    }
372    None
373}
374
375/// Mutable counterpart to [`find_widget_by_id`].  Required when a test
376/// needs to poke at a sub-widget's mutable state (e.g. calling a
377/// `ScrollView::set_scroll_offset`) after finding it by id.
378pub fn find_widget_by_id_mut<'a>(
379    widget: &'a mut dyn Widget,
380    id: &str,
381) -> Option<&'a mut dyn Widget> {
382    if widget.id() == Some(id) {
383        return Some(widget);
384    }
385    for child in widget.children_mut().iter_mut() {
386        if let Some(found) = find_widget_by_id_mut(child.as_mut(), id) {
387            return Some(found);
388        }
389    }
390    None
391}
392
393/// Depth-first search for a widget by its [`Widget::type_name`].  Returns
394/// the first match in paint order.  Used by tests that want to assert on
395/// a specific widget kind inside an opaque content subtree (e.g.
396/// "find the ScrollView inside this window").
397pub fn find_widget_by_type<'a>(widget: &'a dyn Widget, type_name: &str) -> Option<&'a dyn Widget> {
398    if widget.type_name() == type_name {
399        return Some(widget);
400    }
401    for child in widget.children() {
402        if let Some(found) = find_widget_by_type(child.as_ref(), type_name) {
403            return Some(found);
404        }
405    }
406    None
407}
408
409/// Walk the subtree rooted at `widget` and collect an `InspectorNode` per
410/// widget in DFS paint order (root first).
411///
412/// `screen_origin` is the accumulated parent offset in screen Y-up coords.
413pub fn collect_inspector_nodes(
414    widget: &dyn Widget,
415    depth: usize,
416    screen_origin: Point,
417    out: &mut Vec<InspectorNode>,
418) {
419    collect_inspector_nodes_with_path(widget, depth, screen_origin, &[], out);
420}
421
422fn collect_inspector_nodes_with_path(
423    widget: &dyn Widget,
424    depth: usize,
425    screen_origin: Point,
426    path_prefix: &[usize],
427    out: &mut Vec<InspectorNode>,
428) {
429    // Invisible widgets (and their entire subtrees) are excluded from the
430    // inspector — they are not part of the live rendered scene.
431    if !widget.is_visible() {
432        return;
433    }
434    // Utility widgets opt out of the inspector entirely.
435    if !widget.show_in_inspector() {
436        return;
437    }
438
439    let b = widget.bounds();
440    let abs = Rect::new(
441        screen_origin.x + b.x,
442        screen_origin.y + b.y,
443        b.width,
444        b.height,
445    );
446    // Build the properties vec — include the universal `backbuffer` flag
447    // first (so every widget shows it in a consistent location), then the
448    // widget-specific properties.
449    let mut props = vec![(
450        "backbuffer",
451        if widget.has_backbuffer() {
452            "true".to_string()
453        } else {
454            "false".to_string()
455        },
456    )];
457    props.extend(widget.properties());
458    // Reflection-driven property dump.  Widgets that opt into the
459    // companion-props pattern (`Widget::as_reflect`) get their reflected
460    // struct fields surfaced here as `(name, formatted)` pairs — typed,
461    // accurate, and free of the hand-maintained `properties()` strings
462    // they would otherwise need.  Fields that aren't a struct, or that
463    // can't be displayed, are silently skipped.
464    #[cfg(feature = "reflect")]
465    if let Some(reflected) = widget.as_reflect() {
466        props.extend(reflect_fields(reflected));
467    }
468    let (h_anchor, v_anchor) = widget
469        .widget_base()
470        .map(|b| (b.h_anchor, b.v_anchor))
471        .unwrap_or((
472            crate::layout_props::HAnchor::FIT,
473            crate::layout_props::VAnchor::FIT,
474        ));
475    out.push(InspectorNode {
476        type_name: widget.type_name(),
477        screen_bounds: abs,
478        margin: widget.margin(),
479        padding: widget.padding(),
480        h_anchor,
481        v_anchor,
482        depth,
483        path: path_prefix.to_vec(),
484        properties: props,
485    });
486
487    // Widgets that are part of the inspector infrastructure opt out of child
488    // recursion to prevent the inspector from growing its own node list every
489    // frame (exponential growth).  Their sub-trees are still visible in the
490    // inspector on the next frame through the normal layout snapshot.
491    if !widget.contributes_children_to_inspector() {
492        return;
493    }
494
495    let child_origin = Point::new(abs.x, abs.y);
496    let mut child_path: Vec<usize> = Vec::with_capacity(path_prefix.len() + 1);
497    child_path.extend_from_slice(path_prefix);
498    child_path.push(0);
499    for (i, child) in widget.children().iter().enumerate() {
500        *child_path.last_mut().unwrap() = i;
501        collect_inspector_nodes_with_path(
502            child.as_ref(),
503            depth + 1,
504            child_origin,
505            &child_path,
506            out,
507        );
508    }
509}
510
511/// Walk the widget tree from `root` along `path` and return the deepest
512/// reachable widget as a mutable reference.  Returns `None` if the path
513/// indexes past the available children at any level — useful when the path
514/// is stale (e.g. the tree shape changed since the inspector snapshot).
515pub fn walk_path_mut<'a>(
516    root: &'a mut dyn Widget,
517    path: &[usize],
518) -> Option<&'a mut dyn Widget> {
519    let mut node: &mut dyn Widget = root;
520    for &idx in path {
521        let children = node.children_mut();
522        if idx >= children.len() {
523            return None;
524        }
525        node = children[idx].as_mut();
526    }
527    Some(node)
528}
529
530/// A pending inspector edit: navigate to the widget at `path`, look up
531/// `field_path` via reflection, and apply `new_value`.
532///
533/// Edits are queued by the inspector and drained by the host frame loop —
534/// applying them mid-paint or mid-event-dispatch could violate borrow rules
535/// or layout invariants.
536#[cfg(feature = "reflect")]
537pub struct InspectorEdit {
538    pub path: Vec<usize>,
539    /// Reflection path inside the target widget's `as_reflect` value, e.g.
540    /// `"checked"` or `"value"` or `"margin.left"`.
541    pub field_path: String,
542    /// Replacement value, already type-correct for the target field.
543    pub new_value: Box<dyn bevy_reflect::PartialReflect>,
544}
545
546#[cfg(feature = "reflect")]
547impl std::fmt::Debug for InspectorEdit {
548    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
549        f.debug_struct("InspectorEdit")
550            .field("path", &self.path)
551            .field("field_path", &self.field_path)
552            .finish_non_exhaustive()
553    }
554}
555
556// ---------------------------------------------------------------------------
557// WidgetBase live editing (no reflect feature required)
558// ---------------------------------------------------------------------------
559
560/// One field in a widget's [`crate::layout_props::WidgetBase`] that the
561/// inspector can change at runtime.
562#[derive(Clone, Debug)]
563pub enum WidgetBaseField {
564    MarginLeft(f64),
565    MarginRight(f64),
566    MarginTop(f64),
567    MarginBottom(f64),
568    HAnchor(crate::layout_props::HAnchor),
569    VAnchor(crate::layout_props::VAnchor),
570    MinWidth(f64),
571    MinHeight(f64),
572    MaxWidth(f64),
573    MaxHeight(f64),
574}
575
576/// Queued mutation for a widget's `WidgetBase`.  The inspector pushes these;
577/// the host frame loop drains and applies via [`apply_widget_base_edit`].
578#[derive(Clone, Debug)]
579pub struct WidgetBaseEdit {
580    /// Path of child indices from the App root to the target widget.
581    pub path: Vec<usize>,
582    pub field: WidgetBaseField,
583}
584
585/// Apply a single queued `WidgetBaseEdit` against the live widget tree.
586/// Returns `true` when the edit landed, `false` if the path was stale or the
587/// widget does not expose a `WidgetBase`.
588pub fn apply_widget_base_edit(root: &mut dyn Widget, edit: &WidgetBaseEdit) -> bool {
589    let Some(target) = walk_path_mut(root, &edit.path) else {
590        return false;
591    };
592    let Some(base) = target.widget_base_mut() else {
593        return false;
594    };
595    match &edit.field {
596        WidgetBaseField::MarginLeft(v) => base.margin.left = *v,
597        WidgetBaseField::MarginRight(v) => base.margin.right = *v,
598        WidgetBaseField::MarginTop(v) => base.margin.top = *v,
599        WidgetBaseField::MarginBottom(v) => base.margin.bottom = *v,
600        WidgetBaseField::HAnchor(a) => base.h_anchor = *a,
601        WidgetBaseField::VAnchor(a) => base.v_anchor = *a,
602        WidgetBaseField::MinWidth(v) => base.min_size.width = v.max(0.0),
603        WidgetBaseField::MinHeight(v) => base.min_size.height = v.max(0.0),
604        WidgetBaseField::MaxWidth(v) => base.max_size.width = v.max(0.0),
605        WidgetBaseField::MaxHeight(v) => base.max_size.height = v.max(0.0),
606    }
607    target.mark_dirty();
608    crate::animation::request_draw();
609    true
610}
611
612/// Apply a single queued inspector edit against the live widget tree.
613/// Returns `true` if the edit landed; `false` if the path was stale or the
614/// field path didn't resolve.
615#[cfg(feature = "reflect")]
616pub fn apply_inspector_edit(root: &mut dyn Widget, edit: &InspectorEdit) -> bool {
617    use bevy_reflect::{GetPath, PartialReflect};
618    let Some(target) = walk_path_mut(root, &edit.path) else {
619        return false;
620    };
621    let applied;
622    {
623        let Some(reflected) = target.as_reflect_mut() else {
624            return false;
625        };
626        let Ok(field) = reflected.reflect_path_mut(edit.field_path.as_str()) else {
627            return false;
628        };
629        let field: &mut dyn PartialReflect = field;
630        applied = field.try_apply(edit.new_value.as_ref()).is_ok();
631    }
632    // Reflection bypasses the widget's setters, which is where cache
633    // invalidation normally happens (e.g. Label::set_text).  Hand the
634    // widget a single-shot dirty signal so the next paint re-rasterises.
635    if applied {
636        target.mark_dirty();
637        crate::animation::request_draw();
638    }
639    applied
640}