
Editing JSON styles is not very helpful for bringing style changes back to the actual code. This PR adds a buffer that pretends to be Rust, applying any style attribute identifiers it finds. Also supports completions with display of documentation. The effect of the currently selected completion is previewed. Warning diagnostics appear on any unrecognized identifier. https://github.com/user-attachments/assets/af39ff0a-26a5-4835-a052-d8f642b2080c Adds a `#[derive_inspector_reflection]` macro which allows these methods to be enumerated and called by their name. The macro code changes were 95% generated by Zed Agent + Opus 4. Release Notes: * Added an element inspector for development. On debug builds, `dev::ToggleInspector` will open a pane allowing inspecting of element info and modifying styles.
254 lines
8.5 KiB
Rust
254 lines
8.5 KiB
Rust
/// A unique identifier for an element that can be inspected.
|
|
#[derive(Debug, Eq, PartialEq, Hash, Clone)]
|
|
pub struct InspectorElementId {
|
|
/// Stable part of the ID.
|
|
#[cfg(any(feature = "inspector", debug_assertions))]
|
|
pub path: std::rc::Rc<InspectorElementPath>,
|
|
/// Disambiguates elements that have the same path.
|
|
#[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))]
|
|
pub use conditional::*;
|
|
|
|
#[cfg(any(feature = "inspector", debug_assertions))]
|
|
mod conditional {
|
|
use super::*;
|
|
use crate::{AnyElement, App, Context, Empty, IntoElement, Render, Window};
|
|
use collections::FxHashMap;
|
|
use std::any::{Any, TypeId};
|
|
|
|
/// `GlobalElementId` qualified by source location of element construction.
|
|
#[derive(Debug, Eq, PartialEq, Hash)]
|
|
pub struct InspectorElementPath {
|
|
/// The path to the nearest ancestor element that has an `ElementId`.
|
|
#[cfg(any(feature = "inspector", debug_assertions))]
|
|
pub global_id: crate::GlobalElementId,
|
|
/// Source location where this element was constructed.
|
|
#[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: crate::GlobalElementId(self.global_id.0.clone()),
|
|
source_location: self.source_location,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Into<InspectorElementPath> for &InspectorElementPath {
|
|
fn into(self) -> InspectorElementPath {
|
|
self.clone()
|
|
}
|
|
}
|
|
|
|
/// Function set on `App` to render the inspector UI.
|
|
pub type InspectorRenderer =
|
|
Box<dyn Fn(&mut Inspector, &mut Window, &mut Context<Inspector>) -> AnyElement>;
|
|
|
|
/// Manages inspector state - which element is currently selected and whether the inspector is
|
|
/// in picking mode.
|
|
pub struct Inspector {
|
|
active_element: Option<InspectedElement>,
|
|
pub(crate) pick_depth: Option<f32>,
|
|
}
|
|
|
|
struct InspectedElement {
|
|
id: InspectorElementId,
|
|
states: FxHashMap<TypeId, Box<dyn Any>>,
|
|
}
|
|
|
|
impl InspectedElement {
|
|
fn new(id: InspectorElementId) -> Self {
|
|
InspectedElement {
|
|
id,
|
|
states: FxHashMap::default(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Inspector {
|
|
pub(crate) fn new() -> Self {
|
|
Self {
|
|
active_element: None,
|
|
pick_depth: Some(0.0),
|
|
}
|
|
}
|
|
|
|
pub(crate) fn select(&mut self, id: InspectorElementId, window: &mut Window) {
|
|
self.set_active_element_id(id, window);
|
|
self.pick_depth = None;
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
/// ID of the currently hovered or selected element.
|
|
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
|
|
}
|
|
|
|
/// Starts element picking mode, allowing the user to select elements by clicking.
|
|
pub fn start_picking(&mut self) {
|
|
self.pick_depth = Some(0.0);
|
|
}
|
|
|
|
/// Returns whether the inspector is currently in picking mode.
|
|
pub fn is_picking(&self) -> bool {
|
|
self.pick_depth.is_some()
|
|
}
|
|
|
|
/// Renders elements for all registered inspector states of the active inspector element.
|
|
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()
|
|
}),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Provides definitions used by `#[derive_inspector_reflection]`.
|
|
#[cfg(any(feature = "inspector", debug_assertions))]
|
|
pub mod inspector_reflection {
|
|
use std::any::Any;
|
|
|
|
/// Reification of a function that has the signature `fn some_fn(T) -> T`. Provides the name,
|
|
/// documentation, and ability to invoke the function.
|
|
#[derive(Clone, Copy)]
|
|
pub struct FunctionReflection<T> {
|
|
/// The name of the function
|
|
pub name: &'static str,
|
|
/// The method
|
|
pub function: fn(Box<dyn Any>) -> Box<dyn Any>,
|
|
/// Documentation for the function
|
|
pub documentation: Option<&'static str>,
|
|
/// `PhantomData` for the type of the argument and result
|
|
pub _type: std::marker::PhantomData<T>,
|
|
}
|
|
|
|
impl<T: 'static> FunctionReflection<T> {
|
|
/// Invoke this method on a value and return the result.
|
|
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")
|
|
}
|
|
}
|
|
}
|