use super::*;
#[derive(Clone)]
pub struct InspectorNode {
pub type_name: &'static str,
pub screen_bounds: Rect,
pub margin: crate::layout_props::Insets,
pub padding: crate::layout_props::Insets,
pub h_anchor: crate::layout_props::HAnchor,
pub v_anchor: crate::layout_props::VAnchor,
pub depth: usize,
pub path: Vec<usize>,
pub properties: Vec<(&'static str, String)>,
}
#[cfg(feature = "reflect")]
pub fn reflect_fields(reflected: &dyn bevy_reflect::Reflect) -> Vec<(&'static str, String)> {
use bevy_reflect::{ReflectRef, TypeInfo};
let mut out = Vec::new();
if let ReflectRef::Struct(s) = reflected.reflect_ref() {
let names: Vec<&'static str> =
if let Some(TypeInfo::Struct(info)) = reflected.get_represented_type_info() {
(0..s.field_len())
.map(|i| info.field_at(i).map(|f| f.name()).unwrap_or(""))
.collect()
} else {
vec![""; s.field_len()]
};
for i in 0..s.field_len() {
let name = names.get(i).copied().unwrap_or("");
if name.is_empty() {
continue;
}
if let Some(field) = s.field_at(i) {
out.push((name, format_reflect_value(field)));
}
}
}
out
}
#[cfg(feature = "reflect")]
fn format_reflect_value(value: &dyn bevy_reflect::PartialReflect) -> String {
if let Some(v) = value.try_downcast_ref::<bool>() {
return v.to_string();
}
if let Some(v) = value.try_downcast_ref::<f64>() {
return format!("{v:.3}");
}
if let Some(v) = value.try_downcast_ref::<f32>() {
return format!("{v:.3}");
}
if let Some(v) = value.try_downcast_ref::<i32>() {
return v.to_string();
}
if let Some(v) = value.try_downcast_ref::<u32>() {
return v.to_string();
}
if let Some(v) = value.try_downcast_ref::<usize>() {
return v.to_string();
}
if let Some(v) = value.try_downcast_ref::<String>() {
return format!("\"{v}\"");
}
if let Some(v) = value.try_downcast_ref::<crate::color::Color>() {
return format!("rgba({:.2}, {:.2}, {:.2}, {:.2})", v.r, v.g, v.b, v.a);
}
format!("{value:?}")
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct InspectorOverlay {
pub bounds: Rect,
pub margin: crate::layout_props::Insets,
pub padding: crate::layout_props::Insets,
}
thread_local! {
static CURRENT_MOUSE_WORLD: std::cell::Cell<Option<Point>> =
std::cell::Cell::new(None);
static CURRENT_VIEWPORT: std::cell::Cell<Size> =
std::cell::Cell::new(Size::new(1.0, 1.0));
}
pub fn set_current_mouse_world(p: Point) {
CURRENT_MOUSE_WORLD.with(|c| c.set(Some(p)));
}
pub fn current_mouse_world() -> Option<Point> {
CURRENT_MOUSE_WORLD.with(|c| c.get())
}
pub fn set_current_viewport(s: Size) {
CURRENT_VIEWPORT.with(|c| c.set(s));
}
pub fn current_viewport() -> Size {
CURRENT_VIEWPORT.with(|c| c.get())
}
pub fn find_widget_by_id<'a>(widget: &'a dyn Widget, id: &str) -> Option<&'a dyn Widget> {
if widget.id() == Some(id) {
return Some(widget);
}
for child in widget.children() {
if let Some(found) = find_widget_by_id(child.as_ref(), id) {
return Some(found);
}
}
None
}
pub fn find_widget_by_id_mut<'a>(
widget: &'a mut dyn Widget,
id: &str,
) -> Option<&'a mut dyn Widget> {
if widget.id() == Some(id) {
return Some(widget);
}
for child in widget.children_mut().iter_mut() {
if let Some(found) = find_widget_by_id_mut(child.as_mut(), id) {
return Some(found);
}
}
None
}
pub fn find_widget_by_type<'a>(widget: &'a dyn Widget, type_name: &str) -> Option<&'a dyn Widget> {
if widget.type_name() == type_name {
return Some(widget);
}
for child in widget.children() {
if let Some(found) = find_widget_by_type(child.as_ref(), type_name) {
return Some(found);
}
}
None
}
pub fn collect_inspector_nodes(
widget: &dyn Widget,
depth: usize,
screen_origin: Point,
out: &mut Vec<InspectorNode>,
) {
let mut parent_to_screen = crate::TransAffine::new();
parent_to_screen.translate(screen_origin.x, screen_origin.y);
collect_inspector_nodes_with_path(widget, depth, &parent_to_screen, &[], out);
}
fn transform_rect_aabb(t: &crate::TransAffine, rect: Rect) -> Rect {
let corners = [
(rect.x, rect.y),
(rect.x + rect.width, rect.y),
(rect.x, rect.y + rect.height),
(rect.x + rect.width, rect.y + rect.height),
];
let mut min_x = f64::INFINITY;
let mut min_y = f64::INFINITY;
let mut max_x = f64::NEG_INFINITY;
let mut max_y = f64::NEG_INFINITY;
for (mut x, mut y) in corners {
t.transform(&mut x, &mut y);
if x < min_x {
min_x = x;
}
if x > max_x {
max_x = x;
}
if y < min_y {
min_y = y;
}
if y > max_y {
max_y = y;
}
}
Rect::new(
min_x,
min_y,
(max_x - min_x).max(0.0),
(max_y - min_y).max(0.0),
)
}
fn effective_scale(t: &crate::TransAffine) -> f64 {
let sx = (t.sx * t.sx + t.shy * t.shy).sqrt();
let sy = (t.shx * t.shx + t.sy * t.sy).sqrt();
if sx > 0.0 && sy > 0.0 {
(sx * sy).sqrt()
} else {
1.0
}
}
fn collect_inspector_nodes_with_path(
widget: &dyn Widget,
depth: usize,
parent_to_screen: &crate::TransAffine,
path_prefix: &[usize],
out: &mut Vec<InspectorNode>,
) {
if !widget.is_visible() {
return;
}
if !widget.show_in_inspector() {
return;
}
let b = widget.bounds();
let abs = transform_rect_aabb(parent_to_screen, b);
let scale = effective_scale(parent_to_screen);
let mut props = vec![(
"backbuffer",
if widget.has_backbuffer() {
"true".to_string()
} else {
"false".to_string()
},
)];
props.extend(widget.properties());
#[cfg(feature = "reflect")]
if let Some(reflected) = widget.as_reflect() {
props.extend(reflect_fields(reflected));
}
let (h_anchor, v_anchor) = widget
.widget_base()
.map(|b| (b.h_anchor, b.v_anchor))
.unwrap_or((
crate::layout_props::HAnchor::FIT,
crate::layout_props::VAnchor::FIT,
));
let margin_logical = widget.margin();
let padding_logical = widget.padding();
let scaled_margin = crate::layout_props::Insets {
left: margin_logical.left * scale,
right: margin_logical.right * scale,
top: margin_logical.top * scale,
bottom: margin_logical.bottom * scale,
};
let scaled_padding = crate::layout_props::Insets {
left: padding_logical.left * scale,
right: padding_logical.right * scale,
top: padding_logical.top * scale,
bottom: padding_logical.bottom * scale,
};
out.push(InspectorNode {
type_name: widget.type_name(),
screen_bounds: abs,
margin: scaled_margin,
padding: scaled_padding,
h_anchor,
v_anchor,
depth,
path: path_prefix.to_vec(),
properties: props,
});
if !widget.contributes_children_to_inspector() {
return;
}
let mut child_to_screen = *parent_to_screen;
child_to_screen.translate(b.x, b.y);
let extra = widget.inspector_child_transform();
child_to_screen.premultiply(&extra);
let mut child_path: Vec<usize> = Vec::with_capacity(path_prefix.len() + 1);
child_path.extend_from_slice(path_prefix);
child_path.push(0);
for (i, child) in widget.children().iter().enumerate() {
*child_path.last_mut().unwrap() = i;
collect_inspector_nodes_with_path(
child.as_ref(),
depth + 1,
&child_to_screen,
&child_path,
out,
);
}
}
fn collect_needs_draw(
widget: &dyn Widget,
path: &mut Vec<usize>,
out: &mut Vec<(Vec<usize>, &'static str, bool)>,
) {
if !widget.is_visible() {
return;
}
if widget.needs_draw() {
let child_hot = widget.children().iter().any(|c| c.needs_draw());
out.push((path.clone(), widget.type_name(), !child_hot));
}
for (i, child) in widget.children().iter().enumerate() {
path.push(i);
collect_needs_draw(child.as_ref(), path, out);
path.pop();
}
}
#[doc(hidden)]
pub fn debug_draw_report(root: &dyn Widget) -> String {
use std::fmt::Write;
let mut s = String::new();
let (needs_flag, deadline) = crate::animation::peek_draw_signals();
let now = web_time::Instant::now();
let _ = writeln!(s, "== agg-gui draw report ==");
let _ = writeln!(s, "immediate needs_draw flag: {needs_flag}");
match deadline {
Some(when) => {
let remaining_ms = when.saturating_duration_since(now).as_secs_f64() * 1000.0;
let due = now >= when;
let _ = writeln!(
s,
"next scheduled deadline: {remaining_ms:.1} ms{}",
if due { " (DUE)" } else { "" }
);
}
None => {
let _ = writeln!(s, "next scheduled deadline: none");
}
}
let tags = crate::animation::drain_draw_trace();
if tags.is_empty() {
let _ = writeln!(
s,
"draw-trace tags: none (empty in release builds — trace is debug-only)"
);
} else {
let mut counts: Vec<(&'static str, usize)> = Vec::new();
for t in tags {
if let Some(entry) = counts.iter_mut().find(|(name, _)| *name == t) {
entry.1 += 1;
} else {
counts.push((t, 1));
}
}
counts.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(b.0)));
let _ = writeln!(s, "draw-trace tags ({} distinct):", counts.len());
for (name, count) in counts {
let _ = writeln!(s, " {count:>4} x {name}");
}
}
let mut hot: Vec<(Vec<usize>, &'static str, bool)> = Vec::new();
collect_needs_draw(root, &mut Vec::new(), &mut hot);
if hot.is_empty() {
let _ = writeln!(s, "widgets wanting draw: none");
} else {
let _ = writeln!(s, "widgets wanting draw ({}):", hot.len());
for (path, type_name, self_hot) in hot {
let path_str = if path.is_empty() {
"root".to_string()
} else {
path.iter()
.map(|i| i.to_string())
.collect::<Vec<_>>()
.join("/")
};
let _ = writeln!(
s,
" [{path_str}] {type_name}{}",
if self_hot { " <- self" } else { "" }
);
}
}
s
}
pub fn walk_path_mut<'a>(root: &'a mut dyn Widget, path: &[usize]) -> Option<&'a mut dyn Widget> {
let mut node: &mut dyn Widget = root;
for &idx in path {
let children = node.children_mut();
if idx >= children.len() {
return None;
}
node = children[idx].as_mut();
}
Some(node)
}
#[cfg(feature = "reflect")]
pub struct InspectorEdit {
pub path: Vec<usize>,
pub field_path: String,
pub new_value: Box<dyn bevy_reflect::PartialReflect>,
}
#[cfg(feature = "reflect")]
impl std::fmt::Debug for InspectorEdit {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("InspectorEdit")
.field("path", &self.path)
.field("field_path", &self.field_path)
.finish_non_exhaustive()
}
}
#[derive(Clone, Debug)]
pub enum WidgetBaseField {
MarginLeft(f64),
MarginRight(f64),
MarginTop(f64),
MarginBottom(f64),
HAnchor(crate::layout_props::HAnchor),
VAnchor(crate::layout_props::VAnchor),
MinWidth(f64),
MinHeight(f64),
MaxWidth(f64),
MaxHeight(f64),
}
#[derive(Clone, Debug)]
pub struct WidgetBaseEdit {
pub path: Vec<usize>,
pub field: WidgetBaseField,
}
pub fn apply_widget_base_edit(root: &mut dyn Widget, edit: &WidgetBaseEdit) -> bool {
let Some(target) = walk_path_mut(root, &edit.path) else {
return false;
};
let Some(base) = target.widget_base_mut() else {
return false;
};
match &edit.field {
WidgetBaseField::MarginLeft(v) => base.margin.left = *v,
WidgetBaseField::MarginRight(v) => base.margin.right = *v,
WidgetBaseField::MarginTop(v) => base.margin.top = *v,
WidgetBaseField::MarginBottom(v) => base.margin.bottom = *v,
WidgetBaseField::HAnchor(a) => base.h_anchor = *a,
WidgetBaseField::VAnchor(a) => base.v_anchor = *a,
WidgetBaseField::MinWidth(v) => base.min_size.width = v.max(0.0),
WidgetBaseField::MinHeight(v) => base.min_size.height = v.max(0.0),
WidgetBaseField::MaxWidth(v) => base.max_size.width = v.max(0.0),
WidgetBaseField::MaxHeight(v) => base.max_size.height = v.max(0.0),
}
target.mark_dirty();
crate::animation::request_draw();
true
}
#[cfg(feature = "reflect")]
pub fn apply_inspector_edit(root: &mut dyn Widget, edit: &InspectorEdit) -> bool {
use bevy_reflect::{GetPath, PartialReflect};
let Some(target) = walk_path_mut(root, &edit.path) else {
return false;
};
let applied;
{
let Some(reflected) = target.as_reflect_mut() else {
return false;
};
let Ok(field) = reflected.reflect_path_mut(edit.field_path.as_str()) else {
return false;
};
let field: &mut dyn PartialReflect = field;
applied = field.try_apply(edit.new_value.as_ref()).is_ok();
}
if applied {
target.mark_dirty();
crate::animation::request_draw();
}
applied
}