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/// Absolute (screen-space, Y-up) rectangle of the first widget in the
186/// subtree whose [`Widget::id`] matches `id`.
187///
188/// [`find_widget_by_id`] returns the widget itself, and
189/// [`Widget::bounds`] is **parent-local** — many widgets even reset
190/// their own origin to (0, 0) in `layout`, so a caller that wants "where
191/// on the window is this widget" cannot read `bounds()` directly. This
192/// walks the same accumulated transform chain
193/// [`collect_inspector_nodes`] does (translation by each ancestor's
194/// bounds origin, composed with any [`Widget::inspector_child_transform`]),
195/// so the answer matches the inspector overlay's *transform math*.
196///
197/// It differs from the inspector in which subtrees it walks: the
198/// inspector's `show_in_inspector` / `contributes_children_to_inspector`
199/// opt-outs are presentation concerns and are ignored here, but a subtree
200/// whose root reports `!is_visible()` is skipped entirely — a hidden
201/// widget occupies no pixels, so "where on the window is it" has no
202/// answer.
203///
204/// Useful to any host that has to line window-space pixels up with a
205/// widget: screenshot / thumbnail crops, platform overlays (IME, native
206/// video surfaces), and tests asserting on real placement. Returns
207/// `None` when no *visible* widget carries `id`.
208pub fn find_widget_screen_rect(root: &dyn Widget, id: &str) -> Option<Rect> {
209 // The root's own bounds origin is already screen-absolute, so the
210 // walk starts from the identity transform.
211 let root_to_screen = crate::TransAffine::new();
212 find_widget_screen_rect_inner(root, id, &root_to_screen)
213}
214
215fn find_widget_screen_rect_inner(
216 widget: &dyn Widget,
217 id: &str,
218 parent_to_screen: &crate::TransAffine,
219) -> Option<Rect> {
220 if !widget.is_visible() {
221 return None;
222 }
223 let b = widget.bounds();
224 if widget.id() == Some(id) {
225 return Some(transform_rect_aabb(parent_to_screen, b));
226 }
227 let mut child_to_screen = *parent_to_screen;
228 child_to_screen.translate(b.x, b.y);
229 child_to_screen.premultiply(&widget.inspector_child_transform());
230 for child in widget.children() {
231 if let Some(found) = find_widget_screen_rect_inner(child.as_ref(), id, &child_to_screen) {
232 return Some(found);
233 }
234 }
235 None
236}
237
238/// Depth-first search for a widget by its [`Widget::type_name`]. Returns
239/// the first match in paint order. Used by tests that want to assert on
240/// a specific widget kind inside an opaque content subtree (e.g.
241/// "find the ScrollView inside this window").
242pub fn find_widget_by_type<'a>(widget: &'a dyn Widget, type_name: &str) -> Option<&'a dyn Widget> {
243 if widget.type_name() == type_name {
244 return Some(widget);
245 }
246 for child in widget.children() {
247 if let Some(found) = find_widget_by_type(child.as_ref(), type_name) {
248 return Some(found);
249 }
250 }
251 None
252}
253
254/// Walk the subtree rooted at `widget` and collect an `InspectorNode` per
255/// widget in DFS paint order (root first).
256///
257/// `screen_origin` is the accumulated parent offset in screen Y-up coords.
258/// Widgets that apply additional transforms between themselves and their
259/// children (NodeEditor's pan/zoom, etc.) opt in via
260/// [`Widget::inspector_child_transform`]; the traversal composes those
261/// transforms so descendant `screen_bounds` reflect what the user sees.
262pub fn collect_inspector_nodes(
263 widget: &dyn Widget,
264 depth: usize,
265 screen_origin: Point,
266 out: &mut Vec<InspectorNode>,
267) {
268 let mut parent_to_screen = crate::TransAffine::new();
269 parent_to_screen.translate(screen_origin.x, screen_origin.y);
270 collect_inspector_nodes_with_path(widget, depth, &parent_to_screen, &[], out);
271}
272
273/// AABB of `rect`'s four corners after passing through `t` — equals
274/// `t(rect)` exactly for translation + uniform scale (no rotation), and
275/// is the right conservative bound otherwise.
276fn transform_rect_aabb(t: &crate::TransAffine, rect: Rect) -> Rect {
277 let corners = [
278 (rect.x, rect.y),
279 (rect.x + rect.width, rect.y),
280 (rect.x, rect.y + rect.height),
281 (rect.x + rect.width, rect.y + rect.height),
282 ];
283 let mut min_x = f64::INFINITY;
284 let mut min_y = f64::INFINITY;
285 let mut max_x = f64::NEG_INFINITY;
286 let mut max_y = f64::NEG_INFINITY;
287 for (mut x, mut y) in corners {
288 t.transform(&mut x, &mut y);
289 if x < min_x {
290 min_x = x;
291 }
292 if x > max_x {
293 max_x = x;
294 }
295 if y < min_y {
296 min_y = y;
297 }
298 if y > max_y {
299 max_y = y;
300 }
301 }
302 Rect::new(
303 min_x,
304 min_y,
305 (max_x - min_x).max(0.0),
306 (max_y - min_y).max(0.0),
307 )
308}
309
310/// Effective uniform scale extracted from `t` — used to scale logical
311/// margin / padding insets so the F12-style overlay bands stay
312/// visually proportional to the (scaled) widget bounds. For pure
313/// translation + uniform scale this returns the scale exactly; under
314/// non-uniform scale it returns the geometric mean of the axes, which
315/// is the right "single number" for matching the visible band width.
316fn effective_scale(t: &crate::TransAffine) -> f64 {
317 let sx = (t.sx * t.sx + t.shy * t.shy).sqrt();
318 let sy = (t.shx * t.shx + t.sy * t.sy).sqrt();
319 if sx > 0.0 && sy > 0.0 {
320 (sx * sy).sqrt()
321 } else {
322 1.0
323 }
324}
325
326fn collect_inspector_nodes_with_path(
327 widget: &dyn Widget,
328 depth: usize,
329 parent_to_screen: &crate::TransAffine,
330 path_prefix: &[usize],
331 out: &mut Vec<InspectorNode>,
332) {
333 // Invisible widgets (and their entire subtrees) are excluded from the
334 // inspector — they are not part of the live rendered scene.
335 if !widget.is_visible() {
336 return;
337 }
338 // Utility widgets opt out of the inspector entirely.
339 if !widget.show_in_inspector() {
340 return;
341 }
342
343 let b = widget.bounds();
344 // Transform the widget's parent-local bounds rect through the
345 // accumulated parent_to_screen transform. For the common case
346 // (pure translation, no scale) this collapses to the old
347 // `screen_origin + b.x/y` math; under scale it now correctly
348 // reports the on-screen size.
349 let abs = transform_rect_aabb(parent_to_screen, b);
350 let scale = effective_scale(parent_to_screen);
351 // Build the properties vec — include the universal `backbuffer` flag
352 // first (so every widget shows it in a consistent location), then the
353 // widget-specific properties.
354 let mut props = vec![(
355 "backbuffer",
356 if widget.has_backbuffer() {
357 "true".to_string()
358 } else {
359 "false".to_string()
360 },
361 )];
362 props.extend(widget.properties());
363 // Reflection-driven property dump. Widgets that opt into the
364 // companion-props pattern (`Widget::as_reflect`) get their reflected
365 // struct fields surfaced here as `(name, formatted)` pairs — typed,
366 // accurate, and free of the hand-maintained `properties()` strings
367 // they would otherwise need. Fields that aren't a struct, or that
368 // can't be displayed, are silently skipped.
369 #[cfg(feature = "reflect")]
370 if let Some(reflected) = widget.as_reflect() {
371 props.extend(reflect_fields(reflected));
372 }
373 let (h_anchor, v_anchor) = widget
374 .widget_base()
375 .map(|b| (b.h_anchor, b.v_anchor))
376 .unwrap_or((
377 crate::layout_props::HAnchor::FIT,
378 crate::layout_props::VAnchor::FIT,
379 ));
380 let margin_logical = widget.margin();
381 let padding_logical = widget.padding();
382 let scaled_margin = crate::layout_props::Insets {
383 left: margin_logical.left * scale,
384 right: margin_logical.right * scale,
385 top: margin_logical.top * scale,
386 bottom: margin_logical.bottom * scale,
387 };
388 let scaled_padding = crate::layout_props::Insets {
389 left: padding_logical.left * scale,
390 right: padding_logical.right * scale,
391 top: padding_logical.top * scale,
392 bottom: padding_logical.bottom * scale,
393 };
394 out.push(InspectorNode {
395 type_name: widget.type_name(),
396 screen_bounds: abs,
397 margin: scaled_margin,
398 padding: scaled_padding,
399 h_anchor,
400 v_anchor,
401 depth,
402 path: path_prefix.to_vec(),
403 properties: props,
404 });
405
406 // Widgets that are part of the inspector infrastructure opt out of child
407 // recursion to prevent the inspector from growing its own node list every
408 // frame (exponential growth). Their sub-trees are still visible in the
409 // inspector on the next frame through the normal layout snapshot.
410 if !widget.contributes_children_to_inspector() {
411 return;
412 }
413
414 // Compose the transform for child traversal. Order matches the
415 // paint pipeline: parent_to_screen ∘ translate(b.x, b.y) ∘
416 // widget.inspector_child_transform(). That is, applied to a point
417 // in child-local space, we first run the widget's own child
418 // transform (e.g. NodeEditor's pan/zoom), then translate by the
419 // widget's bounds offset (its position in its parent), then through
420 // the parent_to_screen chain.
421 let mut child_to_screen = *parent_to_screen;
422 child_to_screen.translate(b.x, b.y);
423 let extra = widget.inspector_child_transform();
424 child_to_screen.premultiply(&extra);
425
426 let mut child_path: Vec<usize> = Vec::with_capacity(path_prefix.len() + 1);
427 child_path.extend_from_slice(path_prefix);
428 child_path.push(0);
429 for (i, child) in widget.children().iter().enumerate() {
430 *child_path.last_mut().unwrap() = i;
431 collect_inspector_nodes_with_path(
432 child.as_ref(),
433 depth + 1,
434 &child_to_screen,
435 &child_path,
436 out,
437 );
438 }
439}
440
441// ---------------------------------------------------------------------------
442// Runaway-repaint diagnostic report
443// ---------------------------------------------------------------------------
444
445/// Recursively collect every *visible* widget whose [`Widget::needs_draw`]
446/// currently returns `true`, recording its child-index path, type name, and
447/// whether it is "self-hot" (no visible child is independently asking for a
448/// draw, so this widget's own state is the driver).
449///
450/// The walk descends into **all** visible children — it does not prune on a
451/// parent whose `needs_draw()` is `false`. That deliberately catches
452/// propagation gaps (a hot child under a container that forgot to OR its
453/// children into `needs_draw`, the exact class of bug behind the intermittent
454/// runaway), which a prune-on-false walk would hide.
455fn collect_needs_draw(
456 widget: &dyn Widget,
457 path: &mut Vec<usize>,
458 out: &mut Vec<(Vec<usize>, &'static str, bool)>,
459) {
460 // Same visibility gate the host loop uses: invisible subtrees never keep
461 // the app awake, so they are irrelevant to a runaway and are skipped.
462 if !widget.is_visible() {
463 return;
464 }
465 if widget.needs_draw() {
466 let child_hot = widget.children().iter().any(|c| c.needs_draw());
467 out.push((path.clone(), widget.type_name(), !child_hot));
468 }
469 for (i, child) in widget.children().iter().enumerate() {
470 path.push(i);
471 collect_needs_draw(child.as_ref(), path, out);
472 path.pop();
473 }
474}
475
476/// Build a human-readable diagnostic naming everything currently keeping the
477/// reactive host awake, for capturing the intermittent "continuous rendering
478/// never quiesces" runaway on real hardware.
479///
480/// The report contains, in order:
481/// 1. the raw immediate-draw flag (read side-effect-free — see
482/// [`crate::animation::peek_draw_signals`], which unlike `wants_draw`
483/// neither pumps async wakeups nor clears a due deadline),
484/// 2. the next scheduled-draw deadline with remaining time (and a `DUE`
485/// marker when already past),
486/// 3. the drained draw-request provenance tags, deduplicated with counts
487/// (always empty in release builds — the trace is debug-only), and
488/// 4. every visible widget whose `needs_draw()` is `true`, as `[path] Type`,
489/// with a `<- self` marker on the ones whose own state (not a child's)
490/// is the driver.
491///
492/// Takes the tree `root` as a parameter so it can be invoked from a host
493/// shell that holds `&App` (`app.root()`), matching the introspection style
494/// of [`collect_inspector_nodes`].
495#[doc(hidden)]
496pub fn debug_draw_report(root: &dyn Widget) -> String {
497 use std::fmt::Write;
498 let mut s = String::new();
499 let (needs_flag, deadline) = crate::animation::peek_draw_signals();
500 let now = web_time::Instant::now();
501
502 let _ = writeln!(s, "== agg-gui draw report ==");
503 let _ = writeln!(s, "immediate needs_draw flag: {needs_flag}");
504 match deadline {
505 Some(when) => {
506 let remaining_ms = when.saturating_duration_since(now).as_secs_f64() * 1000.0;
507 let due = now >= when;
508 let _ = writeln!(
509 s,
510 "next scheduled deadline: {remaining_ms:.1} ms{}",
511 if due { " (DUE)" } else { "" }
512 );
513 }
514 None => {
515 let _ = writeln!(s, "next scheduled deadline: none");
516 }
517 }
518
519 // (c) Drained provenance tags, deduplicated with counts (most-frequent
520 // first). Draining is what the quiescence guard already does; doing it
521 // here means a subsequent report starts from a clean trace.
522 let tags = crate::animation::drain_draw_trace();
523 if tags.is_empty() {
524 let _ = writeln!(
525 s,
526 "draw-trace tags: none (empty in release builds — trace is debug-only)"
527 );
528 } else {
529 let mut counts: Vec<(&'static str, usize)> = Vec::new();
530 for t in tags {
531 if let Some(entry) = counts.iter_mut().find(|(name, _)| *name == t) {
532 entry.1 += 1;
533 } else {
534 counts.push((t, 1));
535 }
536 }
537 counts.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(b.0)));
538 let _ = writeln!(s, "draw-trace tags ({} distinct):", counts.len());
539 for (name, count) in counts {
540 let _ = writeln!(s, " {count:>4} x {name}");
541 }
542 }
543
544 // (d) Every visible widget whose needs_draw() is true.
545 let mut hot: Vec<(Vec<usize>, &'static str, bool)> = Vec::new();
546 collect_needs_draw(root, &mut Vec::new(), &mut hot);
547 if hot.is_empty() {
548 let _ = writeln!(s, "widgets wanting draw: none");
549 } else {
550 let _ = writeln!(s, "widgets wanting draw ({}):", hot.len());
551 for (path, type_name, self_hot) in hot {
552 let path_str = if path.is_empty() {
553 "root".to_string()
554 } else {
555 path.iter()
556 .map(|i| i.to_string())
557 .collect::<Vec<_>>()
558 .join("/")
559 };
560 let _ = writeln!(
561 s,
562 " [{path_str}] {type_name}{}",
563 if self_hot { " <- self" } else { "" }
564 );
565 }
566 }
567
568 s
569}
570
571/// Walk the widget tree from `root` along `path` and return the deepest
572/// reachable widget as a mutable reference. Returns `None` if the path
573/// indexes past the available children at any level — useful when the path
574/// is stale (e.g. the tree shape changed since the inspector snapshot).
575pub fn walk_path_mut<'a>(root: &'a mut dyn Widget, path: &[usize]) -> Option<&'a mut dyn Widget> {
576 let mut node: &mut dyn Widget = root;
577 for &idx in path {
578 let children = node.children_mut();
579 if idx >= children.len() {
580 return None;
581 }
582 node = children[idx].as_mut();
583 }
584 Some(node)
585}
586
587/// A pending inspector edit: navigate to the widget at `path`, look up
588/// `field_path` via reflection, and apply `new_value`.
589///
590/// Edits are queued by the inspector and drained by the host frame loop —
591/// applying them mid-paint or mid-event-dispatch could violate borrow rules
592/// or layout invariants.
593#[cfg(feature = "reflect")]
594pub struct InspectorEdit {
595 pub path: Vec<usize>,
596 /// Reflection path inside the target widget's `as_reflect` value, e.g.
597 /// `"checked"` or `"value"` or `"margin.left"`.
598 pub field_path: String,
599 /// Replacement value, already type-correct for the target field.
600 pub new_value: Box<dyn bevy_reflect::PartialReflect>,
601}
602
603#[cfg(feature = "reflect")]
604impl std::fmt::Debug for InspectorEdit {
605 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
606 f.debug_struct("InspectorEdit")
607 .field("path", &self.path)
608 .field("field_path", &self.field_path)
609 .finish_non_exhaustive()
610 }
611}
612
613// ---------------------------------------------------------------------------
614// WidgetBase live editing (no reflect feature required)
615// ---------------------------------------------------------------------------
616
617/// One field in a widget's [`crate::layout_props::WidgetBase`] that the
618/// inspector can change at runtime.
619#[derive(Clone, Debug)]
620pub enum WidgetBaseField {
621 MarginLeft(f64),
622 MarginRight(f64),
623 MarginTop(f64),
624 MarginBottom(f64),
625 HAnchor(crate::layout_props::HAnchor),
626 VAnchor(crate::layout_props::VAnchor),
627 MinWidth(f64),
628 MinHeight(f64),
629 MaxWidth(f64),
630 MaxHeight(f64),
631}
632
633/// Queued mutation for a widget's `WidgetBase`. The inspector pushes these;
634/// the host frame loop drains and applies via [`apply_widget_base_edit`].
635#[derive(Clone, Debug)]
636pub struct WidgetBaseEdit {
637 /// Path of child indices from the App root to the target widget.
638 pub path: Vec<usize>,
639 pub field: WidgetBaseField,
640}
641
642/// Apply a single queued `WidgetBaseEdit` against the live widget tree.
643/// Returns `true` when the edit landed, `false` if the path was stale or the
644/// widget does not expose a `WidgetBase`.
645pub fn apply_widget_base_edit(root: &mut dyn Widget, edit: &WidgetBaseEdit) -> bool {
646 let Some(target) = walk_path_mut(root, &edit.path) else {
647 return false;
648 };
649 let Some(base) = target.widget_base_mut() else {
650 return false;
651 };
652 match &edit.field {
653 WidgetBaseField::MarginLeft(v) => base.margin.left = *v,
654 WidgetBaseField::MarginRight(v) => base.margin.right = *v,
655 WidgetBaseField::MarginTop(v) => base.margin.top = *v,
656 WidgetBaseField::MarginBottom(v) => base.margin.bottom = *v,
657 WidgetBaseField::HAnchor(a) => base.h_anchor = *a,
658 WidgetBaseField::VAnchor(a) => base.v_anchor = *a,
659 WidgetBaseField::MinWidth(v) => base.min_size.width = v.max(0.0),
660 WidgetBaseField::MinHeight(v) => base.min_size.height = v.max(0.0),
661 WidgetBaseField::MaxWidth(v) => base.max_size.width = v.max(0.0),
662 WidgetBaseField::MaxHeight(v) => base.max_size.height = v.max(0.0),
663 }
664 target.mark_dirty();
665 crate::animation::request_draw();
666 true
667}
668
669/// Apply a single queued inspector edit against the live widget tree.
670/// Returns `true` if the edit landed; `false` if the path was stale or the
671/// field path didn't resolve.
672#[cfg(feature = "reflect")]
673pub fn apply_inspector_edit(root: &mut dyn Widget, edit: &InspectorEdit) -> bool {
674 use bevy_reflect::{GetPath, PartialReflect};
675 let Some(target) = walk_path_mut(root, &edit.path) else {
676 return false;
677 };
678 let applied;
679 {
680 let Some(reflected) = target.as_reflect_mut() else {
681 return false;
682 };
683 let Ok(field) = reflected.reflect_path_mut(edit.field_path.as_str()) else {
684 return false;
685 };
686 let field: &mut dyn PartialReflect = field;
687 applied = field.try_apply(edit.new_value.as_ref()).is_ok();
688 }
689 // Reflection bypasses the widget's setters, which is where cache
690 // invalidation normally happens (e.g. Label::set_text). Hand the
691 // widget a single-shot dirty signal so the next paint re-rasterises.
692 if applied {
693 target.mark_dirty();
694 crate::animation::request_draw();
695 }
696 applied
697}