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:
parent
685933b5c8
commit
ab59982bf7
74 changed files with 2631 additions and 406 deletions
|
@ -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>),
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue