Skip to main content

agg_gui/widget/
tree_inspector.rs

1//! Inspector + reflection support for the widget tree: flat tree
2//! snapshots for the F12-style inspector panel, reflection-driven field
3//! dumps, id/type widget lookup, and inspector-originated edits.
4//!
5//! Split out of `tree.rs` (traversal + event dispatch) to keep both
6//! files under the 800-line cap.
7
8use super::*;
9
10/// Flat snapshot of one widget for the inspector panel.
11#[derive(Clone)]
12pub struct InspectorNode {
13    pub type_name: &'static str,
14    /// Absolute screen bounds (Y-up), accumulated as the tree is walked.
15    pub screen_bounds: Rect,
16    /// Outer margin in logical units (per-side).  Drawn as the orange band
17    /// outside `screen_bounds` in the Chrome F12-style hover overlay.
18    pub margin: crate::layout_props::Insets,
19    /// Inner padding in logical units (per-side) — only nonzero on container
20    /// widgets that override [`Widget::padding`].  Drawn as the green band
21    /// inset from `screen_bounds`.
22    pub padding: crate::layout_props::Insets,
23    /// Horizontal anchor from the widget's `WidgetBase`, if present.
24    pub h_anchor: crate::layout_props::HAnchor,
25    /// Vertical anchor from the widget's `WidgetBase`, if present.
26    pub v_anchor: crate::layout_props::VAnchor,
27    pub depth: usize,
28    /// Path of child indices from the App root to this widget.  Used by the
29    /// inspector's live-editing pipeline to walk back to the live widget and
30    /// apply a reflected edit.  Empty for the root.
31    pub path: Vec<usize>,
32    /// Type-specific display properties from [`Widget::properties`].
33    pub properties: Vec<(&'static str, String)>,
34}
35
36/// Walk a reflected struct's fields and produce `(name, display)` pairs
37/// suitable for the inspector's property pane.  Public so callers can build
38/// the same typed dump for ad-hoc reflectable values (e.g. a debug hover
39/// inspector outside the widget tree).
40#[cfg(feature = "reflect")]
41pub fn reflect_fields(reflected: &dyn bevy_reflect::Reflect) -> Vec<(&'static str, String)> {
42    use bevy_reflect::{ReflectRef, TypeInfo};
43    let mut out = Vec::new();
44    if let ReflectRef::Struct(s) = reflected.reflect_ref() {
45        // The TypeInfo of the struct gives us field NAMES with `'static`
46        // lifetime — required because `InspectorNode::properties` is
47        // `Vec<(&'static str, String)>`.  Falling back to indexed names
48        // ("field_0") for unrepresented info keeps the dump alive even on
49        // tuple structs that don't carry named fields.
50        let names: Vec<&'static str> =
51            if let Some(TypeInfo::Struct(info)) = reflected.get_represented_type_info() {
52                (0..s.field_len())
53                    .map(|i| info.field_at(i).map(|f| f.name()).unwrap_or(""))
54                    .collect()
55            } else {
56                vec![""; s.field_len()]
57            };
58        for i in 0..s.field_len() {
59            let name = names.get(i).copied().unwrap_or("");
60            if name.is_empty() {
61                continue;
62            }
63            if let Some(field) = s.field_at(i) {
64                out.push((name, format_reflect_value(field)));
65            }
66        }
67    }
68    out
69}
70
71#[cfg(feature = "reflect")]
72fn format_reflect_value(value: &dyn bevy_reflect::PartialReflect) -> String {
73    // Try common primitive types first for clean output, then fall back to
74    // `Debug` via `reflect_short_type_path`.  bevy_reflect's `Debug` impl
75    // for arbitrary reflected values produces verbose "Reflected(..)" style
76    // output — bypass it for the types the inspector sees on a typical frame.
77    if let Some(v) = value.try_downcast_ref::<bool>() {
78        return v.to_string();
79    }
80    if let Some(v) = value.try_downcast_ref::<f64>() {
81        return format!("{v:.3}");
82    }
83    if let Some(v) = value.try_downcast_ref::<f32>() {
84        return format!("{v:.3}");
85    }
86    if let Some(v) = value.try_downcast_ref::<i32>() {
87        return v.to_string();
88    }
89    if let Some(v) = value.try_downcast_ref::<u32>() {
90        return v.to_string();
91    }
92    if let Some(v) = value.try_downcast_ref::<usize>() {
93        return v.to_string();
94    }
95    if let Some(v) = value.try_downcast_ref::<String>() {
96        return format!("\"{v}\"");
97    }
98    if let Some(v) = value.try_downcast_ref::<crate::color::Color>() {
99        return format!("rgba({:.2}, {:.2}, {:.2}, {:.2})", v.r, v.g, v.b, v.a);
100    }
101    // Generic fallback: `Debug`-print the reflected value.
102    format!("{value:?}")
103}
104
105/// Snapshot pushed to the platform render loop so the host can draw a
106/// Chrome F12-style three-band overlay (margin + bounds + padding) around
107/// the widget the inspector is hovering.
108#[derive(Clone, Copy, Debug, PartialEq)]
109pub struct InspectorOverlay {
110    pub bounds: Rect,
111    pub margin: crate::layout_props::Insets,
112    pub padding: crate::layout_props::Insets,
113}
114
115// ── Global mouse-world-pos (for nested drags that can't use widget-
116//    local coords because ancestor layout shifts under them each frame) ─────
117
118thread_local! {
119    static CURRENT_MOUSE_WORLD: std::cell::Cell<Option<Point>> =
120        std::cell::Cell::new(None);
121    static CURRENT_VIEWPORT: std::cell::Cell<Size> =
122        std::cell::Cell::new(Size::new(1.0, 1.0));
123}
124
125/// Record the current mouse cursor position in app-level (world / Y-up
126/// logical) coordinates.  Called by `App`'s mouse entry points.
127pub fn set_current_mouse_world(p: Point) {
128    CURRENT_MOUSE_WORLD.with(|c| c.set(Some(p)));
129}
130
131/// Retrieve the latest world-space mouse position.  Widgets doing a
132/// drag gesture that needs invariance against ancestor-layout shifts
133/// (e.g. a nested `Resize` inside an auto-sized `Window`, where the
134/// window grows/shrinks as the user drags and moves the widget's
135/// ancestor frame) should prefer this over the widget-local `pos`
136/// carried in `Event::Mouse*`.
137pub fn current_mouse_world() -> Option<Point> {
138    CURRENT_MOUSE_WORLD.with(|c| c.get())
139}
140
141/// Record the current app-level viewport in logical Y-up coordinates.
142pub fn set_current_viewport(s: Size) {
143    CURRENT_VIEWPORT.with(|c| c.set(s));
144}
145
146/// Retrieve the latest app-level viewport in logical coordinates.
147pub fn current_viewport() -> Size {
148    CURRENT_VIEWPORT.with(|c| c.get())
149}
150
151/// Depth-first search the subtree rooted at `widget` for one whose
152/// [`Widget::id`] matches `id`.  Returns the first match in paint order,
153/// including `widget` itself.  Used primarily by tests to locate a
154/// specific `Window` by its title without knowing the tree shape.
155pub fn find_widget_by_id<'a>(widget: &'a dyn Widget, id: &str) -> Option<&'a dyn Widget> {
156    if widget.id() == Some(id) {
157        return Some(widget);
158    }
159    for child in widget.children() {
160        if let Some(found) = find_widget_by_id(child.as_ref(), id) {
161            return Some(found);
162        }
163    }
164    None
165}
166
167/// Mutable counterpart to [`find_widget_by_id`].  Required when a test
168/// needs to poke at a sub-widget's mutable state (e.g. calling a
169/// `ScrollView::set_scroll_offset`) after finding it by id.
170pub fn find_widget_by_id_mut<'a>(
171    widget: &'a mut dyn Widget,
172    id: &str,
173) -> Option<&'a mut dyn Widget> {
174    if widget.id() == Some(id) {
175        return Some(widget);
176    }
177    for child in widget.children_mut().iter_mut() {
178        if let Some(found) = find_widget_by_id_mut(child.as_mut(), id) {
179            return Some(found);
180        }
181    }
182    None
183}
184
185/// Depth-first search for a widget by its [`Widget::type_name`].  Returns
186/// the first match in paint order.  Used by tests that want to assert on
187/// a specific widget kind inside an opaque content subtree (e.g.
188/// "find the ScrollView inside this window").
189pub fn find_widget_by_type<'a>(widget: &'a dyn Widget, type_name: &str) -> Option<&'a dyn Widget> {
190    if widget.type_name() == type_name {
191        return Some(widget);
192    }
193    for child in widget.children() {
194        if let Some(found) = find_widget_by_type(child.as_ref(), type_name) {
195            return Some(found);
196        }
197    }
198    None
199}
200
201/// Walk the subtree rooted at `widget` and collect an `InspectorNode` per
202/// widget in DFS paint order (root first).
203///
204/// `screen_origin` is the accumulated parent offset in screen Y-up coords.
205/// Widgets that apply additional transforms between themselves and their
206/// children (NodeEditor's pan/zoom, etc.) opt in via
207/// [`Widget::inspector_child_transform`]; the traversal composes those
208/// transforms so descendant `screen_bounds` reflect what the user sees.
209pub fn collect_inspector_nodes(
210    widget: &dyn Widget,
211    depth: usize,
212    screen_origin: Point,
213    out: &mut Vec<InspectorNode>,
214) {
215    let mut parent_to_screen = crate::TransAffine::new();
216    parent_to_screen.translate(screen_origin.x, screen_origin.y);
217    collect_inspector_nodes_with_path(widget, depth, &parent_to_screen, &[], out);
218}
219
220/// AABB of `rect`'s four corners after passing through `t` — equals
221/// `t(rect)` exactly for translation + uniform scale (no rotation), and
222/// is the right conservative bound otherwise.
223fn transform_rect_aabb(t: &crate::TransAffine, rect: Rect) -> Rect {
224    let corners = [
225        (rect.x, rect.y),
226        (rect.x + rect.width, rect.y),
227        (rect.x, rect.y + rect.height),
228        (rect.x + rect.width, rect.y + rect.height),
229    ];
230    let mut min_x = f64::INFINITY;
231    let mut min_y = f64::INFINITY;
232    let mut max_x = f64::NEG_INFINITY;
233    let mut max_y = f64::NEG_INFINITY;
234    for (mut x, mut y) in corners {
235        t.transform(&mut x, &mut y);
236        if x < min_x {
237            min_x = x;
238        }
239        if x > max_x {
240            max_x = x;
241        }
242        if y < min_y {
243            min_y = y;
244        }
245        if y > max_y {
246            max_y = y;
247        }
248    }
249    Rect::new(
250        min_x,
251        min_y,
252        (max_x - min_x).max(0.0),
253        (max_y - min_y).max(0.0),
254    )
255}
256
257/// Effective uniform scale extracted from `t` — used to scale logical
258/// margin / padding insets so the F12-style overlay bands stay
259/// visually proportional to the (scaled) widget bounds.  For pure
260/// translation + uniform scale this returns the scale exactly; under
261/// non-uniform scale it returns the geometric mean of the axes, which
262/// is the right "single number" for matching the visible band width.
263fn effective_scale(t: &crate::TransAffine) -> f64 {
264    let sx = (t.sx * t.sx + t.shy * t.shy).sqrt();
265    let sy = (t.shx * t.shx + t.sy * t.sy).sqrt();
266    if sx > 0.0 && sy > 0.0 {
267        (sx * sy).sqrt()
268    } else {
269        1.0
270    }
271}
272
273fn collect_inspector_nodes_with_path(
274    widget: &dyn Widget,
275    depth: usize,
276    parent_to_screen: &crate::TransAffine,
277    path_prefix: &[usize],
278    out: &mut Vec<InspectorNode>,
279) {
280    // Invisible widgets (and their entire subtrees) are excluded from the
281    // inspector — they are not part of the live rendered scene.
282    if !widget.is_visible() {
283        return;
284    }
285    // Utility widgets opt out of the inspector entirely.
286    if !widget.show_in_inspector() {
287        return;
288    }
289
290    let b = widget.bounds();
291    // Transform the widget's parent-local bounds rect through the
292    // accumulated parent_to_screen transform.  For the common case
293    // (pure translation, no scale) this collapses to the old
294    // `screen_origin + b.x/y` math; under scale it now correctly
295    // reports the on-screen size.
296    let abs = transform_rect_aabb(parent_to_screen, b);
297    let scale = effective_scale(parent_to_screen);
298    // Build the properties vec — include the universal `backbuffer` flag
299    // first (so every widget shows it in a consistent location), then the
300    // widget-specific properties.
301    let mut props = vec![(
302        "backbuffer",
303        if widget.has_backbuffer() {
304            "true".to_string()
305        } else {
306            "false".to_string()
307        },
308    )];
309    props.extend(widget.properties());
310    // Reflection-driven property dump.  Widgets that opt into the
311    // companion-props pattern (`Widget::as_reflect`) get their reflected
312    // struct fields surfaced here as `(name, formatted)` pairs — typed,
313    // accurate, and free of the hand-maintained `properties()` strings
314    // they would otherwise need.  Fields that aren't a struct, or that
315    // can't be displayed, are silently skipped.
316    #[cfg(feature = "reflect")]
317    if let Some(reflected) = widget.as_reflect() {
318        props.extend(reflect_fields(reflected));
319    }
320    let (h_anchor, v_anchor) = widget
321        .widget_base()
322        .map(|b| (b.h_anchor, b.v_anchor))
323        .unwrap_or((
324            crate::layout_props::HAnchor::FIT,
325            crate::layout_props::VAnchor::FIT,
326        ));
327    let margin_logical = widget.margin();
328    let padding_logical = widget.padding();
329    let scaled_margin = crate::layout_props::Insets {
330        left: margin_logical.left * scale,
331        right: margin_logical.right * scale,
332        top: margin_logical.top * scale,
333        bottom: margin_logical.bottom * scale,
334    };
335    let scaled_padding = crate::layout_props::Insets {
336        left: padding_logical.left * scale,
337        right: padding_logical.right * scale,
338        top: padding_logical.top * scale,
339        bottom: padding_logical.bottom * scale,
340    };
341    out.push(InspectorNode {
342        type_name: widget.type_name(),
343        screen_bounds: abs,
344        margin: scaled_margin,
345        padding: scaled_padding,
346        h_anchor,
347        v_anchor,
348        depth,
349        path: path_prefix.to_vec(),
350        properties: props,
351    });
352
353    // Widgets that are part of the inspector infrastructure opt out of child
354    // recursion to prevent the inspector from growing its own node list every
355    // frame (exponential growth).  Their sub-trees are still visible in the
356    // inspector on the next frame through the normal layout snapshot.
357    if !widget.contributes_children_to_inspector() {
358        return;
359    }
360
361    // Compose the transform for child traversal.  Order matches the
362    // paint pipeline: parent_to_screen ∘ translate(b.x, b.y) ∘
363    // widget.inspector_child_transform().  That is, applied to a point
364    // in child-local space, we first run the widget's own child
365    // transform (e.g. NodeEditor's pan/zoom), then translate by the
366    // widget's bounds offset (its position in its parent), then through
367    // the parent_to_screen chain.
368    let mut child_to_screen = *parent_to_screen;
369    child_to_screen.translate(b.x, b.y);
370    let extra = widget.inspector_child_transform();
371    child_to_screen.premultiply(&extra);
372
373    let mut child_path: Vec<usize> = Vec::with_capacity(path_prefix.len() + 1);
374    child_path.extend_from_slice(path_prefix);
375    child_path.push(0);
376    for (i, child) in widget.children().iter().enumerate() {
377        *child_path.last_mut().unwrap() = i;
378        collect_inspector_nodes_with_path(
379            child.as_ref(),
380            depth + 1,
381            &child_to_screen,
382            &child_path,
383            out,
384        );
385    }
386}
387
388/// Walk the widget tree from `root` along `path` and return the deepest
389/// reachable widget as a mutable reference.  Returns `None` if the path
390/// indexes past the available children at any level — useful when the path
391/// is stale (e.g. the tree shape changed since the inspector snapshot).
392pub fn walk_path_mut<'a>(root: &'a mut dyn Widget, path: &[usize]) -> Option<&'a mut dyn Widget> {
393    let mut node: &mut dyn Widget = root;
394    for &idx in path {
395        let children = node.children_mut();
396        if idx >= children.len() {
397            return None;
398        }
399        node = children[idx].as_mut();
400    }
401    Some(node)
402}
403
404/// A pending inspector edit: navigate to the widget at `path`, look up
405/// `field_path` via reflection, and apply `new_value`.
406///
407/// Edits are queued by the inspector and drained by the host frame loop —
408/// applying them mid-paint or mid-event-dispatch could violate borrow rules
409/// or layout invariants.
410#[cfg(feature = "reflect")]
411pub struct InspectorEdit {
412    pub path: Vec<usize>,
413    /// Reflection path inside the target widget's `as_reflect` value, e.g.
414    /// `"checked"` or `"value"` or `"margin.left"`.
415    pub field_path: String,
416    /// Replacement value, already type-correct for the target field.
417    pub new_value: Box<dyn bevy_reflect::PartialReflect>,
418}
419
420#[cfg(feature = "reflect")]
421impl std::fmt::Debug for InspectorEdit {
422    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
423        f.debug_struct("InspectorEdit")
424            .field("path", &self.path)
425            .field("field_path", &self.field_path)
426            .finish_non_exhaustive()
427    }
428}
429
430// ---------------------------------------------------------------------------
431// WidgetBase live editing (no reflect feature required)
432// ---------------------------------------------------------------------------
433
434/// One field in a widget's [`crate::layout_props::WidgetBase`] that the
435/// inspector can change at runtime.
436#[derive(Clone, Debug)]
437pub enum WidgetBaseField {
438    MarginLeft(f64),
439    MarginRight(f64),
440    MarginTop(f64),
441    MarginBottom(f64),
442    HAnchor(crate::layout_props::HAnchor),
443    VAnchor(crate::layout_props::VAnchor),
444    MinWidth(f64),
445    MinHeight(f64),
446    MaxWidth(f64),
447    MaxHeight(f64),
448}
449
450/// Queued mutation for a widget's `WidgetBase`.  The inspector pushes these;
451/// the host frame loop drains and applies via [`apply_widget_base_edit`].
452#[derive(Clone, Debug)]
453pub struct WidgetBaseEdit {
454    /// Path of child indices from the App root to the target widget.
455    pub path: Vec<usize>,
456    pub field: WidgetBaseField,
457}
458
459/// Apply a single queued `WidgetBaseEdit` against the live widget tree.
460/// Returns `true` when the edit landed, `false` if the path was stale or the
461/// widget does not expose a `WidgetBase`.
462pub fn apply_widget_base_edit(root: &mut dyn Widget, edit: &WidgetBaseEdit) -> bool {
463    let Some(target) = walk_path_mut(root, &edit.path) else {
464        return false;
465    };
466    let Some(base) = target.widget_base_mut() else {
467        return false;
468    };
469    match &edit.field {
470        WidgetBaseField::MarginLeft(v) => base.margin.left = *v,
471        WidgetBaseField::MarginRight(v) => base.margin.right = *v,
472        WidgetBaseField::MarginTop(v) => base.margin.top = *v,
473        WidgetBaseField::MarginBottom(v) => base.margin.bottom = *v,
474        WidgetBaseField::HAnchor(a) => base.h_anchor = *a,
475        WidgetBaseField::VAnchor(a) => base.v_anchor = *a,
476        WidgetBaseField::MinWidth(v) => base.min_size.width = v.max(0.0),
477        WidgetBaseField::MinHeight(v) => base.min_size.height = v.max(0.0),
478        WidgetBaseField::MaxWidth(v) => base.max_size.width = v.max(0.0),
479        WidgetBaseField::MaxHeight(v) => base.max_size.height = v.max(0.0),
480    }
481    target.mark_dirty();
482    crate::animation::request_draw();
483    true
484}
485
486/// Apply a single queued inspector edit against the live widget tree.
487/// Returns `true` if the edit landed; `false` if the path was stale or the
488/// field path didn't resolve.
489#[cfg(feature = "reflect")]
490pub fn apply_inspector_edit(root: &mut dyn Widget, edit: &InspectorEdit) -> bool {
491    use bevy_reflect::{GetPath, PartialReflect};
492    let Some(target) = walk_path_mut(root, &edit.path) else {
493        return false;
494    };
495    let applied;
496    {
497        let Some(reflected) = target.as_reflect_mut() else {
498            return false;
499        };
500        let Ok(field) = reflected.reflect_path_mut(edit.field_path.as_str()) else {
501            return false;
502        };
503        let field: &mut dyn PartialReflect = field;
504        applied = field.try_apply(edit.new_value.as_ref()).is_ok();
505    }
506    // Reflection bypasses the widget's setters, which is where cache
507    // invalidation normally happens (e.g. Label::set_text).  Hand the
508    // widget a single-shot dirty signal so the next paint re-rasterises.
509    if applied {
510        target.mark_dirty();
511        crate::animation::request_draw();
512    }
513    applied
514}