From ab59982bf738896c9fefa7a96d3236bc95edcded Mon Sep 17 00:00:00 2001 From: Michael Sloan Date: Fri, 23 May 2025 17:08:59 -0600 Subject: [PATCH] Add initial element inspector for Zed development (#31315) Open inspector with `dev: toggle inspector` from command palette or `cmd-alt-i` on mac or `ctrl-alt-i` on linux. https://github.com/user-attachments/assets/54c43034-d40b-414e-ba9b-190bed2e6d2f * Picking of elements via the mouse, with scroll wheel to inspect occluded elements. * Temporary manipulation of the selected element. * Layout info and JSON-based style manipulation for `Div`. * Navigation to code that constructed the element. Big thanks to @as-cii and @maxdeviant for sorting out how to implement the core of an inspector. Release Notes: - N/A --------- Co-authored-by: Antonio Scandurra Co-authored-by: Marshall Bowers Co-authored-by: Federico Dionisi --- 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 +- .../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 +- .../src/derive_refineable.rs | 48 +++ crates/refineable/src/refineable.rs | 7 +- crates/terminal_view/src/terminal_element.rs | 29 +- .../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 + .../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, 2631 insertions(+), 406 deletions(-) create mode 100644 crates/gpui/src/inspector.rs create mode 100644 crates/inspector_ui/Cargo.toml create mode 120000 crates/inspector_ui/LICENSE-GPL create mode 100644 crates/inspector_ui/README.md create mode 100644 crates/inspector_ui/build.rs create mode 100644 crates/inspector_ui/src/div_inspector.rs create mode 100644 crates/inspector_ui/src/inspector.rs create mode 100644 crates/inspector_ui/src/inspector_ui.rs diff --git a/Cargo.lock b/Cargo.lock index 1bf8071359..ddc30872fd 100644 --- a/Cargo.lock +++ b/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", diff --git a/Cargo.toml b/Cargo.toml index d25732464e..2c0d20a716 100644 --- a/Cargo.toml +++ b/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"] } diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 67761f0c3c..f5bdb372bb 100644 --- a/assets/keymaps/default-linux.json +++ b/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" } }, { diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index e79466d02b..5ace971049 100644 --- a/assets/keymaps/default-macos.json +++ b/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" } }, { diff --git a/crates/agent/Cargo.toml b/crates/agent/Cargo.toml index f3e8f228a9..cfb75fcd6d 100644 --- a/crates/agent/Cargo.toml +++ b/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 diff --git a/crates/agent/src/message_editor.rs b/crates/agent/src/message_editor.rs index f01fc56048..8662b2bf37 100644 --- a/crates/agent/src/message_editor.rs +++ b/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.), diff --git a/crates/assistant_tools/src/edit_agent/evals/fixtures/disable_cursor_blinking/before.rs b/crates/assistant_tools/src/edit_agent/evals/fixtures/disable_cursor_blinking/before.rs index 1610211702..607daa8ce3 100644 --- a/crates/assistant_tools/src/edit_agent/evals/fixtures/disable_cursor_blinking/before.rs +++ b/crates/assistant_tools/src/edit_agent/evals/fixtures/disable_cursor_blinking/before.rs @@ -7698,7 +7698,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.), diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 0672e7b003..08de22dfd0 100644 --- a/crates/editor/src/editor.rs +++ b/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; diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 8d9bf468d3..4a70283cc3 100644 --- a/crates/editor/src/element.rs +++ b/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, _: &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, _: &mut Self::RequestLayoutState, layout: &mut Self::PrepaintState, diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 7ef3fa318c..a00405a249 100644 --- a/crates/editor/src/items.rs +++ b/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::().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. diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index 8bbbebf444..522b773bca 100644 --- a/crates/gpui/Cargo.toml +++ b/crates/gpui/Cargo.toml @@ -22,6 +22,7 @@ test-support = [ "wayland", "x11", ] +inspector = [] leak-detection = ["backtrace"] runtime_shaders = [] macos-blade = [ diff --git a/crates/gpui/examples/input.rs b/crates/gpui/examples/input.rs index 5d28a8a8a9..52003bb274 100644 --- a/crates/gpui/examples/input.rs +++ b/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 { 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, _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, _request_layout: &mut Self::RequestLayoutState, prepaint: &mut Self::PrepaintState, diff --git a/crates/gpui/examples/opacity.rs b/crates/gpui/examples/opacity.rs index 68e2a3fbd0..634df29a4c 100644 --- a/crates/gpui/examples/opacity.rs +++ b/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), diff --git a/crates/gpui/examples/shadow.rs b/crates/gpui/examples/shadow.rs index 4fa44ca623..c42b0f55f0 100644 --- a/crates/gpui/examples/shadow.rs +++ b/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.)), diff --git a/crates/gpui/examples/text_wrapper.rs b/crates/gpui/examples/text_wrapper.rs index dfc2456bc6..4c6e5e2ac8 100644 --- a/crates/gpui/examples/text_wrapper.rs +++ b/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()) diff --git a/crates/gpui/examples/window_shadow.rs b/crates/gpui/examples/window_shadow.rs index 3c9849ebdd..875ebb93c6 100644 --- a/crates/gpui/examples/window_shadow.rs +++ b/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., diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index f705065b74..d5b55e4bdb 100644 --- a/crates/gpui/src/app.rs +++ b/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>, pub(crate) tracked_entities: FxHashMap>, + #[cfg(any(feature = "inspector", debug_assertions))] + pub(crate) inspector_renderer: Option, + #[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( + &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()`. diff --git a/crates/gpui/src/color.rs b/crates/gpui/src/color.rs index 17665ccc4f..1115d1c99c 100644 --- a/crates/gpui/src/color.rs +++ b/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>(deserializer: D) -> Result { deserializer.deserialize_str(RgbaVisitor) } } +impl Serialize for Rgba { + fn serialize(&self, serializer: S) -> Result + 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 for Rgba { fn from(color: Hsla) -> Self { let h = color.h; @@ -588,20 +628,35 @@ impl From 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(&self, serializer: S) -> Result + where + S: Serializer, + { + Rgba::from(*self).serialize(serializer) + } +} + impl<'de> Deserialize<'de> for Hsla { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { - // First, deserialize it into Rgba - let rgba = Rgba::deserialize(deserializer)?; - - // Then, use the From 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: /// - /// - -#[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. /// /// -#[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. diff --git a/crates/gpui/src/element.rs b/crates/gpui/src/element.rs index 26c8c7a849..2852841b2c 100644 --- a/crates/gpui/src/element.rs +++ b/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; + /// 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, 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, 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(Option); +pub struct Component { + component: Option, + #[cfg(debug_assertions)] + source: &'static core::panic::Location<'static>, +} impl Component { /// 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 Element for Component { 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 Element for Component { fn prepaint( &mut self, _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, _: Bounds, element: &mut AnyElement, window: &mut Window, @@ -209,6 +245,7 @@ impl Element for Component { fn paint( &mut self, _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, _: Bounds, element: &mut Self::RequestLayoutState, _: &mut Self::PrepaintState, @@ -231,6 +268,18 @@ impl IntoElement for Component { #[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 { RequestLayout { layout_id: LayoutId, global_id: Option, + inspector_id: Option, request_layout: RequestLayoutState, }, LayoutComputed { layout_id: LayoutId, global_id: Option, + inspector_id: Option, available_space: Size, request_layout: RequestLayoutState, }, Prepaint { node_id: DispatchNodeId, global_id: Option, + inspector_id: Option, bounds: Bounds, request_layout: RequestLayoutState, prepaint: PrepaintState, @@ -297,8 +349,28 @@ impl Drawable { 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 Drawable { self.phase = ElementDrawPhase::RequestLayout { layout_id, global_id, + inspector_id, request_layout, }; layout_id @@ -320,11 +393,13 @@ impl Drawable { 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 Drawable { 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 Drawable { self.phase = ElementDrawPhase::Prepaint { node_id, global_id, + inspector_id, bounds, request_layout, prepaint, @@ -369,6 +446,7 @@ impl Drawable { ElementDrawPhase::Prepaint { node_id, global_id, + inspector_id, bounds, mut request_layout, mut prepaint, @@ -382,6 +460,7 @@ impl Drawable { 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 Drawable { 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 Drawable { ElementDrawPhase::LayoutComputed { layout_id, global_id, + inspector_id, available_space: prev_available_space, request_layout, } => { @@ -437,6 +519,7 @@ impl Drawable { 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, _: &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, _: &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, _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, _request_layout: &mut Self::RequestLayoutState, _prepaint: &mut Self::PrepaintState, diff --git a/crates/gpui/src/elements/anchored.rs b/crates/gpui/src/elements/anchored.rs index 05aa22c15e..f92593ef8d 100644 --- a/crates/gpui/src/elements/anchored.rs +++ b/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, 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, _request_layout: &mut Self::RequestLayoutState, _prepaint: &mut Self::PrepaintState, diff --git a/crates/gpui/src/elements/animation.rs b/crates/gpui/src/elements/animation.rs index fc2baaaf17..bcdfa3562c 100644 --- a/crates/gpui/src/elements/animation.rs +++ b/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 Element for AnimationElement { 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 Element for AnimationElement { fn prepaint( &mut self, _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, _bounds: crate::Bounds, element: &mut Self::RequestLayoutState, window: &mut Window, @@ -183,6 +191,7 @@ impl Element for AnimationElement { fn paint( &mut self, _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, _bounds: crate::Bounds, element: &mut Self::RequestLayoutState, _: &mut Self::PrepaintState, diff --git a/crates/gpui/src/elements/canvas.rs b/crates/gpui/src/elements/canvas.rs index 60e94386d3..d57d2f6041 100644 --- a/crates/gpui/src/elements/canvas.rs +++ b/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 Element for Canvas { 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 Element for Canvas { fn prepaint( &mut self, _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, bounds: Bounds, _request_layout: &mut Style, window: &mut Window, @@ -68,6 +74,7 @@ impl Element for Canvas { fn paint( &mut self, _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, bounds: Bounds, style: &mut Style, prepaint: &mut Self::PrepaintState, diff --git a/crates/gpui/src/elements/deferred.rs b/crates/gpui/src/elements/deferred.rs index 4a60c812d4..9498734198 100644 --- a/crates/gpui/src/elements/deferred.rs +++ b/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, _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, _request_layout: &mut Self::RequestLayoutState, _prepaint: &mut Self::PrepaintState, diff --git a/crates/gpui/src/elements/div.rs b/crates/gpui/src/elements/div.rs index bc2abc7c46..7167943771 100644 --- a/crates/gpui/src/elements/div.rs +++ b/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 DragMoveEvent { } 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, + /// Inspects the bounds of the element. + pub bounds: Bounds, + /// Size of the children of the element, or `bounds.size` if it has no children. + pub content_size: Size, +} + 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::>(); 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, 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, _request_layout: &mut Self::RequestLayoutState, hitbox: &mut Option, @@ -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, pub(crate) occlude_mouse: bool, - #[cfg(debug_assertions)] - pub(crate) location: Option>, + #[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, @@ -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, _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::( global_id, |element_state, window| { @@ -1478,6 +1543,7 @@ impl Interactivity { pub fn prepaint( &mut self, global_id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, bounds: Bounds, content_size: Size, window: &mut Window, @@ -1485,6 +1551,19 @@ impl Interactivity { f: impl FnOnce(&Style, Point, Option, &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, _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, 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, 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, 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, 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, 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, + ); } } diff --git a/crates/gpui/src/elements/image_cache.rs b/crates/gpui/src/elements/image_cache.rs index 9c84810e3b..e7bdeaf9eb 100644 --- a/crates/gpui/src/elements/image_cache.rs +++ b/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, _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, _request_layout: &mut Self::RequestLayoutState, _prepaint: &mut Self::PrepaintState, diff --git a/crates/gpui/src/elements/img.rs b/crates/gpui/src/elements/img.rs index 8c16f5ba51..c613066777 100644 --- a/crates/gpui/src/elements/img.rs +++ b/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) -> 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, 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, 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, diff --git a/crates/gpui/src/elements/list.rs b/crates/gpui/src/elements/list.rs index af522b0c7f..c9731026c2 100644 --- a/crates/gpui/src/elements/list.rs +++ b/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, _: &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, _: &mut Self::RequestLayoutState, prepaint: &mut Self::PrepaintState, diff --git a/crates/gpui/src/elements/surface.rs b/crates/gpui/src/elements/surface.rs index 707271f2ec..b4fced1001 100644 --- a/crates/gpui/src/elements/surface.rs +++ b/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, _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, _: &mut Self::RequestLayoutState, _: &mut Self::PrepaintState, diff --git a/crates/gpui/src/elements/svg.rs b/crates/gpui/src/elements/svg.rs index abb75156bd..a55245dcdf 100644 --- a/crates/gpui/src/elements/svg.rs +++ b/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, _request_layout: &mut Self::RequestLayoutState, window: &mut Window, @@ -68,6 +78,7 @@ impl Element for Svg { ) -> Option { 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, _request_layout: &mut Self::RequestLayoutState, hitbox: &mut Option, @@ -89,6 +101,7 @@ impl Element for Svg { { self.interactivity.paint( global_id, + inspector_id, bounds, hitbox.as_ref(), window, diff --git a/crates/gpui/src/elements/text.rs b/crates/gpui/src/elements/text.rs index fa1faded35..0fd30ed4f4 100644 --- a/crates/gpui/src/elements/text.rs +++ b/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, 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, 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, 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, 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, _: &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, _: &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, 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, _: &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) }, diff --git a/crates/gpui/src/elements/uniform_list.rs b/crates/gpui/src/elements/uniform_list.rs index 8de7abf0cf..859c8f9552 100644 --- a/crates/gpui/src/elements/uniform_list.rs +++ b/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, 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, request_layout: &mut Self::RequestLayoutState, hitbox: &mut Option, @@ -443,6 +447,7 @@ impl Element for UniformList { ) { self.interactivity.paint( global_id, + inspector_id, bounds, hitbox.as_ref(), window, diff --git a/crates/gpui/src/geometry.rs b/crates/gpui/src/geometry.rs index 54374e9bee..5f0763e12b 100644 --- a/crates/gpui/src/geometry.rs +++ b/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 { /// The x coordinate of the point. @@ -375,12 +378,18 @@ impl Clone for Point { } } +impl Display for Point { + 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 { /// The width component of the size. @@ -649,6 +658,12 @@ where } } +impl Display for Size { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} × {}", self.width, self.height) + } +} + impl From> for Size { fn from(point: Point) -> Self { Self { @@ -1541,6 +1556,18 @@ impl Bounds { } } +impl> Display for Bounds { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{} - {} (size {})", + self.origin, + self.bottom_right(), + self.size + ) + } +} + impl Size { /// Converts the size from physical to logical pixels. pub(crate) fn to_pixels(self, scale_factor: f32) -> Size { @@ -1647,7 +1674,7 @@ impl Copy for Bounds {} /// 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 { /// 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 { /// The value associated with the top left corner. @@ -2508,16 +2535,11 @@ impl From 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 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 { + 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 for Pixels { } } -impl Debug for Pixels { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{} px", self.0) - } -} - impl From 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 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 { + 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 { + 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>(deserializer: D) -> Result { + 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(self, value: &str) -> Result { + AbsoluteLength::try_from(value).map_err(E::custom) + } + } + + deserializer.deserialize_str(StringVisitor) + } +} + +impl Serialize for AbsoluteLength { + fn serialize(&self, serializer: S) -> Result + 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,14 +3319,89 @@ 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 { + if let Some(percentage) = value.strip_suffix('%') { + let fraction: f32 = percentage.parse::().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>(deserializer: D) -> Result { + 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(self, value: &str) -> Result { + DefiniteLength::try_from(value).map_err(E::custom) + } + } + + deserializer.deserialize_str(StringVisitor) + } +} + +impl Serialize for DefiniteLength { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&format!("{self}")) + } +} + impl From for DefiniteLength { fn from(pixels: Pixels) -> Self { Self::Absolute(pixels.into()) @@ -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 { + 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>(deserializer: D) -> Result { + 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(self, value: &str) -> Result { + Length::try_from(value).map_err(E::custom) + } + } + + deserializer.deserialize_str(StringVisitor) + } +} + +impl Serialize for Length { + fn serialize(&self, serializer: S) -> Result + 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. diff --git a/crates/gpui/src/gpui.rs b/crates/gpui/src/gpui.rs index 194c431e6c..496406c5a0 100644 --- a/crates/gpui/src/gpui.rs +++ b/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::*; diff --git a/crates/gpui/src/inspector.rs b/crates/gpui/src/inspector.rs new file mode 100644 index 0000000000..7b50ed54d1 --- /dev/null +++ b/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, + /// Disambiguates elements that have the same path. + #[cfg(any(feature = "inspector", debug_assertions))] + pub instance_id: usize, +} + +impl Into 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 for &InspectorElementPath { + fn into(self) -> InspectorElementPath { + self.clone() + } + } + + /// Function set on `App` to render the inspector UI. + pub type InspectorRenderer = + Box) -> AnyElement>; + + /// Manages inspector state - which element is currently selected and whether the inspector is + /// in picking mode. + pub struct Inspector { + active_element: Option, + pub(crate) pick_depth: Option, + } + + struct InspectedElement { + id: InspectorElementId, + states: FxHashMap>, + } + + 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( + &mut self, + window: &mut Window, + f: impl FnOnce(&mut Option, &mut Window) -> R, + ) -> R { + let Some(active_element) = &mut self.active_element else { + return f(&mut None, window); + }; + + let type_id = TypeId::of::(); + 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, + ) -> Vec { + 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) -> 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 AnyElement>, + >, + } + + impl InspectorElementRegistry { + pub fn register( + &mut self, + f: impl 'static + Fn(InspectorElementId, &T, &mut Window, &mut App) -> R, + ) { + self.renderers_by_type_id.insert( + TypeId::of::(), + Box::new(move |id, value, window, cx| { + let value = value.downcast_ref().unwrap(); + f(id, value, window, cx).into_any_element() + }), + ); + } + } +} diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 51f340f167..e7390fd562 100644 --- a/crates/gpui/src/platform.rs +++ b/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, diff --git a/crates/gpui/src/scene.rs b/crates/gpui/src/scene.rs index fc527bf620..51406ea6dd 100644 --- a/crates/gpui/src/scene.rs +++ b/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 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. diff --git a/crates/gpui/src/style.rs b/crates/gpui/src/style.rs index 9066fb9e1b..91d148047e 100644 --- a/crates/gpui/src/style.rs +++ b/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, /// Box shadow of the element - pub box_shadow: SmallVec<[BoxShadow; 2]>, + pub box_shadow: Vec, /// 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`. +/// +/// +#[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 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 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 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 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 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 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 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}; diff --git a/crates/gpui/src/styled.rs b/crates/gpui/src/styled.rs index 569ab77578..c91cfabce0 100644 --- a/crates/gpui/src/styled.rs +++ b/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 } diff --git a/crates/gpui/src/taffy.rs b/crates/gpui/src/taffy.rs index b8c71d9731..094f8281f3 100644 --- a/crates/gpui/src/taffy.rs +++ b/crates/gpui/src/taffy.rs @@ -250,10 +250,10 @@ trait ToTaffy { impl ToTaffy 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 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, diff --git a/crates/gpui/src/text_system.rs b/crates/gpui/src/text_system.rs index 058ecf5aae..3576c2e04a 100644 --- a/crates/gpui/src/text_system.rs +++ b/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] diff --git a/crates/gpui/src/text_system/line_wrapper.rs b/crates/gpui/src/text_system/line_wrapper.rs index 29fc95c7af..5de26511d3 100644 --- a/crates/gpui/src/text_system/line_wrapper.rs +++ b/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, ) -> 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) { diff --git a/crates/gpui/src/view.rs b/crates/gpui/src/view.rs index 933a04b5f3..f461e2f7d0 100644 --- a/crates/gpui/src/view.rs +++ b/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 Element for Entity { 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 Element for Entity { fn prepaint( &mut self, _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, _: Bounds, element: &mut Self::RequestLayoutState, window: &mut Window, @@ -61,6 +67,7 @@ impl Element for Entity { fn paint( &mut self, _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, _: Bounds, 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, element: &mut Self::RequestLayoutState, window: &mut Window, @@ -176,70 +194,69 @@ impl Element for AnyView { ) -> Option { 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::( - 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::( + 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, _: &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::( global_id.unwrap(), |element_state, window| { diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 9b1e3e9b72..d3c50a5cd7 100644 --- a/crates/gpui/src/window.rs +++ b/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, #[cfg(any(test, feature = "test-support"))] pub(crate) debug_bounds: FxHashMap>, + #[cfg(any(feature = "inspector", debug_assertions))] + pub(crate) next_inspector_instance_ids: FxHashMap, usize>, + #[cfg(any(feature = "inspector", debug_assertions))] + pub(crate) inspector_hitboxes: FxHashMap, } #[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) -> HitTest { @@ -648,6 +666,8 @@ pub struct Window { pub(crate) pending_input_observers: SubscriberSet<(), AnyObserver>, prompt: Option, pub(crate) client_inset: Option, + #[cfg(any(feature = "inspector", debug_assertions))] + inspector: Option>, } #[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::>(); @@ -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 { @@ -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 { 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( + &mut self, + _inspector_id: Option<&crate::InspectorElementId>, + cx: &mut App, + f: impl FnOnce(&mut Option, &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 { + 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, 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::().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::().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::() { + // 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), } diff --git a/crates/gpui_macros/src/derive_into_element.rs b/crates/gpui_macros/src/derive_into_element.rs index a737458569..89d609ae65 100644 --- a/crates/gpui_macros/src/derive_into_element.rs +++ b/crates/gpui_macros/src/derive_into_element.rs @@ -13,6 +13,7 @@ pub fn derive_into_element(input: TokenStream) -> TokenStream { { type Element = gpui::Component; + #[track_caller] fn into_element(self) -> Self::Element { gpui::Component::new(self) } diff --git a/crates/gpui_macros/src/styles.rs b/crates/gpui_macros/src/styles.rs index b8a4d8ac2f..4e3dda9ed2 100644 --- a/crates/gpui_macros/src/styles.rs +++ b/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) -> 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.), diff --git a/crates/inspector_ui/Cargo.toml b/crates/inspector_ui/Cargo.toml new file mode 100644 index 0000000000..083651a40d --- /dev/null +++ b/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 diff --git a/crates/inspector_ui/LICENSE-GPL b/crates/inspector_ui/LICENSE-GPL new file mode 120000 index 0000000000..89e542f750 --- /dev/null +++ b/crates/inspector_ui/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/inspector_ui/README.md b/crates/inspector_ui/README.md new file mode 100644 index 0000000000..a134965624 --- /dev/null +++ b/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; + 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. diff --git a/crates/inspector_ui/build.rs b/crates/inspector_ui/build.rs new file mode 100644 index 0000000000..e7d393b5ef --- /dev/null +++ b/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()); +} diff --git a/crates/inspector_ui/src/div_inspector.rs b/crates/inspector_ui/src/div_inspector.rs new file mode 100644 index 0000000000..950daf8b1f --- /dev/null +++ b/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, + inspector_id: Option, + state: Option, + style_buffer: Option>, + style_editor: Option>, + last_error: Option, +} + +impl DivInspector { + pub fn new( + project: Entity, + window: &mut Window, + cx: &mut Context, + ) -> 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, + this: WeakEntity, + 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, + ) { + 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::( + 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) -> 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() + }), + ) +} diff --git a/crates/inspector_ui/src/inspector.rs b/crates/inspector_ui/src/inspector.rs new file mode 100644 index 0000000000..dff83cbceb --- /dev/null +++ b/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, 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, +) -> 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(()) + } +} diff --git a/crates/inspector_ui/src/inspector_ui.rs b/crates/inspector_ui/src/inspector_ui.rs new file mode 100644 index 0000000000..1342007005 --- /dev/null +++ b/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, 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::()]); + }); +} diff --git a/crates/languages/Cargo.toml b/crates/languages/Cargo.toml index f4cc1e14c7..90e70263bd 100644 --- a/crates/languages/Cargo.toml +++ b/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 diff --git a/crates/languages/src/json.rs b/crates/languages/src/json.rs index 31fa5a471a..f208ae004a 100644 --- a/crates/languages/src/json.rs +++ b/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::(); + + serde_json_lenient::to_value(schema).unwrap() +} + #[async_trait(?Send)] impl LspAdapter for JsonLspAdapter { fn name(&self) -> LanguageServerName { diff --git a/crates/markdown/src/markdown.rs b/crates/markdown/src/markdown.rs index a3a8e7c456..72442dcc8c 100644 --- a/crates/markdown/src/markdown.rs +++ b/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, 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, rendered_markdown: &mut Self::RequestLayoutState, hitbox: &mut Self::PrepaintState, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index e319db10c3..d04d44aa94 100644 --- a/crates/project/src/project.rs +++ b/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, - ) -> Task, AnyEntity)>> { + ) -> Task, Entity)>> { 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)) }) } diff --git a/crates/refineable/derive_refineable/src/derive_refineable.rs b/crates/refineable/derive_refineable/src/derive_refineable.rs index 4af33df85e..3c03504653 100644 --- a/crates/refineable/derive_refineable/src/derive_refineable.rs +++ b/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 = 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 = 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 { diff --git a/crates/refineable/src/refineable.rs b/crates/refineable/src/refineable.rs index 93e2e40ac6..f5e8f895a4 100644 --- a/crates/refineable/src/refineable.rs +++ b/crates/refineable/src/refineable.rs @@ -1,7 +1,7 @@ pub use derive_refineable::Refineable; pub trait Refineable: Clone { - type Refinement: Refineable + Default; + type Refinement: Refineable + 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(Vec>); impl Default for Cascade { diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index 2014f91602..07389b1627 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/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, _: &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, _: &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, diff --git a/crates/ui/src/components/button/split_button.rs b/crates/ui/src/components/button/split_button.rs index 3d50340755..c0811ecbab 100644 --- a/crates/ui/src/components/button/split_button.rs +++ b/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.), diff --git a/crates/ui/src/components/indent_guides.rs b/crates/ui/src/components/indent_guides.rs index 3a5ba8d835..dacfa16325 100644 --- a/crates/ui/src/components/indent_guides.rs +++ b/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, _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, _request_layout: &mut Self::RequestLayoutState, prepaint: &mut Self::PrepaintState, diff --git a/crates/ui/src/components/keybinding_hint.rs b/crates/ui/src/components/keybinding_hint.rs index 9cd63d544b..4c8c893636 100644 --- a/crates/ui/src/components/keybinding_hint.rs +++ b/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.), diff --git a/crates/ui/src/components/popover_menu.rs b/crates/ui/src/components/popover_menu.rs index f0c9e74c86..385b686bda 100644 --- a/crates/ui/src/components/popover_menu.rs +++ b/crates/ui/src/components/popover_menu.rs @@ -316,9 +316,14 @@ impl Element for PopoverMenu { 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 Element for PopoverMenu { fn prepaint( &mut self, global_id: Option<&GlobalElementId>, + _inspector_id: Option<&gpui::InspectorElementId>, _bounds: Bounds, request_layout: &mut Self::RequestLayoutState, window: &mut Window, @@ -422,6 +428,7 @@ impl Element for PopoverMenu { fn paint( &mut self, _id: Option<&GlobalElementId>, + _inspector_id: Option<&gpui::InspectorElementId>, _: Bounds, request_layout: &mut Self::RequestLayoutState, child_hitbox: &mut Option, diff --git a/crates/ui/src/components/progress/progress_bar.rs b/crates/ui/src/components/progress/progress_bar.rs index 3ea214082c..67b6be6723 100644 --- a/crates/ui/src/components/progress/progress_bar.rs +++ b/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.), diff --git a/crates/ui/src/components/right_click_menu.rs b/crates/ui/src/components/right_click_menu.rs index bea79972e3..79d5130079 100644 --- a/crates/ui/src/components/right_click_menu.rs +++ b/crates/ui/src/components/right_click_menu.rs @@ -116,9 +116,14 @@ impl Element for RightClickMenu { 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 Element for RightClickMenu { fn prepaint( &mut self, _id: Option<&GlobalElementId>, + _inspector_id: Option<&gpui::InspectorElementId>, bounds: Bounds, request_layout: &mut Self::RequestLayoutState, window: &mut Window, @@ -200,6 +206,7 @@ impl Element for RightClickMenu { fn paint( &mut self, id: Option<&GlobalElementId>, + _inspector_id: Option<&gpui::InspectorElementId>, _bounds: Bounds, request_layout: &mut Self::RequestLayoutState, prepaint_state: &mut Self::PrepaintState, diff --git a/crates/ui/src/components/scrollbar.rs b/crates/ui/src/components/scrollbar.rs index 8787321409..468f90a578 100644 --- a/crates/ui/src/components/scrollbar.rs +++ b/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 { 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, _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, _request_layout: &mut Self::RequestLayoutState, _prepaint: &mut Self::PrepaintState, diff --git a/crates/ui/src/styles/elevation.rs b/crates/ui/src/styles/elevation.rs index 5967a8f6b8..35e8e499b9 100644 --- a/crates/ui/src/styles/elevation.rs +++ b/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 { 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![], } } diff --git a/crates/ui/src/utils/with_rem_size.rs b/crates/ui/src/utils/with_rem_size.rs index 59fcc45823..b9770b086c 100644 --- a/crates/ui/src/utils/with_rem_size.rs +++ b/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, 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, 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, + ) }) } } diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index c8c93986ad..c781854741 100644 --- a/crates/workspace/src/pane_group.rs +++ b/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, _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, _: &mut Self::RequestLayoutState, layout: &mut Self::PrepaintState, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 42d0cc4e40..3d9efc27ea 100644 --- a/crates/workspace/src/workspace.rs +++ b/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., diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 7b5a4e02c5..b89427d5ba 100644 --- a/crates/zed/Cargo.toml +++ b/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 diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 77120cd7cb..54606f76b0 100644 --- a/crates/zed/src/main.rs +++ b/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::({ let fs = fs.clone(); diff --git a/crates/zed_actions/src/lib.rs b/crates/zed_actions/src/lib.rs index 8ad7a6edc9..4619562ed7 100644 --- a/crates/zed_actions/src/lib.rs +++ b/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; diff --git a/crates/zeta/src/completion_diff_element.rs b/crates/zeta/src/completion_diff_element.rs index b395f18824..3b7355d797 100644 --- a/crates/zeta/src/completion_diff_element.rs +++ b/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, _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, _request_layout: &mut Self::RequestLayoutState, _prepaint: &mut Self::PrepaintState,