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}