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

@ -1,3 +1,5 @@
#[cfg(any(feature = "inspector", debug_assertions))]
use crate::Inspector;
use crate::{
Action, AnyDrag, AnyElement, AnyImageCache, AnyTooltip, AnyView, App, AppContext, Arena, Asset,
AsyncWindowContext, AvailableSpace, Background, BorderStyle, Bounds, BoxShadow, Context,
@ -13,7 +15,7 @@ use crate::{
SubscriberSet, Subscription, TaffyLayoutEngine, Task, TextStyle, TextStyleRefinement,
TransformationMatrix, Underline, UnderlineStyle, WindowAppearance, WindowBackgroundAppearance,
WindowBounds, WindowControls, WindowDecorations, WindowOptions, WindowParams, WindowTextSystem,
point, prelude::*, px, size, transparent_black,
point, prelude::*, px, rems, size, transparent_black,
};
use anyhow::{Context as _, Result, anyhow};
use collections::{FxHashMap, FxHashSet};
@ -412,7 +414,7 @@ pub(crate) struct CursorStyleRequest {
}
/// An identifier for a [Hitbox].
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Hash)]
pub struct HitboxId(usize);
impl HitboxId {
@ -502,6 +504,10 @@ pub(crate) struct Frame {
pub(crate) cursor_styles: Vec<CursorStyleRequest>,
#[cfg(any(test, feature = "test-support"))]
pub(crate) debug_bounds: FxHashMap<String, Bounds<Pixels>>,
#[cfg(any(feature = "inspector", debug_assertions))]
pub(crate) next_inspector_instance_ids: FxHashMap<Rc<crate::InspectorElementPath>, usize>,
#[cfg(any(feature = "inspector", debug_assertions))]
pub(crate) inspector_hitboxes: FxHashMap<HitboxId, crate::InspectorElementId>,
}
#[derive(Clone, Default)]
@ -542,6 +548,12 @@ impl Frame {
#[cfg(any(test, feature = "test-support"))]
debug_bounds: FxHashMap::default(),
#[cfg(any(feature = "inspector", debug_assertions))]
next_inspector_instance_ids: FxHashMap::default(),
#[cfg(any(feature = "inspector", debug_assertions))]
inspector_hitboxes: FxHashMap::default(),
}
}
@ -557,6 +569,12 @@ impl Frame {
self.hitboxes.clear();
self.deferred_draws.clear();
self.focus = None;
#[cfg(any(feature = "inspector", debug_assertions))]
{
self.next_inspector_instance_ids.clear();
self.inspector_hitboxes.clear();
}
}
pub(crate) fn hit_test(&self, position: Point<Pixels>) -> HitTest {
@ -648,6 +666,8 @@ pub struct Window {
pub(crate) pending_input_observers: SubscriberSet<(), AnyObserver>,
prompt: Option<RenderablePromptHandle>,
pub(crate) client_inset: Option<Pixels>,
#[cfg(any(feature = "inspector", debug_assertions))]
inspector: Option<Entity<Inspector>>,
}
#[derive(Clone, Debug, Default)]
@ -935,6 +955,8 @@ impl Window {
prompt: None,
client_inset: None,
image_cache_stack: Vec::new(),
#[cfg(any(feature = "inspector", debug_assertions))]
inspector: None,
})
}
@ -1658,9 +1680,30 @@ impl Window {
self.invalidator.set_phase(DrawPhase::Prepaint);
self.tooltip_bounds.take();
let _inspector_width: Pixels = rems(30.0).to_pixels(self.rem_size());
let root_size = {
#[cfg(any(feature = "inspector", debug_assertions))]
{
if self.inspector.is_some() {
let mut size = self.viewport_size;
size.width = (size.width - _inspector_width).max(px(0.0));
size
} else {
self.viewport_size
}
}
#[cfg(not(any(feature = "inspector", debug_assertions)))]
{
self.viewport_size
}
};
// Layout all root elements.
let mut root_element = self.root.as_ref().unwrap().clone().into_any();
root_element.prepaint_as_root(Point::default(), self.viewport_size.into(), self, cx);
root_element.prepaint_as_root(Point::default(), root_size.into(), self, cx);
#[cfg(any(feature = "inspector", debug_assertions))]
let inspector_element = self.prepaint_inspector(_inspector_width, cx);
let mut sorted_deferred_draws =
(0..self.next_frame.deferred_draws.len()).collect::<SmallVec<[_; 8]>>();
@ -1672,7 +1715,7 @@ impl Window {
let mut tooltip_element = None;
if let Some(prompt) = self.prompt.take() {
let mut element = prompt.view.any_view().into_any();
element.prepaint_as_root(Point::default(), self.viewport_size.into(), self, cx);
element.prepaint_as_root(Point::default(), root_size.into(), self, cx);
prompt_element = Some(element);
self.prompt = Some(prompt);
} else if let Some(active_drag) = cx.active_drag.take() {
@ -1691,6 +1734,9 @@ impl Window {
self.invalidator.set_phase(DrawPhase::Paint);
root_element.paint(self, cx);
#[cfg(any(feature = "inspector", debug_assertions))]
self.paint_inspector(inspector_element, cx);
self.paint_deferred_draws(&sorted_deferred_draws, cx);
if let Some(mut prompt_element) = prompt_element {
@ -1700,6 +1746,9 @@ impl Window {
} else if let Some(mut tooltip_element) = tooltip_element {
tooltip_element.paint(self, cx);
}
#[cfg(any(feature = "inspector", debug_assertions))]
self.paint_inspector_hitbox(cx);
}
fn prepaint_tooltip(&mut self, cx: &mut App) -> Option<AnyElement> {
@ -3200,6 +3249,13 @@ impl Window {
self.reset_cursor_style(cx);
}
#[cfg(any(feature = "inspector", debug_assertions))]
if self.is_inspector_picking(cx) {
self.handle_inspector_mouse_event(event, cx);
// When inspector is picking, all other mouse handling is skipped.
return;
}
let mut mouse_listeners = mem::take(&mut self.rendered_frame.mouse_listeners);
// Capture phase, events bubble from back to front. Handlers for this phase are used for
@ -3830,6 +3886,197 @@ impl Window {
pub fn gpu_specs(&self) -> Option<GpuSpecs> {
self.platform_window.gpu_specs()
}
/// Toggles the inspector mode on this window.
#[cfg(any(feature = "inspector", debug_assertions))]
pub fn toggle_inspector(&mut self, cx: &mut App) {
self.inspector = match self.inspector {
None => Some(cx.new(|_| Inspector::new())),
Some(_) => None,
};
self.refresh();
}
/// Returns true if the window is in inspector mode.
pub fn is_inspector_picking(&self, _cx: &App) -> bool {
#[cfg(any(feature = "inspector", debug_assertions))]
{
if let Some(inspector) = &self.inspector {
return inspector.read(_cx).is_picking();
}
}
false
}
/// Executes the provided function with mutable access to an inspector state.
#[cfg(any(feature = "inspector", debug_assertions))]
pub fn with_inspector_state<T: 'static, R>(
&mut self,
_inspector_id: Option<&crate::InspectorElementId>,
cx: &mut App,
f: impl FnOnce(&mut Option<T>, &mut Self) -> R,
) -> R {
if let Some(inspector_id) = _inspector_id {
if let Some(inspector) = &self.inspector {
let inspector = inspector.clone();
let active_element_id = inspector.read(cx).active_element_id();
if Some(inspector_id) == active_element_id {
return inspector.update(cx, |inspector, _cx| {
inspector.with_active_element_state(self, f)
});
}
}
}
f(&mut None, self)
}
#[cfg(any(feature = "inspector", debug_assertions))]
pub(crate) fn build_inspector_element_id(
&mut self,
path: crate::InspectorElementPath,
) -> crate::InspectorElementId {
self.invalidator.debug_assert_paint_or_prepaint();
let path = Rc::new(path);
let next_instance_id = self
.next_frame
.next_inspector_instance_ids
.entry(path.clone())
.or_insert(0);
let instance_id = *next_instance_id;
*next_instance_id += 1;
crate::InspectorElementId { path, instance_id }
}
#[cfg(any(feature = "inspector", debug_assertions))]
fn prepaint_inspector(&mut self, inspector_width: Pixels, cx: &mut App) -> Option<AnyElement> {
if let Some(inspector) = self.inspector.take() {
let mut inspector_element = AnyView::from(inspector.clone()).into_any_element();
inspector_element.prepaint_as_root(
point(self.viewport_size.width - inspector_width, px(0.0)),
size(inspector_width, self.viewport_size.height).into(),
self,
cx,
);
self.inspector = Some(inspector);
Some(inspector_element)
} else {
None
}
}
#[cfg(any(feature = "inspector", debug_assertions))]
fn paint_inspector(&mut self, mut inspector_element: Option<AnyElement>, cx: &mut App) {
if let Some(mut inspector_element) = inspector_element {
inspector_element.paint(self, cx);
};
}
/// Registers a hitbox that can be used for inspector picking mode, allowing users to select and
/// inspect UI elements by clicking on them.
#[cfg(any(feature = "inspector", debug_assertions))]
pub fn insert_inspector_hitbox(
&mut self,
hitbox_id: HitboxId,
inspector_id: Option<&crate::InspectorElementId>,
cx: &App,
) {
self.invalidator.debug_assert_paint_or_prepaint();
if !self.is_inspector_picking(cx) {
return;
}
if let Some(inspector_id) = inspector_id {
self.next_frame
.inspector_hitboxes
.insert(hitbox_id, inspector_id.clone());
}
}
#[cfg(any(feature = "inspector", debug_assertions))]
fn paint_inspector_hitbox(&mut self, cx: &App) {
if let Some(inspector) = self.inspector.as_ref() {
let inspector = inspector.read(cx);
if let Some((hitbox_id, _)) = self.hovered_inspector_hitbox(inspector, &self.next_frame)
{
if let Some(hitbox) = self
.next_frame
.hitboxes
.iter()
.find(|hitbox| hitbox.id == hitbox_id)
{
self.paint_quad(crate::fill(hitbox.bounds, crate::rgba(0x61afef4d)));
}
}
}
}
#[cfg(any(feature = "inspector", debug_assertions))]
fn handle_inspector_mouse_event(&mut self, event: &dyn Any, cx: &mut App) {
let Some(inspector) = self.inspector.clone() else {
return;
};
if event.downcast_ref::<MouseMoveEvent>().is_some() {
inspector.update(cx, |inspector, _cx| {
if let Some((_, inspector_id)) =
self.hovered_inspector_hitbox(inspector, &self.rendered_frame)
{
inspector.hover(inspector_id, self);
}
});
} else if event.downcast_ref::<crate::MouseDownEvent>().is_some() {
inspector.update(cx, |inspector, _cx| {
if let Some((_, inspector_id)) =
self.hovered_inspector_hitbox(inspector, &self.rendered_frame)
{
inspector.select(inspector_id, self);
}
});
} else if let Some(event) = event.downcast_ref::<crate::ScrollWheelEvent>() {
// This should be kept in sync with SCROLL_LINES in x11 platform.
const SCROLL_LINES: f32 = 3.0;
const SCROLL_PIXELS_PER_LAYER: f32 = 36.0;
let delta_y = event
.delta
.pixel_delta(px(SCROLL_PIXELS_PER_LAYER / SCROLL_LINES))
.y;
if let Some(inspector) = self.inspector.clone() {
inspector.update(cx, |inspector, _cx| {
if let Some(depth) = inspector.pick_depth.as_mut() {
*depth += delta_y.0 / SCROLL_PIXELS_PER_LAYER;
let max_depth = self.mouse_hit_test.0.len() as f32 - 0.5;
if *depth < 0.0 {
*depth = 0.0;
} else if *depth > max_depth {
*depth = max_depth;
}
if let Some((_, inspector_id)) =
self.hovered_inspector_hitbox(inspector, &self.rendered_frame)
{
inspector.set_active_element_id(inspector_id.clone(), self);
}
}
});
}
}
}
#[cfg(any(feature = "inspector", debug_assertions))]
fn hovered_inspector_hitbox(
&self,
inspector: &Inspector,
frame: &Frame,
) -> Option<(HitboxId, crate::InspectorElementId)> {
if let Some(pick_depth) = inspector.pick_depth {
let depth = (pick_depth as i64).try_into().unwrap_or(0);
let max_skipped = self.mouse_hit_test.0.len().saturating_sub(1);
let skip_count = (depth as usize).min(max_skipped);
for hitbox_id in self.mouse_hit_test.0.iter().skip(skip_count) {
if let Some(inspector_id) = frame.inspector_hitboxes.get(hitbox_id) {
return Some((*hitbox_id, inspector_id.clone()));
}
}
}
return None;
}
}
// #[derive(Clone, Copy, Eq, PartialEq, Hash)]
@ -4069,7 +4316,7 @@ pub enum ElementId {
FocusHandle(FocusId),
/// A combination of a name and an integer.
NamedInteger(SharedString, u64),
/// A path
/// A path.
Path(Arc<std::path::Path>),
}