Add initial element inspector for Zed development (#31315)

Open inspector with `dev: toggle inspector` from command palette or
`cmd-alt-i` on mac or `ctrl-alt-i` on linux.

https://github.com/user-attachments/assets/54c43034-d40b-414e-ba9b-190bed2e6d2f

* Picking of elements via the mouse, with scroll wheel to inspect
occluded elements.

* Temporary manipulation of the selected element.

* Layout info and JSON-based style manipulation for `Div`.

* Navigation to code that constructed the element.

Big thanks to @as-cii and @maxdeviant for sorting out how to implement
the core of an inspector.

Release Notes:

- N/A

---------

Co-authored-by: Antonio Scandurra <me@as-cii.com>
Co-authored-by: Marshall Bowers <git@maxdeviant.com>
Co-authored-by: Federico Dionisi <code@fdionisi.me>
This commit is contained in:
Michael Sloan 2025-05-23 17:08:59 -06:00 committed by GitHub
parent 685933b5c8
commit ab59982bf7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
74 changed files with 2631 additions and 406 deletions

View file

@ -33,11 +33,16 @@
use crate::{
App, ArenaBox, AvailableSpace, Bounds, Context, DispatchNodeId, ELEMENT_ARENA, ElementId,
FocusHandle, LayoutId, Pixels, Point, Size, Style, Window, util::FluentBuilder,
FocusHandle, InspectorElementId, LayoutId, Pixels, Point, Size, Style, Window,
util::FluentBuilder,
};
use derive_more::{Deref, DerefMut};
pub(crate) use smallvec::SmallVec;
use std::{any::Any, fmt::Debug, mem};
use std::{
any::Any,
fmt::{self, Debug, Display},
mem, panic,
};
/// Implemented by types that participate in laying out and painting the contents of a window.
/// Elements form a tree and are laid out according to web-based layout rules, as implemented by Taffy.
@ -59,11 +64,16 @@ pub trait Element: 'static + IntoElement {
/// frames. This id must be unique among children of the first containing element with an id.
fn id(&self) -> Option<ElementId>;
/// Source location where this element was constructed, used to disambiguate elements in the
/// inspector and navigate to their source code.
fn source_location(&self) -> Option<&'static panic::Location<'static>>;
/// Before an element can be painted, we need to know where it's going to be and how big it is.
/// Use this method to request a layout from Taffy and initialize the element's state.
fn request_layout(
&mut self,
id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (LayoutId, Self::RequestLayoutState);
@ -73,6 +83,7 @@ pub trait Element: 'static + IntoElement {
fn prepaint(
&mut self,
id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
request_layout: &mut Self::RequestLayoutState,
window: &mut Window,
@ -84,6 +95,7 @@ pub trait Element: 'static + IntoElement {
fn paint(
&mut self,
id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
request_layout: &mut Self::RequestLayoutState,
prepaint: &mut Self::PrepaintState,
@ -167,12 +179,21 @@ pub trait ParentElement {
/// An element for rendering components. An implementation detail of the [`IntoElement`] derive macro
/// for [`RenderOnce`]
#[doc(hidden)]
pub struct Component<C: RenderOnce>(Option<C>);
pub struct Component<C: RenderOnce> {
component: Option<C>,
#[cfg(debug_assertions)]
source: &'static core::panic::Location<'static>,
}
impl<C: RenderOnce> Component<C> {
/// Create a new component from the given RenderOnce type.
#[track_caller]
pub fn new(component: C) -> Self {
Component(Some(component))
Component {
component: Some(component),
#[cfg(debug_assertions)]
source: core::panic::Location::caller(),
}
}
}
@ -184,13 +205,27 @@ impl<C: RenderOnce> Element for Component<C> {
None
}
fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
#[cfg(debug_assertions)]
return Some(self.source);
#[cfg(not(debug_assertions))]
return None;
}
fn request_layout(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (LayoutId, Self::RequestLayoutState) {
let mut element = self.0.take().unwrap().render(window, cx).into_any_element();
let mut element = self
.component
.take()
.unwrap()
.render(window, cx)
.into_any_element();
let layout_id = element.request_layout(window, cx);
(layout_id, element)
}
@ -198,6 +233,7 @@ impl<C: RenderOnce> Element for Component<C> {
fn prepaint(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
_: Bounds<Pixels>,
element: &mut AnyElement,
window: &mut Window,
@ -209,6 +245,7 @@ impl<C: RenderOnce> Element for Component<C> {
fn paint(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
_: Bounds<Pixels>,
element: &mut Self::RequestLayoutState,
_: &mut Self::PrepaintState,
@ -231,6 +268,18 @@ impl<C: RenderOnce> IntoElement for Component<C> {
#[derive(Deref, DerefMut, Default, Debug, Eq, PartialEq, Hash)]
pub struct GlobalElementId(pub(crate) SmallVec<[ElementId; 32]>);
impl Display for GlobalElementId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for (i, element_id) in self.0.iter().enumerate() {
if i > 0 {
write!(f, ".")?;
}
write!(f, "{}", element_id)?;
}
Ok(())
}
}
trait ElementObject {
fn inner_element(&mut self) -> &mut dyn Any;
@ -262,17 +311,20 @@ enum ElementDrawPhase<RequestLayoutState, PrepaintState> {
RequestLayout {
layout_id: LayoutId,
global_id: Option<GlobalElementId>,
inspector_id: Option<InspectorElementId>,
request_layout: RequestLayoutState,
},
LayoutComputed {
layout_id: LayoutId,
global_id: Option<GlobalElementId>,
inspector_id: Option<InspectorElementId>,
available_space: Size<AvailableSpace>,
request_layout: RequestLayoutState,
},
Prepaint {
node_id: DispatchNodeId,
global_id: Option<GlobalElementId>,
inspector_id: Option<InspectorElementId>,
bounds: Bounds<Pixels>,
request_layout: RequestLayoutState,
prepaint: PrepaintState,
@ -297,8 +349,28 @@ impl<E: Element> Drawable<E> {
GlobalElementId(window.element_id_stack.clone())
});
let (layout_id, request_layout) =
self.element.request_layout(global_id.as_ref(), window, cx);
let inspector_id;
#[cfg(any(feature = "inspector", debug_assertions))]
{
inspector_id = self.element.source_location().map(|source| {
let path = crate::InspectorElementPath {
global_id: GlobalElementId(window.element_id_stack.clone()),
source_location: source,
};
window.build_inspector_element_id(path)
});
}
#[cfg(not(any(feature = "inspector", debug_assertions)))]
{
inspector_id = None;
}
let (layout_id, request_layout) = self.element.request_layout(
global_id.as_ref(),
inspector_id.as_ref(),
window,
cx,
);
if global_id.is_some() {
window.element_id_stack.pop();
@ -307,6 +379,7 @@ impl<E: Element> Drawable<E> {
self.phase = ElementDrawPhase::RequestLayout {
layout_id,
global_id,
inspector_id,
request_layout,
};
layout_id
@ -320,11 +393,13 @@ impl<E: Element> Drawable<E> {
ElementDrawPhase::RequestLayout {
layout_id,
global_id,
inspector_id,
mut request_layout,
}
| ElementDrawPhase::LayoutComputed {
layout_id,
global_id,
inspector_id,
mut request_layout,
..
} => {
@ -337,6 +412,7 @@ impl<E: Element> Drawable<E> {
let node_id = window.next_frame.dispatch_tree.push_node();
let prepaint = self.element.prepaint(
global_id.as_ref(),
inspector_id.as_ref(),
bounds,
&mut request_layout,
window,
@ -351,6 +427,7 @@ impl<E: Element> Drawable<E> {
self.phase = ElementDrawPhase::Prepaint {
node_id,
global_id,
inspector_id,
bounds,
request_layout,
prepaint,
@ -369,6 +446,7 @@ impl<E: Element> Drawable<E> {
ElementDrawPhase::Prepaint {
node_id,
global_id,
inspector_id,
bounds,
mut request_layout,
mut prepaint,
@ -382,6 +460,7 @@ impl<E: Element> Drawable<E> {
window.next_frame.dispatch_tree.set_active_node(node_id);
self.element.paint(
global_id.as_ref(),
inspector_id.as_ref(),
bounds,
&mut request_layout,
&mut prepaint,
@ -414,12 +493,14 @@ impl<E: Element> Drawable<E> {
ElementDrawPhase::RequestLayout {
layout_id,
global_id,
inspector_id,
request_layout,
} => {
window.compute_layout(layout_id, available_space, cx);
self.phase = ElementDrawPhase::LayoutComputed {
layout_id,
global_id,
inspector_id,
available_space,
request_layout,
};
@ -428,6 +509,7 @@ impl<E: Element> Drawable<E> {
ElementDrawPhase::LayoutComputed {
layout_id,
global_id,
inspector_id,
available_space: prev_available_space,
request_layout,
} => {
@ -437,6 +519,7 @@ impl<E: Element> Drawable<E> {
self.phase = ElementDrawPhase::LayoutComputed {
layout_id,
global_id,
inspector_id,
available_space,
request_layout,
};
@ -570,9 +653,14 @@ impl Element for AnyElement {
None
}
fn source_location(&self) -> Option<&'static panic::Location<'static>> {
None
}
fn request_layout(
&mut self,
_: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (LayoutId, Self::RequestLayoutState) {
@ -583,6 +671,7 @@ impl Element for AnyElement {
fn prepaint(
&mut self,
_: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
_: Bounds<Pixels>,
_: &mut Self::RequestLayoutState,
window: &mut Window,
@ -594,6 +683,7 @@ impl Element for AnyElement {
fn paint(
&mut self,
_: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
_: Bounds<Pixels>,
_: &mut Self::RequestLayoutState,
_: &mut Self::PrepaintState,
@ -635,9 +725,14 @@ impl Element for Empty {
None
}
fn source_location(&self) -> Option<&'static panic::Location<'static>> {
None
}
fn request_layout(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (LayoutId, Self::RequestLayoutState) {
@ -647,6 +742,7 @@ impl Element for Empty {
fn prepaint(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
_bounds: Bounds<Pixels>,
_state: &mut Self::RequestLayoutState,
_window: &mut Window,
@ -657,6 +753,7 @@ impl Element for Empty {
fn paint(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
_bounds: Bounds<Pixels>,
_request_layout: &mut Self::RequestLayoutState,
_prepaint: &mut Self::PrepaintState,