#[derive(Debug, Eq, PartialEq, Hash, Clone)]
pub struct InspectorElementId {
#[cfg(any(feature = "inspector", debug_assertions))]
pub path: std::rc::Rc<InspectorElementPath>,
#[cfg(any(feature = "inspector", debug_assertions))]
pub instance_id: usize,
}
impl Into<InspectorElementId> for &InspectorElementId {
fn into(self) -> InspectorElementId {
self.clone()
}
}
#[cfg(any(feature = "inspector", debug_assertions))]
impl InspectorElementId {
pub fn short_label(&self) -> String {
if let Some(last) = self.path.global_id.0.last() {
last.to_string()
} else {
self.path.source_location.file().to_string()
}
}
pub fn source_label(&self) -> String {
let loc = self.path.source_location;
format!("{}:{}", loc.file(), loc.line())
}
pub fn tree_key(&self) -> String {
let loc = self.path.source_location;
format!(
"{}#{}@{}:{}",
self.path.global_id,
self.instance_id,
loc.file(),
loc.line()
)
}
}
#[cfg(any(feature = "inspector", debug_assertions))]
pub use conditional::*;
#[cfg(any(feature = "inspector", debug_assertions))]
mod conditional {
use super::*;
use crate::collections::{FxHashMap, TypeIdHashMap};
use crate::{AnyElement, App, Bounds, Context, Empty, IntoElement, Pixels, Render, Window};
use std::any::{Any, TypeId};
#[derive(Debug, Eq, PartialEq, Hash)]
pub struct InspectorElementPath {
#[cfg(any(feature = "inspector", debug_assertions))]
pub global_id: crate::GlobalElementId,
#[cfg(any(feature = "inspector", debug_assertions))]
pub source_location: &'static std::panic::Location<'static>,
}
impl Clone for InspectorElementPath {
fn clone(&self) -> Self {
Self {
global_id: self.global_id.clone(),
source_location: self.source_location,
}
}
}
impl Into<InspectorElementPath> for &InspectorElementPath {
fn into(self) -> InspectorElementPath {
self.clone()
}
}
pub type InspectorRenderer =
Box<dyn Fn(&mut Inspector, &mut Window, &mut Context<Inspector>) -> AnyElement>;
pub struct Inspector {
active_element: Option<InspectedElement>,
pub(crate) pick_depth: Option<f32>,
}
struct InspectedElement {
id: InspectorElementId,
states: TypeIdHashMap<Box<dyn Any>>,
}
impl InspectedElement {
fn new(id: InspectorElementId) -> Self {
InspectedElement {
id,
states: Default::default(),
}
}
}
impl Inspector {
pub(crate) fn new() -> Self {
Self {
active_element: None,
pick_depth: Some(0.0),
}
}
pub fn select(&mut self, id: InspectorElementId, window: &mut Window) {
self.set_active_element_id(id, window);
self.pick_depth = None;
}
pub fn select_ancestor(&mut self, levels_up: usize, window: &mut Window) -> bool {
let Some(active_id) = self.active_element_id().cloned() else {
return false;
};
if levels_up == 0 {
return true;
}
let active_global = active_id.path.global_id.clone();
let active_len = active_global.0.len();
if levels_up > active_len {
return false;
}
let target_len = active_len - levels_up;
let target_prefix = &active_global.0[..target_len];
let active_bounds = window
.inspector_bounds_for_id(&active_id)
.or_else(|| window.next_inspector_bounds_for_id(&active_id));
let mut candidates: Vec<(InspectorElementId, crate::Bounds<crate::Pixels>)> =
Vec::new();
for frame in [&window.rendered_frame, &window.next_frame] {
for (hitbox_id, inspector_id) in frame.inspector_hitboxes.iter() {
if inspector_id.path.global_id.0.as_ref() != target_prefix {
continue;
}
if let Some(hitbox) =
frame.hitboxes.iter().find(|hitbox| hitbox.id == *hitbox_id)
{
candidates.push((inspector_id.clone(), hitbox.bounds));
}
}
}
if candidates.is_empty() {
return false;
}
let chosen = if let Some(active_bounds) = active_bounds.as_ref() {
candidates
.iter()
.filter(|(_, bounds)| bounds_contains(bounds, active_bounds))
.min_by(|a, b| {
bounds_area(&a.1)
.partial_cmp(&bounds_area(&b.1))
.unwrap_or(std::cmp::Ordering::Equal)
})
.map(|(id, _)| id.clone())
} else {
None
};
let chosen = chosen.unwrap_or_else(|| {
let mut seen = std::collections::HashSet::new();
candidates
.into_iter()
.map(|(id, _)| id)
.find(|id| seen.insert(id.clone()))
.expect("候选非空")
});
self.select(chosen, window);
true
}
pub(crate) fn hover(&mut self, id: InspectorElementId, window: &mut Window) {
if self.is_picking() {
let changed = self.set_active_element_id(id, window);
if changed {
self.pick_depth = Some(0.0);
}
}
}
pub(crate) fn set_active_element_id(
&mut self,
id: InspectorElementId,
window: &mut Window,
) -> bool {
let changed = Some(&id) != self.active_element_id();
if changed {
self.active_element = Some(InspectedElement::new(id));
window.refresh();
}
changed
}
pub fn active_element_id(&self) -> Option<&InspectorElementId> {
self.active_element.as_ref().map(|e| &e.id)
}
pub(crate) fn with_active_element_state<T: 'static, R>(
&mut self,
window: &mut Window,
f: impl FnOnce(&mut Option<T>, &mut Window) -> R,
) -> R {
let Some(active_element) = &mut self.active_element else {
return f(&mut None, window);
};
let type_id = TypeId::of::<T>();
let mut inspector_state = active_element
.states
.remove(&type_id)
.map(|state| *state.downcast().unwrap());
let result = f(&mut inspector_state, window);
if let Some(inspector_state) = inspector_state {
active_element
.states
.insert(type_id, Box::new(inspector_state));
}
result
}
pub fn start_picking(&mut self) {
self.pick_depth = Some(0.0);
}
pub fn is_picking(&self) -> bool {
self.pick_depth.is_some()
}
pub fn render_inspector_states(
&mut self,
window: &mut Window,
cx: &mut Context<Self>,
) -> Vec<AnyElement> {
let mut elements = Vec::new();
if let Some(active_element) = self.active_element.take() {
for (type_id, state) in &active_element.states {
if let Some(render_inspector) = cx
.inspector_element_registry
.renderers_by_type_id
.remove(type_id)
{
let mut element = (render_inspector)(
active_element.id.clone(),
state.as_ref(),
window,
cx,
);
elements.push(element);
cx.inspector_element_registry
.renderers_by_type_id
.insert(*type_id, render_inspector);
}
}
self.active_element = Some(active_element);
}
elements
}
}
impl Render for Inspector {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
if let Some(inspector_renderer) = cx.inspector_renderer.take() {
let result = inspector_renderer(self, window, cx);
cx.inspector_renderer = Some(inspector_renderer);
result
} else {
Empty.into_any_element()
}
}
}
#[derive(Default)]
pub(crate) struct InspectorElementRegistry {
renderers_by_type_id: FxHashMap<
TypeId,
Box<dyn Fn(InspectorElementId, &dyn Any, &mut Window, &mut App) -> AnyElement>,
>,
}
impl InspectorElementRegistry {
pub fn register<T: 'static, R: IntoElement>(
&mut self,
f: impl 'static + Fn(InspectorElementId, &T, &mut Window, &mut App) -> R,
) {
self.renderers_by_type_id.insert(
TypeId::of::<T>(),
Box::new(move |id, value, window, cx| {
let value = value.downcast_ref().unwrap();
f(id, value, window, cx).into_any_element()
}),
);
}
}
#[derive(Debug, Clone, Default)]
pub(crate) struct InspectorTreeNode {
pub(crate) parent: Option<InspectorElementId>,
pub(crate) children: Vec<InspectorElementId>,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct SnapshotNode {
pub key: String,
pub label: String,
pub source: String,
pub instance: usize,
pub parent: Option<String>,
pub bounds: Option<[f32; 4]>,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct InspectorSnapshot {
pub version: u32,
pub timestamp_millis: u64,
pub active: Option<SnapshotNode>,
pub ancestors: Vec<SnapshotNode>,
pub tree_total: usize,
pub tree: Vec<SnapshotNode>,
pub errors: Vec<(u64, String)>,
pub viewport: [f32; 2],
}
fn bounds_contains(outer: &Bounds<Pixels>, inner: &Bounds<Pixels>) -> bool {
const EPS: f32 = 1.0;
let outer_right = outer.origin.x.as_f32() + outer.size.width.as_f32();
let outer_bottom = outer.origin.y.as_f32() + outer.size.height.as_f32();
let inner_right = inner.origin.x.as_f32() + inner.size.width.as_f32();
let inner_bottom = inner.origin.y.as_f32() + inner.size.height.as_f32();
outer.origin.x.as_f32() <= inner.origin.x.as_f32() + EPS
&& outer.origin.y.as_f32() <= inner.origin.y.as_f32() + EPS
&& outer_right + EPS >= inner_right
&& outer_bottom + EPS >= inner_bottom
}
fn bounds_area(bounds: &Bounds<Pixels>) -> f32 {
bounds.size.width.as_f32() * bounds.size.height.as_f32()
}
#[crate::test]
fn inspector_width_roundtrip(cx: &mut crate::TestAppContext) {
use crate::{Context, IntoElement, Render, div};
struct Probe;
impl Render for Probe {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
div()
}
}
let (_view, cx) = cx.add_window_view(|_, _| Probe);
cx.update(|window, cx| {
assert!(window.inspector_width().is_none());
window.set_inspector_width(Some(crate::px(400.0)));
let _ = window.draw(cx);
assert_eq!(window.inspector_width(), Some(crate::px(400.0)));
window.set_inspector_width(None);
assert!(window.inspector_width().is_none());
});
}
#[crate::test]
fn capture_snapshot_smoke(cx: &mut crate::TestAppContext) {
use crate::{Context, IntoElement, Render, div};
struct Probe;
impl Render for Probe {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
div()
}
}
let (_view, cx) = cx.add_window_view(|_, _| Probe);
cx.update(|window, cx| {
window.toggle_inspector(cx);
let _ = window.draw(cx);
let snapshot = window
.capture_inspector_snapshot(cx)
.expect("检查器打开应有快照");
assert_eq!(snapshot.version, 1);
assert!(snapshot.viewport[0] > 0.0);
});
}
#[crate::test]
fn tree_text_marks_selected_with_source(cx: &mut crate::TestAppContext) {
use crate::{Context, InteractiveElement as _, IntoElement, ParentElement, Render, div};
struct Probe;
impl Render for Probe {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
div().id("probe-root").child(div().id("probe-leaf"))
}
}
let (_view, cx) = cx.add_window_view(|_, _| Probe);
cx.update(|window, cx| {
window.toggle_inspector(cx);
let _ = window.draw(cx);
let text = window.inspector_tree_text(cx, 2000).unwrap();
assert!(text.contains("# 检查器树"), "缺头部:{text}");
assert!(text.contains("probe-leaf"), "缺叶子节点:{text}");
fn find_leaf(
window: &Window,
id: &crate::InspectorElementId,
) -> Option<crate::InspectorElementId> {
if id.short_label() == "probe-leaf" {
return Some(id.clone());
}
window
.inspector_tree_children(id)
.iter()
.find_map(|child| find_leaf(window, child))
}
let leaf = window
.inspector_tree_roots()
.iter()
.find_map(|root| find_leaf(window, root))
.expect("树中应有叶子");
assert!(window.select_inspector_element(&leaf, cx));
let _ = window.draw(cx);
let text = window.inspector_tree_text(cx, 2000).unwrap();
assert!(
text.contains("[*]") && text.contains("probe-leaf"),
"缺选中标记:{text}"
);
});
}
#[test]
fn snapshot_serializes_stable_shape() {
let snapshot = InspectorSnapshot {
version: 1,
timestamp_millis: 0,
active: Some(SnapshotNode {
key: "k".to_string(),
label: "l".to_string(),
source: "s:1".to_string(),
instance: 0,
parent: None,
bounds: Some([0.0, 0.0, 10.0, 10.0]),
}),
ancestors: Vec::new(),
tree_total: 1,
tree: Vec::new(),
errors: vec![(0, "boom".to_string())],
viewport: [800.0, 600.0],
};
let json = serde_json::to_string(&snapshot).unwrap();
for key in [
"version",
"timestamp_millis",
"active",
"ancestors",
"tree_total",
"tree",
"errors",
"viewport",
"bounds",
"source",
] {
assert!(json.contains(key), "快照缺字段 {key}");
}
}
}
#[cfg(any(feature = "inspector", debug_assertions))]
pub mod inspector_reflection {
use std::any::Any;
#[derive(Clone, Copy)]
pub struct FunctionReflection<T> {
pub name: &'static str,
pub function: fn(Box<dyn Any>) -> Box<dyn Any>,
pub documentation: Option<&'static str>,
pub _type: std::marker::PhantomData<T>,
}
impl<T: 'static> FunctionReflection<T> {
pub fn invoke(&self, value: T) -> T {
let boxed = Box::new(value) as Box<dyn Any>;
let result = (self.function)(boxed);
*result
.downcast::<T>()
.expect("Type mismatch in reflection invoke")
}
}
}