Add initial element inspector for Zed development (#31315)

Open inspector with `dev: toggle inspector` from command palette or
`cmd-alt-i` on mac or `ctrl-alt-i` on linux.

https://github.com/user-attachments/assets/54c43034-d40b-414e-ba9b-190bed2e6d2f

* Picking of elements via the mouse, with scroll wheel to inspect
occluded elements.

* Temporary manipulation of the selected element.

* Layout info and JSON-based style manipulation for `Div`.

* Navigation to code that constructed the element.

Big thanks to @as-cii and @maxdeviant for sorting out how to implement
the core of an inspector.

Release Notes:

- N/A

---------

Co-authored-by: Antonio Scandurra <me@as-cii.com>
Co-authored-by: Marshall Bowers <git@maxdeviant.com>
Co-authored-by: Federico Dionisi <code@fdionisi.me>
This commit is contained in:
Michael Sloan 2025-05-23 17:08:59 -06:00 committed by GitHub
parent 685933b5c8
commit ab59982bf7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
74 changed files with 2631 additions and 406 deletions

24
Cargo.lock generated
View file

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

View file

@ -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"] }

View file

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

View file

@ -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"
}
},
{

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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())

View file

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

View file

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

View file

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

View file

@ -33,11 +33,16 @@
use crate::{
App, ArenaBox, AvailableSpace, Bounds, Context, DispatchNodeId, ELEMENT_ARENA, ElementId,
FocusHandle, LayoutId, Pixels, Point, Size, Style, Window, util::FluentBuilder,
FocusHandle, InspectorElementId, LayoutId, Pixels, Point, Size, Style, Window,
util::FluentBuilder,
};
use derive_more::{Deref, DerefMut};
pub(crate) use smallvec::SmallVec;
use std::{any::Any, fmt::Debug, mem};
use std::{
any::Any,
fmt::{self, Debug, Display},
mem, panic,
};
/// Implemented by types that participate in laying out and painting the contents of a window.
/// Elements form a tree and are laid out according to web-based layout rules, as implemented by Taffy.
@ -59,11 +64,16 @@ pub trait Element: 'static + IntoElement {
/// frames. This id must be unique among children of the first containing element with an id.
fn id(&self) -> Option<ElementId>;
/// Source location where this element was constructed, used to disambiguate elements in the
/// inspector and navigate to their source code.
fn source_location(&self) -> Option<&'static panic::Location<'static>>;
/// Before an element can be painted, we need to know where it's going to be and how big it is.
/// Use this method to request a layout from Taffy and initialize the element's state.
fn request_layout(
&mut self,
id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (LayoutId, Self::RequestLayoutState);
@ -73,6 +83,7 @@ pub trait Element: 'static + IntoElement {
fn prepaint(
&mut self,
id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
request_layout: &mut Self::RequestLayoutState,
window: &mut Window,
@ -84,6 +95,7 @@ pub trait Element: 'static + IntoElement {
fn paint(
&mut self,
id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
request_layout: &mut Self::RequestLayoutState,
prepaint: &mut Self::PrepaintState,
@ -167,12 +179,21 @@ pub trait ParentElement {
/// An element for rendering components. An implementation detail of the [`IntoElement`] derive macro
/// for [`RenderOnce`]
#[doc(hidden)]
pub struct Component<C: RenderOnce>(Option<C>);
pub struct Component<C: RenderOnce> {
component: Option<C>,
#[cfg(debug_assertions)]
source: &'static core::panic::Location<'static>,
}
impl<C: RenderOnce> Component<C> {
/// Create a new component from the given RenderOnce type.
#[track_caller]
pub fn new(component: C) -> Self {
Component(Some(component))
Component {
component: Some(component),
#[cfg(debug_assertions)]
source: core::panic::Location::caller(),
}
}
}
@ -184,13 +205,27 @@ impl<C: RenderOnce> Element for Component<C> {
None
}
fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
#[cfg(debug_assertions)]
return Some(self.source);
#[cfg(not(debug_assertions))]
return None;
}
fn request_layout(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (LayoutId, Self::RequestLayoutState) {
let mut element = self.0.take().unwrap().render(window, cx).into_any_element();
let mut element = self
.component
.take()
.unwrap()
.render(window, cx)
.into_any_element();
let layout_id = element.request_layout(window, cx);
(layout_id, element)
}
@ -198,6 +233,7 @@ impl<C: RenderOnce> Element for Component<C> {
fn prepaint(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
_: Bounds<Pixels>,
element: &mut AnyElement,
window: &mut Window,
@ -209,6 +245,7 @@ impl<C: RenderOnce> Element for Component<C> {
fn paint(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
_: Bounds<Pixels>,
element: &mut Self::RequestLayoutState,
_: &mut Self::PrepaintState,
@ -231,6 +268,18 @@ impl<C: RenderOnce> IntoElement for Component<C> {
#[derive(Deref, DerefMut, Default, Debug, Eq, PartialEq, Hash)]
pub struct GlobalElementId(pub(crate) SmallVec<[ElementId; 32]>);
impl Display for GlobalElementId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for (i, element_id) in self.0.iter().enumerate() {
if i > 0 {
write!(f, ".")?;
}
write!(f, "{}", element_id)?;
}
Ok(())
}
}
trait ElementObject {
fn inner_element(&mut self) -> &mut dyn Any;
@ -262,17 +311,20 @@ enum ElementDrawPhase<RequestLayoutState, PrepaintState> {
RequestLayout {
layout_id: LayoutId,
global_id: Option<GlobalElementId>,
inspector_id: Option<InspectorElementId>,
request_layout: RequestLayoutState,
},
LayoutComputed {
layout_id: LayoutId,
global_id: Option<GlobalElementId>,
inspector_id: Option<InspectorElementId>,
available_space: Size<AvailableSpace>,
request_layout: RequestLayoutState,
},
Prepaint {
node_id: DispatchNodeId,
global_id: Option<GlobalElementId>,
inspector_id: Option<InspectorElementId>,
bounds: Bounds<Pixels>,
request_layout: RequestLayoutState,
prepaint: PrepaintState,
@ -297,8 +349,28 @@ impl<E: Element> Drawable<E> {
GlobalElementId(window.element_id_stack.clone())
});
let (layout_id, request_layout) =
self.element.request_layout(global_id.as_ref(), window, cx);
let inspector_id;
#[cfg(any(feature = "inspector", debug_assertions))]
{
inspector_id = self.element.source_location().map(|source| {
let path = crate::InspectorElementPath {
global_id: GlobalElementId(window.element_id_stack.clone()),
source_location: source,
};
window.build_inspector_element_id(path)
});
}
#[cfg(not(any(feature = "inspector", debug_assertions)))]
{
inspector_id = None;
}
let (layout_id, request_layout) = self.element.request_layout(
global_id.as_ref(),
inspector_id.as_ref(),
window,
cx,
);
if global_id.is_some() {
window.element_id_stack.pop();
@ -307,6 +379,7 @@ impl<E: Element> Drawable<E> {
self.phase = ElementDrawPhase::RequestLayout {
layout_id,
global_id,
inspector_id,
request_layout,
};
layout_id
@ -320,11 +393,13 @@ impl<E: Element> Drawable<E> {
ElementDrawPhase::RequestLayout {
layout_id,
global_id,
inspector_id,
mut request_layout,
}
| ElementDrawPhase::LayoutComputed {
layout_id,
global_id,
inspector_id,
mut request_layout,
..
} => {
@ -337,6 +412,7 @@ impl<E: Element> Drawable<E> {
let node_id = window.next_frame.dispatch_tree.push_node();
let prepaint = self.element.prepaint(
global_id.as_ref(),
inspector_id.as_ref(),
bounds,
&mut request_layout,
window,
@ -351,6 +427,7 @@ impl<E: Element> Drawable<E> {
self.phase = ElementDrawPhase::Prepaint {
node_id,
global_id,
inspector_id,
bounds,
request_layout,
prepaint,
@ -369,6 +446,7 @@ impl<E: Element> Drawable<E> {
ElementDrawPhase::Prepaint {
node_id,
global_id,
inspector_id,
bounds,
mut request_layout,
mut prepaint,
@ -382,6 +460,7 @@ impl<E: Element> Drawable<E> {
window.next_frame.dispatch_tree.set_active_node(node_id);
self.element.paint(
global_id.as_ref(),
inspector_id.as_ref(),
bounds,
&mut request_layout,
&mut prepaint,
@ -414,12 +493,14 @@ impl<E: Element> Drawable<E> {
ElementDrawPhase::RequestLayout {
layout_id,
global_id,
inspector_id,
request_layout,
} => {
window.compute_layout(layout_id, available_space, cx);
self.phase = ElementDrawPhase::LayoutComputed {
layout_id,
global_id,
inspector_id,
available_space,
request_layout,
};
@ -428,6 +509,7 @@ impl<E: Element> Drawable<E> {
ElementDrawPhase::LayoutComputed {
layout_id,
global_id,
inspector_id,
available_space: prev_available_space,
request_layout,
} => {
@ -437,6 +519,7 @@ impl<E: Element> Drawable<E> {
self.phase = ElementDrawPhase::LayoutComputed {
layout_id,
global_id,
inspector_id,
available_space,
request_layout,
};
@ -570,9 +653,14 @@ impl Element for AnyElement {
None
}
fn source_location(&self) -> Option<&'static panic::Location<'static>> {
None
}
fn request_layout(
&mut self,
_: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (LayoutId, Self::RequestLayoutState) {
@ -583,6 +671,7 @@ impl Element for AnyElement {
fn prepaint(
&mut self,
_: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
_: Bounds<Pixels>,
_: &mut Self::RequestLayoutState,
window: &mut Window,
@ -594,6 +683,7 @@ impl Element for AnyElement {
fn paint(
&mut self,
_: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
_: Bounds<Pixels>,
_: &mut Self::RequestLayoutState,
_: &mut Self::PrepaintState,
@ -635,9 +725,14 @@ impl Element for Empty {
None
}
fn source_location(&self) -> Option<&'static panic::Location<'static>> {
None
}
fn request_layout(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (LayoutId, Self::RequestLayoutState) {
@ -647,6 +742,7 @@ impl Element for Empty {
fn prepaint(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
_bounds: Bounds<Pixels>,
_state: &mut Self::RequestLayoutState,
_window: &mut Window,
@ -657,6 +753,7 @@ impl Element for Empty {
fn paint(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
_bounds: Bounds<Pixels>,
_request_layout: &mut Self::RequestLayoutState,
_prepaint: &mut Self::PrepaintState,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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::*;

View file

@ -0,0 +1,223 @@
/// A unique identifier for an element that can be inspected.
#[derive(Debug, Eq, PartialEq, Hash, Clone)]
pub struct InspectorElementId {
/// Stable part of the ID.
#[cfg(any(feature = "inspector", debug_assertions))]
pub path: std::rc::Rc<InspectorElementPath>,
/// Disambiguates elements that have the same path.
#[cfg(any(feature = "inspector", debug_assertions))]
pub instance_id: usize,
}
impl Into<InspectorElementId> for &InspectorElementId {
fn into(self) -> InspectorElementId {
self.clone()
}
}
#[cfg(any(feature = "inspector", debug_assertions))]
pub use conditional::*;
#[cfg(any(feature = "inspector", debug_assertions))]
mod conditional {
use super::*;
use crate::{AnyElement, App, Context, Empty, IntoElement, Render, Window};
use collections::FxHashMap;
use std::any::{Any, TypeId};
/// `GlobalElementId` qualified by source location of element construction.
#[derive(Debug, Eq, PartialEq, Hash)]
pub struct InspectorElementPath {
/// The path to the nearest ancestor element that has an `ElementId`.
#[cfg(any(feature = "inspector", debug_assertions))]
pub global_id: crate::GlobalElementId,
/// Source location where this element was constructed.
#[cfg(any(feature = "inspector", debug_assertions))]
pub source_location: &'static std::panic::Location<'static>,
}
impl Clone for InspectorElementPath {
fn clone(&self) -> Self {
Self {
global_id: crate::GlobalElementId(self.global_id.0.clone()),
source_location: self.source_location,
}
}
}
impl Into<InspectorElementPath> for &InspectorElementPath {
fn into(self) -> InspectorElementPath {
self.clone()
}
}
/// Function set on `App` to render the inspector UI.
pub type InspectorRenderer =
Box<dyn Fn(&mut Inspector, &mut Window, &mut Context<Inspector>) -> AnyElement>;
/// Manages inspector state - which element is currently selected and whether the inspector is
/// in picking mode.
pub struct Inspector {
active_element: Option<InspectedElement>,
pub(crate) pick_depth: Option<f32>,
}
struct InspectedElement {
id: InspectorElementId,
states: FxHashMap<TypeId, Box<dyn Any>>,
}
impl InspectedElement {
fn new(id: InspectorElementId) -> Self {
InspectedElement {
id,
states: FxHashMap::default(),
}
}
}
impl Inspector {
pub(crate) fn new() -> Self {
Self {
active_element: None,
pick_depth: Some(0.0),
}
}
pub(crate) fn select(&mut self, id: InspectorElementId, window: &mut Window) {
self.set_active_element_id(id, window);
self.pick_depth = None;
}
pub(crate) fn hover(&mut self, id: InspectorElementId, window: &mut Window) {
if self.is_picking() {
let changed = self.set_active_element_id(id, window);
if changed {
self.pick_depth = Some(0.0);
}
}
}
pub(crate) fn set_active_element_id(
&mut self,
id: InspectorElementId,
window: &mut Window,
) -> bool {
let changed = Some(&id) != self.active_element_id();
if changed {
self.active_element = Some(InspectedElement::new(id));
window.refresh();
}
changed
}
/// ID of the currently hovered or selected element.
pub fn active_element_id(&self) -> Option<&InspectorElementId> {
self.active_element.as_ref().map(|e| &e.id)
}
pub(crate) fn with_active_element_state<T: 'static, R>(
&mut self,
window: &mut Window,
f: impl FnOnce(&mut Option<T>, &mut Window) -> R,
) -> R {
let Some(active_element) = &mut self.active_element else {
return f(&mut None, window);
};
let type_id = TypeId::of::<T>();
let mut inspector_state = active_element
.states
.remove(&type_id)
.map(|state| *state.downcast().unwrap());
let result = f(&mut inspector_state, window);
if let Some(inspector_state) = inspector_state {
active_element
.states
.insert(type_id, Box::new(inspector_state));
}
result
}
/// Starts element picking mode, allowing the user to select elements by clicking.
pub fn start_picking(&mut self) {
self.pick_depth = Some(0.0);
}
/// Returns whether the inspector is currently in picking mode.
pub fn is_picking(&self) -> bool {
self.pick_depth.is_some()
}
/// Renders elements for all registered inspector states of the active inspector element.
pub fn render_inspector_states(
&mut self,
window: &mut Window,
cx: &mut Context<Self>,
) -> Vec<AnyElement> {
let mut elements = Vec::new();
if let Some(active_element) = self.active_element.take() {
for (type_id, state) in &active_element.states {
if let Some(render_inspector) = cx
.inspector_element_registry
.renderers_by_type_id
.remove(&type_id)
{
let mut element = (render_inspector)(
active_element.id.clone(),
state.as_ref(),
window,
cx,
);
elements.push(element);
cx.inspector_element_registry
.renderers_by_type_id
.insert(*type_id, render_inspector);
}
}
self.active_element = Some(active_element);
}
elements
}
}
impl Render for Inspector {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
if let Some(inspector_renderer) = cx.inspector_renderer.take() {
let result = inspector_renderer(self, window, cx);
cx.inspector_renderer = Some(inspector_renderer);
result
} else {
Empty.into_any_element()
}
}
}
#[derive(Default)]
pub(crate) struct InspectorElementRegistry {
renderers_by_type_id: FxHashMap<
TypeId,
Box<dyn Fn(InspectorElementId, &dyn Any, &mut Window, &mut App) -> AnyElement>,
>,
}
impl InspectorElementRegistry {
pub fn register<T: 'static, R: IntoElement>(
&mut self,
f: impl 'static + Fn(InspectorElementId, &T, &mut Window, &mut App) -> R,
) {
self.renderers_by_type_id.insert(
TypeId::of::<T>(),
Box::new(move |id, value, window, cx| {
let value = value.downcast_ref().unwrap();
f(id, value, window, cx).into_any_element()
}),
);
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,7 +1,7 @@
use crate::{
AnyElement, AnyEntity, AnyWeakEntity, App, Bounds, ContentMask, Context, Element, ElementId,
Entity, EntityId, GlobalElementId, IntoElement, LayoutId, PaintIndex, Pixels,
PrepaintStateIndex, Render, Style, StyleRefinement, TextStyle, WeakEntity,
Entity, EntityId, GlobalElementId, InspectorElementId, IntoElement, LayoutId, PaintIndex,
Pixels, PrepaintStateIndex, Render, Style, StyleRefinement, TextStyle, WeakEntity,
};
use crate::{Empty, Window};
use anyhow::Result;
@ -33,9 +33,14 @@ impl<V: Render> Element for Entity<V> {
Some(ElementId::View(self.entity_id()))
}
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
None
}
fn request_layout(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (LayoutId, Self::RequestLayoutState) {
@ -49,6 +54,7 @@ impl<V: Render> Element for Entity<V> {
fn prepaint(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
_: Bounds<Pixels>,
element: &mut Self::RequestLayoutState,
window: &mut Window,
@ -61,6 +67,7 @@ impl<V: Render> Element for Entity<V> {
fn paint(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
_: Bounds<Pixels>,
element: &mut Self::RequestLayoutState,
_: &mut Self::PrepaintState,
@ -146,29 +153,40 @@ 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() {
// 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)
} else {
}
_ => {
let mut element = (self.render)(self, window, cx);
let layout_id = element.request_layout(window, cx);
(layout_id, Some(element))
}
}
})
}
fn prepaint(
&mut self,
global_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
element: &mut Self::RequestLayoutState,
window: &mut Window,
@ -176,7 +194,11 @@ impl Element for AnyView {
) -> Option<AnyElement> {
window.set_view_id(self.entity_id());
window.with_rendered_view(self.entity_id(), |window| {
if self.cached_style.is_some() {
if let Some(mut element) = element.take() {
element.prepaint(window, cx);
return Some(element);
}
window.with_element_state::<AnyViewState, _>(
global_id.unwrap(),
|element_state, window| {
@ -228,18 +250,13 @@ impl Element for AnyView {
)
},
)
} else {
let mut element = element.take().unwrap();
element.prepaint(window, cx);
Some(element)
}
})
}
fn paint(
&mut self,
global_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
_bounds: Bounds<Pixels>,
_: &mut Self::RequestLayoutState,
element: &mut Self::PrepaintState,
@ -247,7 +264,8 @@ impl Element for AnyView {
cx: &mut App,
) {
window.with_rendered_view(self.entity_id(), |window| {
if self.cached_style.is_some() {
let caching_disabled = window.is_inspector_picking(cx);
if self.cached_style.is_some() && !caching_disabled {
window.with_element_state::<AnyViewState, _>(
global_id.unwrap(),
|element_state, window| {

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1 @@
../../LICENSE-GPL

View file

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

View file

@ -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());
}

View file

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

View file

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

View file

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

View file

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

View file

@ -97,18 +97,8 @@ 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();
// This can be viewed via `dev: open language server logs` -> `json-language-server` ->
// `Server Info`
serde_json::json!({
"json": {
"format": {
"enable": true,
},
"validate":
{
"enable": true,
},
"schemas": [
#[allow(unused_mut)]
let mut schemas = serde_json::json!([
{
"fileMatch": ["tsconfig.json"],
"schema":tsconfig_schema
@ -151,9 +141,33 @@ impl JsonLspAdapter {
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!({
"json": {
"format": {
"enable": true,
},
"validate":
{
"enable": true,
},
"schemas": schemas
}
})
}
@ -180,6 +194,16 @@ impl JsonLspAdapter {
}
}
#[cfg(debug_assertions)]
fn generate_inspector_style_schema() -> serde_json_lenient::Value {
let schema = schemars::r#gen::SchemaSettings::draft07()
.with(|settings| settings.option_add_null_type = false)
.into_generator()
.into_root_schema_for::<gpui::StyleRefinement>();
serde_json_lenient::to_value(schema).unwrap()
}
#[async_trait(?Send)]
impl LspAdapter for JsonLspAdapter {
fn name(&self) -> LanguageServerName {

View file

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

View file

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

View file

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

View file

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

View file

@ -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| {
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)
});
},
);
(layout_id, ())
}
fn prepaint(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&gpui::InspectorElementId>,
bounds: Bounds<Pixels>,
_: &mut Self::RequestLayoutState,
window: &mut Window,
@ -620,6 +630,7 @@ impl Element for TerminalElement {
let rem_size = self.rem_size(cx);
self.interactivity.prepaint(
global_id,
inspector_id,
bounds,
bounds.size,
window,
@ -904,6 +915,7 @@ impl Element for TerminalElement {
fn paint(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&gpui::InspectorElementId>,
bounds: Bounds<Pixels>,
_: &mut Self::RequestLayoutState,
layout: &mut Self::PrepaintState,
@ -947,6 +959,7 @@ impl Element for TerminalElement {
let block_below_cursor_element = layout.block_below_cursor_element.take();
self.interactivity.paint(
global_id,
inspector_id,
bounds,
Some(&layout.hitbox),
window,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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