Add initial element inspector for Zed development (#31315)

Michael Sloan , Antonio Scandurra , Marshall Bowers , and Federico Dionisi created

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>

Change summary

Cargo.lock                                                                             |  24 
Cargo.toml                                                                             |   3 
assets/keymaps/default-linux.json                                                      |   2 
assets/keymaps/default-macos.json                                                      |   2 
crates/agent/Cargo.toml                                                                |   1 
crates/agent/src/message_editor.rs                                                     |   2 
crates/assistant_tools/src/edit_agent/evals/fixtures/disable_cursor_blinking/before.rs |   2 
crates/editor/src/editor.rs                                                            |   7 
crates/editor/src/element.rs                                                           |  11 
crates/editor/src/items.rs                                                             |  11 
crates/gpui/Cargo.toml                                                                 |   1 
crates/gpui/examples/input.rs                                                          |   8 
crates/gpui/examples/opacity.rs                                                        |   2 
crates/gpui/examples/shadow.rs                                                         |  74 
crates/gpui/examples/text_wrapper.rs                                                   |   4 
crates/gpui/examples/window_shadow.rs                                                  |   4 
crates/gpui/src/app.rs                                                                 |  25 
crates/gpui/src/color.rs                                                               |  75 
crates/gpui/src/element.rs                                                             | 111 
crates/gpui/src/elements/anchored.rs                                                   |  13 
crates/gpui/src/elements/animation.rs                                                  |  11 
crates/gpui/src/elements/canvas.rs                                                     |  11 
crates/gpui/src/elements/deferred.rs                                                   |  10 
crates/gpui/src/elements/div.rs                                                        | 184 
crates/gpui/src/elements/image_cache.rs                                                |  12 
crates/gpui/src/elements/img.rs                                                        |  21 
crates/gpui/src/elements/list.rs                                                       |  12 
crates/gpui/src/elements/surface.rs                                                    |  11 
crates/gpui/src/elements/svg.rs                                                        |  31 
crates/gpui/src/elements/text.rs                                                       |  60 
crates/gpui/src/elements/uniform_list.rs                                               |  23 
crates/gpui/src/geometry.rs                                                            | 334 
crates/gpui/src/gpui.rs                                                                |   2 
crates/gpui/src/inspector.rs                                                           | 223 
crates/gpui/src/platform.rs                                                            |   3 
crates/gpui/src/scene.rs                                                               |   5 
crates/gpui/src/style.rs                                                               | 339 
crates/gpui/src/styled.rs                                                              |  14 
crates/gpui/src/taffy.rs                                                               |  16 
crates/gpui/src/text_system.rs                                                         |   4 
crates/gpui/src/text_system/line_wrapper.rs                                            |  31 
crates/gpui/src/view.rs                                                                | 154 
crates/gpui/src/window.rs                                                              | 257 
crates/gpui_macros/src/derive_into_element.rs                                          |   1 
crates/gpui_macros/src/styles.rs                                                       |  22 
crates/inspector_ui/Cargo.toml                                                         |  28 
crates/inspector_ui/LICENSE-GPL                                                        |   1 
crates/inspector_ui/README.md                                                          |  84 
crates/inspector_ui/build.rs                                                           |  20 
crates/inspector_ui/src/div_inspector.rs                                               | 223 
crates/inspector_ui/src/inspector.rs                                                   | 168 
crates/inspector_ui/src/inspector_ui.rs                                                |  24 
crates/languages/Cargo.toml                                                            |   2 
crates/languages/src/json.rs                                                           | 116 
crates/markdown/src/markdown.rs                                                        |   7 
crates/project/src/project.rs                                                          |   9 
crates/refineable/derive_refineable/src/derive_refineable.rs                           |  48 
crates/refineable/src/refineable.rs                                                    |   7 
crates/terminal_view/src/terminal_element.rs                                           |  29 
crates/ui/src/components/button/split_button.rs                                        |   2 
crates/ui/src/components/indent_guides.rs                                              |   7 
crates/ui/src/components/keybinding_hint.rs                                            |   3 
crates/ui/src/components/popover_menu.rs                                               |   7 
crates/ui/src/components/progress/progress_bar.rs                                      |   2 
crates/ui/src/components/right_click_menu.rs                                           |   7 
crates/ui/src/components/scrollbar.rs                                                  |   8 
crates/ui/src/styles/elevation.rs                                                      |  15 
crates/ui/src/utils/with_rem_size.rs                                                   |  23 
crates/workspace/src/pane_group.rs                                                     |   7 
crates/workspace/src/workspace.rs                                                      |   2 
crates/zed/Cargo.toml                                                                  |   1 
crates/zed/src/main.rs                                                                 |   1 
crates/zed_actions/src/lib.rs                                                          |   6 
crates/zeta/src/completion_diff_element.rs                                             |   7 
74 files changed, 2,631 insertions(+), 406 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -112,7 +112,6 @@ dependencies = [
  "serde_json",
  "serde_json_lenient",
  "settings",
- "smallvec",
  "smol",
  "streaming_diff",
  "telemetry",
@@ -8159,6 +8158,26 @@ dependencies = [
  "generic-array",
 ]
 
+[[package]]
+name = "inspector_ui"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "command_palette_hooks",
+ "editor",
+ "gpui",
+ "language",
+ "project",
+ "serde_json",
+ "serde_json_lenient",
+ "theme",
+ "ui",
+ "util",
+ "workspace",
+ "workspace-hack",
+ "zed_actions",
+]
+
 [[package]]
 name = "install_cli"
 version = "0.1.0"
@@ -8931,8 +8950,10 @@ dependencies = [
  "regex",
  "rope",
  "rust-embed",
+ "schemars",
  "serde",
  "serde_json",
+ "serde_json_lenient",
  "settings",
  "smol",
  "snippet_provider",
@@ -19750,6 +19771,7 @@ dependencies = [
  "image_viewer",
  "indoc",
  "inline_completion_button",
+ "inspector_ui",
  "install_cli",
  "jj_ui",
  "journal",

Cargo.toml 🔗

@@ -73,6 +73,7 @@ members = [
     "crates/indexed_docs",
     "crates/inline_completion",
     "crates/inline_completion_button",
+    "crates/inspector_ui",
     "crates/install_cli",
     "crates/jj",
     "crates/jj_ui",
@@ -279,6 +280,7 @@ image_viewer = { path = "crates/image_viewer" }
 indexed_docs = { path = "crates/indexed_docs" }
 inline_completion = { path = "crates/inline_completion" }
 inline_completion_button = { path = "crates/inline_completion_button" }
+inspector_ui = { path = "crates/inspector_ui" }
 install_cli = { path = "crates/install_cli" }
 jj = { path = "crates/jj" }
 jj_ui = { path = "crates/jj_ui" }
@@ -447,6 +449,7 @@ futures-batch = "0.6.1"
 futures-lite = "1.13"
 git2 = { version = "0.20.1", default-features = false }
 globset = "0.4"
+hashbrown = "0.15.3"
 handlebars = "4.3"
 heck = "0.5"
 heed = { version = "0.21.0", features = ["read-txn-no-tls"] }

assets/keymaps/default-linux.json 🔗

@@ -675,7 +675,7 @@
   {
     "bindings": {
       "ctrl-alt-shift-f": "workspace::FollowNextCollaborator",
-      "ctrl-alt-i": "zed::DebugElements"
+      "ctrl-alt-i": "dev::ToggleInspector"
     }
   },
   {

assets/keymaps/default-macos.json 🔗

@@ -735,7 +735,7 @@
       "ctrl-alt-cmd-f": "workspace::FollowNextCollaborator",
       // TODO: Move this to a dock open action
       "cmd-shift-c": "collab_panel::ToggleFocus",
-      "cmd-alt-i": "zed::DebugElements"
+      "cmd-alt-i": "dev::ToggleInspector"
     }
   },
   {

crates/agent/Cargo.toml 🔗

@@ -76,7 +76,6 @@ serde.workspace = true
 serde_json.workspace = true
 serde_json_lenient.workspace = true
 settings.workspace = true
-smallvec.workspace = true
 smol.workspace = true
 streaming_diff.workspace = true
 telemetry.workspace = true

crates/agent/src/message_editor.rs 🔗

@@ -842,7 +842,7 @@ impl MessageEditor {
             .border_b_0()
             .border_color(border_color)
             .rounded_t_md()
-            .shadow(smallvec::smallvec![gpui::BoxShadow {
+            .shadow(vec![gpui::BoxShadow {
                 color: gpui::black().opacity(0.15),
                 offset: point(px(1.), px(-1.)),
                 blur_radius: px(3.),

crates/editor/src/editor.rs 🔗

@@ -138,7 +138,6 @@ pub use git::blame::BlameRenderer;
 pub use proposed_changes_editor::{
     ProposedChangeLocation, ProposedChangesEditor, ProposedChangesEditorToolbar,
 };
-use smallvec::smallvec;
 use std::{cell::OnceCell, iter::Peekable, ops::Not};
 use task::{ResolvedTask, RunnableTag, TaskTemplate, TaskVariables};
 
@@ -176,7 +175,7 @@ use selections_collection::{
 };
 use serde::{Deserialize, Serialize};
 use settings::{Settings, SettingsLocation, SettingsStore, update_settings_file};
-use smallvec::SmallVec;
+use smallvec::{SmallVec, smallvec};
 use snippet::Snippet;
 use std::sync::Arc;
 use std::{
@@ -7993,7 +7992,7 @@ impl Editor {
                     .gap_1()
                     // Workaround: For some reason, there's a gap if we don't do this
                     .ml(-BORDER_WIDTH)
-                    .shadow(smallvec![gpui::BoxShadow {
+                    .shadow(vec![gpui::BoxShadow {
                         color: gpui::black().opacity(0.05),
                         offset: point(px(1.), px(1.)),
                         blur_radius: px(2.),
@@ -16708,7 +16707,7 @@ impl Editor {
     }
 
     pub fn wrap_guides(&self, cx: &App) -> SmallVec<[(usize, bool); 2]> {
-        let mut wrap_guides = smallvec::smallvec![];
+        let mut wrap_guides = smallvec![];
 
         if self.show_wrap_guides == Some(false) {
             return wrap_guides;

crates/editor/src/element.rs 🔗

@@ -7181,9 +7181,14 @@ impl Element for EditorElement {
         None
     }
 
+    fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
+        None
+    }
+
     fn request_layout(
         &mut self,
         _: Option<&GlobalElementId>,
+        __inspector_id: Option<&gpui::InspectorElementId>,
         window: &mut Window,
         cx: &mut App,
     ) -> (gpui::LayoutId, ()) {
@@ -7290,6 +7295,7 @@ impl Element for EditorElement {
     fn prepaint(
         &mut self,
         _: Option<&GlobalElementId>,
+        _inspector_id: Option<&gpui::InspectorElementId>,
         bounds: Bounds<Pixels>,
         _: &mut Self::RequestLayoutState,
         window: &mut Window,
@@ -7761,7 +7767,7 @@ impl Element for EditorElement {
                         // If the fold widths have changed, we need to prepaint
                         // the element again to account for any changes in
                         // wrapping.
-                        return self.prepaint(None, bounds, &mut (), window, cx);
+                        return self.prepaint(None, _inspector_id, bounds, &mut (), window, cx);
                     }
 
                     let longest_line_blame_width = self
@@ -7846,7 +7852,7 @@ impl Element for EditorElement {
                             self.editor.update(cx, |editor, cx| {
                                 editor.resize_blocks(resized_blocks, autoscroll_request, cx)
                             });
-                            return self.prepaint(None, bounds, &mut (), window, cx);
+                            return self.prepaint(None, _inspector_id, bounds, &mut (), window, cx);
                         }
                     };
 
@@ -8345,6 +8351,7 @@ impl Element for EditorElement {
     fn paint(
         &mut self,
         _: Option<&GlobalElementId>,
+        __inspector_id: Option<&gpui::InspectorElementId>,
         bounds: Bounds<gpui::Pixels>,
         _: &mut Self::RequestLayoutState,
         layout: &mut Self::PrepaintState,

crates/editor/src/items.rs 🔗

@@ -1135,7 +1135,7 @@ impl SerializableItem for Editor {
                 mtime,
                 ..
             } => {
-                let project_item = project.update(cx, |project, cx| {
+                let opened_buffer = project.update(cx, |project, cx| {
                     let (worktree, path) = project.find_worktree(&abs_path, cx)?;
                     let project_path = ProjectPath {
                         worktree_id: worktree.read(cx).id(),
@@ -1144,13 +1144,10 @@ impl SerializableItem for Editor {
                     Some(project.open_path(project_path, cx))
                 });
 
-                match project_item {
-                    Some(project_item) => {
+                match opened_buffer {
+                    Some(opened_buffer) => {
                         window.spawn(cx, async move |cx| {
-                            let (_, project_item) = project_item.await?;
-                            let buffer = project_item.downcast::<Buffer>().map_err(|_| {
-                                anyhow!("Project item at stored path was not a buffer")
-                            })?;
+                            let (_, buffer) = opened_buffer.await?;
 
                             // This is a bit wasteful: we're loading the whole buffer from
                             // disk and then overwrite the content.

crates/gpui/Cargo.toml 🔗

@@ -22,6 +22,7 @@ test-support = [
     "wayland",
     "x11",
 ]
+inspector = []
 leak-detection = ["backtrace"]
 runtime_shaders = []
 macos-blade = [

crates/gpui/examples/input.rs 🔗

@@ -404,16 +404,20 @@ impl IntoElement for TextElement {
 
 impl Element for TextElement {
     type RequestLayoutState = ();
-
     type PrepaintState = PrepaintState;
 
     fn id(&self) -> Option<ElementId> {
         None
     }
 
+    fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
+        None
+    }
+
     fn request_layout(
         &mut self,
         _id: Option<&GlobalElementId>,
+        _inspector_id: Option<&gpui::InspectorElementId>,
         window: &mut Window,
         cx: &mut App,
     ) -> (LayoutId, Self::RequestLayoutState) {
@@ -426,6 +430,7 @@ impl Element for TextElement {
     fn prepaint(
         &mut self,
         _id: Option<&GlobalElementId>,
+        _inspector_id: Option<&gpui::InspectorElementId>,
         bounds: Bounds<Pixels>,
         _request_layout: &mut Self::RequestLayoutState,
         window: &mut Window,
@@ -523,6 +528,7 @@ impl Element for TextElement {
     fn paint(
         &mut self,
         _id: Option<&GlobalElementId>,
+        _inspector_id: Option<&gpui::InspectorElementId>,
         bounds: Bounds<Pixels>,
         _request_layout: &mut Self::RequestLayoutState,
         prepaint: &mut Self::PrepaintState,

crates/gpui/examples/opacity.rs 🔗

@@ -121,7 +121,7 @@ impl Render for HelloWorld {
                             .bg(gpui::blue())
                             .border_3()
                             .border_color(gpui::black())
-                            .shadow(smallvec::smallvec![BoxShadow {
+                            .shadow(vec![BoxShadow {
                                 color: hsla(0.0, 0.0, 0.0, 0.5),
                                 blur_radius: px(1.0),
                                 spread_radius: px(5.0),

crates/gpui/examples/shadow.rs 🔗

@@ -3,8 +3,6 @@ use gpui::{
     WindowOptions, div, hsla, point, prelude::*, px, relative, rgb, size,
 };
 
-use smallvec::smallvec;
-
 struct Shadow {}
 
 impl Shadow {
@@ -103,7 +101,7 @@ impl Render for Shadow {
                         example(
                             "Square",
                             Shadow::square()
-                                .shadow(smallvec![BoxShadow {
+                                .shadow(vec![BoxShadow {
                                     color: hsla(0.0, 0.5, 0.5, 0.3),
                                     offset: point(px(0.), px(8.)),
                                     blur_radius: px(8.),
@@ -113,7 +111,7 @@ impl Render for Shadow {
                         example(
                             "Rounded 4",
                             Shadow::rounded_small()
-                                .shadow(smallvec![BoxShadow {
+                                .shadow(vec![BoxShadow {
                                     color: hsla(0.0, 0.5, 0.5, 0.3),
                                     offset: point(px(0.), px(8.)),
                                     blur_radius: px(8.),
@@ -123,7 +121,7 @@ impl Render for Shadow {
                         example(
                             "Rounded 8",
                             Shadow::rounded_medium()
-                                .shadow(smallvec![BoxShadow {
+                                .shadow(vec![BoxShadow {
                                     color: hsla(0.0, 0.5, 0.5, 0.3),
                                     offset: point(px(0.), px(8.)),
                                     blur_radius: px(8.),
@@ -133,7 +131,7 @@ impl Render for Shadow {
                         example(
                             "Rounded 16",
                             Shadow::rounded_large()
-                                .shadow(smallvec![BoxShadow {
+                                .shadow(vec![BoxShadow {
                                     color: hsla(0.0, 0.5, 0.5, 0.3),
                                     offset: point(px(0.), px(8.)),
                                     blur_radius: px(8.),
@@ -143,7 +141,7 @@ impl Render for Shadow {
                         example(
                             "Circle",
                             Shadow::base()
-                                .shadow(smallvec![BoxShadow {
+                                .shadow(vec![BoxShadow {
                                     color: hsla(0.0, 0.5, 0.5, 0.3),
                                     offset: point(px(0.), px(8.)),
                                     blur_radius: px(8.),
@@ -175,7 +173,7 @@ impl Render for Shadow {
                     .children(vec![
                         example(
                             "Blur 0",
-                            Shadow::base().shadow(smallvec![BoxShadow {
+                            Shadow::base().shadow(vec![BoxShadow {
                                 color: hsla(0.0, 0.0, 0.0, 0.3),
                                 offset: point(px(0.), px(8.)),
                                 blur_radius: px(0.),
@@ -184,7 +182,7 @@ impl Render for Shadow {
                         ),
                         example(
                             "Blur 2",
-                            Shadow::base().shadow(smallvec![BoxShadow {
+                            Shadow::base().shadow(vec![BoxShadow {
                                 color: hsla(0.0, 0.0, 0.0, 0.3),
                                 offset: point(px(0.), px(8.)),
                                 blur_radius: px(2.),
@@ -193,7 +191,7 @@ impl Render for Shadow {
                         ),
                         example(
                             "Blur 4",
-                            Shadow::base().shadow(smallvec![BoxShadow {
+                            Shadow::base().shadow(vec![BoxShadow {
                                 color: hsla(0.0, 0.0, 0.0, 0.3),
                                 offset: point(px(0.), px(8.)),
                                 blur_radius: px(4.),
@@ -202,7 +200,7 @@ impl Render for Shadow {
                         ),
                         example(
                             "Blur 8",
-                            Shadow::base().shadow(smallvec![BoxShadow {
+                            Shadow::base().shadow(vec![BoxShadow {
                                 color: hsla(0.0, 0.0, 0.0, 0.3),
                                 offset: point(px(0.), px(8.)),
                                 blur_radius: px(8.),
@@ -211,7 +209,7 @@ impl Render for Shadow {
                         ),
                         example(
                             "Blur 16",
-                            Shadow::base().shadow(smallvec![BoxShadow {
+                            Shadow::base().shadow(vec![BoxShadow {
                                 color: hsla(0.0, 0.0, 0.0, 0.3),
                                 offset: point(px(0.), px(8.)),
                                 blur_radius: px(16.),
@@ -227,7 +225,7 @@ impl Render for Shadow {
                     .children(vec![
                         example(
                             "Spread 0",
-                            Shadow::base().shadow(smallvec![BoxShadow {
+                            Shadow::base().shadow(vec![BoxShadow {
                                 color: hsla(0.0, 0.0, 0.0, 0.3),
                                 offset: point(px(0.), px(8.)),
                                 blur_radius: px(8.),
@@ -236,7 +234,7 @@ impl Render for Shadow {
                         ),
                         example(
                             "Spread 2",
-                            Shadow::base().shadow(smallvec![BoxShadow {
+                            Shadow::base().shadow(vec![BoxShadow {
                                 color: hsla(0.0, 0.0, 0.0, 0.3),
                                 offset: point(px(0.), px(8.)),
                                 blur_radius: px(8.),
@@ -245,7 +243,7 @@ impl Render for Shadow {
                         ),
                         example(
                             "Spread 4",
-                            Shadow::base().shadow(smallvec![BoxShadow {
+                            Shadow::base().shadow(vec![BoxShadow {
                                 color: hsla(0.0, 0.0, 0.0, 0.3),
                                 offset: point(px(0.), px(8.)),
                                 blur_radius: px(8.),
@@ -254,7 +252,7 @@ impl Render for Shadow {
                         ),
                         example(
                             "Spread 8",
-                            Shadow::base().shadow(smallvec![BoxShadow {
+                            Shadow::base().shadow(vec![BoxShadow {
                                 color: hsla(0.0, 0.0, 0.0, 0.3),
                                 offset: point(px(0.), px(8.)),
                                 blur_radius: px(8.),
@@ -263,7 +261,7 @@ impl Render for Shadow {
                         ),
                         example(
                             "Spread 16",
-                            Shadow::base().shadow(smallvec![BoxShadow {
+                            Shadow::base().shadow(vec![BoxShadow {
                                 color: hsla(0.0, 0.0, 0.0, 0.3),
                                 offset: point(px(0.), px(8.)),
                                 blur_radius: px(8.),
@@ -279,7 +277,7 @@ impl Render for Shadow {
                     .children(vec![
                         example(
                             "Square Spread 0",
-                            Shadow::square().shadow(smallvec![BoxShadow {
+                            Shadow::square().shadow(vec![BoxShadow {
                                 color: hsla(0.0, 0.0, 0.0, 0.3),
                                 offset: point(px(0.), px(8.)),
                                 blur_radius: px(8.),
@@ -288,7 +286,7 @@ impl Render for Shadow {
                         ),
                         example(
                             "Square Spread 8",
-                            Shadow::square().shadow(smallvec![BoxShadow {
+                            Shadow::square().shadow(vec![BoxShadow {
                                 color: hsla(0.0, 0.0, 0.0, 0.3),
                                 offset: point(px(0.), px(8.)),
                                 blur_radius: px(8.),
@@ -297,7 +295,7 @@ impl Render for Shadow {
                         ),
                         example(
                             "Square Spread 16",
-                            Shadow::square().shadow(smallvec![BoxShadow {
+                            Shadow::square().shadow(vec![BoxShadow {
                                 color: hsla(0.0, 0.0, 0.0, 0.3),
                                 offset: point(px(0.), px(8.)),
                                 blur_radius: px(8.),
@@ -313,7 +311,7 @@ impl Render for Shadow {
                     .children(vec![
                         example(
                             "Rounded Large Spread 0",
-                            Shadow::rounded_large().shadow(smallvec![BoxShadow {
+                            Shadow::rounded_large().shadow(vec![BoxShadow {
                                 color: hsla(0.0, 0.0, 0.0, 0.3),
                                 offset: point(px(0.), px(8.)),
                                 blur_radius: px(8.),
@@ -322,7 +320,7 @@ impl Render for Shadow {
                         ),
                         example(
                             "Rounded Large Spread 8",
-                            Shadow::rounded_large().shadow(smallvec![BoxShadow {
+                            Shadow::rounded_large().shadow(vec![BoxShadow {
                                 color: hsla(0.0, 0.0, 0.0, 0.3),
                                 offset: point(px(0.), px(8.)),
                                 blur_radius: px(8.),
@@ -331,7 +329,7 @@ impl Render for Shadow {
                         ),
                         example(
                             "Rounded Large Spread 16",
-                            Shadow::rounded_large().shadow(smallvec![BoxShadow {
+                            Shadow::rounded_large().shadow(vec![BoxShadow {
                                 color: hsla(0.0, 0.0, 0.0, 0.3),
                                 offset: point(px(0.), px(8.)),
                                 blur_radius: px(8.),
@@ -347,7 +345,7 @@ impl Render for Shadow {
                     .children(vec![
                         example(
                             "Left",
-                            Shadow::base().shadow(smallvec![BoxShadow {
+                            Shadow::base().shadow(vec![BoxShadow {
                                 color: hsla(0.0, 0.5, 0.5, 0.3),
                                 offset: point(px(-8.), px(0.)),
                                 blur_radius: px(8.),
@@ -356,7 +354,7 @@ impl Render for Shadow {
                         ),
                         example(
                             "Right",
-                            Shadow::base().shadow(smallvec![BoxShadow {
+                            Shadow::base().shadow(vec![BoxShadow {
                                 color: hsla(0.0, 0.5, 0.5, 0.3),
                                 offset: point(px(8.), px(0.)),
                                 blur_radius: px(8.),
@@ -365,7 +363,7 @@ impl Render for Shadow {
                         ),
                         example(
                             "Top",
-                            Shadow::base().shadow(smallvec![BoxShadow {
+                            Shadow::base().shadow(vec![BoxShadow {
                                 color: hsla(0.0, 0.5, 0.5, 0.3),
                                 offset: point(px(0.), px(-8.)),
                                 blur_radius: px(8.),
@@ -374,7 +372,7 @@ impl Render for Shadow {
                         ),
                         example(
                             "Bottom",
-                            Shadow::base().shadow(smallvec![BoxShadow {
+                            Shadow::base().shadow(vec![BoxShadow {
                                 color: hsla(0.0, 0.5, 0.5, 0.3),
                                 offset: point(px(0.), px(8.)),
                                 blur_radius: px(8.),
@@ -390,7 +388,7 @@ impl Render for Shadow {
                     .children(vec![
                         example(
                             "Square Left",
-                            Shadow::square().shadow(smallvec![BoxShadow {
+                            Shadow::square().shadow(vec![BoxShadow {
                                 color: hsla(0.0, 0.5, 0.5, 0.3),
                                 offset: point(px(-8.), px(0.)),
                                 blur_radius: px(8.),
@@ -399,7 +397,7 @@ impl Render for Shadow {
                         ),
                         example(
                             "Square Right",
-                            Shadow::square().shadow(smallvec![BoxShadow {
+                            Shadow::square().shadow(vec![BoxShadow {
                                 color: hsla(0.0, 0.5, 0.5, 0.3),
                                 offset: point(px(8.), px(0.)),
                                 blur_radius: px(8.),
@@ -408,7 +406,7 @@ impl Render for Shadow {
                         ),
                         example(
                             "Square Top",
-                            Shadow::square().shadow(smallvec![BoxShadow {
+                            Shadow::square().shadow(vec![BoxShadow {
                                 color: hsla(0.0, 0.5, 0.5, 0.3),
                                 offset: point(px(0.), px(-8.)),
                                 blur_radius: px(8.),
@@ -417,7 +415,7 @@ impl Render for Shadow {
                         ),
                         example(
                             "Square Bottom",
-                            Shadow::square().shadow(smallvec![BoxShadow {
+                            Shadow::square().shadow(vec![BoxShadow {
                                 color: hsla(0.0, 0.5, 0.5, 0.3),
                                 offset: point(px(0.), px(8.)),
                                 blur_radius: px(8.),
@@ -433,7 +431,7 @@ impl Render for Shadow {
                     .children(vec![
                         example(
                             "Rounded Large Left",
-                            Shadow::rounded_large().shadow(smallvec![BoxShadow {
+                            Shadow::rounded_large().shadow(vec![BoxShadow {
                                 color: hsla(0.0, 0.5, 0.5, 0.3),
                                 offset: point(px(-8.), px(0.)),
                                 blur_radius: px(8.),
@@ -442,7 +440,7 @@ impl Render for Shadow {
                         ),
                         example(
                             "Rounded Large Right",
-                            Shadow::rounded_large().shadow(smallvec![BoxShadow {
+                            Shadow::rounded_large().shadow(vec![BoxShadow {
                                 color: hsla(0.0, 0.5, 0.5, 0.3),
                                 offset: point(px(8.), px(0.)),
                                 blur_radius: px(8.),
@@ -451,7 +449,7 @@ impl Render for Shadow {
                         ),
                         example(
                             "Rounded Large Top",
-                            Shadow::rounded_large().shadow(smallvec![BoxShadow {
+                            Shadow::rounded_large().shadow(vec![BoxShadow {
                                 color: hsla(0.0, 0.5, 0.5, 0.3),
                                 offset: point(px(0.), px(-8.)),
                                 blur_radius: px(8.),
@@ -460,7 +458,7 @@ impl Render for Shadow {
                         ),
                         example(
                             "Rounded Large Bottom",
-                            Shadow::rounded_large().shadow(smallvec![BoxShadow {
+                            Shadow::rounded_large().shadow(vec![BoxShadow {
                                 color: hsla(0.0, 0.5, 0.5, 0.3),
                                 offset: point(px(0.), px(8.)),
                                 blur_radius: px(8.),
@@ -476,7 +474,7 @@ impl Render for Shadow {
                     .children(vec![
                         example(
                             "Circle Multiple",
-                            Shadow::base().shadow(smallvec![
+                            Shadow::base().shadow(vec![
                                 BoxShadow {
                                     color: hsla(0.0 / 360., 1.0, 0.5, 0.3), // Red
                                     offset: point(px(0.), px(-12.)),
@@ -505,7 +503,7 @@ impl Render for Shadow {
                         ),
                         example(
                             "Square Multiple",
-                            Shadow::square().shadow(smallvec![
+                            Shadow::square().shadow(vec![
                                 BoxShadow {
                                     color: hsla(0.0 / 360., 1.0, 0.5, 0.3), // Red
                                     offset: point(px(0.), px(-12.)),
@@ -534,7 +532,7 @@ impl Render for Shadow {
                         ),
                         example(
                             "Rounded Large Multiple",
-                            Shadow::rounded_large().shadow(smallvec![
+                            Shadow::rounded_large().shadow(vec![
                                 BoxShadow {
                                     color: hsla(0.0 / 360., 1.0, 0.5, 0.3), // Red
                                     offset: point(px(0.), px(-12.)),

crates/gpui/examples/text_wrapper.rs 🔗

@@ -73,7 +73,7 @@ impl Render for HelloWorld {
                     .flex_shrink_0()
                     .text_xl()
                     .overflow_hidden()
-                    .text_overflow(TextOverflow::Ellipsis(""))
+                    .text_overflow(TextOverflow::Truncate("".into()))
                     .border_1()
                     .border_color(gpui::green())
                     .child("TRUNCATE: ".to_owned() + text),
@@ -83,7 +83,7 @@ impl Render for HelloWorld {
                     .flex_shrink_0()
                     .text_xl()
                     .overflow_hidden()
-                    .text_overflow(TextOverflow::Ellipsis(""))
+                    .text_overflow(TextOverflow::Truncate("".into()))
                     .line_clamp(3)
                     .border_1()
                     .border_color(gpui::green())

crates/gpui/examples/window_shadow.rs 🔗

@@ -104,7 +104,7 @@ impl Render for WindowShadow {
                             .when(!tiling.left, |div| div.border_l(border_size))
                             .when(!tiling.right, |div| div.border_r(border_size))
                             .when(!tiling.is_tiled(), |div| {
-                                div.shadow(smallvec::smallvec![gpui::BoxShadow {
+                                div.shadow(vec![gpui::BoxShadow {
                                     color: Hsla {
                                         h: 0.,
                                         s: 0.,
@@ -144,7 +144,7 @@ impl Render for WindowShadow {
                                         .w(px(200.0))
                                         .h(px(100.0))
                                         .bg(green())
-                                        .shadow(smallvec::smallvec![gpui::BoxShadow {
+                                        .shadow(vec![gpui::BoxShadow {
                                             color: Hsla {
                                                 h: 0.,
                                                 s: 0.,

crates/gpui/src/app.rs 🔗

@@ -30,6 +30,8 @@ use smallvec::SmallVec;
 pub use test_context::*;
 use util::{ResultExt, debug_panic};
 
+#[cfg(any(feature = "inspector", debug_assertions))]
+use crate::InspectorElementRegistry;
 use crate::{
     Action, ActionBuildError, ActionRegistry, Any, AnyView, AnyWindowHandle, AppContext, Asset,
     AssetSource, BackgroundExecutor, Bounds, ClipboardItem, CursorStyle, DispatchPhase, DisplayId,
@@ -281,6 +283,10 @@ pub struct App {
     pub(crate) window_invalidators_by_entity:
         FxHashMap<EntityId, FxHashMap<WindowId, WindowInvalidator>>,
     pub(crate) tracked_entities: FxHashMap<WindowId, FxHashSet<EntityId>>,
+    #[cfg(any(feature = "inspector", debug_assertions))]
+    pub(crate) inspector_renderer: Option<crate::InspectorRenderer>,
+    #[cfg(any(feature = "inspector", debug_assertions))]
+    pub(crate) inspector_element_registry: InspectorElementRegistry,
     #[cfg(any(test, feature = "test-support", debug_assertions))]
     pub(crate) name: Option<&'static str>,
     quitting: bool,
@@ -345,6 +351,10 @@ impl App {
                 layout_id_buffer: Default::default(),
                 propagate_event: true,
                 prompt_builder: Some(PromptBuilder::Default),
+                #[cfg(any(feature = "inspector", debug_assertions))]
+                inspector_renderer: None,
+                #[cfg(any(feature = "inspector", debug_assertions))]
+                inspector_element_registry: InspectorElementRegistry::default(),
                 quitting: false,
 
                 #[cfg(any(test, feature = "test-support", debug_assertions))]
@@ -1669,6 +1679,21 @@ impl App {
         }
     }
 
+    /// Sets the renderer for the inspector.
+    #[cfg(any(feature = "inspector", debug_assertions))]
+    pub fn set_inspector_renderer(&mut self, f: crate::InspectorRenderer) {
+        self.inspector_renderer = Some(f);
+    }
+
+    /// Registers a renderer specific to an inspector state.
+    #[cfg(any(feature = "inspector", debug_assertions))]
+    pub fn register_inspector_element<T: 'static, R: crate::IntoElement>(
+        &mut self,
+        f: impl 'static + Fn(crate::InspectorElementId, &T, &mut Window, &mut App) -> R,
+    ) {
+        self.inspector_element_registry.register(f);
+    }
+
     /// Initializes gpui's default colors for the application.
     ///
     /// These colors can be accessed through `cx.default_colors()`.

crates/gpui/src/color.rs 🔗

@@ -1,5 +1,9 @@
 use anyhow::{Context as _, bail};
-use serde::de::{self, Deserialize, Deserializer, Visitor};
+use schemars::{JsonSchema, SchemaGenerator, schema::Schema};
+use serde::{
+    Deserialize, Deserializer, Serialize, Serializer,
+    de::{self, Visitor},
+};
 use std::{
     fmt::{self, Display, Formatter},
     hash::{Hash, Hasher},
@@ -94,12 +98,48 @@ impl Visitor<'_> for RgbaVisitor {
     }
 }
 
+impl JsonSchema for Rgba {
+    fn schema_name() -> String {
+        "Rgba".to_string()
+    }
+
+    fn json_schema(_generator: &mut SchemaGenerator) -> Schema {
+        use schemars::schema::{InstanceType, SchemaObject, StringValidation};
+
+        Schema::Object(SchemaObject {
+            instance_type: Some(InstanceType::String.into()),
+            string: Some(Box::new(StringValidation {
+                pattern: Some(
+                    r"^#([0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$".to_string(),
+                ),
+                ..Default::default()
+            })),
+            ..Default::default()
+        })
+    }
+}
+
 impl<'de> Deserialize<'de> for Rgba {
     fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
         deserializer.deserialize_str(RgbaVisitor)
     }
 }
 
+impl Serialize for Rgba {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: Serializer,
+    {
+        let r = (self.r * 255.0).round() as u8;
+        let g = (self.g * 255.0).round() as u8;
+        let b = (self.b * 255.0).round() as u8;
+        let a = (self.a * 255.0).round() as u8;
+
+        let s = format!("#{r:02x}{g:02x}{b:02x}{a:02x}");
+        serializer.serialize_str(&s)
+    }
+}
+
 impl From<Hsla> for Rgba {
     fn from(color: Hsla) -> Self {
         let h = color.h;
@@ -588,20 +628,35 @@ impl From<Rgba> for Hsla {
     }
 }
 
+impl JsonSchema for Hsla {
+    fn schema_name() -> String {
+        Rgba::schema_name()
+    }
+
+    fn json_schema(generator: &mut SchemaGenerator) -> Schema {
+        Rgba::json_schema(generator)
+    }
+}
+
+impl Serialize for Hsla {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: Serializer,
+    {
+        Rgba::from(*self).serialize(serializer)
+    }
+}
+
 impl<'de> Deserialize<'de> for Hsla {
     fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
     where
         D: Deserializer<'de>,
     {
-        // First, deserialize it into Rgba
-        let rgba = Rgba::deserialize(deserializer)?;
-
-        // Then, use the From<Rgba> for Hsla implementation to convert it
-        Ok(Hsla::from(rgba))
+        Ok(Rgba::deserialize(deserializer)?.into())
     }
 }
 
-#[derive(Debug, Clone, Copy, PartialEq)]
+#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)]
 #[repr(C)]
 pub(crate) enum BackgroundTag {
     Solid = 0,
@@ -614,7 +669,7 @@ pub(crate) enum BackgroundTag {
 /// References:
 /// - <https://developer.mozilla.org/en-US/docs/Web/CSS/color-interpolation-method>
 /// - <https://www.w3.org/TR/css-color-4/#typedef-color-space>
-#[derive(Debug, Clone, Copy, PartialEq, Default)]
+#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize, JsonSchema)]
 #[repr(C)]
 pub enum ColorSpace {
     #[default]
@@ -634,7 +689,7 @@ impl Display for ColorSpace {
 }
 
 /// A background color, which can be either a solid color or a linear gradient.
-#[derive(Clone, Copy, PartialEq)]
+#[derive(Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)]
 #[repr(C)]
 pub struct Background {
     pub(crate) tag: BackgroundTag,
@@ -727,7 +782,7 @@ pub fn linear_gradient(
 /// A color stop in a linear gradient.
 ///
 /// <https://developer.mozilla.org/en-US/docs/Web/CSS/gradient/linear-gradient#linear-color-stop>
-#[derive(Debug, Clone, Copy, Default, PartialEq)]
+#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
 #[repr(C)]
 pub struct LinearColorStop {
     /// The color of the color stop.

crates/gpui/src/element.rs 🔗

@@ -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,

crates/gpui/src/elements/anchored.rs 🔗

@@ -1,9 +1,9 @@
 use smallvec::SmallVec;
-use taffy::style::{Display, Position};
 
 use crate::{
-    AnyElement, App, Axis, Bounds, Corner, Edges, Element, GlobalElementId, IntoElement, LayoutId,
-    ParentElement, Pixels, Point, Size, Style, Window, point, px,
+    AnyElement, App, Axis, Bounds, Corner, Display, Edges, Element, GlobalElementId,
+    InspectorElementId, IntoElement, LayoutId, ParentElement, Pixels, Point, Position, Size, Style,
+    Window, point, px,
 };
 
 /// The state that the anchored element element uses to track its children.
@@ -91,9 +91,14 @@ impl Element for Anchored {
         None
     }
 
+    fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
+        None
+    }
+
     fn request_layout(
         &mut self,
         _id: Option<&GlobalElementId>,
+        _inspector_id: Option<&InspectorElementId>,
         window: &mut Window,
         cx: &mut App,
     ) -> (crate::LayoutId, Self::RequestLayoutState) {
@@ -117,6 +122,7 @@ impl Element for Anchored {
     fn prepaint(
         &mut self,
         _id: Option<&GlobalElementId>,
+        _inspector_id: Option<&InspectorElementId>,
         bounds: Bounds<Pixels>,
         request_layout: &mut Self::RequestLayoutState,
         window: &mut Window,
@@ -213,6 +219,7 @@ impl Element for Anchored {
     fn paint(
         &mut self,
         _id: Option<&GlobalElementId>,
+        _inspector_id: Option<&InspectorElementId>,
         _bounds: crate::Bounds<crate::Pixels>,
         _request_layout: &mut Self::RequestLayoutState,
         _prepaint: &mut Self::PrepaintState,

crates/gpui/src/elements/animation.rs 🔗

@@ -1,6 +1,8 @@
 use std::time::{Duration, Instant};
 
-use crate::{AnyElement, App, Element, ElementId, GlobalElementId, IntoElement, Window};
+use crate::{
+    AnyElement, App, Element, ElementId, GlobalElementId, InspectorElementId, IntoElement, Window,
+};
 
 pub use easing::*;
 use smallvec::SmallVec;
@@ -121,9 +123,14 @@ impl<E: IntoElement + 'static> Element for AnimationElement<E> {
         Some(self.id.clone())
     }
 
+    fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
+        None
+    }
+
     fn request_layout(
         &mut self,
         global_id: Option<&GlobalElementId>,
+        _inspector_id: Option<&InspectorElementId>,
         window: &mut Window,
         cx: &mut App,
     ) -> (crate::LayoutId, Self::RequestLayoutState) {
@@ -172,6 +179,7 @@ impl<E: IntoElement + 'static> Element for AnimationElement<E> {
     fn prepaint(
         &mut self,
         _id: Option<&GlobalElementId>,
+        _inspector_id: Option<&InspectorElementId>,
         _bounds: crate::Bounds<crate::Pixels>,
         element: &mut Self::RequestLayoutState,
         window: &mut Window,
@@ -183,6 +191,7 @@ impl<E: IntoElement + 'static> Element for AnimationElement<E> {
     fn paint(
         &mut self,
         _id: Option<&GlobalElementId>,
+        _inspector_id: Option<&InspectorElementId>,
         _bounds: crate::Bounds<crate::Pixels>,
         element: &mut Self::RequestLayoutState,
         _: &mut Self::PrepaintState,

crates/gpui/src/elements/canvas.rs 🔗

@@ -1,8 +1,8 @@
 use refineable::Refineable as _;
 
 use crate::{
-    App, Bounds, Element, ElementId, GlobalElementId, IntoElement, Pixels, Style, StyleRefinement,
-    Styled, Window,
+    App, Bounds, Element, ElementId, GlobalElementId, InspectorElementId, IntoElement, Pixels,
+    Style, StyleRefinement, Styled, Window,
 };
 
 /// Construct a canvas element with the given paint callback.
@@ -42,9 +42,14 @@ impl<T: 'static> Element for Canvas<T> {
         None
     }
 
+    fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
+        None
+    }
+
     fn request_layout(
         &mut self,
         _id: Option<&GlobalElementId>,
+        _inspector_id: Option<&InspectorElementId>,
         window: &mut Window,
         cx: &mut App,
     ) -> (crate::LayoutId, Self::RequestLayoutState) {
@@ -57,6 +62,7 @@ impl<T: 'static> Element for Canvas<T> {
     fn prepaint(
         &mut self,
         _id: Option<&GlobalElementId>,
+        _inspector_id: Option<&InspectorElementId>,
         bounds: Bounds<Pixels>,
         _request_layout: &mut Style,
         window: &mut Window,
@@ -68,6 +74,7 @@ impl<T: 'static> Element for Canvas<T> {
     fn paint(
         &mut self,
         _id: Option<&GlobalElementId>,
+        _inspector_id: Option<&InspectorElementId>,
         bounds: Bounds<Pixels>,
         style: &mut Style,
         prepaint: &mut Self::PrepaintState,

crates/gpui/src/elements/deferred.rs 🔗

@@ -1,5 +1,6 @@
 use crate::{
-    AnyElement, App, Bounds, Element, GlobalElementId, IntoElement, LayoutId, Pixels, Window,
+    AnyElement, App, Bounds, Element, GlobalElementId, InspectorElementId, IntoElement, LayoutId,
+    Pixels, Window,
 };
 
 /// Builds a `Deferred` element, which delays the layout and paint of its child.
@@ -35,9 +36,14 @@ impl Element for Deferred {
         None
     }
 
+    fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
+        None
+    }
+
     fn request_layout(
         &mut self,
         _id: Option<&GlobalElementId>,
+        _inspector_id: Option<&InspectorElementId>,
         window: &mut Window,
         cx: &mut App,
     ) -> (LayoutId, ()) {
@@ -48,6 +54,7 @@ impl Element for Deferred {
     fn prepaint(
         &mut self,
         _id: Option<&GlobalElementId>,
+        _inspector_id: Option<&InspectorElementId>,
         _bounds: Bounds<Pixels>,
         _request_layout: &mut Self::RequestLayoutState,
         window: &mut Window,
@@ -61,6 +68,7 @@ impl Element for Deferred {
     fn paint(
         &mut self,
         _id: Option<&GlobalElementId>,
+        _inspector_id: Option<&InspectorElementId>,
         _bounds: Bounds<Pixels>,
         _request_layout: &mut Self::RequestLayoutState,
         _prepaint: &mut Self::PrepaintState,

crates/gpui/src/elements/div.rs 🔗

@@ -18,10 +18,10 @@
 use crate::{
     Action, AnyDrag, AnyElement, AnyTooltip, AnyView, App, Bounds, ClickEvent, DispatchPhase,
     Element, ElementId, Entity, FocusHandle, Global, GlobalElementId, Hitbox, HitboxId,
-    IntoElement, IsZero, KeyContext, KeyDownEvent, KeyUpEvent, LayoutId, ModifiersChangedEvent,
-    MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, Point,
-    Render, ScrollWheelEvent, SharedString, Size, Style, StyleRefinement, Styled, Task, TooltipId,
-    Visibility, Window, point, px, size,
+    InspectorElementId, IntoElement, IsZero, KeyContext, KeyDownEvent, KeyUpEvent, LayoutId,
+    ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Overflow,
+    ParentElement, Pixels, Point, Render, ScrollWheelEvent, SharedString, Size, Style,
+    StyleRefinement, Styled, Task, TooltipId, Visibility, Window, point, px, size,
 };
 use collections::HashMap;
 use refineable::Refineable;
@@ -37,7 +37,6 @@ use std::{
     sync::Arc,
     time::Duration,
 };
-use taffy::style::Overflow;
 use util::ResultExt;
 
 use super::ImageCacheProvider;
@@ -83,6 +82,35 @@ impl<T: 'static> DragMoveEvent<T> {
 }
 
 impl Interactivity {
+    /// Create an `Interactivity`, capturing the caller location in debug mode.
+    #[cfg(any(feature = "inspector", debug_assertions))]
+    #[track_caller]
+    pub fn new() -> Interactivity {
+        Interactivity {
+            source_location: Some(core::panic::Location::caller()),
+            ..Default::default()
+        }
+    }
+
+    /// Create an `Interactivity`, capturing the caller location in debug mode.
+    #[cfg(not(any(feature = "inspector", debug_assertions)))]
+    pub fn new() -> Interactivity {
+        Interactivity::default()
+    }
+
+    /// Gets the source location of construction. Returns `None` when not in debug mode.
+    pub fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
+        #[cfg(any(feature = "inspector", debug_assertions))]
+        {
+            self.source_location
+        }
+
+        #[cfg(not(any(feature = "inspector", debug_assertions)))]
+        {
+            None
+        }
+    }
+
     /// Bind the given callback to the mouse down event for the given mouse button, during the bubble phase
     /// The imperative API equivalent of [`InteractiveElement::on_mouse_down`]
     ///
@@ -1138,17 +1166,8 @@ pub(crate) type ActionListener =
 /// Construct a new [`Div`] element
 #[track_caller]
 pub fn div() -> Div {
-    #[cfg(debug_assertions)]
-    let interactivity = Interactivity {
-        location: Some(*core::panic::Location::caller()),
-        ..Default::default()
-    };
-
-    #[cfg(not(debug_assertions))]
-    let interactivity = Interactivity::default();
-
     Div {
-        interactivity,
+        interactivity: Interactivity::new(),
         children: SmallVec::default(),
         prepaint_listener: None,
         image_cache: None,
@@ -1191,6 +1210,20 @@ pub struct DivFrameState {
     child_layout_ids: SmallVec<[LayoutId; 2]>,
 }
 
+/// Interactivity state displayed an manipulated in the inspector.
+#[derive(Clone)]
+pub struct DivInspectorState {
+    /// The inspected element's base style. This is used for both inspecting and modifying the
+    /// state. In the future it will make sense to separate the read and write, possibly tracking
+    /// the modifications.
+    #[cfg(any(feature = "inspector", debug_assertions))]
+    pub base_style: Box<StyleRefinement>,
+    /// Inspects the bounds of the element.
+    pub bounds: Bounds<Pixels>,
+    /// Size of the children of the element, or `bounds.size` if it has no children.
+    pub content_size: Size<Pixels>,
+}
+
 impl Styled for Div {
     fn style(&mut self) -> &mut StyleRefinement {
         &mut self.interactivity.base_style
@@ -1217,9 +1250,14 @@ impl Element for Div {
         self.interactivity.element_id.clone()
     }
 
+    fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
+        self.interactivity.source_location()
+    }
+
     fn request_layout(
         &mut self,
         global_id: Option<&GlobalElementId>,
+        inspector_id: Option<&InspectorElementId>,
         window: &mut Window,
         cx: &mut App,
     ) -> (LayoutId, Self::RequestLayoutState) {
@@ -1230,8 +1268,12 @@ impl Element for Div {
             .map(|provider| provider.provide(window, cx));
 
         let layout_id = window.with_image_cache(image_cache, |window| {
-            self.interactivity
-                .request_layout(global_id, window, cx, |style, window, cx| {
+            self.interactivity.request_layout(
+                global_id,
+                inspector_id,
+                window,
+                cx,
+                |style, window, cx| {
                     window.with_text_style(style.text_style().cloned(), |window| {
                         child_layout_ids = self
                             .children
@@ -1240,7 +1282,8 @@ impl Element for Div {
                             .collect::<SmallVec<_>>();
                         window.request_layout(style, child_layout_ids.iter().copied(), cx)
                     })
-                })
+                },
+            )
         });
 
         (layout_id, DivFrameState { child_layout_ids })
@@ -1249,6 +1292,7 @@ impl Element for Div {
     fn prepaint(
         &mut self,
         global_id: Option<&GlobalElementId>,
+        inspector_id: Option<&InspectorElementId>,
         bounds: Bounds<Pixels>,
         request_layout: &mut Self::RequestLayoutState,
         window: &mut Window,
@@ -1294,6 +1338,7 @@ impl Element for Div {
 
         self.interactivity.prepaint(
             global_id,
+            inspector_id,
             bounds,
             content_size,
             window,
@@ -1317,6 +1362,7 @@ impl Element for Div {
     fn paint(
         &mut self,
         global_id: Option<&GlobalElementId>,
+        inspector_id: Option<&InspectorElementId>,
         bounds: Bounds<Pixels>,
         _request_layout: &mut Self::RequestLayoutState,
         hitbox: &mut Option<Hitbox>,
@@ -1331,6 +1377,7 @@ impl Element for Div {
         window.with_image_cache(image_cache, |window| {
             self.interactivity.paint(
                 global_id,
+                inspector_id,
                 bounds,
                 hitbox.as_ref(),
                 window,
@@ -1403,8 +1450,8 @@ pub struct Interactivity {
     pub(crate) tooltip_builder: Option<TooltipBuilder>,
     pub(crate) occlude_mouse: bool,
 
-    #[cfg(debug_assertions)]
-    pub(crate) location: Option<core::panic::Location<'static>>,
+    #[cfg(any(feature = "inspector", debug_assertions))]
+    pub(crate) source_location: Option<&'static core::panic::Location<'static>>,
 
     #[cfg(any(test, feature = "test-support"))]
     pub(crate) debug_selector: Option<String>,
@@ -1415,10 +1462,28 @@ impl Interactivity {
     pub fn request_layout(
         &mut self,
         global_id: Option<&GlobalElementId>,
+        _inspector_id: Option<&InspectorElementId>,
         window: &mut Window,
         cx: &mut App,
         f: impl FnOnce(Style, &mut Window, &mut App) -> LayoutId,
     ) -> LayoutId {
+        #[cfg(any(feature = "inspector", debug_assertions))]
+        window.with_inspector_state(
+            _inspector_id,
+            cx,
+            |inspector_state: &mut Option<DivInspectorState>, _window| {
+                if let Some(inspector_state) = inspector_state {
+                    self.base_style = inspector_state.base_style.clone();
+                } else {
+                    *inspector_state = Some(DivInspectorState {
+                        base_style: self.base_style.clone(),
+                        bounds: Default::default(),
+                        content_size: Default::default(),
+                    })
+                }
+            },
+        );
+
         window.with_optional_element_state::<InteractiveElementState, _>(
             global_id,
             |element_state, window| {
@@ -1478,6 +1543,7 @@ impl Interactivity {
     pub fn prepaint<R>(
         &mut self,
         global_id: Option<&GlobalElementId>,
+        _inspector_id: Option<&InspectorElementId>,
         bounds: Bounds<Pixels>,
         content_size: Size<Pixels>,
         window: &mut Window,
@@ -1485,6 +1551,19 @@ impl Interactivity {
         f: impl FnOnce(&Style, Point<Pixels>, Option<Hitbox>, &mut Window, &mut App) -> R,
     ) -> R {
         self.content_size = content_size;
+
+        #[cfg(any(feature = "inspector", debug_assertions))]
+        window.with_inspector_state(
+            _inspector_id,
+            cx,
+            |inspector_state: &mut Option<DivInspectorState>, _window| {
+                if let Some(inspector_state) = inspector_state {
+                    inspector_state.bounds = bounds;
+                    inspector_state.content_size = content_size;
+                }
+            },
+        );
+
         if let Some(focus_handle) = self.tracked_focus_handle.as_ref() {
             window.set_focus_handle(focus_handle, cx);
         }
@@ -1514,7 +1593,7 @@ impl Interactivity {
                     window.with_content_mask(
                         style.overflow_mask(bounds, window.rem_size()),
                         |window| {
-                            let hitbox = if self.should_insert_hitbox(&style) {
+                            let hitbox = if self.should_insert_hitbox(&style, window, cx) {
                                 Some(window.insert_hitbox(bounds, self.occlude_mouse))
                             } else {
                                 None
@@ -1531,7 +1610,7 @@ impl Interactivity {
         )
     }
 
-    fn should_insert_hitbox(&self, style: &Style) -> bool {
+    fn should_insert_hitbox(&self, style: &Style, window: &Window, cx: &App) -> bool {
         self.occlude_mouse
             || style.mouse_cursor.is_some()
             || self.group.is_some()
@@ -1548,6 +1627,7 @@ impl Interactivity {
             || self.drag_listener.is_some()
             || !self.drop_listeners.is_empty()
             || self.tooltip_builder.is_some()
+            || window.is_inspector_picking(cx)
     }
 
     fn clamp_scroll_position(
@@ -1605,6 +1685,7 @@ impl Interactivity {
     pub fn paint(
         &mut self,
         global_id: Option<&GlobalElementId>,
+        _inspector_id: Option<&InspectorElementId>,
         bounds: Bounds<Pixels>,
         hitbox: Option<&Hitbox>,
         window: &mut Window,
@@ -1672,7 +1753,14 @@ impl Interactivity {
                                     self.paint_keyboard_listeners(window, cx);
                                     f(&style, window, cx);
 
-                                    if hitbox.is_some() {
+                                    if let Some(_hitbox) = hitbox {
+                                        #[cfg(any(feature = "inspector", debug_assertions))]
+                                        window.insert_inspector_hitbox(
+                                            _hitbox.id,
+                                            _inspector_id,
+                                            cx,
+                                        );
+
                                         if let Some(group) = self.group.as_ref() {
                                             GroupHitboxes::pop(group, cx);
                                         }
@@ -1727,7 +1815,7 @@ impl Interactivity {
                         origin: hitbox.origin,
                         size: text.size(FONT_SIZE),
                     };
-                    if self.location.is_some()
+                    if self.source_location.is_some()
                         && text_bounds.contains(&window.mouse_position())
                         && window.modifiers().secondary()
                     {
@@ -1758,7 +1846,7 @@ impl Interactivity {
 
                         window.on_mouse_event({
                             let hitbox = hitbox.clone();
-                            let location = self.location.unwrap();
+                            let location = self.source_location.unwrap();
                             move |e: &crate::MouseDownEvent, phase, window, cx| {
                                 if text_bounds.contains(&e.position)
                                     && phase.capture()
@@ -2721,37 +2809,52 @@ where
         self.element.id()
     }
 
+    fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
+        self.element.source_location()
+    }
+
     fn request_layout(
         &mut self,
         id: Option<&GlobalElementId>,
+        inspector_id: Option<&InspectorElementId>,
         window: &mut Window,
         cx: &mut App,
     ) -> (LayoutId, Self::RequestLayoutState) {
-        self.element.request_layout(id, window, cx)
+        self.element.request_layout(id, inspector_id, window, cx)
     }
 
     fn prepaint(
         &mut self,
         id: Option<&GlobalElementId>,
+        inspector_id: Option<&InspectorElementId>,
         bounds: Bounds<Pixels>,
         state: &mut Self::RequestLayoutState,
         window: &mut Window,
         cx: &mut App,
     ) -> E::PrepaintState {
-        self.element.prepaint(id, bounds, state, window, cx)
+        self.element
+            .prepaint(id, inspector_id, bounds, state, window, cx)
     }
 
     fn paint(
         &mut self,
         id: Option<&GlobalElementId>,
+        inspector_id: Option<&InspectorElementId>,
         bounds: Bounds<Pixels>,
         request_layout: &mut Self::RequestLayoutState,
         prepaint: &mut Self::PrepaintState,
         window: &mut Window,
         cx: &mut App,
     ) {
-        self.element
-            .paint(id, bounds, request_layout, prepaint, window, cx)
+        self.element.paint(
+            id,
+            inspector_id,
+            bounds,
+            request_layout,
+            prepaint,
+            window,
+            cx,
+        )
     }
 }
 
@@ -2818,37 +2921,52 @@ where
         self.element.id()
     }
 
+    fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
+        self.element.source_location()
+    }
+
     fn request_layout(
         &mut self,
         id: Option<&GlobalElementId>,
+        inspector_id: Option<&InspectorElementId>,
         window: &mut Window,
         cx: &mut App,
     ) -> (LayoutId, Self::RequestLayoutState) {
-        self.element.request_layout(id, window, cx)
+        self.element.request_layout(id, inspector_id, window, cx)
     }
 
     fn prepaint(
         &mut self,
         id: Option<&GlobalElementId>,
+        inspector_id: Option<&InspectorElementId>,
         bounds: Bounds<Pixels>,
         state: &mut Self::RequestLayoutState,
         window: &mut Window,
         cx: &mut App,
     ) -> E::PrepaintState {
-        self.element.prepaint(id, bounds, state, window, cx)
+        self.element
+            .prepaint(id, inspector_id, bounds, state, window, cx)
     }
 
     fn paint(
         &mut self,
         id: Option<&GlobalElementId>,
+        inspector_id: Option<&InspectorElementId>,
         bounds: Bounds<Pixels>,
         request_layout: &mut Self::RequestLayoutState,
         prepaint: &mut Self::PrepaintState,
         window: &mut Window,
         cx: &mut App,
     ) {
-        self.element
-            .paint(id, bounds, request_layout, prepaint, window, cx);
+        self.element.paint(
+            id,
+            inspector_id,
+            bounds,
+            request_layout,
+            prepaint,
+            window,
+            cx,
+        );
     }
 }
 

crates/gpui/src/elements/image_cache.rs 🔗

@@ -1,7 +1,8 @@
 use crate::{
     AnyElement, AnyEntity, App, AppContext, Asset, AssetLogger, Bounds, Element, ElementId, Entity,
-    GlobalElementId, ImageAssetLoader, ImageCacheError, IntoElement, LayoutId, ParentElement,
-    Pixels, RenderImage, Resource, Style, StyleRefinement, Styled, Task, Window, hash,
+    GlobalElementId, ImageAssetLoader, ImageCacheError, InspectorElementId, IntoElement, LayoutId,
+    ParentElement, Pixels, RenderImage, Resource, Style, StyleRefinement, Styled, Task, Window,
+    hash,
 };
 
 use futures::{FutureExt, future::Shared};
@@ -102,9 +103,14 @@ impl Element for ImageCacheElement {
         None
     }
 
+    fn source_location(&self) -> Option<&'static core::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) {
@@ -125,6 +131,7 @@ impl Element for ImageCacheElement {
     fn prepaint(
         &mut self,
         _id: Option<&GlobalElementId>,
+        _inspector_id: Option<&InspectorElementId>,
         _bounds: Bounds<Pixels>,
         _request_layout: &mut Self::RequestLayoutState,
         window: &mut Window,
@@ -138,6 +145,7 @@ impl Element for ImageCacheElement {
     fn paint(
         &mut self,
         _id: Option<&GlobalElementId>,
+        _inspector_id: Option<&InspectorElementId>,
         _bounds: Bounds<Pixels>,
         _request_layout: &mut Self::RequestLayoutState,
         _prepaint: &mut Self::PrepaintState,

crates/gpui/src/elements/img.rs 🔗

@@ -1,9 +1,9 @@
 use crate::{
     AbsoluteLength, AnyElement, AnyImageCache, App, Asset, AssetLogger, Bounds, DefiniteLength,
-    Element, ElementId, Entity, GlobalElementId, Hitbox, Image, ImageCache, InteractiveElement,
-    Interactivity, IntoElement, LayoutId, Length, ObjectFit, Pixels, RenderImage, Resource,
-    SMOOTH_SVG_SCALE_FACTOR, SharedString, SharedUri, StyleRefinement, Styled, SvgSize, Task,
-    Window, px, swap_rgba_pa_to_bgra,
+    Element, ElementId, Entity, GlobalElementId, Hitbox, Image, ImageCache, InspectorElementId,
+    InteractiveElement, Interactivity, IntoElement, LayoutId, Length, ObjectFit, Pixels,
+    RenderImage, Resource, SMOOTH_SVG_SCALE_FACTOR, SharedString, SharedUri, StyleRefinement,
+    Styled, SvgSize, Task, Window, px, swap_rgba_pa_to_bgra,
 };
 use anyhow::{Context as _, Result};
 
@@ -194,9 +194,10 @@ pub struct Img {
 }
 
 /// Create a new image element.
+#[track_caller]
 pub fn img(source: impl Into<ImageSource>) -> Img {
     Img {
-        interactivity: Interactivity::default(),
+        interactivity: Interactivity::new(),
         source: source.into(),
         style: ImageStyle::default(),
         image_cache: None,
@@ -266,9 +267,14 @@ impl Element for Img {
         self.interactivity.element_id.clone()
     }
 
+    fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
+        self.interactivity.source_location()
+    }
+
     fn request_layout(
         &mut self,
         global_id: Option<&GlobalElementId>,
+        inspector_id: Option<&InspectorElementId>,
         window: &mut Window,
         cx: &mut App,
     ) -> (LayoutId, Self::RequestLayoutState) {
@@ -290,6 +296,7 @@ impl Element for Img {
 
             let layout_id = self.interactivity.request_layout(
                 global_id,
+                inspector_id,
                 window,
                 cx,
                 |mut style, window, cx| {
@@ -408,6 +415,7 @@ impl Element for Img {
     fn prepaint(
         &mut self,
         global_id: Option<&GlobalElementId>,
+        inspector_id: Option<&InspectorElementId>,
         bounds: Bounds<Pixels>,
         request_layout: &mut Self::RequestLayoutState,
         window: &mut Window,
@@ -415,6 +423,7 @@ impl Element for Img {
     ) -> Self::PrepaintState {
         self.interactivity.prepaint(
             global_id,
+            inspector_id,
             bounds,
             bounds.size,
             window,
@@ -432,6 +441,7 @@ impl Element for Img {
     fn paint(
         &mut self,
         global_id: Option<&GlobalElementId>,
+        inspector_id: Option<&InspectorElementId>,
         bounds: Bounds<Pixels>,
         layout_state: &mut Self::RequestLayoutState,
         hitbox: &mut Self::PrepaintState,
@@ -441,6 +451,7 @@ impl Element for Img {
         let source = self.source.clone();
         self.interactivity.paint(
             global_id,
+            inspector_id,
             bounds,
             hitbox.as_ref(),
             window,

crates/gpui/src/elements/list.rs 🔗

@@ -9,14 +9,13 @@
 
 use crate::{
     AnyElement, App, AvailableSpace, Bounds, ContentMask, DispatchPhase, Edges, Element, EntityId,
-    FocusHandle, GlobalElementId, Hitbox, IntoElement, Pixels, Point, ScrollWheelEvent, Size,
-    Style, StyleRefinement, Styled, Window, point, px, size,
+    FocusHandle, GlobalElementId, Hitbox, InspectorElementId, IntoElement, Overflow, Pixels, Point,
+    ScrollWheelEvent, Size, Style, StyleRefinement, Styled, Window, point, px, size,
 };
 use collections::VecDeque;
 use refineable::Refineable as _;
 use std::{cell::RefCell, ops::Range, rc::Rc};
 use sum_tree::{Bias, SumTree};
-use taffy::style::Overflow;
 
 /// Construct a new list element
 pub fn list(state: ListState) -> List {
@@ -820,9 +819,14 @@ impl Element for List {
         None
     }
 
+    fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
+        None
+    }
+
     fn request_layout(
         &mut self,
         _id: Option<&GlobalElementId>,
+        _inspector_id: Option<&InspectorElementId>,
         window: &mut Window,
         cx: &mut App,
     ) -> (crate::LayoutId, Self::RequestLayoutState) {
@@ -890,6 +894,7 @@ impl Element for List {
     fn prepaint(
         &mut self,
         _id: Option<&GlobalElementId>,
+        _inspector_id: Option<&InspectorElementId>,
         bounds: Bounds<Pixels>,
         _: &mut Self::RequestLayoutState,
         window: &mut Window,
@@ -938,6 +943,7 @@ impl Element for List {
     fn paint(
         &mut self,
         _id: Option<&GlobalElementId>,
+        _inspector_id: Option<&InspectorElementId>,
         bounds: Bounds<crate::Pixels>,
         _: &mut Self::RequestLayoutState,
         prepaint: &mut Self::PrepaintState,

crates/gpui/src/elements/surface.rs 🔗

@@ -1,6 +1,6 @@
 use crate::{
-    App, Bounds, Element, ElementId, GlobalElementId, IntoElement, LayoutId, ObjectFit, Pixels,
-    Style, StyleRefinement, Styled, Window,
+    App, Bounds, Element, ElementId, GlobalElementId, InspectorElementId, IntoElement, LayoutId,
+    ObjectFit, Pixels, Style, StyleRefinement, Styled, Window,
 };
 #[cfg(target_os = "macos")]
 use core_video::pixel_buffer::CVPixelBuffer;
@@ -53,9 +53,14 @@ impl Element for Surface {
         None
     }
 
+    fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
+        None
+    }
+
     fn request_layout(
         &mut self,
         _global_id: Option<&GlobalElementId>,
+        _inspector_id: Option<&InspectorElementId>,
         window: &mut Window,
         cx: &mut App,
     ) -> (LayoutId, Self::RequestLayoutState) {
@@ -68,6 +73,7 @@ impl Element for Surface {
     fn prepaint(
         &mut self,
         _global_id: Option<&GlobalElementId>,
+        _inspector_id: Option<&InspectorElementId>,
         _bounds: Bounds<Pixels>,
         _request_layout: &mut Self::RequestLayoutState,
         _window: &mut Window,
@@ -78,6 +84,7 @@ impl Element for Surface {
     fn paint(
         &mut self,
         _global_id: Option<&GlobalElementId>,
+        _inspector_id: Option<&InspectorElementId>,
         #[cfg_attr(not(target_os = "macos"), allow(unused_variables))] bounds: Bounds<Pixels>,
         _: &mut Self::RequestLayoutState,
         _: &mut Self::PrepaintState,

crates/gpui/src/elements/svg.rs 🔗

@@ -1,7 +1,8 @@
 use crate::{
-    App, Bounds, Element, GlobalElementId, Hitbox, InteractiveElement, Interactivity, IntoElement,
-    LayoutId, Pixels, Point, Radians, SharedString, Size, StyleRefinement, Styled,
-    TransformationMatrix, Window, geometry::Negate as _, point, px, radians, size,
+    App, Bounds, Element, GlobalElementId, Hitbox, InspectorElementId, InteractiveElement,
+    Interactivity, IntoElement, LayoutId, Pixels, Point, Radians, SharedString, Size,
+    StyleRefinement, Styled, TransformationMatrix, Window, geometry::Negate as _, point, px,
+    radians, size,
 };
 use util::ResultExt;
 
@@ -13,9 +14,10 @@ pub struct Svg {
 }
 
 /// Create a new SVG element.
+#[track_caller]
 pub fn svg() -> Svg {
     Svg {
-        interactivity: Interactivity::default(),
+        interactivity: Interactivity::new(),
         transformation: None,
         path: None,
     }
@@ -44,23 +46,31 @@ impl Element for Svg {
         self.interactivity.element_id.clone()
     }
 
+    fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
+        self.interactivity.source_location()
+    }
+
     fn request_layout(
         &mut self,
         global_id: Option<&GlobalElementId>,
+        inspector_id: Option<&InspectorElementId>,
         window: &mut Window,
         cx: &mut App,
     ) -> (LayoutId, Self::RequestLayoutState) {
-        let layout_id =
-            self.interactivity
-                .request_layout(global_id, window, cx, |style, window, cx| {
-                    window.request_layout(style, None, cx)
-                });
+        let layout_id = self.interactivity.request_layout(
+            global_id,
+            inspector_id,
+            window,
+            cx,
+            |style, window, cx| window.request_layout(style, None, cx),
+        );
         (layout_id, ())
     }
 
     fn prepaint(
         &mut self,
         global_id: Option<&GlobalElementId>,
+        inspector_id: Option<&InspectorElementId>,
         bounds: Bounds<Pixels>,
         _request_layout: &mut Self::RequestLayoutState,
         window: &mut Window,
@@ -68,6 +78,7 @@ impl Element for Svg {
     ) -> Option<Hitbox> {
         self.interactivity.prepaint(
             global_id,
+            inspector_id,
             bounds,
             bounds.size,
             window,
@@ -79,6 +90,7 @@ impl Element for Svg {
     fn paint(
         &mut self,
         global_id: Option<&GlobalElementId>,
+        inspector_id: Option<&InspectorElementId>,
         bounds: Bounds<Pixels>,
         _request_layout: &mut Self::RequestLayoutState,
         hitbox: &mut Option<Hitbox>,
@@ -89,6 +101,7 @@ impl Element for Svg {
     {
         self.interactivity.paint(
             global_id,
+            inspector_id,
             bounds,
             hitbox.as_ref(),
             window,

crates/gpui/src/elements/text.rs 🔗

@@ -1,8 +1,9 @@
 use crate::{
     ActiveTooltip, AnyView, App, Bounds, DispatchPhase, Element, ElementId, GlobalElementId,
-    HighlightStyle, Hitbox, IntoElement, LayoutId, MouseDownEvent, MouseMoveEvent, MouseUpEvent,
-    Pixels, Point, SharedString, Size, TextOverflow, TextRun, TextStyle, TooltipId, WhiteSpace,
-    Window, WrappedLine, WrappedLineLayout, register_tooltip_mouse_handlers, set_tooltip_on_window,
+    HighlightStyle, Hitbox, InspectorElementId, IntoElement, LayoutId, MouseDownEvent,
+    MouseMoveEvent, MouseUpEvent, Pixels, Point, SharedString, Size, TextOverflow, TextRun,
+    TextStyle, TooltipId, WhiteSpace, Window, WrappedLine, WrappedLineLayout,
+    register_tooltip_mouse_handlers, set_tooltip_on_window,
 };
 use anyhow::Context as _;
 use smallvec::SmallVec;
@@ -23,9 +24,14 @@ impl Element for &'static str {
         None
     }
 
+    fn source_location(&self) -> Option<&'static core::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) {
@@ -37,6 +43,7 @@ impl Element for &'static str {
     fn prepaint(
         &mut self,
         _id: Option<&GlobalElementId>,
+        _inspector_id: Option<&InspectorElementId>,
         bounds: Bounds<Pixels>,
         text_layout: &mut Self::RequestLayoutState,
         _window: &mut Window,
@@ -48,6 +55,7 @@ impl Element for &'static str {
     fn paint(
         &mut self,
         _id: Option<&GlobalElementId>,
+        _inspector_id: Option<&InspectorElementId>,
         _bounds: Bounds<Pixels>,
         text_layout: &mut TextLayout,
         _: &mut (),
@@ -82,11 +90,14 @@ impl Element for SharedString {
         None
     }
 
+    fn source_location(&self) -> Option<&'static core::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) {
@@ -98,6 +109,7 @@ impl Element for SharedString {
     fn prepaint(
         &mut self,
         _id: Option<&GlobalElementId>,
+        _inspector_id: Option<&InspectorElementId>,
         bounds: Bounds<Pixels>,
         text_layout: &mut Self::RequestLayoutState,
         _window: &mut Window,
@@ -109,6 +121,7 @@ impl Element for SharedString {
     fn paint(
         &mut self,
         _id: Option<&GlobalElementId>,
+        _inspector_id: Option<&InspectorElementId>,
         _bounds: Bounds<Pixels>,
         text_layout: &mut Self::RequestLayoutState,
         _: &mut Self::PrepaintState,
@@ -225,9 +238,14 @@ impl Element for StyledText {
         None
     }
 
+    fn source_location(&self) -> Option<&'static core::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) {
@@ -244,6 +262,7 @@ impl Element for StyledText {
     fn prepaint(
         &mut self,
         _id: Option<&GlobalElementId>,
+        _inspector_id: Option<&InspectorElementId>,
         bounds: Bounds<Pixels>,
         _: &mut Self::RequestLayoutState,
         _window: &mut Window,
@@ -255,6 +274,7 @@ impl Element for StyledText {
     fn paint(
         &mut self,
         _id: Option<&GlobalElementId>,
+        _inspector_id: Option<&InspectorElementId>,
         _bounds: Bounds<Pixels>,
         _: &mut Self::RequestLayoutState,
         _: &mut Self::PrepaintState,
@@ -319,8 +339,8 @@ impl TextLayout {
                     None
                 };
 
-                let (truncate_width, ellipsis) =
-                    if let Some(text_overflow) = text_style.text_overflow {
+                let (truncate_width, truncation_suffix) =
+                    if let Some(text_overflow) = text_style.text_overflow.clone() {
                         let width = known_dimensions.width.or(match available_space.width {
                             crate::AvailableSpace::Definite(x) => match text_style.line_clamp {
                                 Some(max_lines) => Some(x * max_lines),
@@ -330,10 +350,10 @@ impl TextLayout {
                         });
 
                         match text_overflow {
-                            TextOverflow::Ellipsis(s) => (width, Some(s)),
+                            TextOverflow::Truncate(s) => (width, s),
                         }
                     } else {
-                        (None, None)
+                        (None, "".into())
                     };
 
                 if let Some(text_layout) = element_state.0.borrow().as_ref() {
@@ -346,7 +366,12 @@ impl TextLayout {
 
                 let mut line_wrapper = cx.text_system().line_wrapper(text_style.font(), font_size);
                 let text = if let Some(truncate_width) = truncate_width {
-                    line_wrapper.truncate_line(text.clone(), truncate_width, ellipsis, &mut runs)
+                    line_wrapper.truncate_line(
+                        text.clone(),
+                        truncate_width,
+                        &truncation_suffix,
+                        &mut runs,
+                    )
                 } else {
                     text.clone()
                 };
@@ -673,18 +698,24 @@ impl Element for InteractiveText {
         Some(self.element_id.clone())
     }
 
+    fn source_location(&self) -> Option<&'static core::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) {
-        self.text.request_layout(None, window, cx)
+        self.text.request_layout(None, inspector_id, window, cx)
     }
 
     fn prepaint(
         &mut self,
         global_id: Option<&GlobalElementId>,
+        inspector_id: Option<&InspectorElementId>,
         bounds: Bounds<Pixels>,
         state: &mut Self::RequestLayoutState,
         window: &mut Window,
@@ -706,7 +737,8 @@ impl Element for InteractiveText {
                     }
                 }
 
-                self.text.prepaint(None, bounds, state, window, cx);
+                self.text
+                    .prepaint(None, inspector_id, bounds, state, window, cx);
                 let hitbox = window.insert_hitbox(bounds, false);
                 (hitbox, interactive_state)
             },
@@ -716,6 +748,7 @@ impl Element for InteractiveText {
     fn paint(
         &mut self,
         global_id: Option<&GlobalElementId>,
+        inspector_id: Option<&InspectorElementId>,
         bounds: Bounds<Pixels>,
         _: &mut Self::RequestLayoutState,
         hitbox: &mut Hitbox,
@@ -853,7 +886,8 @@ impl Element for InteractiveText {
                     );
                 }
 
-                self.text.paint(None, bounds, &mut (), &mut (), window, cx);
+                self.text
+                    .paint(None, inspector_id, bounds, &mut (), &mut (), window, cx);
 
                 ((), interactive_state)
             },

crates/gpui/src/elements/uniform_list.rs 🔗

@@ -6,13 +6,12 @@
 
 use crate::{
     AnyElement, App, AvailableSpace, Bounds, ContentMask, Context, Element, ElementId, Entity,
-    GlobalElementId, Hitbox, InteractiveElement, Interactivity, IntoElement, IsZero, LayoutId,
-    ListSizingBehavior, Pixels, Render, ScrollHandle, Size, StyleRefinement, Styled, Window, point,
-    size,
+    GlobalElementId, Hitbox, InspectorElementId, InteractiveElement, Interactivity, IntoElement,
+    IsZero, LayoutId, ListSizingBehavior, Overflow, Pixels, Render, ScrollHandle, Size,
+    StyleRefinement, Styled, Window, point, size,
 };
 use smallvec::SmallVec;
 use std::{cell::RefCell, cmp, ops::Range, rc::Rc};
-use taffy::style::Overflow;
 
 use super::ListHorizontalSizingBehavior;
 
@@ -52,11 +51,7 @@ where
         interactivity: Interactivity {
             element_id: Some(id),
             base_style: Box::new(base_style),
-
-            #[cfg(debug_assertions)]
-            location: Some(*core::panic::Location::caller()),
-
-            ..Default::default()
+            ..Interactivity::new()
         },
         scroll_handle: None,
         sizing_behavior: ListSizingBehavior::default(),
@@ -166,9 +161,14 @@ impl Element for UniformList {
         self.interactivity.element_id.clone()
     }
 
+    fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
+        None
+    }
+
     fn request_layout(
         &mut self,
         global_id: Option<&GlobalElementId>,
+        inspector_id: Option<&InspectorElementId>,
         window: &mut Window,
         cx: &mut App,
     ) -> (LayoutId, Self::RequestLayoutState) {
@@ -176,6 +176,7 @@ impl Element for UniformList {
         let item_size = self.measure_item(None, window, cx);
         let layout_id = self.interactivity.request_layout(
             global_id,
+            inspector_id,
             window,
             cx,
             |style, window, cx| match self.sizing_behavior {
@@ -223,6 +224,7 @@ impl Element for UniformList {
     fn prepaint(
         &mut self,
         global_id: Option<&GlobalElementId>,
+        inspector_id: Option<&InspectorElementId>,
         bounds: Bounds<Pixels>,
         frame_state: &mut Self::RequestLayoutState,
         window: &mut Window,
@@ -271,6 +273,7 @@ impl Element for UniformList {
 
         self.interactivity.prepaint(
             global_id,
+            inspector_id,
             bounds,
             content_size,
             window,
@@ -435,6 +438,7 @@ impl Element for UniformList {
     fn paint(
         &mut self,
         global_id: Option<&GlobalElementId>,
+        inspector_id: Option<&InspectorElementId>,
         bounds: Bounds<crate::Pixels>,
         request_layout: &mut Self::RequestLayoutState,
         hitbox: &mut Option<Hitbox>,
@@ -443,6 +447,7 @@ impl Element for UniformList {
     ) {
         self.interactivity.paint(
             global_id,
+            inspector_id,
             bounds,
             hitbox.as_ref(),
             window,

crates/gpui/src/geometry.rs 🔗

@@ -2,13 +2,15 @@
 //! can be used to describe common units, concepts, and the relationships
 //! between them.
 
+use anyhow::{Context as _, anyhow};
 use core::fmt::Debug;
 use derive_more::{Add, AddAssign, Div, DivAssign, Mul, Neg, Sub, SubAssign};
 use refineable::Refineable;
-use serde_derive::{Deserialize, Serialize};
+use schemars::{JsonSchema, SchemaGenerator, schema::Schema};
+use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
 use std::{
     cmp::{self, PartialOrd},
-    fmt,
+    fmt::{self, Display},
     hash::Hash,
     ops::{Add, Div, Mul, MulAssign, Neg, Sub},
 };
@@ -71,9 +73,10 @@ pub trait Along {
     Eq,
     Serialize,
     Deserialize,
+    JsonSchema,
     Hash,
 )]
-#[refineable(Debug)]
+#[refineable(Debug, Serialize, Deserialize, JsonSchema)]
 #[repr(C)]
 pub struct Point<T: Default + Clone + Debug> {
     /// The x coordinate of the point.
@@ -375,12 +378,18 @@ impl<T: Clone + Default + Debug> Clone for Point<T> {
     }
 }
 
+impl<T: Default + Clone + Debug + Display> Display for Point<T> {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "({}, {})", self.x, self.y)
+    }
+}
+
 /// A structure representing a two-dimensional size with width and height in a given unit.
 ///
 /// This struct is generic over the type `T`, which can be any type that implements `Clone`, `Default`, and `Debug`.
 /// It is commonly used to specify dimensions for elements in a UI, such as a window or element.
 #[derive(Refineable, Default, Clone, Copy, PartialEq, Div, Hash, Serialize, Deserialize)]
-#[refineable(Debug)]
+#[refineable(Debug, Serialize, Deserialize, JsonSchema)]
 #[repr(C)]
 pub struct Size<T: Clone + Default + Debug> {
     /// The width component of the size.
@@ -649,6 +658,12 @@ where
     }
 }
 
+impl<T: Default + Clone + Debug + Display> Display for Size<T> {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{} × {}", self.width, self.height)
+    }
+}
+
 impl<T: Clone + Default + Debug> From<Point<T>> for Size<T> {
     fn from(point: Point<T>) -> Self {
         Self {
@@ -1541,6 +1556,18 @@ impl<T: PartialOrd + Default + Debug + Clone> Bounds<T> {
     }
 }
 
+impl<T: Default + Clone + Debug + Display + Add<T, Output = T>> Display for Bounds<T> {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(
+            f,
+            "{} - {} (size {})",
+            self.origin,
+            self.bottom_right(),
+            self.size
+        )
+    }
+}
+
 impl Size<DevicePixels> {
     /// Converts the size from physical to logical pixels.
     pub(crate) fn to_pixels(self, scale_factor: f32) -> Size<Pixels> {
@@ -1647,7 +1674,7 @@ impl<T: Clone + Debug + Copy + Default> Copy for Bounds<T> {}
 /// assert_eq!(edges.left, 40.0);
 /// ```
 #[derive(Refineable, Clone, Default, Debug, Eq, PartialEq)]
-#[refineable(Debug)]
+#[refineable(Debug, Serialize, Deserialize, JsonSchema)]
 #[repr(C)]
 pub struct Edges<T: Clone + Default + Debug> {
     /// The size of the top edge.
@@ -2124,7 +2151,7 @@ impl Corner {
 ///
 /// Each field represents the size of the corner on one side of the box: `top_left`, `top_right`, `bottom_right`, and `bottom_left`.
 #[derive(Refineable, Clone, Default, Debug, Eq, PartialEq)]
-#[refineable(Debug)]
+#[refineable(Debug, Serialize, Deserialize, JsonSchema)]
 #[repr(C)]
 pub struct Corners<T: Clone + Default + Debug> {
     /// The value associated with the top left corner.
@@ -2508,16 +2535,11 @@ impl From<Percentage> for Radians {
     PartialEq,
     Serialize,
     Deserialize,
+    JsonSchema,
 )]
 #[repr(transparent)]
 pub struct Pixels(pub f32);
 
-impl std::fmt::Display for Pixels {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        f.write_fmt(format_args!("{}px", self.0))
-    }
-}
-
 impl Div for Pixels {
     type Output = f32;
 
@@ -2584,6 +2606,30 @@ impl MulAssign<f32> for Pixels {
     }
 }
 
+impl Display for Pixels {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{}px", self.0)
+    }
+}
+
+impl Debug for Pixels {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        Display::fmt(self, f)
+    }
+}
+
+impl TryFrom<&'_ str> for Pixels {
+    type Error = anyhow::Error;
+
+    fn try_from(value: &'_ str) -> Result<Self, Self::Error> {
+        value
+            .strip_suffix("px")
+            .context("expected 'px' suffix")
+            .and_then(|number| Ok(number.parse()?))
+            .map(Self)
+    }
+}
+
 impl Pixels {
     /// Represents zero pixels.
     pub const ZERO: Pixels = Pixels(0.0);
@@ -2706,12 +2752,6 @@ impl From<f32> for Pixels {
     }
 }
 
-impl Debug for Pixels {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        write!(f, "{} px", self.0)
-    }
-}
-
 impl From<Pixels> for f32 {
     fn from(pixels: Pixels) -> Self {
         pixels.0
@@ -2910,7 +2950,7 @@ impl Ord for ScaledPixels {
 
 impl Debug for ScaledPixels {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        write!(f, "{} px (scaled)", self.0)
+        write!(f, "{}px (scaled)", self.0)
     }
 }
 
@@ -3032,9 +3072,27 @@ impl Mul<Pixels> for Rems {
     }
 }
 
+impl Display for Rems {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{}rem", self.0)
+    }
+}
+
 impl Debug for Rems {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        write!(f, "{} rem", self.0)
+        Display::fmt(self, f)
+    }
+}
+
+impl TryFrom<&'_ str> for Rems {
+    type Error = anyhow::Error;
+
+    fn try_from(value: &'_ str) -> Result<Self, Self::Error> {
+        value
+            .strip_suffix("rem")
+            .context("expected 'rem' suffix")
+            .and_then(|number| Ok(number.parse()?))
+            .map(Self)
     }
 }
 
@@ -3044,7 +3102,7 @@ impl Debug for Rems {
 /// affected by the current font size, or a number of rems, which is relative to the font size of
 /// the root element. It is used for specifying dimensions that are either independent of or
 /// related to the typographic scale.
-#[derive(Clone, Copy, Debug, Neg, PartialEq)]
+#[derive(Clone, Copy, Neg, PartialEq)]
 pub enum AbsoluteLength {
     /// A length in pixels.
     Pixels(Pixels),
@@ -3126,6 +3184,87 @@ impl Default for AbsoluteLength {
     }
 }
 
+impl Display for AbsoluteLength {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            Self::Pixels(pixels) => write!(f, "{pixels}"),
+            Self::Rems(rems) => write!(f, "{rems}"),
+        }
+    }
+}
+
+impl Debug for AbsoluteLength {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        Display::fmt(self, f)
+    }
+}
+
+const EXPECTED_ABSOLUTE_LENGTH: &str = "number with 'px' or 'rem' suffix";
+
+impl TryFrom<&'_ str> for AbsoluteLength {
+    type Error = anyhow::Error;
+
+    fn try_from(value: &'_ str) -> Result<Self, Self::Error> {
+        if let Ok(pixels) = value.try_into() {
+            Ok(Self::Pixels(pixels))
+        } else if let Ok(rems) = value.try_into() {
+            Ok(Self::Rems(rems))
+        } else {
+            Err(anyhow!(
+                "invalid AbsoluteLength '{value}', expected {EXPECTED_ABSOLUTE_LENGTH}"
+            ))
+        }
+    }
+}
+
+impl JsonSchema for AbsoluteLength {
+    fn schema_name() -> String {
+        "AbsoluteLength".to_string()
+    }
+
+    fn json_schema(_generator: &mut SchemaGenerator) -> Schema {
+        use schemars::schema::{InstanceType, SchemaObject, StringValidation};
+
+        Schema::Object(SchemaObject {
+            instance_type: Some(InstanceType::String.into()),
+            string: Some(Box::new(StringValidation {
+                pattern: Some(r"^-?\d+(\.\d+)?(px|rem)$".to_string()),
+                ..Default::default()
+            })),
+            ..Default::default()
+        })
+    }
+}
+
+impl<'de> Deserialize<'de> for AbsoluteLength {
+    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
+        struct StringVisitor;
+
+        impl de::Visitor<'_> for StringVisitor {
+            type Value = AbsoluteLength;
+
+            fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
+                write!(f, "{EXPECTED_ABSOLUTE_LENGTH}")
+            }
+
+            fn visit_str<E: de::Error>(self, value: &str) -> Result<Self::Value, E> {
+                AbsoluteLength::try_from(value).map_err(E::custom)
+            }
+        }
+
+        deserializer.deserialize_str(StringVisitor)
+    }
+}
+
+impl Serialize for AbsoluteLength {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: Serializer,
+    {
+        serializer.serialize_str(&format!("{self}"))
+    }
+}
+
 /// A non-auto length that can be defined in pixels, rems, or percent of parent.
 ///
 /// This enum represents lengths that have a specific value, as opposed to lengths that are automatically
@@ -3180,11 +3319,86 @@ impl DefiniteLength {
 }
 
 impl Debug for DefiniteLength {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        Display::fmt(self, f)
+    }
+}
+
+impl Display for DefiniteLength {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         match self {
-            DefiniteLength::Absolute(length) => Debug::fmt(length, f),
-            DefiniteLength::Fraction(fract) => write!(f, "{}%", (fract * 100.0) as i32),
+            DefiniteLength::Absolute(length) => write!(f, "{length}"),
+            DefiniteLength::Fraction(fraction) => write!(f, "{}%", (fraction * 100.0) as i32),
+        }
+    }
+}
+
+const EXPECTED_DEFINITE_LENGTH: &str = "expected number with 'px', 'rem', or '%' suffix";
+
+impl TryFrom<&'_ str> for DefiniteLength {
+    type Error = anyhow::Error;
+
+    fn try_from(value: &'_ str) -> Result<Self, Self::Error> {
+        if let Some(percentage) = value.strip_suffix('%') {
+            let fraction: f32 = percentage.parse::<f32>().with_context(|| {
+                format!("invalid DefiniteLength '{value}', expected {EXPECTED_DEFINITE_LENGTH}")
+            })?;
+            Ok(DefiniteLength::Fraction(fraction / 100.0))
+        } else if let Ok(absolute_length) = value.try_into() {
+            Ok(DefiniteLength::Absolute(absolute_length))
+        } else {
+            Err(anyhow!(
+                "invalid DefiniteLength '{value}', expected {EXPECTED_DEFINITE_LENGTH}"
+            ))
+        }
+    }
+}
+
+impl JsonSchema for DefiniteLength {
+    fn schema_name() -> String {
+        "DefiniteLength".to_string()
+    }
+
+    fn json_schema(_generator: &mut SchemaGenerator) -> Schema {
+        use schemars::schema::{InstanceType, SchemaObject, StringValidation};
+
+        Schema::Object(SchemaObject {
+            instance_type: Some(InstanceType::String.into()),
+            string: Some(Box::new(StringValidation {
+                pattern: Some(r"^-?\d+(\.\d+)?(px|rem|%)$".to_string()),
+                ..Default::default()
+            })),
+            ..Default::default()
+        })
+    }
+}
+
+impl<'de> Deserialize<'de> for DefiniteLength {
+    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
+        struct StringVisitor;
+
+        impl de::Visitor<'_> for StringVisitor {
+            type Value = DefiniteLength;
+
+            fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
+                write!(f, "{EXPECTED_DEFINITE_LENGTH}")
+            }
+
+            fn visit_str<E: de::Error>(self, value: &str) -> Result<Self::Value, E> {
+                DefiniteLength::try_from(value).map_err(E::custom)
+            }
         }
+
+        deserializer.deserialize_str(StringVisitor)
+    }
+}
+
+impl Serialize for DefiniteLength {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: Serializer,
+    {
+        serializer.serialize_str(&format!("{self}"))
     }
 }
 
@@ -3222,14 +3436,86 @@ pub enum Length {
 }
 
 impl Debug for Length {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        Display::fmt(self, f)
+    }
+}
+
+impl Display for Length {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         match self {
-            Length::Definite(definite_length) => write!(f, "{:?}", definite_length),
+            Length::Definite(definite_length) => write!(f, "{}", definite_length),
             Length::Auto => write!(f, "auto"),
         }
     }
 }
 
+const EXPECTED_LENGTH: &str = "expected 'auto' or number with 'px', 'rem', or '%' suffix";
+
+impl TryFrom<&'_ str> for Length {
+    type Error = anyhow::Error;
+
+    fn try_from(value: &'_ str) -> Result<Self, Self::Error> {
+        if value == "auto" {
+            Ok(Length::Auto)
+        } else if let Ok(definite_length) = value.try_into() {
+            Ok(Length::Definite(definite_length))
+        } else {
+            Err(anyhow!(
+                "invalid Length '{value}', expected {EXPECTED_LENGTH}"
+            ))
+        }
+    }
+}
+
+impl JsonSchema for Length {
+    fn schema_name() -> String {
+        "Length".to_string()
+    }
+
+    fn json_schema(_generator: &mut SchemaGenerator) -> Schema {
+        use schemars::schema::{InstanceType, SchemaObject, StringValidation};
+
+        Schema::Object(SchemaObject {
+            instance_type: Some(InstanceType::String.into()),
+            string: Some(Box::new(StringValidation {
+                pattern: Some(r"^(auto|-?\d+(\.\d+)?(px|rem|%))$".to_string()),
+                ..Default::default()
+            })),
+            ..Default::default()
+        })
+    }
+}
+
+impl<'de> Deserialize<'de> for Length {
+    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
+        struct StringVisitor;
+
+        impl de::Visitor<'_> for StringVisitor {
+            type Value = Length;
+
+            fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
+                write!(f, "{EXPECTED_LENGTH}")
+            }
+
+            fn visit_str<E: de::Error>(self, value: &str) -> Result<Self::Value, E> {
+                Length::try_from(value).map_err(E::custom)
+            }
+        }
+
+        deserializer.deserialize_str(StringVisitor)
+    }
+}
+
+impl Serialize for Length {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: Serializer,
+    {
+        serializer.serialize_str(&format!("{self}"))
+    }
+}
+
 /// Constructs a `DefiniteLength` representing a relative fraction of a parent size.
 ///
 /// This function creates a `DefiniteLength` that is a specified fraction of a parent's dimension.

crates/gpui/src/gpui.rs 🔗

@@ -81,6 +81,7 @@ mod executor;
 mod geometry;
 mod global;
 mod input;
+mod inspector;
 mod interactive;
 mod key_dispatch;
 mod keymap;
@@ -135,6 +136,7 @@ pub use global::*;
 pub use gpui_macros::{AppContext, IntoElement, Render, VisualContext, register_action, test};
 pub use http_client;
 pub use input::*;
+pub use inspector::*;
 pub use interactive::*;
 use key_dispatch::*;
 pub use keymap::*;

crates/gpui/src/inspector.rs 🔗

@@ -0,0 +1,223 @@
+/// 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()
+                }),
+            );
+        }
+    }
+}

crates/gpui/src/platform.rs 🔗

@@ -45,6 +45,7 @@ use image::codecs::gif::GifDecoder;
 use image::{AnimationDecoder as _, Frame};
 use parking::Unparker;
 use raw_window_handle::{HasDisplayHandle, HasWindowHandle};
+use schemars::JsonSchema;
 use seahash::SeaHasher;
 use serde::{Deserialize, Serialize};
 use smallvec::SmallVec;
@@ -1244,7 +1245,7 @@ pub enum PromptLevel {
 }
 
 /// The style of the cursor (pointer)
-#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
 pub enum CursorStyle {
     /// The default cursor
     Arrow,

crates/gpui/src/scene.rs 🔗

@@ -1,6 +1,9 @@
 // todo("windows"): remove
 #![cfg_attr(windows, allow(dead_code))]
 
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+
 use crate::{
     AtlasTextureId, AtlasTile, Background, Bounds, ContentMask, Corners, Edges, Hsla, Pixels,
     Point, Radians, ScaledPixels, Size, bounds_tree::BoundsTree, point,
@@ -506,7 +509,7 @@ impl From<Shadow> for Primitive {
 }
 
 /// The style of a border.
-#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)]
+#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
 #[repr(C)]
 pub enum BorderStyle {
     /// A solid border.

crates/gpui/src/style.rs 🔗

@@ -13,11 +13,8 @@ use crate::{
 };
 use collections::HashSet;
 use refineable::Refineable;
-use smallvec::SmallVec;
-pub use taffy::style::{
-    AlignContent, AlignItems, AlignSelf, Display, FlexDirection, FlexWrap, JustifyContent,
-    Overflow, Position,
-};
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
 
 /// Use this struct for interfacing with the 'debug_below' styling from your own elements.
 /// If a parent element has this style set on it, then this struct will be set as a global in
@@ -143,7 +140,7 @@ impl ObjectFit {
 
 /// The CSS styling that can be applied to an element via the `Styled` trait
 #[derive(Clone, Refineable, Debug)]
-#[refineable(Debug)]
+#[refineable(Debug, Serialize, Deserialize, JsonSchema)]
 pub struct Style {
     /// What layout strategy should be used?
     pub display: Display,
@@ -252,7 +249,7 @@ pub struct Style {
     pub corner_radii: Corners<AbsoluteLength>,
 
     /// Box shadow of the element
-    pub box_shadow: SmallVec<[BoxShadow; 2]>,
+    pub box_shadow: Vec<BoxShadow>,
 
     /// The text style of this element
     pub text: TextStyleRefinement,
@@ -279,7 +276,7 @@ impl Styled for StyleRefinement {
 }
 
 /// The value of the visibility property, similar to the CSS property `visibility`
-#[derive(Default, Clone, Copy, Debug, Eq, PartialEq)]
+#[derive(Default, Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
 pub enum Visibility {
     /// The element should be drawn as normal.
     #[default]
@@ -289,7 +286,7 @@ pub enum Visibility {
 }
 
 /// The possible values of the box-shadow property
-#[derive(Clone, Debug)]
+#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
 pub struct BoxShadow {
     /// What color should the shadow have?
     pub color: Hsla,
@@ -302,7 +299,7 @@ pub struct BoxShadow {
 }
 
 /// How to handle whitespace in text
-#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
+#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
 pub enum WhiteSpace {
     /// Normal line wrapping when text overflows the width of the element
     #[default]
@@ -312,14 +309,15 @@ pub enum WhiteSpace {
 }
 
 /// How to truncate text that overflows the width of the element
-#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
 pub enum TextOverflow {
-    /// Truncate the text with an ellipsis, same as: `text-overflow: ellipsis;` in CSS
-    Ellipsis(&'static str),
+    /// Truncate the text when it doesn't fit, and represent this truncation by displaying the
+    /// provided string.
+    Truncate(SharedString),
 }
 
 /// How to align text within the element
-#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
+#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
 pub enum TextAlign {
     /// Align the text to the left of the element
     #[default]
@@ -334,7 +332,7 @@ pub enum TextAlign {
 
 /// The properties that can be used to style text in GPUI
 #[derive(Refineable, Clone, Debug, PartialEq)]
-#[refineable(Debug)]
+#[refineable(Debug, Serialize, Deserialize, JsonSchema)]
 pub struct TextStyle {
     /// The color of the text
     pub color: Hsla,
@@ -769,8 +767,9 @@ impl Default for Style {
 }
 
 /// The properties that can be applied to an underline.
-#[derive(Refineable, Copy, Clone, Default, Debug, PartialEq, Eq, Hash)]
-#[refineable(Debug)]
+#[derive(
+    Refineable, Copy, Clone, Default, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema,
+)]
 pub struct UnderlineStyle {
     /// The thickness of the underline.
     pub thickness: Pixels,
@@ -783,8 +782,9 @@ pub struct UnderlineStyle {
 }
 
 /// The properties that can be applied to a strikethrough.
-#[derive(Refineable, Copy, Clone, Default, Debug, PartialEq, Eq, Hash)]
-#[refineable(Debug)]
+#[derive(
+    Refineable, Copy, Clone, Default, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema,
+)]
 pub struct StrikethroughStyle {
     /// The thickness of the strikethrough.
     pub thickness: Pixels,
@@ -794,7 +794,7 @@ pub struct StrikethroughStyle {
 }
 
 /// The kinds of fill that can be applied to a shape.
-#[derive(Clone, Debug)]
+#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
 pub enum Fill {
     /// A solid color fill.
     Color(Background),
@@ -984,6 +984,305 @@ pub fn combine_highlights(
     })
 }
 
+/// Used to control how child nodes are aligned.
+/// For Flexbox it controls alignment in the cross axis
+/// For Grid it controls alignment in the block axis
+///
+/// [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/align-items)
+#[derive(Copy, Clone, PartialEq, Eq, Debug, Serialize, Deserialize, JsonSchema)]
+// Copy of taffy::style type of the same name, to derive JsonSchema.
+pub enum AlignItems {
+    /// Items are packed toward the start of the axis
+    Start,
+    /// Items are packed toward the end of the axis
+    End,
+    /// Items are packed towards the flex-relative start of the axis.
+    ///
+    /// For flex containers with flex_direction RowReverse or ColumnReverse this is equivalent
+    /// to End. In all other cases it is equivalent to Start.
+    FlexStart,
+    /// Items are packed towards the flex-relative end of the axis.
+    ///
+    /// For flex containers with flex_direction RowReverse or ColumnReverse this is equivalent
+    /// to Start. In all other cases it is equivalent to End.
+    FlexEnd,
+    /// Items are packed along the center of the cross axis
+    Center,
+    /// Items are aligned such as their baselines align
+    Baseline,
+    /// Stretch to fill the container
+    Stretch,
+}
+/// Used to control how child nodes are aligned.
+/// Does not apply to Flexbox, and will be ignored if specified on a flex container
+/// For Grid it controls alignment in the inline axis
+///
+/// [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/justify-items)
+pub type JustifyItems = AlignItems;
+/// Used to control how the specified nodes is aligned.
+/// Overrides the parent Node's `AlignItems` property.
+/// For Flexbox it controls alignment in the cross axis
+/// For Grid it controls alignment in the block axis
+///
+/// [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/align-self)
+pub type AlignSelf = AlignItems;
+/// Used to control how the specified nodes is aligned.
+/// Overrides the parent Node's `JustifyItems` property.
+/// Does not apply to Flexbox, and will be ignored if specified on a flex child
+/// For Grid it controls alignment in the inline axis
+///
+/// [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/justify-self)
+pub type JustifySelf = AlignItems;
+
+/// Sets the distribution of space between and around content items
+/// For Flexbox it controls alignment in the cross axis
+/// For Grid it controls alignment in the block axis
+///
+/// [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/align-content)
+#[derive(Copy, Clone, PartialEq, Eq, Debug, Serialize, Deserialize, JsonSchema)]
+// Copy of taffy::style type of the same name, to derive JsonSchema.
+pub enum AlignContent {
+    /// Items are packed toward the start of the axis
+    Start,
+    /// Items are packed toward the end of the axis
+    End,
+    /// Items are packed towards the flex-relative start of the axis.
+    ///
+    /// For flex containers with flex_direction RowReverse or ColumnReverse this is equivalent
+    /// to End. In all other cases it is equivalent to Start.
+    FlexStart,
+    /// Items are packed towards the flex-relative end of the axis.
+    ///
+    /// For flex containers with flex_direction RowReverse or ColumnReverse this is equivalent
+    /// to Start. In all other cases it is equivalent to End.
+    FlexEnd,
+    /// Items are centered around the middle of the axis
+    Center,
+    /// Items are stretched to fill the container
+    Stretch,
+    /// The first and last items are aligned flush with the edges of the container (no gap)
+    /// The gap between items is distributed evenly.
+    SpaceBetween,
+    /// The gap between the first and last items is exactly THE SAME as the gap between items.
+    /// The gaps are distributed evenly
+    SpaceEvenly,
+    /// The gap between the first and last items is exactly HALF the gap between items.
+    /// The gaps are distributed evenly in proportion to these ratios.
+    SpaceAround,
+}
+
+/// Sets the distribution of space between and around content items
+/// For Flexbox it controls alignment in the main axis
+/// For Grid it controls alignment in the inline axis
+///
+/// [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/justify-content)
+pub type JustifyContent = AlignContent;
+
+/// Sets the layout used for the children of this node
+///
+/// The default values depends on on which feature flags are enabled. The order of precedence is: Flex, Grid, Block, None.
+#[derive(Copy, Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize, JsonSchema)]
+// Copy of taffy::style type of the same name, to derive JsonSchema.
+pub enum Display {
+    /// The children will follow the block layout algorithm
+    Block,
+    /// The children will follow the flexbox layout algorithm
+    #[default]
+    Flex,
+    /// The children will follow the CSS Grid layout algorithm
+    Grid,
+    /// The children will not be laid out, and will follow absolute positioning
+    None,
+}
+
+/// Controls whether flex items are forced onto one line or can wrap onto multiple lines.
+///
+/// Defaults to [`FlexWrap::NoWrap`]
+///
+/// [Specification](https://www.w3.org/TR/css-flexbox-1/#flex-wrap-property)
+#[derive(Copy, Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize, JsonSchema)]
+// Copy of taffy::style type of the same name, to derive JsonSchema.
+pub enum FlexWrap {
+    /// Items will not wrap and stay on a single line
+    #[default]
+    NoWrap,
+    /// Items will wrap according to this item's [`FlexDirection`]
+    Wrap,
+    /// Items will wrap in the opposite direction to this item's [`FlexDirection`]
+    WrapReverse,
+}
+
+/// The direction of the flexbox layout main axis.
+///
+/// There are always two perpendicular layout axes: main (or primary) and cross (or secondary).
+/// Adding items will cause them to be positioned adjacent to each other along the main axis.
+/// By varying this value throughout your tree, you can create complex axis-aligned layouts.
+///
+/// Items are always aligned relative to the cross axis, and justified relative to the main axis.
+///
+/// The default behavior is [`FlexDirection::Row`].
+///
+/// [Specification](https://www.w3.org/TR/css-flexbox-1/#flex-direction-property)
+#[derive(Copy, Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize, JsonSchema)]
+// Copy of taffy::style type of the same name, to derive JsonSchema.
+pub enum FlexDirection {
+    /// Defines +x as the main axis
+    ///
+    /// Items will be added from left to right in a row.
+    #[default]
+    Row,
+    /// Defines +y as the main axis
+    ///
+    /// Items will be added from top to bottom in a column.
+    Column,
+    /// Defines -x as the main axis
+    ///
+    /// Items will be added from right to left in a row.
+    RowReverse,
+    /// Defines -y as the main axis
+    ///
+    /// Items will be added from bottom to top in a column.
+    ColumnReverse,
+}
+
+/// How children overflowing their container should affect layout
+///
+/// In CSS the primary effect of this property is to control whether contents of a parent container that overflow that container should
+/// be displayed anyway, be clipped, or trigger the container to become a scroll container. However it also has secondary effects on layout,
+/// the main ones being:
+///
+///   - The automatic minimum size Flexbox/CSS Grid items with non-`Visible` overflow is `0` rather than being content based
+///   - `Overflow::Scroll` nodes have space in the layout reserved for a scrollbar (width controlled by the `scrollbar_width` property)
+///
+/// In Taffy, we only implement the layout related secondary effects as we are not concerned with drawing/painting. The amount of space reserved for
+/// a scrollbar is controlled by the `scrollbar_width` property. If this is `0` then `Scroll` behaves identically to `Hidden`.
+///
+/// <https://developer.mozilla.org/en-US/docs/Web/CSS/overflow>
+#[derive(Copy, Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize, JsonSchema)]
+// Copy of taffy::style type of the same name, to derive JsonSchema.
+pub enum Overflow {
+    /// The automatic minimum size of this node as a flexbox/grid item should be based on the size of its content.
+    /// Content that overflows this node *should* contribute to the scroll region of its parent.
+    #[default]
+    Visible,
+    /// The automatic minimum size of this node as a flexbox/grid item should be based on the size of its content.
+    /// Content that overflows this node should *not* contribute to the scroll region of its parent.
+    Clip,
+    /// The automatic minimum size of this node as a flexbox/grid item should be `0`.
+    /// Content that overflows this node should *not* contribute to the scroll region of its parent.
+    Hidden,
+    /// The automatic minimum size of this node as a flexbox/grid item should be `0`. Additionally, space should be reserved
+    /// for a scrollbar. The amount of space reserved is controlled by the `scrollbar_width` property.
+    /// Content that overflows this node should *not* contribute to the scroll region of its parent.
+    Scroll,
+}
+
+/// The positioning strategy for this item.
+///
+/// This controls both how the origin is determined for the [`Style::position`] field,
+/// and whether or not the item will be controlled by flexbox's layout algorithm.
+///
+/// WARNING: this enum follows the behavior of [CSS's `position` property](https://developer.mozilla.org/en-US/docs/Web/CSS/position),
+/// which can be unintuitive.
+///
+/// [`Position::Relative`] is the default value, in contrast to the default behavior in CSS.
+#[derive(Copy, Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize, JsonSchema)]
+// Copy of taffy::style type of the same name, to derive JsonSchema.
+pub enum Position {
+    /// The offset is computed relative to the final position given by the layout algorithm.
+    /// Offsets do not affect the position of any other items; they are effectively a correction factor applied at the end.
+    #[default]
+    Relative,
+    /// The offset is computed relative to this item's closest positioned ancestor, if any.
+    /// Otherwise, it is placed relative to the origin.
+    /// No space is created for the item in the page layout, and its size will not be altered.
+    ///
+    /// WARNING: to opt-out of layouting entirely, you must use [`Display::None`] instead on your [`Style`] object.
+    Absolute,
+}
+
+impl From<AlignItems> for taffy::style::AlignItems {
+    fn from(value: AlignItems) -> Self {
+        match value {
+            AlignItems::Start => Self::Start,
+            AlignItems::End => Self::End,
+            AlignItems::FlexStart => Self::FlexStart,
+            AlignItems::FlexEnd => Self::FlexEnd,
+            AlignItems::Center => Self::Center,
+            AlignItems::Baseline => Self::Baseline,
+            AlignItems::Stretch => Self::Stretch,
+        }
+    }
+}
+
+impl From<AlignContent> for taffy::style::AlignContent {
+    fn from(value: AlignContent) -> Self {
+        match value {
+            AlignContent::Start => Self::Start,
+            AlignContent::End => Self::End,
+            AlignContent::FlexStart => Self::FlexStart,
+            AlignContent::FlexEnd => Self::FlexEnd,
+            AlignContent::Center => Self::Center,
+            AlignContent::Stretch => Self::Stretch,
+            AlignContent::SpaceBetween => Self::SpaceBetween,
+            AlignContent::SpaceEvenly => Self::SpaceEvenly,
+            AlignContent::SpaceAround => Self::SpaceAround,
+        }
+    }
+}
+
+impl From<Display> for taffy::style::Display {
+    fn from(value: Display) -> Self {
+        match value {
+            Display::Block => Self::Block,
+            Display::Flex => Self::Flex,
+            Display::Grid => Self::Grid,
+            Display::None => Self::None,
+        }
+    }
+}
+
+impl From<FlexWrap> for taffy::style::FlexWrap {
+    fn from(value: FlexWrap) -> Self {
+        match value {
+            FlexWrap::NoWrap => Self::NoWrap,
+            FlexWrap::Wrap => Self::Wrap,
+            FlexWrap::WrapReverse => Self::WrapReverse,
+        }
+    }
+}
+
+impl From<FlexDirection> for taffy::style::FlexDirection {
+    fn from(value: FlexDirection) -> Self {
+        match value {
+            FlexDirection::Row => Self::Row,
+            FlexDirection::Column => Self::Column,
+            FlexDirection::RowReverse => Self::RowReverse,
+            FlexDirection::ColumnReverse => Self::ColumnReverse,
+        }
+    }
+}
+
+impl From<Overflow> for taffy::style::Overflow {
+    fn from(value: Overflow) -> Self {
+        match value {
+            Overflow::Visible => Self::Visible,
+            Overflow::Clip => Self::Clip,
+            Overflow::Hidden => Self::Hidden,
+            Overflow::Scroll => Self::Scroll,
+        }
+    }
+}
+
+impl From<Position> for taffy::style::Position {
+    fn from(value: Position) -> Self {
+        match value {
+            Position::Relative => Self::Relative,
+            Position::Absolute => Self::Absolute,
+        }
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use crate::{blue, green, red, yellow};

crates/gpui/src/styled.rs 🔗

@@ -1,18 +1,16 @@
 use crate::{
-    self as gpui, AbsoluteLength, AlignItems, BorderStyle, CursorStyle, DefiniteLength, Fill,
-    FlexDirection, FlexWrap, Font, FontStyle, FontWeight, Hsla, JustifyContent, Length,
-    SharedString, StrikethroughStyle, StyleRefinement, TextOverflow, UnderlineStyle, WhiteSpace,
-    px, relative, rems,
+    self as gpui, AbsoluteLength, AlignContent, AlignItems, BorderStyle, CursorStyle,
+    DefiniteLength, Display, Fill, FlexDirection, FlexWrap, Font, FontStyle, FontWeight, Hsla,
+    JustifyContent, Length, SharedString, StrikethroughStyle, StyleRefinement, TextAlign,
+    TextOverflow, TextStyleRefinement, UnderlineStyle, WhiteSpace, px, relative, rems,
 };
-use crate::{TextAlign, TextStyleRefinement};
 pub use gpui_macros::{
     border_style_methods, box_shadow_style_methods, cursor_style_methods, margin_style_methods,
     overflow_style_methods, padding_style_methods, position_style_methods,
     visibility_style_methods,
 };
-use taffy::style::{AlignContent, Display};
 
-const ELLIPSIS: &str = "…";
+const ELLIPSIS: SharedString = SharedString::new_static("…");
 
 /// A trait for elements that can be styled.
 /// Use this to opt-in to a utility CSS-like styling API.
@@ -67,7 +65,7 @@ pub trait Styled: Sized {
     fn text_ellipsis(mut self) -> Self {
         self.text_style()
             .get_or_insert_with(Default::default)
-            .text_overflow = Some(TextOverflow::Ellipsis(ELLIPSIS));
+            .text_overflow = Some(TextOverflow::Truncate(ELLIPSIS));
         self
     }
 

crates/gpui/src/taffy.rs 🔗

@@ -250,10 +250,10 @@ trait ToTaffy<Output> {
 impl ToTaffy<taffy::style::Style> for Style {
     fn to_taffy(&self, rem_size: Pixels) -> taffy::style::Style {
         taffy::style::Style {
-            display: self.display,
+            display: self.display.into(),
             overflow: self.overflow.into(),
             scrollbar_width: self.scrollbar_width,
-            position: self.position,
+            position: self.position.into(),
             inset: self.inset.to_taffy(rem_size),
             size: self.size.to_taffy(rem_size),
             min_size: self.min_size.to_taffy(rem_size),
@@ -262,13 +262,13 @@ impl ToTaffy<taffy::style::Style> for Style {
             margin: self.margin.to_taffy(rem_size),
             padding: self.padding.to_taffy(rem_size),
             border: self.border_widths.to_taffy(rem_size),
-            align_items: self.align_items,
-            align_self: self.align_self,
-            align_content: self.align_content,
-            justify_content: self.justify_content,
+            align_items: self.align_items.map(|x| x.into()),
+            align_self: self.align_self.map(|x| x.into()),
+            align_content: self.align_content.map(|x| x.into()),
+            justify_content: self.justify_content.map(|x| x.into()),
             gap: self.gap.to_taffy(rem_size),
-            flex_direction: self.flex_direction,
-            flex_wrap: self.flex_wrap,
+            flex_direction: self.flex_direction.into(),
+            flex_wrap: self.flex_wrap.into(),
             flex_basis: self.flex_basis.to_taffy(rem_size),
             flex_grow: self.flex_grow,
             flex_shrink: self.flex_shrink,

crates/gpui/src/text_system.rs 🔗

@@ -583,7 +583,7 @@ impl DerefMut for LineWrapperHandle {
 
 /// The degree of blackness or stroke thickness of a font. This value ranges from 100.0 to 900.0,
 /// with 400.0 as normal.
-#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Deserialize, Serialize, JsonSchema)]
+#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Serialize, Deserialize, JsonSchema)]
 pub struct FontWeight(pub f32);
 
 impl Default for FontWeight {
@@ -636,7 +636,7 @@ impl FontWeight {
 }
 
 /// Allows italic or oblique faces to be selected.
-#[derive(Clone, Copy, Eq, PartialEq, Debug, Hash, Default)]
+#[derive(Clone, Copy, Eq, PartialEq, Debug, Hash, Default, Serialize, Deserialize, JsonSchema)]
 pub enum FontStyle {
     /// A face that is neither italic not obliqued.
     #[default]

crates/gpui/src/text_system/line_wrapper.rs 🔗

@@ -133,21 +133,18 @@ impl LineWrapper {
         &mut self,
         line: SharedString,
         truncate_width: Pixels,
-        ellipsis: Option<&str>,
+        truncation_suffix: &str,
         runs: &mut Vec<TextRun>,
     ) -> SharedString {
         let mut width = px(0.);
-        let mut ellipsis_width = px(0.);
-        if let Some(ellipsis) = ellipsis {
-            for c in ellipsis.chars() {
-                ellipsis_width += self.width_for_char(c);
-            }
-        }
-
+        let mut suffix_width = truncation_suffix
+            .chars()
+            .map(|c| self.width_for_char(c))
+            .fold(px(0.0), |a, x| a + x);
         let mut char_indices = line.char_indices();
         let mut truncate_ix = 0;
         for (ix, c) in char_indices {
-            if width + ellipsis_width < truncate_width {
+            if width + suffix_width < truncate_width {
                 truncate_ix = ix;
             }
 
@@ -155,9 +152,9 @@ impl LineWrapper {
             width += char_width;
 
             if width.floor() > truncate_width {
-                let ellipsis = ellipsis.unwrap_or("");
-                let result = SharedString::from(format!("{}{}", &line[..truncate_ix], ellipsis));
-                update_runs_after_truncation(&result, ellipsis, runs);
+                let result =
+                    SharedString::from(format!("{}{}", &line[..truncate_ix], truncation_suffix));
+                update_runs_after_truncation(&result, truncation_suffix, runs);
 
                 return result;
             }
@@ -500,7 +497,7 @@ mod tests {
             wrapper: &mut LineWrapper,
             text: &'static str,
             result: &'static str,
-            ellipsis: Option<&str>,
+            ellipsis: &str,
         ) {
             let dummy_run_lens = vec![text.len()];
             let mut dummy_runs = generate_test_runs(&dummy_run_lens);
@@ -515,19 +512,19 @@ mod tests {
             &mut wrapper,
             "aa bbb cccc ddddd eeee ffff gggg",
             "aa bbb cccc ddddd eeee",
-            None,
+            "",
         );
         perform_test(
             &mut wrapper,
             "aa bbb cccc ddddd eeee ffff gggg",
             "aa bbb cccc ddddd eee…",
-            Some("…"),
+            "…",
         );
         perform_test(
             &mut wrapper,
             "aa bbb cccc ddddd eeee ffff gggg",
             "aa bbb cccc dddd......",
-            Some("......"),
+            "......",
         );
     }
 
@@ -545,7 +542,7 @@ mod tests {
         ) {
             let mut dummy_runs = generate_test_runs(run_lens);
             assert_eq!(
-                wrapper.truncate_line(text.into(), line_width, Some("…"), &mut dummy_runs),
+                wrapper.truncate_line(text.into(), line_width, "…", &mut dummy_runs),
                 result
             );
             for (run, result_len) in dummy_runs.iter().zip(result_run_len) {

crates/gpui/src/view.rs 🔗

@@ -1,7 +1,7 @@
 use crate::{
     AnyElement, AnyEntity, AnyWeakEntity, App, Bounds, ContentMask, Context, Element, ElementId,
-    Entity, EntityId, GlobalElementId, IntoElement, LayoutId, PaintIndex, Pixels,
-    PrepaintStateIndex, Render, Style, StyleRefinement, TextStyle, WeakEntity,
+    Entity, EntityId, GlobalElementId, InspectorElementId, IntoElement, LayoutId, PaintIndex,
+    Pixels, PrepaintStateIndex, Render, Style, StyleRefinement, TextStyle, WeakEntity,
 };
 use crate::{Empty, Window};
 use anyhow::Result;
@@ -33,9 +33,14 @@ impl<V: Render> Element for Entity<V> {
         Some(ElementId::View(self.entity_id()))
     }
 
+    fn source_location(&self) -> Option<&'static std::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) {
@@ -49,6 +54,7 @@ impl<V: Render> Element for Entity<V> {
     fn prepaint(
         &mut self,
         _id: Option<&GlobalElementId>,
+        _inspector_id: Option<&InspectorElementId>,
         _: Bounds<Pixels>,
         element: &mut Self::RequestLayoutState,
         window: &mut Window,
@@ -61,6 +67,7 @@ impl<V: Render> Element for Entity<V> {
     fn paint(
         &mut self,
         _id: Option<&GlobalElementId>,
+        _inspector_id: Option<&InspectorElementId>,
         _: Bounds<Pixels>,
         element: &mut Self::RequestLayoutState,
         _: &mut Self::PrepaintState,
@@ -146,22 +153,32 @@ impl Element for AnyView {
         Some(ElementId::View(self.entity_id()))
     }
 
+    fn source_location(&self) -> Option<&'static core::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) {
         window.with_rendered_view(self.entity_id(), |window| {
-            if let Some(style) = self.cached_style.as_ref() {
-                let mut root_style = Style::default();
-                root_style.refine(style);
-                let layout_id = window.request_layout(root_style, None, cx);
-                (layout_id, None)
-            } else {
-                let mut element = (self.render)(self, window, cx);
-                let layout_id = element.request_layout(window, cx);
-                (layout_id, Some(element))
+            // Disable caching when inspecting so that mouse_hit_test has all hitboxes.
+            let caching_disabled = window.is_inspector_picking(cx);
+            match self.cached_style.as_ref() {
+                Some(style) if !caching_disabled => {
+                    let mut root_style = Style::default();
+                    root_style.refine(style);
+                    let layout_id = window.request_layout(root_style, None, cx);
+                    (layout_id, None)
+                }
+                _ => {
+                    let mut element = (self.render)(self, window, cx);
+                    let layout_id = element.request_layout(window, cx);
+                    (layout_id, Some(element))
+                }
             }
         })
     }
@@ -169,6 +186,7 @@ impl Element for AnyView {
     fn prepaint(
         &mut self,
         global_id: Option<&GlobalElementId>,
+        _inspector_id: Option<&InspectorElementId>,
         bounds: Bounds<Pixels>,
         element: &mut Self::RequestLayoutState,
         window: &mut Window,
@@ -176,70 +194,69 @@ impl Element for AnyView {
     ) -> Option<AnyElement> {
         window.set_view_id(self.entity_id());
         window.with_rendered_view(self.entity_id(), |window| {
-            if self.cached_style.is_some() {
-                window.with_element_state::<AnyViewState, _>(
-                    global_id.unwrap(),
-                    |element_state, window| {
-                        let content_mask = window.content_mask();
-                        let text_style = window.text_style();
-
-                        if let Some(mut element_state) = element_state {
-                            if element_state.cache_key.bounds == bounds
-                                && element_state.cache_key.content_mask == content_mask
-                                && element_state.cache_key.text_style == text_style
-                                && !window.dirty_views.contains(&self.entity_id())
-                                && !window.refreshing
-                            {
-                                let prepaint_start = window.prepaint_index();
-                                window.reuse_prepaint(element_state.prepaint_range.clone());
-                                cx.entities
-                                    .extend_accessed(&element_state.accessed_entities);
-                                let prepaint_end = window.prepaint_index();
-                                element_state.prepaint_range = prepaint_start..prepaint_end;
-
-                                return (None, element_state);
-                            }
-                        }
-
-                        let refreshing = mem::replace(&mut window.refreshing, true);
-                        let prepaint_start = window.prepaint_index();
-                        let (mut element, accessed_entities) = cx.detect_accessed_entities(|cx| {
-                            let mut element = (self.render)(self, window, cx);
-                            element.layout_as_root(bounds.size.into(), window, cx);
-                            element.prepaint_at(bounds.origin, window, cx);
-                            element
-                        });
-
-                        let prepaint_end = window.prepaint_index();
-                        window.refreshing = refreshing;
-
-                        (
-                            Some(element),
-                            AnyViewState {
-                                accessed_entities,
-                                prepaint_range: prepaint_start..prepaint_end,
-                                paint_range: PaintIndex::default()..PaintIndex::default(),
-                                cache_key: ViewCacheKey {
-                                    bounds,
-                                    content_mask,
-                                    text_style,
-                                },
-                            },
-                        )
-                    },
-                )
-            } else {
-                let mut element = element.take().unwrap();
+            if let Some(mut element) = element.take() {
                 element.prepaint(window, cx);
-
-                Some(element)
+                return Some(element);
             }
+
+            window.with_element_state::<AnyViewState, _>(
+                global_id.unwrap(),
+                |element_state, window| {
+                    let content_mask = window.content_mask();
+                    let text_style = window.text_style();
+
+                    if let Some(mut element_state) = element_state {
+                        if element_state.cache_key.bounds == bounds
+                            && element_state.cache_key.content_mask == content_mask
+                            && element_state.cache_key.text_style == text_style
+                            && !window.dirty_views.contains(&self.entity_id())
+                            && !window.refreshing
+                        {
+                            let prepaint_start = window.prepaint_index();
+                            window.reuse_prepaint(element_state.prepaint_range.clone());
+                            cx.entities
+                                .extend_accessed(&element_state.accessed_entities);
+                            let prepaint_end = window.prepaint_index();
+                            element_state.prepaint_range = prepaint_start..prepaint_end;
+
+                            return (None, element_state);
+                        }
+                    }
+
+                    let refreshing = mem::replace(&mut window.refreshing, true);
+                    let prepaint_start = window.prepaint_index();
+                    let (mut element, accessed_entities) = cx.detect_accessed_entities(|cx| {
+                        let mut element = (self.render)(self, window, cx);
+                        element.layout_as_root(bounds.size.into(), window, cx);
+                        element.prepaint_at(bounds.origin, window, cx);
+                        element
+                    });
+
+                    let prepaint_end = window.prepaint_index();
+                    window.refreshing = refreshing;
+
+                    (
+                        Some(element),
+                        AnyViewState {
+                            accessed_entities,
+                            prepaint_range: prepaint_start..prepaint_end,
+                            paint_range: PaintIndex::default()..PaintIndex::default(),
+                            cache_key: ViewCacheKey {
+                                bounds,
+                                content_mask,
+                                text_style,
+                            },
+                        },
+                    )
+                },
+            )
         })
     }
 
     fn paint(
         &mut self,
         global_id: Option<&GlobalElementId>,
+        _inspector_id: Option<&InspectorElementId>,
         _bounds: Bounds<Pixels>,
         _: &mut Self::RequestLayoutState,
         element: &mut Self::PrepaintState,
@@ -247,7 +264,8 @@ impl Element for AnyView {
         cx: &mut App,
     ) {
         window.with_rendered_view(self.entity_id(), |window| {
-            if self.cached_style.is_some() {
+            let caching_disabled = window.is_inspector_picking(cx);
+            if self.cached_style.is_some() && !caching_disabled {
                 window.with_element_state::<AnyViewState, _>(
                     global_id.unwrap(),
                     |element_state, window| {

crates/gpui/src/window.rs 🔗

@@ -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>),
 }
 

crates/gpui_macros/src/derive_into_element.rs 🔗

@@ -13,6 +13,7 @@ pub fn derive_into_element(input: TokenStream) -> TokenStream {
         {
             type Element = gpui::Component<Self>;
 
+            #[track_caller]
             fn into_element(self) -> Self::Element {
                 gpui::Component::new(self)
             }

crates/gpui_macros/src/styles.rs 🔗

@@ -393,7 +393,7 @@ pub fn box_shadow_style_methods(input: TokenStream) -> TokenStream {
     let output = quote! {
         /// Sets the box shadow of the element.
         /// [Docs](https://tailwindcss.com/docs/box-shadow)
-        #visibility fn shadow(mut self, shadows: smallvec::SmallVec<[gpui::BoxShadow; 2]>) -> Self {
+        #visibility fn shadow(mut self, shadows: std::vec::Vec<gpui::BoxShadow>) -> Self {
             self.style().box_shadow = Some(shadows);
             self
         }
@@ -409,9 +409,9 @@ pub fn box_shadow_style_methods(input: TokenStream) -> TokenStream {
         /// [Docs](https://tailwindcss.com/docs/box-shadow)
         #visibility fn shadow_sm(mut self) -> Self {
             use gpui::{BoxShadow, hsla, point, px};
-            use smallvec::smallvec;
+            use std::vec;
 
-            self.style().box_shadow = Some(smallvec![BoxShadow {
+            self.style().box_shadow = Some(vec![BoxShadow {
                 color: hsla(0., 0., 0., 0.05),
                 offset: point(px(0.), px(1.)),
                 blur_radius: px(2.),
@@ -424,9 +424,9 @@ pub fn box_shadow_style_methods(input: TokenStream) -> TokenStream {
         /// [Docs](https://tailwindcss.com/docs/box-shadow)
         #visibility fn shadow_md(mut self) -> Self {
             use gpui::{BoxShadow, hsla, point, px};
-            use smallvec::smallvec;
+            use std::vec;
 
-            self.style().box_shadow = Some(smallvec![
+            self.style().box_shadow = Some(vec![
                 BoxShadow {
                     color: hsla(0.5, 0., 0., 0.1),
                     offset: point(px(0.), px(4.)),
@@ -447,9 +447,9 @@ pub fn box_shadow_style_methods(input: TokenStream) -> TokenStream {
         /// [Docs](https://tailwindcss.com/docs/box-shadow)
         #visibility fn shadow_lg(mut self) -> Self {
             use gpui::{BoxShadow, hsla, point, px};
-            use smallvec::smallvec;
+            use std::vec;
 
-            self.style().box_shadow = Some(smallvec![
+            self.style().box_shadow = Some(vec![
                 BoxShadow {
                     color: hsla(0., 0., 0., 0.1),
                     offset: point(px(0.), px(10.)),
@@ -470,9 +470,9 @@ pub fn box_shadow_style_methods(input: TokenStream) -> TokenStream {
         /// [Docs](https://tailwindcss.com/docs/box-shadow)
         #visibility fn shadow_xl(mut self) -> Self {
             use gpui::{BoxShadow, hsla, point, px};
-            use smallvec::smallvec;
+            use std::vec;
 
-            self.style().box_shadow = Some(smallvec![
+            self.style().box_shadow = Some(vec![
                 BoxShadow {
                     color: hsla(0., 0., 0., 0.1),
                     offset: point(px(0.), px(20.)),
@@ -493,9 +493,9 @@ pub fn box_shadow_style_methods(input: TokenStream) -> TokenStream {
         /// [Docs](https://tailwindcss.com/docs/box-shadow)
         #visibility fn shadow_2xl(mut self) -> Self {
             use gpui::{BoxShadow, hsla, point, px};
-            use smallvec::smallvec;
+            use std::vec;
 
-            self.style().box_shadow = Some(smallvec![BoxShadow {
+            self.style().box_shadow = Some(vec![BoxShadow {
                 color: hsla(0., 0., 0., 0.25),
                 offset: point(px(0.), px(25.)),
                 blur_radius: px(50.),

crates/inspector_ui/Cargo.toml 🔗

@@ -0,0 +1,28 @@
+[package]
+name = "inspector_ui"
+version = "0.1.0"
+publish.workspace = true
+edition.workspace = true
+license = "GPL-3.0-or-later"
+
+[lints]
+workspace = true
+
+[lib]
+path = "src/inspector_ui.rs"
+
+[dependencies]
+anyhow.workspace = true
+command_palette_hooks.workspace = true
+editor.workspace = true
+gpui.workspace = true
+language.workspace = true
+project.workspace = true
+serde_json.workspace = true
+serde_json_lenient.workspace = true
+theme.workspace = true
+ui.workspace = true
+util.workspace = true
+workspace.workspace = true
+workspace-hack.workspace = true
+zed_actions.workspace = true

crates/inspector_ui/README.md 🔗

@@ -0,0 +1,84 @@
+# Inspector
+
+This is a tool for inspecting and manipulating rendered elements in Zed. It is
+only available in debug builds. Use the `dev::ToggleInspector` action to toggle
+inspector mode and click on UI elements to inspect them.
+
+# Current features
+
+* 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.
+
+# Known bugs
+
+* The style inspector buffer will leak memory over time due to building up
+history on each change of inspected element. Instead of using `Project` to
+create it, should just directly build the `Buffer` and `File` each time the inspected element changes.
+
+# Future features
+
+* Info and manipulation of element types other than `Div`.
+
+* Ability to highlight current element after it's been picked.
+
+* Indicate when the picked element has disappeared.
+
+* Hierarchy view?
+
+## Better manipulation than JSON
+
+The current approach is not easy to move back to the code. Possibilities:
+
+* Editable list of style attributes to apply.
+
+* Rust buffer of code that does a very lenient parse to get the style attributes. Some options:
+
+  - Take all the identifier-like tokens and use them if they are the name of an attribute. A custom completion provider in a buffer could be used.
+
+  - Use TreeSitter to parse out the fluent style method chain. With this approach the buffer could even be the actual code file. Tricky part of this is LSP - ideally the LSP already being used by the developer's Zed would be used.
+
+## Source locations
+
+* Mode to navigate to source code on every element change while picking.
+
+* Tracking of more source locations - currently the source location is often in a ui compoenent. Ideally this would have a way for the components to indicate that they are probably not the source location the user is looking for.
+
+## Persistent modification
+
+Currently, element modifications disappear when picker mode is started. Handling this well is tricky. Potential features:
+
+* Support modifying multiple elements at once. This requires a way to specify which elements are modified - possibly wildcards in a match of the `InspectorElementId` path. This might default to ignoring all numeric parts and just matching on the names.
+
+* Show a list of active modifications in the UI.
+
+* Support for modifications being partial overrides instead of snapshots. A trickiness here is that multiple modifications may apply to the same element.
+
+* The code should probably distinguish the data that is provided by the element and the modifications from the inspector. Currently these are conflated in element states.
+
+# Code cleanups
+
+## Remove special side pane rendering
+
+Currently the inspector has special rendering in the UI, but maybe it could just be a workspace item.
+
+## Pull more inspector logic out of GPUI
+
+Currently `crates/gpui/inspector.rs` and `crates/inspector_ui/inspector.rs` are quite entangled.  It seems cleaner to pull as much logic a possible out of GPUI.
+
+## Cleaner lifecycle for inspector state viewers / editors
+
+Currently element state inspectors are just called on render. Ideally instead they would be implementors of some trait like:
+
+```
+trait StateInspector: Render {
+    fn new(cx: &mut App) -> Task<Self>;
+    fn element_changed(inspector_id: &InspectorElementId, window: &mut Window, cx: &mut App);
+}
+```
+
+See `div_inspector.rs` - it needs to initialize itself, keep track of its own loading state, and keep track of the last inspected ID in its render function.

crates/inspector_ui/build.rs 🔗

@@ -0,0 +1,20 @@
+fn main() {
+    let cargo_manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
+    let mut path = std::path::PathBuf::from(&cargo_manifest_dir);
+
+    if path.file_name().as_ref().and_then(|name| name.to_str()) != Some("inspector_ui") {
+        panic!(
+            "expected CARGO_MANIFEST_DIR to end with crates/inspector_ui, but got {cargo_manifest_dir}"
+        );
+    }
+    path.pop();
+
+    if path.file_name().as_ref().and_then(|name| name.to_str()) != Some("crates") {
+        panic!(
+            "expected CARGO_MANIFEST_DIR to end with crates/inspector_ui, but got {cargo_manifest_dir}"
+        );
+    }
+    path.pop();
+
+    println!("cargo:rustc-env=ZED_REPO_DIR={}", path.display());
+}

crates/inspector_ui/src/div_inspector.rs 🔗

@@ -0,0 +1,223 @@
+use anyhow::Result;
+use editor::{Editor, EditorEvent, EditorMode, MultiBuffer};
+use gpui::{
+    AsyncWindowContext, DivInspectorState, Entity, InspectorElementId, IntoElement, WeakEntity,
+    Window,
+};
+use language::Buffer;
+use language::language_settings::SoftWrap;
+use project::{Project, ProjectPath};
+use std::path::Path;
+use ui::{Label, LabelSize, Tooltip, prelude::*, v_flex};
+
+/// Path used for unsaved buffer that contains style json. To support the json language server, this
+/// matches the name used in the generated schemas.
+const ZED_INSPECTOR_STYLE_PATH: &str = "/zed-inspector-style.json";
+
+pub(crate) struct DivInspector {
+    project: Entity<Project>,
+    inspector_id: Option<InspectorElementId>,
+    state: Option<DivInspectorState>,
+    style_buffer: Option<Entity<Buffer>>,
+    style_editor: Option<Entity<Editor>>,
+    last_error: Option<SharedString>,
+}
+
+impl DivInspector {
+    pub fn new(
+        project: Entity<Project>,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) -> DivInspector {
+        // Open the buffer once, so it can then be used for each editor.
+        cx.spawn_in(window, {
+            let project = project.clone();
+            async move |this, cx| Self::open_style_buffer(project, this, cx).await
+        })
+        .detach();
+
+        DivInspector {
+            project,
+            inspector_id: None,
+            state: None,
+            style_buffer: None,
+            style_editor: None,
+            last_error: None,
+        }
+    }
+
+    async fn open_style_buffer(
+        project: Entity<Project>,
+        this: WeakEntity<DivInspector>,
+        cx: &mut AsyncWindowContext,
+    ) -> Result<()> {
+        let worktree = project
+            .update(cx, |project, cx| {
+                project.create_worktree(ZED_INSPECTOR_STYLE_PATH, false, cx)
+            })?
+            .await?;
+
+        let project_path = worktree.read_with(cx, |worktree, _cx| ProjectPath {
+            worktree_id: worktree.id(),
+            path: Path::new("").into(),
+        })?;
+
+        let style_buffer = project
+            .update(cx, |project, cx| project.open_path(project_path, cx))?
+            .await?
+            .1;
+
+        project.update(cx, |project, cx| {
+            project.register_buffer_with_language_servers(&style_buffer, cx)
+        })?;
+
+        this.update_in(cx, |this, window, cx| {
+            this.style_buffer = Some(style_buffer);
+            if let Some(id) = this.inspector_id.clone() {
+                let state =
+                    window.with_inspector_state(Some(&id), cx, |state, _window| state.clone());
+                if let Some(state) = state {
+                    this.update_inspected_element(&id, state, window, cx);
+                    cx.notify();
+                }
+            }
+        })?;
+
+        Ok(())
+    }
+
+    pub fn update_inspected_element(
+        &mut self,
+        id: &InspectorElementId,
+        state: DivInspectorState,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        let base_style_json = serde_json::to_string_pretty(&state.base_style);
+        self.state = Some(state);
+
+        if self.inspector_id.as_ref() == Some(id) {
+            return;
+        } else {
+            self.inspector_id = Some(id.clone());
+        }
+        let Some(style_buffer) = self.style_buffer.clone() else {
+            return;
+        };
+
+        let base_style_json = match base_style_json {
+            Ok(base_style_json) => base_style_json,
+            Err(err) => {
+                self.style_editor = None;
+                self.last_error =
+                    Some(format!("Failed to convert base_style to JSON: {err}").into());
+                return;
+            }
+        };
+        self.last_error = None;
+
+        style_buffer.update(cx, |style_buffer, cx| {
+            style_buffer.set_text(base_style_json, cx)
+        });
+
+        let style_editor = cx.new(|cx| {
+            let multi_buffer = cx.new(|cx| MultiBuffer::singleton(style_buffer, cx));
+            let mut editor = Editor::new(
+                EditorMode::full(),
+                multi_buffer,
+                Some(self.project.clone()),
+                window,
+                cx,
+            );
+            editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
+            editor.set_show_line_numbers(false, cx);
+            editor.set_show_code_actions(false, cx);
+            editor.set_show_breakpoints(false, cx);
+            editor.set_show_git_diff_gutter(false, cx);
+            editor.set_show_runnables(false, cx);
+            editor.set_show_edit_predictions(Some(false), window, cx);
+            editor
+        });
+
+        cx.subscribe_in(&style_editor, window, {
+            let id = id.clone();
+            move |this, editor, event: &EditorEvent, window, cx| match event {
+                EditorEvent::BufferEdited => {
+                    let base_style_json = editor.read(cx).text(cx);
+                    match serde_json_lenient::from_str(&base_style_json) {
+                        Ok(new_base_style) => {
+                            window.with_inspector_state::<DivInspectorState, _>(
+                                Some(&id),
+                                cx,
+                                |state, _window| {
+                                    if let Some(state) = state.as_mut() {
+                                        *state.base_style = new_base_style;
+                                    }
+                                },
+                            );
+                            window.refresh();
+                            this.last_error = None;
+                        }
+                        Err(err) => this.last_error = Some(err.to_string().into()),
+                    }
+                }
+                _ => {}
+            }
+        })
+        .detach();
+
+        self.style_editor = Some(style_editor);
+    }
+}
+
+impl Render for DivInspector {
+    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+        v_flex()
+            .size_full()
+            .gap_2()
+            .when_some(self.state.as_ref(), |this, state| {
+                this.child(
+                    v_flex()
+                        .child(Label::new("Layout").size(LabelSize::Large))
+                        .child(render_layout_state(state, cx)),
+                )
+            })
+            .when_some(self.style_editor.as_ref(), |this, style_editor| {
+                this.child(
+                    v_flex()
+                        .gap_2()
+                        .child(Label::new("Style").size(LabelSize::Large))
+                        .child(div().h_128().child(style_editor.clone()))
+                        .when_some(self.last_error.as_ref(), |this, last_error| {
+                            this.child(
+                                div()
+                                    .w_full()
+                                    .border_1()
+                                    .border_color(Color::Error.color(cx))
+                                    .child(Label::new(last_error)),
+                            )
+                        }),
+                )
+            })
+            .when_none(&self.style_editor, |this| {
+                this.child(Label::new("Loading..."))
+            })
+            .into_any_element()
+    }
+}
+
+fn render_layout_state(state: &DivInspectorState, cx: &App) -> Div {
+    v_flex()
+        .child(div().text_ui(cx).child(format!("Bounds: {}", state.bounds)))
+        .child(
+            div()
+                .id("content-size")
+                .text_ui(cx)
+                .tooltip(Tooltip::text("Size of the element's children"))
+                .child(if state.content_size != state.bounds.size {
+                    format!("Content size: {}", state.content_size)
+                } else {
+                    "".to_string()
+                }),
+        )
+}

crates/inspector_ui/src/inspector.rs 🔗

@@ -0,0 +1,168 @@
+use anyhow::{Context as _, anyhow};
+use gpui::{App, DivInspectorState, Inspector, InspectorElementId, IntoElement, Window};
+use std::{cell::OnceCell, path::Path, sync::Arc};
+use ui::{Label, Tooltip, prelude::*};
+use util::{ResultExt as _, command::new_smol_command};
+use workspace::AppState;
+
+use crate::div_inspector::DivInspector;
+
+pub fn init(app_state: Arc<AppState>, cx: &mut App) {
+    cx.on_action(|_: &zed_actions::dev::ToggleInspector, cx| {
+        let Some(active_window) = cx
+            .active_window()
+            .context("no active window to toggle inspector")
+            .log_err()
+        else {
+            return;
+        };
+        // This is deferred to avoid double lease due to window already being updated.
+        cx.defer(move |cx| {
+            active_window
+                .update(cx, |_, window, cx| window.toggle_inspector(cx))
+                .log_err();
+        });
+    });
+
+    // Project used for editor buffers + LSP support
+    let project = project::Project::local(
+        app_state.client.clone(),
+        app_state.node_runtime.clone(),
+        app_state.user_store.clone(),
+        app_state.languages.clone(),
+        app_state.fs.clone(),
+        None,
+        cx,
+    );
+
+    let div_inspector = OnceCell::new();
+    cx.register_inspector_element(move |id, state: &DivInspectorState, window, cx| {
+        let div_inspector = div_inspector
+            .get_or_init(|| cx.new(|cx| DivInspector::new(project.clone(), window, cx)));
+        div_inspector.update(cx, |div_inspector, cx| {
+            div_inspector.update_inspected_element(&id, state.clone(), window, cx);
+            div_inspector.render(window, cx).into_any_element()
+        })
+    });
+
+    cx.set_inspector_renderer(Box::new(render_inspector));
+}
+
+fn render_inspector(
+    inspector: &mut Inspector,
+    window: &mut Window,
+    cx: &mut Context<Inspector>,
+) -> AnyElement {
+    let ui_font = theme::setup_ui_font(window, cx);
+    let colors = cx.theme().colors();
+    let inspector_id = inspector.active_element_id();
+    v_flex()
+        .id("gpui-inspector")
+        .size_full()
+        .bg(colors.panel_background)
+        .text_color(colors.text)
+        .font(ui_font)
+        .border_l_1()
+        .border_color(colors.border)
+        .overflow_y_scroll()
+        .child(
+            h_flex()
+                .p_2()
+                .border_b_1()
+                .border_color(colors.border_variant)
+                .child(
+                    IconButton::new("pick-mode", IconName::MagnifyingGlass)
+                        .tooltip(Tooltip::text("Start inspector pick mode"))
+                        .selected_icon_color(Color::Selected)
+                        .toggle_state(inspector.is_picking())
+                        .on_click(cx.listener(|inspector, _, window, _cx| {
+                            inspector.start_picking();
+                            window.refresh();
+                        })),
+                )
+                .child(
+                    h_flex()
+                        .w_full()
+                        .justify_end()
+                        .child(Label::new("GPUI Inspector").size(LabelSize::Large)),
+                ),
+        )
+        .child(
+            v_flex()
+                .p_2()
+                .gap_2()
+                .when_some(inspector_id, |this, inspector_id| {
+                    this.child(render_inspector_id(inspector_id, cx))
+                })
+                .children(inspector.render_inspector_states(window, cx)),
+        )
+        .into_any_element()
+}
+
+fn render_inspector_id(inspector_id: &InspectorElementId, cx: &App) -> Div {
+    let source_location = inspector_id.path.source_location;
+    v_flex()
+        .child(Label::new("Element ID").size(LabelSize::Large))
+        .when(inspector_id.instance_id != 0, |this| {
+            this.child(
+                div()
+                    .id("instance-id")
+                    .text_ui(cx)
+                    .tooltip(Tooltip::text(
+                        "Disambiguates elements from the same source location",
+                    ))
+                    .child(format!("Instance {}", inspector_id.instance_id)),
+            )
+        })
+        .child(
+            div()
+                .id("source-location")
+                .text_ui(cx)
+                .bg(cx.theme().colors().editor_foreground.opacity(0.025))
+                .underline()
+                .child(format!("{}", source_location))
+                .tooltip(Tooltip::text("Click to open by running zed cli"))
+                .on_click(move |_, _window, cx| {
+                    cx.background_spawn(open_zed_source_location(source_location))
+                        .detach_and_log_err(cx);
+                }),
+        )
+        .child(
+            div()
+                .id("global-id")
+                .text_ui(cx)
+                .min_h_12()
+                .tooltip(Tooltip::text(
+                    "GlobalElementId of the nearest ancestor with an ID",
+                ))
+                .child(inspector_id.path.global_id.to_string()),
+        )
+}
+
+async fn open_zed_source_location(
+    location: &'static std::panic::Location<'static>,
+) -> anyhow::Result<()> {
+    let mut path = Path::new(env!("ZED_REPO_DIR")).to_path_buf();
+    path.push(Path::new(location.file()));
+    let path_arg = format!(
+        "{}:{}:{}",
+        path.display(),
+        location.line(),
+        location.column()
+    );
+
+    let output = new_smol_command("zed")
+        .arg(&path_arg)
+        .output()
+        .await
+        .with_context(|| format!("running zed to open {path_arg} failed"))?;
+
+    if !output.status.success() {
+        Err(anyhow!(
+            "running zed to open {path_arg} failed with stderr: {}",
+            String::from_utf8_lossy(&output.stderr)
+        ))
+    } else {
+        Ok(())
+    }
+}

crates/inspector_ui/src/inspector_ui.rs 🔗

@@ -0,0 +1,24 @@
+#[cfg(debug_assertions)]
+mod div_inspector;
+#[cfg(debug_assertions)]
+mod inspector;
+
+#[cfg(debug_assertions)]
+pub use inspector::init;
+
+#[cfg(not(debug_assertions))]
+pub fn init(_app_state: std::sync::Arc<workspace::AppState>, cx: &mut gpui::App) {
+    use std::any::TypeId;
+    use workspace::notifications::NotifyResultExt as _;
+
+    cx.on_action(|_: &zed_actions::dev::ToggleInspector, cx| {
+        Err::<(), anyhow::Error>(anyhow::anyhow!(
+            "dev::ToggleInspector is only available in debug builds"
+        ))
+        .notify_app_err(cx);
+    });
+
+    command_palette_hooks::CommandPaletteFilter::update_global(cx, |filter, _cx| {
+        filter.hide_action_types(&[TypeId::of::<zed_actions::dev::ToggleInspector>()]);
+    });
+}

crates/languages/Cargo.toml 🔗

@@ -59,8 +59,10 @@ project.workspace = true
 regex.workspace = true
 rope.workspace = true
 rust-embed.workspace = true
+schemars.workspace = true
 serde.workspace = true
 serde_json.workspace = true
+serde_json_lenient.workspace = true
 settings.workspace = true
 smol.workspace = true
 snippet_provider.workspace = true

crates/languages/src/json.rs 🔗

@@ -97,6 +97,65 @@ impl JsonLspAdapter {
         let tsconfig_schema = serde_json::Value::from_str(TSCONFIG_SCHEMA).unwrap();
         let package_json_schema = serde_json::Value::from_str(PACKAGE_JSON_SCHEMA).unwrap();
 
+        #[allow(unused_mut)]
+        let mut schemas = serde_json::json!([
+            {
+                "fileMatch": ["tsconfig.json"],
+                "schema":tsconfig_schema
+            },
+            {
+                "fileMatch": ["package.json"],
+                "schema":package_json_schema
+            },
+            {
+                "fileMatch": [
+                    schema_file_match(paths::settings_file()),
+                    paths::local_settings_file_relative_path()
+                ],
+                "schema": settings_schema,
+            },
+            {
+                "fileMatch": [schema_file_match(paths::keymap_file())],
+                "schema": keymap_schema,
+            },
+            {
+                "fileMatch": [
+                    schema_file_match(paths::tasks_file()),
+                    paths::local_tasks_file_relative_path()
+                ],
+                "schema": tasks_schema,
+            },
+            {
+                "fileMatch": [
+                    schema_file_match(
+                        paths::snippets_dir()
+                            .join("*.json")
+                            .as_path()
+                    )
+                ],
+                "schema": snippets_schema,
+            },
+            {
+                "fileMatch": [
+                    schema_file_match(paths::debug_scenarios_file()),
+                    paths::local_debug_file_relative_path()
+                ],
+                "schema": debug_schema,
+            },
+        ]);
+
+        #[cfg(debug_assertions)]
+        {
+            schemas.as_array_mut().unwrap().push(serde_json::json!(
+                {
+                    "fileMatch": [
+                        "zed-inspector-style.json"
+                    ],
+                    "schema": generate_inspector_style_schema(),
+                }
+            ))
+        }
+
         // This can be viewed via `dev: open language server logs` -> `json-language-server` ->
         // `Server Info`
         serde_json::json!({
@@ -108,52 +167,7 @@ impl JsonLspAdapter {
                 {
                     "enable": true,
                 },
-                "schemas": [
-                    {
-                        "fileMatch": ["tsconfig.json"],
-                        "schema":tsconfig_schema
-                    },
-                    {
-                        "fileMatch": ["package.json"],
-                        "schema":package_json_schema
-                    },
-                    {
-                        "fileMatch": [
-                            schema_file_match(paths::settings_file()),
-                            paths::local_settings_file_relative_path()
-                        ],
-                        "schema": settings_schema,
-                    },
-                    {
-                        "fileMatch": [schema_file_match(paths::keymap_file())],
-                        "schema": keymap_schema,
-                    },
-                    {
-                        "fileMatch": [
-                            schema_file_match(paths::tasks_file()),
-                            paths::local_tasks_file_relative_path()
-                        ],
-                        "schema": tasks_schema,
-                    },
-                    {
-                        "fileMatch": [
-                            schema_file_match(
-                                paths::snippets_dir()
-                                    .join("*.json")
-                                    .as_path()
-                            )
-                        ],
-                        "schema": snippets_schema,
-                    },
-                    {
-                        "fileMatch": [
-                            schema_file_match(paths::debug_scenarios_file()),
-                            paths::local_debug_file_relative_path()
-                        ],
-                        "schema": debug_schema,
-
-                    },
-                ]
+                "schemas": schemas
             }
         })
     }
@@ -180,6 +194,16 @@ impl JsonLspAdapter {
     }
 }
 
+#[cfg(debug_assertions)]
+fn generate_inspector_style_schema() -> serde_json_lenient::Value {
+    let schema = schemars::r#gen::SchemaSettings::draft07()
+        .with(|settings| settings.option_add_null_type = false)
+        .into_generator()
+        .into_root_schema_for::<gpui::StyleRefinement>();
+
+    serde_json_lenient::to_value(schema).unwrap()
+}
+
 #[async_trait(?Send)]
 impl LspAdapter for JsonLspAdapter {
     fn name(&self) -> LanguageServerName {

crates/markdown/src/markdown.rs 🔗

@@ -715,9 +715,14 @@ impl Element for MarkdownElement {
         None
     }
 
+    fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
+        None
+    }
+
     fn request_layout(
         &mut self,
         _id: Option<&GlobalElementId>,
+        _inspector_id: Option<&gpui::InspectorElementId>,
         window: &mut Window,
         cx: &mut App,
     ) -> (gpui::LayoutId, Self::RequestLayoutState) {
@@ -1189,6 +1194,7 @@ impl Element for MarkdownElement {
     fn prepaint(
         &mut self,
         _id: Option<&GlobalElementId>,
+        _inspector_id: Option<&gpui::InspectorElementId>,
         bounds: Bounds<Pixels>,
         rendered_markdown: &mut Self::RequestLayoutState,
         window: &mut Window,
@@ -1206,6 +1212,7 @@ impl Element for MarkdownElement {
     fn paint(
         &mut self,
         _id: Option<&GlobalElementId>,
+        _inspector_id: Option<&gpui::InspectorElementId>,
         bounds: Bounds<Pixels>,
         rendered_markdown: &mut Self::RequestLayoutState,
         hitbox: &mut Self::PrepaintState,

crates/project/src/project.rs 🔗

@@ -66,8 +66,8 @@ use image_store::{ImageItemEvent, ImageStoreEvent};
 
 use ::git::{blame::Blame, status::FileStatus};
 use gpui::{
-    AnyEntity, App, AppContext, AsyncApp, BorrowAppContext, Context, Entity, EventEmitter, Hsla,
-    SharedString, Task, WeakEntity, Window,
+    App, AppContext, AsyncApp, BorrowAppContext, Context, Entity, EventEmitter, Hsla, SharedString,
+    Task, WeakEntity, Window,
 };
 use itertools::Itertools;
 use language::{
@@ -2322,7 +2322,7 @@ impl Project {
         &mut self,
         path: ProjectPath,
         cx: &mut Context<Self>,
-    ) -> Task<Result<(Option<ProjectEntryId>, AnyEntity)>> {
+    ) -> Task<Result<(Option<ProjectEntryId>, Entity<Buffer>)>> {
         let task = self.open_buffer(path.clone(), cx);
         cx.spawn(async move |_project, cx| {
             let buffer = task.await?;
@@ -2330,8 +2330,7 @@ impl Project {
                 File::from_dyn(buffer.file()).and_then(|file| file.project_entry_id(cx))
             })?;
 
-            let buffer: &AnyEntity = &buffer;
-            Ok((project_entry_id, buffer.clone()))
+            Ok((project_entry_id, buffer))
         })
     }
 

crates/refineable/derive_refineable/src/derive_refineable.rs 🔗

@@ -19,6 +19,7 @@ pub fn derive_refineable(input: TokenStream) -> TokenStream {
     let refineable_attr = attrs.iter().find(|attr| attr.path().is_ident("refineable"));
 
     let mut impl_debug_on_refinement = false;
+    let mut derives_serialize = false;
     let mut refinement_traits_to_derive = vec![];
 
     if let Some(refineable_attr) = refineable_attr {
@@ -26,6 +27,9 @@ pub fn derive_refineable(input: TokenStream) -> TokenStream {
             if meta.path.is_ident("Debug") {
                 impl_debug_on_refinement = true;
             } else {
+                if meta.path.is_ident("Serialize") {
+                    derives_serialize = true;
+                }
                 refinement_traits_to_derive.push(meta.path);
             }
             Ok(())
@@ -47,6 +51,21 @@ pub fn derive_refineable(input: TokenStream) -> TokenStream {
     let field_visibilities: Vec<_> = fields.iter().map(|f| &f.vis).collect();
     let wrapped_types: Vec<_> = fields.iter().map(|f| get_wrapper_type(f, &f.ty)).collect();
 
+    let field_attributes: Vec<TokenStream2> = fields
+        .iter()
+        .map(|f| {
+            if derives_serialize {
+                if is_refineable_field(f) {
+                    quote! { #[serde(default, skip_serializing_if = "::refineable::IsEmpty::is_empty")] }
+                } else {
+                    quote! { #[serde(skip_serializing_if = "::std::option::Option::is_none")] }
+                }
+            } else {
+                quote! {}
+            }
+        })
+        .collect();
+
     // Create trait bound that each wrapped type must implement Clone // & Default
     let type_param_bounds: Vec<_> = wrapped_types
         .iter()
@@ -234,6 +253,26 @@ pub fn derive_refineable(input: TokenStream) -> TokenStream {
         quote! {}
     };
 
+    let refinement_is_empty_conditions: Vec<TokenStream2> = fields
+        .iter()
+        .enumerate()
+        .map(|(i, field)| {
+            let name = &field.ident;
+
+            let condition = if is_refineable_field(field) {
+                quote! { self.#name.is_empty() }
+            } else {
+                quote! { self.#name.is_none() }
+            };
+
+            if i < fields.len() - 1 {
+                quote! { #condition && }
+            } else {
+                condition
+            }
+        })
+        .collect();
+
     let mut derive_stream = quote! {};
     for trait_to_derive in refinement_traits_to_derive {
         derive_stream.extend(quote! { #[derive(#trait_to_derive)] })
@@ -246,6 +285,7 @@ pub fn derive_refineable(input: TokenStream) -> TokenStream {
         pub struct #refinement_ident #impl_generics {
             #(
                 #[allow(missing_docs)]
+                #field_attributes
                 #field_visibilities #field_names: #wrapped_types
             ),*
         }
@@ -280,6 +320,14 @@ pub fn derive_refineable(input: TokenStream) -> TokenStream {
             }
         }
 
+        impl #impl_generics ::refineable::IsEmpty for #refinement_ident #ty_generics
+            #where_clause
+        {
+            fn is_empty(&self) -> bool {
+                #( #refinement_is_empty_conditions )*
+            }
+        }
+
         impl #impl_generics From<#refinement_ident #ty_generics> for #ident #ty_generics
             #where_clause
         {

crates/refineable/src/refineable.rs 🔗

@@ -1,7 +1,7 @@
 pub use derive_refineable::Refineable;
 
 pub trait Refineable: Clone {
-    type Refinement: Refineable<Refinement = Self::Refinement> + Default;
+    type Refinement: Refineable<Refinement = Self::Refinement> + IsEmpty + Default;
 
     fn refine(&mut self, refinement: &Self::Refinement);
     fn refined(self, refinement: Self::Refinement) -> Self;
@@ -13,6 +13,11 @@ pub trait Refineable: Clone {
     }
 }
 
+pub trait IsEmpty {
+    /// When `true`, indicates that use applying this refinement does nothing.
+    fn is_empty(&self) -> bool;
+}
+
 pub struct Cascade<S: Refineable>(Vec<Option<S::Refinement>>);
 
 impl<S: Refineable + Default> Default for Cascade<S> {

crates/terminal_view/src/terminal_element.rs 🔗

@@ -581,9 +581,14 @@ impl Element for TerminalElement {
         self.interactivity.element_id.clone()
     }
 
+    fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
+        None
+    }
+
     fn request_layout(
         &mut self,
         global_id: Option<&GlobalElementId>,
+        inspector_id: Option<&gpui::InspectorElementId>,
         window: &mut Window,
         cx: &mut App,
     ) -> (LayoutId, Self::RequestLayoutState) {
@@ -597,21 +602,26 @@ impl Element for TerminalElement {
             }
         }
 
-        let layout_id =
-            self.interactivity
-                .request_layout(global_id, window, cx, |mut style, window, cx| {
-                    style.size.width = relative(1.).into();
-                    style.size.height = relative(1.).into();
-                    // style.overflow = point(Overflow::Hidden, Overflow::Hidden);
+        let layout_id = self.interactivity.request_layout(
+            global_id,
+            inspector_id,
+            window,
+            cx,
+            |mut style, window, cx| {
+                style.size.width = relative(1.).into();
+                style.size.height = relative(1.).into();
+                // style.overflow = point(Overflow::Hidden, Overflow::Hidden);
 
-                    window.request_layout(style, None, cx)
-                });
+                window.request_layout(style, None, cx)
+            },
+        );
         (layout_id, ())
     }
 
     fn prepaint(
         &mut self,
         global_id: Option<&GlobalElementId>,
+        inspector_id: Option<&gpui::InspectorElementId>,
         bounds: Bounds<Pixels>,
         _: &mut Self::RequestLayoutState,
         window: &mut Window,
@@ -620,6 +630,7 @@ impl Element for TerminalElement {
         let rem_size = self.rem_size(cx);
         self.interactivity.prepaint(
             global_id,
+            inspector_id,
             bounds,
             bounds.size,
             window,
@@ -904,6 +915,7 @@ impl Element for TerminalElement {
     fn paint(
         &mut self,
         global_id: Option<&GlobalElementId>,
+        inspector_id: Option<&gpui::InspectorElementId>,
         bounds: Bounds<Pixels>,
         _: &mut Self::RequestLayoutState,
         layout: &mut Self::PrepaintState,
@@ -947,6 +959,7 @@ impl Element for TerminalElement {
             let block_below_cursor_element = layout.block_below_cursor_element.take();
             self.interactivity.paint(
                 global_id,
+                inspector_id,
                 bounds,
                 Some(&layout.hitbox),
                 window,

crates/ui/src/components/button/split_button.rs 🔗

@@ -41,7 +41,7 @@ impl RenderOnce for SplitButton {
             )
             .child(self.right)
             .bg(ElevationIndex::Surface.on_elevation_bg(cx))
-            .shadow(smallvec::smallvec![BoxShadow {
+            .shadow(vec![BoxShadow {
                 color: hsla(0.0, 0.0, 0.0, 0.16),
                 offset: point(px(0.), px(1.)),
                 blur_radius: px(0.),

crates/ui/src/components/indent_guides.rs 🔗

@@ -227,9 +227,14 @@ mod uniform_list {
             None
         }
 
+        fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
+            None
+        }
+
         fn request_layout(
             &mut self,
             _id: Option<&gpui::GlobalElementId>,
+            _inspector_id: Option<&gpui::InspectorElementId>,
             window: &mut Window,
             cx: &mut App,
         ) -> (gpui::LayoutId, Self::RequestLayoutState) {
@@ -239,6 +244,7 @@ mod uniform_list {
         fn prepaint(
             &mut self,
             _id: Option<&gpui::GlobalElementId>,
+            _inspector_id: Option<&gpui::InspectorElementId>,
             _bounds: Bounds<Pixels>,
             _request_layout: &mut Self::RequestLayoutState,
             window: &mut Window,
@@ -264,6 +270,7 @@ mod uniform_list {
         fn paint(
             &mut self,
             _id: Option<&gpui::GlobalElementId>,
+            _inspector_id: Option<&gpui::InspectorElementId>,
             _bounds: Bounds<Pixels>,
             _request_layout: &mut Self::RequestLayoutState,
             prepaint: &mut Self::PrepaintState,

crates/ui/src/components/keybinding_hint.rs 🔗

@@ -1,7 +1,6 @@
 use crate::KeyBinding;
 use crate::{h_flex, prelude::*};
 use gpui::{AnyElement, App, BoxShadow, FontStyle, Hsla, IntoElement, Window, point};
-use smallvec::smallvec;
 use theme::Appearance;
 
 /// Represents a hint for a keybinding, optionally with a prefix and suffix.
@@ -193,7 +192,7 @@ impl RenderOnce for KeybindingHint {
                     .border_1()
                     .border_color(border_color)
                     .bg(bg_color)
-                    .shadow(smallvec![BoxShadow {
+                    .shadow(vec![BoxShadow {
                         color: shadow_color,
                         offset: point(px(0.), px(1.)),
                         blur_radius: px(0.),

crates/ui/src/components/popover_menu.rs 🔗

@@ -316,9 +316,14 @@ impl<M: ManagedView> Element for PopoverMenu<M> {
         Some(self.id.clone())
     }
 
+    fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
+        None
+    }
+
     fn request_layout(
         &mut self,
         global_id: Option<&GlobalElementId>,
+        _inspector_id: Option<&gpui::InspectorElementId>,
         window: &mut Window,
         cx: &mut App,
     ) -> (gpui::LayoutId, Self::RequestLayoutState) {
@@ -394,6 +399,7 @@ impl<M: ManagedView> Element for PopoverMenu<M> {
     fn prepaint(
         &mut self,
         global_id: Option<&GlobalElementId>,
+        _inspector_id: Option<&gpui::InspectorElementId>,
         _bounds: Bounds<Pixels>,
         request_layout: &mut Self::RequestLayoutState,
         window: &mut Window,
@@ -422,6 +428,7 @@ impl<M: ManagedView> Element for PopoverMenu<M> {
     fn paint(
         &mut self,
         _id: Option<&GlobalElementId>,
+        _inspector_id: Option<&gpui::InspectorElementId>,
         _: Bounds<gpui::Pixels>,
         request_layout: &mut Self::RequestLayoutState,
         child_hitbox: &mut Option<HitboxId>,

crates/ui/src/components/progress/progress_bar.rs 🔗

@@ -72,7 +72,7 @@ impl RenderOnce for ProgressBar {
             .py(px(2.0))
             .px(px(4.0))
             .bg(self.bg_color)
-            .shadow(smallvec::smallvec![gpui::BoxShadow {
+            .shadow(vec![gpui::BoxShadow {
                 color: gpui::black().opacity(0.08),
                 offset: point(px(0.), px(1.)),
                 blur_radius: px(0.),

crates/ui/src/components/right_click_menu.rs 🔗

@@ -116,9 +116,14 @@ impl<M: ManagedView> Element for RightClickMenu<M> {
         Some(self.id.clone())
     }
 
+    fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
+        None
+    }
+
     fn request_layout(
         &mut self,
         id: Option<&GlobalElementId>,
+        _inspector_id: Option<&gpui::InspectorElementId>,
         window: &mut Window,
         cx: &mut App,
     ) -> (gpui::LayoutId, Self::RequestLayoutState) {
@@ -174,6 +179,7 @@ impl<M: ManagedView> Element for RightClickMenu<M> {
     fn prepaint(
         &mut self,
         _id: Option<&GlobalElementId>,
+        _inspector_id: Option<&gpui::InspectorElementId>,
         bounds: Bounds<Pixels>,
         request_layout: &mut Self::RequestLayoutState,
         window: &mut Window,
@@ -200,6 +206,7 @@ impl<M: ManagedView> Element for RightClickMenu<M> {
     fn paint(
         &mut self,
         id: Option<&GlobalElementId>,
+        _inspector_id: Option<&gpui::InspectorElementId>,
         _bounds: Bounds<gpui::Pixels>,
         request_layout: &mut Self::RequestLayoutState,
         prepaint_state: &mut Self::PrepaintState,

crates/ui/src/components/scrollbar.rs 🔗

@@ -162,16 +162,20 @@ impl Scrollbar {
 
 impl Element for Scrollbar {
     type RequestLayoutState = ();
-
     type PrepaintState = Hitbox;
 
     fn id(&self) -> Option<ElementId> {
         None
     }
 
+    fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
+        None
+    }
+
     fn request_layout(
         &mut self,
         _id: Option<&GlobalElementId>,
+        _inspector_id: Option<&gpui::InspectorElementId>,
         window: &mut Window,
         cx: &mut App,
     ) -> (LayoutId, Self::RequestLayoutState) {
@@ -193,6 +197,7 @@ impl Element for Scrollbar {
     fn prepaint(
         &mut self,
         _id: Option<&GlobalElementId>,
+        _inspector_id: Option<&gpui::InspectorElementId>,
         bounds: Bounds<Pixels>,
         _request_layout: &mut Self::RequestLayoutState,
         window: &mut Window,
@@ -206,6 +211,7 @@ impl Element for Scrollbar {
     fn paint(
         &mut self,
         _id: Option<&GlobalElementId>,
+        _inspector_id: Option<&gpui::InspectorElementId>,
         bounds: Bounds<Pixels>,
         _request_layout: &mut Self::RequestLayoutState,
         _prepaint: &mut Self::PrepaintState,

crates/ui/src/styles/elevation.rs 🔗

@@ -1,7 +1,6 @@
 use std::fmt::{self, Display, Formatter};
 
 use gpui::{App, BoxShadow, Hsla, hsla, point, px};
-use smallvec::{SmallVec, smallvec};
 use theme::{ActiveTheme, Appearance};
 
 /// Today, elevation is primarily used to add shadows to elements, and set the correct background for elements like buttons.
@@ -40,14 +39,14 @@ impl Display for ElevationIndex {
 
 impl ElevationIndex {
     /// Returns an appropriate shadow for the given elevation index.
-    pub fn shadow(self, cx: &App) -> SmallVec<[BoxShadow; 2]> {
+    pub fn shadow(self, cx: &App) -> Vec<BoxShadow> {
         let is_light = cx.theme().appearance() == Appearance::Light;
 
         match self {
-            ElevationIndex::Surface => smallvec![],
-            ElevationIndex::EditorSurface => smallvec![],
+            ElevationIndex::Surface => vec![],
+            ElevationIndex::EditorSurface => vec![],
 
-            ElevationIndex::ElevatedSurface => smallvec![
+            ElevationIndex::ElevatedSurface => vec![
                 BoxShadow {
                     color: hsla(0., 0., 0., 0.12),
                     offset: point(px(0.), px(2.)),
@@ -59,10 +58,10 @@ impl ElevationIndex {
                     offset: point(px(1.), px(1.)),
                     blur_radius: px(0.),
                     spread_radius: px(0.),
-                }
+                },
             ],
 
-            ElevationIndex::ModalSurface => smallvec![
+            ElevationIndex::ModalSurface => vec![
                 BoxShadow {
                     color: hsla(0., 0., 0., if is_light { 0.06 } else { 0.12 }),
                     offset: point(px(0.), px(2.)),
@@ -89,7 +88,7 @@ impl ElevationIndex {
                 },
             ],
 
-            _ => smallvec![],
+            _ => vec![],
         }
     }
 

crates/ui/src/utils/with_rem_size.rs 🔗

@@ -50,33 +50,41 @@ impl Element for WithRemSize {
         Element::id(&self.div)
     }
 
+    fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
+        Element::source_location(&self.div)
+    }
+
     fn request_layout(
         &mut self,
         id: Option<&GlobalElementId>,
+        inspector_id: Option<&gpui::InspectorElementId>,
         window: &mut Window,
         cx: &mut App,
     ) -> (LayoutId, Self::RequestLayoutState) {
         window.with_rem_size(Some(self.rem_size), |window| {
-            self.div.request_layout(id, window, cx)
+            self.div.request_layout(id, inspector_id, window, cx)
         })
     }
 
     fn prepaint(
         &mut self,
         id: Option<&GlobalElementId>,
+        inspector_id: Option<&gpui::InspectorElementId>,
         bounds: Bounds<Pixels>,
         request_layout: &mut Self::RequestLayoutState,
         window: &mut Window,
         cx: &mut App,
     ) -> Self::PrepaintState {
         window.with_rem_size(Some(self.rem_size), |window| {
-            self.div.prepaint(id, bounds, request_layout, window, cx)
+            self.div
+                .prepaint(id, inspector_id, bounds, request_layout, window, cx)
         })
     }
 
     fn paint(
         &mut self,
         id: Option<&GlobalElementId>,
+        inspector_id: Option<&gpui::InspectorElementId>,
         bounds: Bounds<Pixels>,
         request_layout: &mut Self::RequestLayoutState,
         prepaint: &mut Self::PrepaintState,
@@ -84,8 +92,15 @@ impl Element for WithRemSize {
         cx: &mut App,
     ) {
         window.with_rem_size(Some(self.rem_size), |window| {
-            self.div
-                .paint(id, bounds, request_layout, prepaint, window, cx)
+            self.div.paint(
+                id,
+                inspector_id,
+                bounds,
+                request_layout,
+                prepaint,
+                window,
+                cx,
+            )
         })
     }
 }

crates/workspace/src/pane_group.rs 🔗

@@ -1113,9 +1113,14 @@ mod element {
             Some(self.basis.into())
         }
 
+        fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
+            None
+        }
+
         fn request_layout(
             &mut self,
             _global_id: Option<&GlobalElementId>,
+            _inspector_id: Option<&gpui::InspectorElementId>,
             window: &mut Window,
             cx: &mut App,
         ) -> (gpui::LayoutId, Self::RequestLayoutState) {
@@ -1132,6 +1137,7 @@ mod element {
         fn prepaint(
             &mut self,
             global_id: Option<&GlobalElementId>,
+            _inspector_id: Option<&gpui::InspectorElementId>,
             bounds: Bounds<Pixels>,
             _state: &mut Self::RequestLayoutState,
             window: &mut Window,
@@ -1224,6 +1230,7 @@ mod element {
         fn paint(
             &mut self,
             _id: Option<&GlobalElementId>,
+            _inspector_id: Option<&gpui::InspectorElementId>,
             bounds: gpui::Bounds<ui::prelude::Pixels>,
             _: &mut Self::RequestLayoutState,
             layout: &mut Self::PrepaintState,

crates/workspace/src/workspace.rs 🔗

@@ -7277,7 +7277,7 @@ pub fn client_side_decorations(
                         .when(!tiling.left, |div| div.border_l(BORDER_SIZE))
                         .when(!tiling.right, |div| div.border_r(BORDER_SIZE))
                         .when(!tiling.is_tiled(), |div| {
-                            div.shadow(smallvec::smallvec![gpui::BoxShadow {
+                            div.shadow(vec![gpui::BoxShadow {
                                 color: Hsla {
                                     h: 0.,
                                     s: 0.,

crates/zed/Cargo.toml 🔗

@@ -67,6 +67,7 @@ http_client.workspace = true
 image_viewer.workspace = true
 indoc.workspace = true
 inline_completion_button.workspace = true
+inspector_ui.workspace = true
 install_cli.workspace = true
 jj_ui.workspace = true
 journal.workspace = true

crates/zed/src/main.rs 🔗

@@ -574,6 +574,7 @@ fn main() {
         settings_ui::init(cx);
         extensions_ui::init(cx);
         zeta::init(cx);
+        inspector_ui::init(app_state.clone(), cx);
 
         cx.observe_global::<SettingsStore>({
             let fs = fs.clone();

crates/zed_actions/src/lib.rs 🔗

@@ -111,6 +111,12 @@ impl_actions!(
     ]
 );
 
+pub mod dev {
+    use gpui::actions;
+
+    actions!(dev, [ToggleInspector]);
+}
+
 pub mod workspace {
     use gpui::action_with_deprecated_aliases;
 

crates/zeta/src/completion_diff_element.rs 🔗

@@ -105,9 +105,14 @@ impl Element for CompletionDiffElement {
         None
     }
 
+    fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
+        None
+    }
+
     fn request_layout(
         &mut self,
         _id: Option<&gpui::GlobalElementId>,
+        _inspector_id: Option<&gpui::InspectorElementId>,
         window: &mut Window,
         cx: &mut App,
     ) -> (gpui::LayoutId, Self::RequestLayoutState) {
@@ -117,6 +122,7 @@ impl Element for CompletionDiffElement {
     fn prepaint(
         &mut self,
         _id: Option<&gpui::GlobalElementId>,
+        _inspector_id: Option<&gpui::InspectorElementId>,
         _bounds: gpui::Bounds<Pixels>,
         _request_layout: &mut Self::RequestLayoutState,
         window: &mut Window,
@@ -128,6 +134,7 @@ impl Element for CompletionDiffElement {
     fn paint(
         &mut self,
         _id: Option<&gpui::GlobalElementId>,
+        _inspector_id: Option<&gpui::InspectorElementId>,
         _bounds: gpui::Bounds<Pixels>,
         _request_layout: &mut Self::RequestLayoutState,
         _prepaint: &mut Self::PrepaintState,