From 395f208f731d50093c8dab18261d735aa5b78880 Mon Sep 17 00:00:00 2001 From: Danilo Leal Date: Mon, 25 May 2026 20:40:36 -0300 Subject: [PATCH 01/11] Add `inset` field to BoxShadow struct and update all existing call sites --- .../disable_cursor_blinking/before.rs | 19 ++++++-- .../src/conversation_view/thread_view.rs | 1 + .../src/agent_api_keys_onboarding.rs | 1 + crates/editor/src/edit_prediction.rs | 1 + crates/gpui/examples/opacity.rs | 1 + crates/gpui/examples/shadow.rs | 45 +++++++++++++++++++ crates/gpui/examples/window_shadow.rs | 2 + crates/gpui/src/style.rs | 2 + crates/gpui_macros/src/styles.rs | 11 +++++ .../ui/src/components/button/split_button.rs | 1 + crates/ui/src/components/keybinding_hint.rs | 1 + .../src/components/progress/progress_bar.rs | 1 + crates/ui/src/styles/elevation.rs | 6 +++ crates/workspace/src/workspace.rs | 1 + 14 files changed, 90 insertions(+), 3 deletions(-) diff --git a/crates/agent/src/tools/evals/fixtures/disable_cursor_blinking/before.rs b/crates/agent/src/tools/evals/fixtures/disable_cursor_blinking/before.rs index bdf160d8ffe2c6..dfd16a49c853e2 100644 --- a/crates/agent/src/tools/evals/fixtures/disable_cursor_blinking/before.rs +++ b/crates/agent/src/tools/evals/fixtures/disable_cursor_blinking/before.rs @@ -7703,6 +7703,7 @@ impl Editor { offset: point(px(1.), px(1.)), blur_radius: px(2.), spread_radius: px(0.), + inset: false, }]) .bg(Editor::edit_prediction_line_popover_bg_color(cx)) .border(BORDER_WIDTH) @@ -7837,7 +7838,11 @@ impl Editor { h_flex() .px_0p5() .when(is_platform_style_mac, |parent| parent.gap_0p5()) - .font(theme_settings::ThemeSettings::get_global(cx).buffer_font.clone()) + .font( + theme_settings::ThemeSettings::get_global(cx) + .buffer_font + .clone(), + ) .text_size(TextSize::XSmall.rems(cx)) .child(h_flex().children(ui::render_modifiers( &accept_keystroke.modifiers, @@ -8149,7 +8154,11 @@ impl Editor { .px_2() .child( h_flex() - .font(theme_settings::ThemeSettings::get_global(cx).buffer_font.clone()) + .font( + theme_settings::ThemeSettings::get_global(cx) + .buffer_font + .clone(), + ) .when(is_platform_style_mac, |parent| parent.gap_1()) .child(h_flex().children(ui::render_modifiers( &accept_keystroke.modifiers, @@ -8258,7 +8267,11 @@ impl Editor { .gap_2() .pr_1() .overflow_x_hidden() - .font(theme_settings::ThemeSettings::get_global(cx).buffer_font.clone()) + .font( + theme_settings::ThemeSettings::get_global(cx) + .buffer_font + .clone(), + ) .child(left) .child(preview), ) diff --git a/crates/agent_ui/src/conversation_view/thread_view.rs b/crates/agent_ui/src/conversation_view/thread_view.rs index 4f0adce1ad4836..80cdc5d7db4d84 100644 --- a/crates/agent_ui/src/conversation_view/thread_view.rs +++ b/crates/agent_ui/src/conversation_view/thread_view.rs @@ -2595,6 +2595,7 @@ impl ThreadView { offset: point(px(1.), px(-1.)), blur_radius: px(2.), spread_radius: px(0.), + inset: false, }]) .when_some(subagents_awaiting_permission, |this, element| { this.child(element) diff --git a/crates/ai_onboarding/src/agent_api_keys_onboarding.rs b/crates/ai_onboarding/src/agent_api_keys_onboarding.rs index 47197ec2331b97..56b4e7ffaa686b 100644 --- a/crates/ai_onboarding/src/agent_api_keys_onboarding.rs +++ b/crates/ai_onboarding/src/agent_api_keys_onboarding.rs @@ -73,6 +73,7 @@ impl Render for ApiKeysWithProviders { offset: point(px(1.), px(-1.)), blur_radius: px(3.), spread_radius: px(0.), + inset: false, }]) .child( h_flex() diff --git a/crates/editor/src/edit_prediction.rs b/crates/editor/src/edit_prediction.rs index e7c942bd000c9e..bdaef3430eda18 100644 --- a/crates/editor/src/edit_prediction.rs +++ b/crates/editor/src/edit_prediction.rs @@ -1980,6 +1980,7 @@ impl Editor { offset: point(px(1.), px(1.)), blur_radius: px(2.), spread_radius: px(0.), + inset: false, }]) .bg(Editor::edit_prediction_line_popover_bg_color(cx)) .border(BORDER_WIDTH) diff --git a/crates/gpui/examples/opacity.rs b/crates/gpui/examples/opacity.rs index ba61d0f5daca2e..1d74127f4cd355 100644 --- a/crates/gpui/examples/opacity.rs +++ b/crates/gpui/examples/opacity.rs @@ -119,6 +119,7 @@ impl Render for HelloWorld { blur_radius: px(1.0), spread_radius: px(5.0), offset: point(px(10.0), px(10.0)), + inset: false, }]) .child(img("image/app-icon.png").size_8()) .child("Opacity Panel (Click to test)") diff --git a/crates/gpui/examples/shadow.rs b/crates/gpui/examples/shadow.rs index d39a2eb62ed74b..753567e37f940d 100644 --- a/crates/gpui/examples/shadow.rs +++ b/crates/gpui/examples/shadow.rs @@ -109,6 +109,7 @@ impl Render for Shadow { offset: point(px(0.), px(8.)), blur_radius: px(8.), spread_radius: px(0.), + inset: false, }]), ), example( @@ -119,6 +120,7 @@ impl Render for Shadow { offset: point(px(0.), px(8.)), blur_radius: px(8.), spread_radius: px(0.), + inset: false, }]), ), example( @@ -129,6 +131,7 @@ impl Render for Shadow { offset: point(px(0.), px(8.)), blur_radius: px(8.), spread_radius: px(0.), + inset: false, }]), ), example( @@ -139,6 +142,7 @@ impl Render for Shadow { offset: point(px(0.), px(8.)), blur_radius: px(8.), spread_radius: px(0.), + inset: false, }]), ), example( @@ -149,6 +153,7 @@ impl Render for Shadow { offset: point(px(0.), px(8.)), blur_radius: px(8.), spread_radius: px(0.), + inset: false, }]), ), ]), @@ -185,6 +190,7 @@ impl Render for Shadow { offset: point(px(0.), px(8.)), blur_radius: px(0.), spread_radius: px(0.), + inset: false, }]), ), example( @@ -194,6 +200,7 @@ impl Render for Shadow { offset: point(px(0.), px(8.)), blur_radius: px(2.), spread_radius: px(0.), + inset: false, }]), ), example( @@ -203,6 +210,7 @@ impl Render for Shadow { offset: point(px(0.), px(8.)), blur_radius: px(4.), spread_radius: px(0.), + inset: false, }]), ), example( @@ -212,6 +220,7 @@ impl Render for Shadow { offset: point(px(0.), px(8.)), blur_radius: px(8.), spread_radius: px(0.), + inset: false, }]), ), example( @@ -221,6 +230,7 @@ impl Render for Shadow { offset: point(px(0.), px(8.)), blur_radius: px(16.), spread_radius: px(0.), + inset: false, }]), ), ]), @@ -237,6 +247,7 @@ impl Render for Shadow { offset: point(px(0.), px(8.)), blur_radius: px(8.), spread_radius: px(0.), + inset: false, }]), ), example( @@ -246,6 +257,7 @@ impl Render for Shadow { offset: point(px(0.), px(8.)), blur_radius: px(8.), spread_radius: px(2.), + inset: false, }]), ), example( @@ -255,6 +267,7 @@ impl Render for Shadow { offset: point(px(0.), px(8.)), blur_radius: px(8.), spread_radius: px(4.), + inset: false, }]), ), example( @@ -264,6 +277,7 @@ impl Render for Shadow { offset: point(px(0.), px(8.)), blur_radius: px(8.), spread_radius: px(8.), + inset: false, }]), ), example( @@ -273,6 +287,7 @@ impl Render for Shadow { offset: point(px(0.), px(8.)), blur_radius: px(8.), spread_radius: px(16.), + inset: false, }]), ), ]), @@ -289,6 +304,7 @@ impl Render for Shadow { offset: point(px(0.), px(8.)), blur_radius: px(8.), spread_radius: px(0.), + inset: false, }]), ), example( @@ -298,6 +314,7 @@ impl Render for Shadow { offset: point(px(0.), px(8.)), blur_radius: px(8.), spread_radius: px(8.), + inset: false, }]), ), example( @@ -307,6 +324,7 @@ impl Render for Shadow { offset: point(px(0.), px(8.)), blur_radius: px(8.), spread_radius: px(16.), + inset: false, }]), ), ]), @@ -323,6 +341,7 @@ impl Render for Shadow { offset: point(px(0.), px(8.)), blur_radius: px(8.), spread_radius: px(0.), + inset: false, }]), ), example( @@ -332,6 +351,7 @@ impl Render for Shadow { offset: point(px(0.), px(8.)), blur_radius: px(8.), spread_radius: px(8.), + inset: false, }]), ), example( @@ -341,6 +361,7 @@ impl Render for Shadow { offset: point(px(0.), px(8.)), blur_radius: px(8.), spread_radius: px(16.), + inset: false, }]), ), ]), @@ -357,6 +378,7 @@ impl Render for Shadow { offset: point(px(-8.), px(0.)), blur_radius: px(8.), spread_radius: px(0.), + inset: false, }]), ), example( @@ -366,6 +388,7 @@ impl Render for Shadow { offset: point(px(8.), px(0.)), blur_radius: px(8.), spread_radius: px(0.), + inset: false, }]), ), example( @@ -375,6 +398,7 @@ impl Render for Shadow { offset: point(px(0.), px(-8.)), blur_radius: px(8.), spread_radius: px(0.), + inset: false, }]), ), example( @@ -384,6 +408,7 @@ impl Render for Shadow { offset: point(px(0.), px(8.)), blur_radius: px(8.), spread_radius: px(0.), + inset: false, }]), ), ]), @@ -400,6 +425,7 @@ impl Render for Shadow { offset: point(px(-8.), px(0.)), blur_radius: px(8.), spread_radius: px(0.), + inset: false, }]), ), example( @@ -409,6 +435,7 @@ impl Render for Shadow { offset: point(px(8.), px(0.)), blur_radius: px(8.), spread_radius: px(0.), + inset: false, }]), ), example( @@ -418,6 +445,7 @@ impl Render for Shadow { offset: point(px(0.), px(-8.)), blur_radius: px(8.), spread_radius: px(0.), + inset: false, }]), ), example( @@ -427,6 +455,7 @@ impl Render for Shadow { offset: point(px(0.), px(8.)), blur_radius: px(8.), spread_radius: px(0.), + inset: false, }]), ), ]), @@ -443,6 +472,7 @@ impl Render for Shadow { offset: point(px(-8.), px(0.)), blur_radius: px(8.), spread_radius: px(0.), + inset: false, }]), ), example( @@ -452,6 +482,7 @@ impl Render for Shadow { offset: point(px(8.), px(0.)), blur_radius: px(8.), spread_radius: px(0.), + inset: false, }]), ), example( @@ -461,6 +492,7 @@ impl Render for Shadow { offset: point(px(0.), px(-8.)), blur_radius: px(8.), spread_radius: px(0.), + inset: false, }]), ), example( @@ -470,6 +502,7 @@ impl Render for Shadow { offset: point(px(0.), px(8.)), blur_radius: px(8.), spread_radius: px(0.), + inset: false, }]), ), ]), @@ -487,24 +520,28 @@ impl Render for Shadow { offset: point(px(0.), px(-12.)), blur_radius: px(8.), spread_radius: px(2.), + inset: false, }, BoxShadow { color: hsla(60.0 / 360., 1.0, 0.5, 0.3), // Yellow offset: point(px(12.), px(0.)), blur_radius: px(8.), spread_radius: px(2.), + inset: false, }, BoxShadow { color: hsla(120.0 / 360., 1.0, 0.5, 0.3), // Green offset: point(px(0.), px(12.)), blur_radius: px(8.), spread_radius: px(2.), + inset: false, }, BoxShadow { color: hsla(240.0 / 360., 1.0, 0.5, 0.3), // Blue offset: point(px(-12.), px(0.)), blur_radius: px(8.), spread_radius: px(2.), + inset: false, }, ]), ), @@ -516,24 +553,28 @@ impl Render for Shadow { offset: point(px(0.), px(-12.)), blur_radius: px(8.), spread_radius: px(2.), + inset: false, }, BoxShadow { color: hsla(60.0 / 360., 1.0, 0.5, 0.3), // Yellow offset: point(px(12.), px(0.)), blur_radius: px(8.), spread_radius: px(2.), + inset: false, }, BoxShadow { color: hsla(120.0 / 360., 1.0, 0.5, 0.3), // Green offset: point(px(0.), px(12.)), blur_radius: px(8.), spread_radius: px(2.), + inset: false, }, BoxShadow { color: hsla(240.0 / 360., 1.0, 0.5, 0.3), // Blue offset: point(px(-12.), px(0.)), blur_radius: px(8.), spread_radius: px(2.), + inset: false, }, ]), ), @@ -545,24 +586,28 @@ impl Render for Shadow { offset: point(px(0.), px(-12.)), blur_radius: px(8.), spread_radius: px(2.), + inset: false, }, BoxShadow { color: hsla(60.0 / 360., 1.0, 0.5, 0.3), // Yellow offset: point(px(12.), px(0.)), blur_radius: px(8.), spread_radius: px(2.), + inset: false, }, BoxShadow { color: hsla(120.0 / 360., 1.0, 0.5, 0.3), // Green offset: point(px(0.), px(12.)), blur_radius: px(8.), spread_radius: px(2.), + inset: false, }, BoxShadow { color: hsla(240.0 / 360., 1.0, 0.5, 0.3), // Blue offset: point(px(-12.), px(0.)), blur_radius: px(8.), spread_radius: px(2.), + inset: false, }, ]), ), diff --git a/crates/gpui/examples/window_shadow.rs b/crates/gpui/examples/window_shadow.rs index b8c052693da9f3..3f06098c1981c6 100644 --- a/crates/gpui/examples/window_shadow.rs +++ b/crates/gpui/examples/window_shadow.rs @@ -116,6 +116,7 @@ impl Render for WindowShadow { }, blur_radius: shadow_size / 2., spread_radius: px(0.), + inset: false, offset: point(px(0.0), px(0.0)), }]) }), @@ -156,6 +157,7 @@ impl Render for WindowShadow { }, blur_radius: px(20.0), spread_radius: px(0.0), + inset: false, offset: point(px(0.0), px(0.0)), }]) .map(|div| match decorations { diff --git a/crates/gpui/src/style.rs b/crates/gpui/src/style.rs index 05bc083d1d7ad7..eaf0c5b481fb71 100644 --- a/crates/gpui/src/style.rs +++ b/crates/gpui/src/style.rs @@ -351,6 +351,8 @@ pub struct BoxShadow { pub blur_radius: Pixels, /// How much should the shadow spread? pub spread_radius: Pixels, + /// Whether this is an inset shadow (drawn inside the element's bounds). + pub inset: bool, } /// How to handle whitespace in text diff --git a/crates/gpui_macros/src/styles.rs b/crates/gpui_macros/src/styles.rs index fdbc64f623a474..6a0095a6c798be 100644 --- a/crates/gpui_macros/src/styles.rs +++ b/crates/gpui_macros/src/styles.rs @@ -410,6 +410,7 @@ pub fn box_shadow_style_methods(input: TokenStream) -> TokenStream { offset: point(px(0.), px(1.)), blur_radius: px(0.), spread_radius: px(0.), + inset: false, }]); self } @@ -425,6 +426,7 @@ pub fn box_shadow_style_methods(input: TokenStream) -> TokenStream { offset: point(px(0.), px(1.)), blur_radius: px(2.), spread_radius: px(0.), + inset: false, }]); self } @@ -441,12 +443,14 @@ pub fn box_shadow_style_methods(input: TokenStream) -> TokenStream { offset: point(px(0.), px(1.)), blur_radius: px(3.), spread_radius: px(0.), + inset: false, }, BoxShadow { color: hsla(0., 0., 0., 0.1), offset: point(px(0.), px(1.)), blur_radius: px(2.), spread_radius: px(-1.), + inset: false, } ]); self @@ -464,12 +468,14 @@ pub fn box_shadow_style_methods(input: TokenStream) -> TokenStream { offset: point(px(0.), px(4.)), blur_radius: px(6.), spread_radius: px(-1.), + inset: false, }, BoxShadow { color: hsla(0., 0., 0., 0.1), offset: point(px(0.), px(2.)), blur_radius: px(4.), spread_radius: px(-2.), + inset: false, } ]); self @@ -487,12 +493,14 @@ pub fn box_shadow_style_methods(input: TokenStream) -> TokenStream { offset: point(px(0.), px(10.)), blur_radius: px(15.), spread_radius: px(-3.), + inset: false, }, BoxShadow { color: hsla(0., 0., 0., 0.1), offset: point(px(0.), px(4.)), blur_radius: px(6.), spread_radius: px(-4.), + inset: false, } ]); self @@ -510,12 +518,14 @@ pub fn box_shadow_style_methods(input: TokenStream) -> TokenStream { offset: point(px(0.), px(20.)), blur_radius: px(25.), spread_radius: px(-5.), + inset: false, }, BoxShadow { color: hsla(0., 0., 0., 0.1), offset: point(px(0.), px(8.)), blur_radius: px(10.), spread_radius: px(-6.), + inset: false, } ]); self @@ -532,6 +542,7 @@ pub fn box_shadow_style_methods(input: TokenStream) -> TokenStream { offset: point(px(0.), px(25.)), blur_radius: px(50.), spread_radius: px(-12.), + inset: false, }]); self } diff --git a/crates/ui/src/components/button/split_button.rs b/crates/ui/src/components/button/split_button.rs index e6821e949009fe..7871ea3e98ce36 100644 --- a/crates/ui/src/components/button/split_button.rs +++ b/crates/ui/src/components/button/split_button.rs @@ -94,6 +94,7 @@ impl RenderOnce for SplitButton { offset: point(px(0.), px(1.)), blur_radius: px(0.), spread_radius: px(0.), + inset: false, }]) }) } diff --git a/crates/ui/src/components/keybinding_hint.rs b/crates/ui/src/components/keybinding_hint.rs index 9da470c4ee4173..2d6f1f72f9cf13 100644 --- a/crates/ui/src/components/keybinding_hint.rs +++ b/crates/ui/src/components/keybinding_hint.rs @@ -247,6 +247,7 @@ impl RenderOnce for KeybindingHint { offset: point(px(0.), px(1.)), blur_radius: px(0.), spread_radius: px(0.), + inset: false, }]) .child(self.keybinding.size(rems_from_px(kb_size))), ) diff --git a/crates/ui/src/components/progress/progress_bar.rs b/crates/ui/src/components/progress/progress_bar.rs index b46b0523681e84..31e9e74289466a 100644 --- a/crates/ui/src/components/progress/progress_bar.rs +++ b/crates/ui/src/components/progress/progress_bar.rs @@ -76,6 +76,7 @@ impl RenderOnce for ProgressBar { offset: point(px(0.), px(1.)), blur_radius: px(0.), spread_radius: px(0.), + inset: false, }]) .child( div() diff --git a/crates/ui/src/styles/elevation.rs b/crates/ui/src/styles/elevation.rs index e6df718b8f7642..2f0bc2618e42a3 100644 --- a/crates/ui/src/styles/elevation.rs +++ b/crates/ui/src/styles/elevation.rs @@ -52,12 +52,14 @@ impl ElevationIndex { offset: point(px(0.), px(2.)), blur_radius: px(3.), spread_radius: px(0.), + inset: false, }, BoxShadow { color: hsla(0., 0., 0., if is_light { 0.03 } else { 0.06 }), offset: point(px(0.), px(1.)), blur_radius: px(0.), spread_radius: px(0.), + inset: false, }, ], @@ -67,24 +69,28 @@ impl ElevationIndex { offset: point(px(0.), px(2.)), blur_radius: px(3.), spread_radius: px(0.), + inset: false, }, BoxShadow { color: hsla(0., 0., 0., if is_light { 0.06 } else { 0.08 }), offset: point(px(0.), px(3.)), blur_radius: px(6.), spread_radius: px(0.), + inset: false, }, BoxShadow { color: hsla(0., 0., 0., 0.04), offset: point(px(0.), px(6.)), blur_radius: px(12.), spread_radius: px(0.), + inset: false, }, BoxShadow { color: hsla(0., 0., 0., if is_light { 0.04 } else { 0.12 }), offset: point(px(0.), px(1.)), blur_radius: px(0.), spread_radius: px(0.), + inset: false, }, ], diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 3cd02c1599bc9b..dc446a4e6d113c 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -10637,6 +10637,7 @@ pub fn client_side_decorations( }, blur_radius: theme::CLIENT_SIDE_DECORATION_SHADOW / 2., spread_radius: px(0.), + inset: false, offset: point(px(0.0), px(0.0)), }]) }), From c0cce50408cba379328b776af1cdcd4dbbe36e48 Mon Sep 17 00:00:00 2001 From: Danilo Leal Date: Mon, 25 May 2026 20:41:36 -0300 Subject: [PATCH 02/11] Render inset box shadows on the GPU through the Metal and WGSL shaders --- crates/gpui/src/scene.rs | 8 ++++ crates/gpui/src/window.rs | 62 ++++++++++++++++++++++++----- crates/gpui_macos/src/shaders.metal | 44 +++++++++++++++----- crates/gpui_wgpu/src/shaders.wgsl | 43 ++++++++++++++++---- 4 files changed, 129 insertions(+), 28 deletions(-) diff --git a/crates/gpui/src/scene.rs b/crates/gpui/src/scene.rs index ef37caa7b95cfb..de1dcbc34c43ae 100644 --- a/crates/gpui/src/scene.rs +++ b/crates/gpui/src/scene.rs @@ -530,6 +530,14 @@ pub struct Shadow { pub corner_radii: Corners, pub content_mask: ContentMask, pub color: Hsla, + pub element_bounds: Bounds, + pub element_corner_radii: Corners, + /// 0 = drop shadow (rendered outside the element), 1 = inset shadow (rendered inside). + pub inset: u32, + /// Explicit trailing padding to match the 8-byte struct alignment WGSL infers from the + /// `vec2` inside `Bounds`. Without this, the Rust struct would be 108 bytes while the + /// WGSL struct would be 112, corrupting every shadow instance after the first. + pub _pad: u32, } impl From for Primitive { diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 1590fe1795fe29..4a3badd495d087 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -3449,7 +3449,8 @@ impl Window { result } - /// Paint one or more drop shadows into the scene for the next frame at the current z-index. + /// Paint one or more box shadows (drop or inset) into the scene for the next frame at the + /// current z-index. Mirrors CSS `box-shadow`, including the `inset` keyword. /// /// This method should only be called as part of the paint phase of element drawing. pub fn paint_shadows( @@ -3463,16 +3464,57 @@ impl Window { let scale_factor = self.scale_factor(); let content_mask = self.snapped_content_mask(); let opacity = self.element_opacity(); + let element_bounds = self.cover_bounds(bounds); + let element_corner_radii = corner_radii.scale(scale_factor); for shadow in shadows { - let shadow_bounds = (bounds + shadow.offset).dilate(shadow.spread_radius); - self.next_frame.scene.insert_primitive(Shadow { - order: 0, - blur_radius: shadow.blur_radius.scale(scale_factor), - bounds: self.cover_bounds(shadow_bounds), - content_mask, - corner_radii: corner_radii.scale(scale_factor), - color: shadow.color.opacity(opacity), - }); + if shadow.inset { + // CSS inset shadow: the visible shadow is the blurred *complement* of a "hole" + // rect, masked to the element. The hole is the element offset by `offset` and + // shrunk on every side by `spread_radius`. Negative spread expands the hole + // (and therefore shrinks the visible shadow); CSS allows this. + let hole = (bounds + shadow.offset).dilate(-shadow.spread_radius); + // Match CSS: each corner of the hole = max(0, element_corner - spread). + // `Pixels` supports subtraction; clamp at zero so a large spread can't produce + // negative radii (which would be nonsensical and could break the SDF). + let zero = Pixels::ZERO; + let hole_corner_radii = Corners { + top_left: (corner_radii.top_left - shadow.spread_radius).max(zero), + top_right: (corner_radii.top_right - shadow.spread_radius).max(zero), + bottom_right: (corner_radii.bottom_right - shadow.spread_radius).max(zero), + bottom_left: (corner_radii.bottom_left - shadow.spread_radius).max(zero), + }; + self.next_frame.scene.insert_primitive(Shadow { + order: 0, + blur_radius: shadow.blur_radius.scale(scale_factor), + bounds: self.cover_bounds(hole), + content_mask, + corner_radii: hole_corner_radii.scale(scale_factor), + color: shadow.color.opacity(opacity), + element_bounds, + element_corner_radii, + inset: 1, + _pad: 0, + }); + } else { + // CSS drop shadow: shadow rect is the element offset by `offset` and dilated by + // `spread_radius`. The shader inflates this further by `3 * blur_radius` for the + // gaussian tail. + let shadow_bounds = (bounds + shadow.offset).dilate(shadow.spread_radius); + self.next_frame.scene.insert_primitive(Shadow { + order: 0, + blur_radius: shadow.blur_radius.scale(scale_factor), + bounds: self.cover_bounds(shadow_bounds), + content_mask, + corner_radii: corner_radii.scale(scale_factor), + color: shadow.color.opacity(opacity), + // Unused by the shader for drop shadows, but we still pass valid values so + // the GPU struct is well-defined. + element_bounds, + element_corner_radii, + inset: 0, + _pad: 0, + }); + } } } diff --git a/crates/gpui_macos/src/shaders.metal b/crates/gpui_macos/src/shaders.metal index 4dc2d334e1e092..824d16ced21320 100644 --- a/crates/gpui_macos/src/shaders.metal +++ b/crates/gpui_macos/src/shaders.metal @@ -468,14 +468,22 @@ vertex ShadowVertexOutput shadow_vertex( float2 unit_vertex = unit_vertices[unit_vertex_id]; Shadow shadow = shadows[shadow_id]; - float margin = 3. * shadow.blur_radius; - // Set the bounds of the shadow and adjust its size based on the shadow's - // spread radius to achieve the spreading effect - Bounds_ScaledPixels bounds = shadow.bounds; - bounds.origin.x -= margin; - bounds.origin.y -= margin; - bounds.size.width += 2. * margin; - bounds.size.height += 2. * margin; + // The geometry rect we rasterize differs between drop and inset shadows. + // - Drop: shadow.bounds (already offset + spread-dilated by the CPU), inflated by + // 3 * blur_radius so the gaussian tail has room outside the rect. + // - Inset: the element's own bounds. The shadow lives entirely inside the element, + // so the geometry never needs to extend beyond it. + Bounds_ScaledPixels bounds; + if (shadow.inset != 0u) { + bounds = shadow.element_bounds; + } else { + float margin = 3. * shadow.blur_radius; + bounds = shadow.bounds; + bounds.origin.x -= margin; + bounds.origin.y -= margin; + bounds.size.width += 2. * margin; + bounds.size.height += 2. * margin; + } float4 device_position = to_device_position(unit_vertex, bounds, viewport_size); @@ -515,6 +523,10 @@ fragment float4 shadow_fragment(ShadowFragmentInput input [[stage_in]], } } + // `alpha` is the (blurred) coverage of `shadow.bounds`: + // - For drop shadows, that's the shadow rect itself, so this is the shadow's intensity. + // - For inset shadows, that's the "hole" rect, so we'll invert it below to get the + // blurred *complement* (the dark ring) and then clip it to the element. float alpha; if (shadow.blur_radius == 0.) { float distance = quad_sdf(input.position.xy, shadow.bounds, shadow.corner_radii); @@ -538,6 +550,16 @@ fragment float4 shadow_fragment(ShadowFragmentInput input [[stage_in]], } } + if (shadow.inset != 0u) { + // Invert: the inset shadow lives *outside* the hole rect. + alpha = 1. - alpha; + // Clip to the element's rounded rect so the shadow never escapes the element. + // `saturate(0.5 - d)` gives a 1-pixel antialiased edge: d <= -0.5 -> 1, d >= 0.5 -> 0. + float element_distance = quad_sdf(input.position.xy, shadow.element_bounds, + shadow.element_corner_radii); + alpha *= saturate(0.5 - element_distance); + } + return input.color * float4(1., 1., 1., alpha); } @@ -1251,14 +1273,14 @@ float4 fill_color(Background background, // checkerboard float size = background.gradient_angle_or_pattern_height; float2 relative_position = position - float2(bounds.origin.x, bounds.origin.y); - + float x_index = floor(relative_position.x / size); float y_index = floor(relative_position.y / size); float should_be_colored = fmod(x_index + y_index, 2.0); - + color = solid_color; color.a *= saturate(should_be_colored); - break; + break; } } diff --git a/crates/gpui_wgpu/src/shaders.wgsl b/crates/gpui_wgpu/src/shaders.wgsl index b700697f47b932..e66044c914de9b 100644 --- a/crates/gpui_wgpu/src/shaders.wgsl +++ b/crates/gpui_wgpu/src/shaders.wgsl @@ -951,10 +951,23 @@ fn fmod(a: f32, b: f32) -> f32 { struct Shadow { order: u32, blur_radius: f32, + // For drop shadows: the shadow rect (element offset + spread-dilated by the CPU). + // For inset shadows: the "hole" rect. bounds: Bounds, + // Corner radii of `bounds`. For inset, these are the element's radii minus spread. corner_radii: Corners, content_mask: Bounds, color: Hsla, + // Only consulted when `inset == 1u`: the element's own bounds, used as a rounded-rect + // clip so the shadow never escapes the element. + element_bounds: Bounds, + element_corner_radii: Corners, + // 0 = drop shadow, 1 = inset shadow. + inset: u32, + // Trailing padding to keep this struct at 112 bytes, matching the Rust + Metal layout. + // Without it the WGSL struct would pad to 112 while the C/Rust struct would be 108, + // corrupting every instance after the first. + _pad: u32, } @group(1) @binding(0) var b_shadows: array; @@ -971,17 +984,24 @@ fn vs_shadow(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) ins let unit_vertex = vec2(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u)); var shadow = b_shadows[instance_id]; - let margin = 3.0 * shadow.blur_radius; - // Set the bounds of the shadow and adjust its size based on the shadow's - // spread radius to achieve the spreading effect - shadow.bounds.origin -= vec2(margin); - shadow.bounds.size += 2.0 * vec2(margin); + // The geometry rect differs between drop and inset shadows. + // - Drop: shadow.bounds, inflated by 3 * blur_radius for the gaussian tail. + // - Inset: shadow.element_bounds (the shadow stays inside the element). + var geometry: Bounds; + if (shadow.inset != 0u) { + geometry = shadow.element_bounds; + } else { + let margin = 3.0 * shadow.blur_radius; + geometry = shadow.bounds; + geometry.origin -= vec2(margin); + geometry.size += 2.0 * vec2(margin); + } var out = ShadowVarying(); - out.position = to_device_position(unit_vertex, shadow.bounds); + out.position = to_device_position(unit_vertex, geometry); out.color = hsla_to_rgba(shadow.color); out.shadow_id = instance_id; - out.clip_distances = distance_from_clip_rect(unit_vertex, shadow.bounds, shadow.content_mask); + out.clip_distances = distance_from_clip_rect(unit_vertex, geometry, shadow.content_mask); return out; } @@ -1016,6 +1036,15 @@ fn fs_shadow(input: ShadowVarying) -> @location(0) vec4 { y += step; } + if (shadow.inset != 0u) { + // Invert the rect coverage to get the blurred complement (the dark ring), + // then clip to the element's rounded rect with a 1-pixel AA edge. + alpha = 1.0 - alpha; + let element_distance = quad_sdf(input.position.xy, shadow.element_bounds, + shadow.element_corner_radii); + alpha *= saturate(0.5 - element_distance); + } + return blend_color(input.color, alpha); } From 6970ef04a90a39631d766bff78bc174bc93d4565 Mon Sep 17 00:00:00 2001 From: Danilo Leal Date: Mon, 25 May 2026 20:41:58 -0300 Subject: [PATCH 03/11] Paint inset shadows on top of the background to match the CSS paint order --- crates/gpui/src/style.rs | 2 ++ crates/gpui/src/window.rs | 32 ++++++++++++++++++++++++++++++-- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/crates/gpui/src/style.rs b/crates/gpui/src/style.rs index eaf0c5b481fb71..d74f7c8fa28ce9 100644 --- a/crates/gpui/src/style.rs +++ b/crates/gpui/src/style.rs @@ -696,6 +696,8 @@ impl Style { )); } + window.paint_inset_shadows(bounds, corner_radii, &self.box_shadow); + continuation(window, cx); if self.is_border_visible() { diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 4a3badd495d087..2ca09a8c79f902 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -3449,8 +3449,11 @@ impl Window { result } - /// Paint one or more box shadows (drop or inset) into the scene for the next frame at the - /// current z-index. Mirrors CSS `box-shadow`, including the `inset` keyword. + /// Paint the **drop** (non-inset) box shadows from `shadows` into the scene at the current + /// z-index. Inset shadows in the list are ignored here; call [`Self::paint_inset_shadows`] + /// after the background quad is painted so they appear *on top of* the background, matching + /// CSS paint order: + /// 1. drop shadows -> 2. background -> 3. inset shadows -> 4. border -> 5. content. /// /// This method should only be called as part of the paint phase of element drawing. pub fn paint_shadows( @@ -3458,6 +3461,28 @@ impl Window { bounds: Bounds, corner_radii: Corners, shadows: &[BoxShadow], + ) { + self.paint_shadows_filtered(bounds, corner_radii, shadows, false); + } + + /// Paint the **inset** box shadows from `shadows`. Should be called after the background + /// quad so that the inset shadow ends up painted on top of the fill. Drop shadows in the + /// list are ignored. + pub fn paint_inset_shadows( + &mut self, + bounds: Bounds, + corner_radii: Corners, + shadows: &[BoxShadow], + ) { + self.paint_shadows_filtered(bounds, corner_radii, shadows, true); + } + + fn paint_shadows_filtered( + &mut self, + bounds: Bounds, + corner_radii: Corners, + shadows: &[BoxShadow], + want_inset: bool, ) { self.invalidator.debug_assert_paint(); @@ -3467,6 +3492,9 @@ impl Window { let element_bounds = self.cover_bounds(bounds); let element_corner_radii = corner_radii.scale(scale_factor); for shadow in shadows { + if shadow.inset != want_inset { + continue; + } if shadow.inset { // CSS inset shadow: the visible shadow is the blurred *complement* of a "hole" // rect, masked to the element. The hole is the element offset by `offset` and From bca8a9afb3b2d0b9b5a683decc2e50e25f5013e6 Mon Sep 17 00:00:00 2001 From: Danilo Leal Date: Mon, 25 May 2026 20:42:03 -0300 Subject: [PATCH 04/11] Demonstrate inset box shadows in the existing GPUI shadow example program --- crates/gpui/examples/shadow.rs | 85 ++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/crates/gpui/examples/shadow.rs b/crates/gpui/examples/shadow.rs index 753567e37f940d..5cb54fe9a66485 100644 --- a/crates/gpui/examples/shadow.rs +++ b/crates/gpui/examples/shadow.rs @@ -612,6 +612,91 @@ impl Render for Shadow { ]), ), ]), + // Inset shadows (CSS `box-shadow: inset ...`). + div() + .border_b_1() + .border_color(hsla(0.0, 0.0, 0.0, 1.0)) + .flex() + .w_full() + .children(vec![ + example( + "Inset basic", + Shadow::base().shadow(vec![BoxShadow { + color: hsla(0.0, 0.0, 0.0, 0.5), + offset: point(px(0.), px(0.)), + blur_radius: px(12.), + spread_radius: px(0.), + inset: true, + }]), + ), + example( + "Inset offset", + Shadow::base().shadow(vec![BoxShadow { + color: hsla(0.0, 0.0, 0.0, 0.5), + offset: point(px(6.), px(6.)), + blur_radius: px(8.), + spread_radius: px(0.), + inset: true, + }]), + ), + example( + "Inset spread", + Shadow::base().shadow(vec![BoxShadow { + color: hsla(0.0, 0.0, 0.0, 0.5), + offset: point(px(0.), px(0.)), + blur_radius: px(4.), + spread_radius: px(8.), + inset: true, + }]), + ), + example( + "Inset rounded", + Shadow::rounded_large().shadow(vec![BoxShadow { + color: hsla(0.0, 0.0, 0.0, 0.5), + offset: point(px(0.), px(4.)), + blur_radius: px(10.), + spread_radius: px(2.), + inset: true, + }]), + ), + example( + "Inset sharp", + Shadow::square().shadow(vec![BoxShadow { + color: hsla(0.0, 0.0, 0.0, 0.6), + offset: point(px(0.), px(0.)), + blur_radius: px(0.), + spread_radius: px(6.), + inset: true, + }]), + ), + ]), + // Combined: drop + inset shadows on the same element. + div() + .border_b_1() + .border_color(hsla(0.0, 0.0, 0.0, 1.0)) + .flex() + .w_full() + .children(vec![ + example( + "Drop + Inset", + Shadow::rounded_medium().shadow(vec![ + BoxShadow { + color: hsla(0.0, 0.0, 0.0, 0.25), + offset: point(px(0.), px(8.)), + blur_radius: px(12.), + spread_radius: px(0.), + inset: false, + }, + BoxShadow { + color: hsla(0.0, 0.0, 0.0, 0.4), + offset: point(px(0.), px(2.)), + blur_radius: px(4.), + spread_radius: px(0.), + inset: true, + }, + ]), + ), + ]), ])) } } From e283c266d2ffb8ef7545136f3608edf30a2571dc Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Tue, 26 May 2026 09:57:09 -0300 Subject: [PATCH 05/11] Repliace paint_shadows_filtered with drop and inset versions --- crates/gpui/src/window.rs | 124 +++++++++++++++++--------------------- 1 file changed, 55 insertions(+), 69 deletions(-) diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 2ca09a8c79f902..8ce2a025e4ac1a 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -3449,40 +3449,52 @@ impl Window { result } - /// Paint the **drop** (non-inset) box shadows from `shadows` into the scene at the current - /// z-index. Inset shadows in the list are ignored here; call [`Self::paint_inset_shadows`] - /// after the background quad is painted so they appear *on top of* the background, matching - /// CSS paint order: - /// 1. drop shadows -> 2. background -> 3. inset shadows -> 4. border -> 5. content. + /// Paint the drop (non-inset) shadows from `shadows` into the scene at the current + /// z-index. Inset shadows are skipped; paint those with [`Self::paint_inset_shadows`] + /// after the element's background so they layer on top of the fill. /// /// This method should only be called as part of the paint phase of element drawing. - pub fn paint_shadows( + pub(crate) fn paint_drop_shadows( &mut self, bounds: Bounds, corner_radii: Corners, shadows: &[BoxShadow], ) { - self.paint_shadows_filtered(bounds, corner_radii, shadows, false); - } + self.invalidator.debug_assert_paint(); - /// Paint the **inset** box shadows from `shadows`. Should be called after the background - /// quad so that the inset shadow ends up painted on top of the fill. Drop shadows in the - /// list are ignored. - pub fn paint_inset_shadows( - &mut self, - bounds: Bounds, - corner_radii: Corners, - shadows: &[BoxShadow], - ) { - self.paint_shadows_filtered(bounds, corner_radii, shadows, true); + let scale_factor = self.scale_factor(); + let content_mask = self.snapped_content_mask(); + let opacity = self.element_opacity(); + let element_bounds = self.cover_bounds(bounds); + let element_corner_radii = corner_radii.scale(scale_factor); + for shadow in shadows { + if shadow.inset { + continue; + } + let shadow_bounds = (bounds + shadow.offset).dilate(shadow.spread_radius); + self.next_frame.scene.insert_primitive(Shadow { + order: 0, + blur_radius: shadow.blur_radius.scale(scale_factor), + bounds: self.cover_bounds(shadow_bounds), + content_mask, + corner_radii: corner_radii.scale(scale_factor), + color: shadow.color.opacity(opacity), + element_bounds, + element_corner_radii, + inset: 0, + _pad: 0, + }); + } } - fn paint_shadows_filtered( + /// Paint the inset shadows from `shadows` into the scene at the current z-index. Should + /// be called after the element's background so the shadow layers on top of the fill. + /// Drop shadows are skipped; paint those with [`Self::paint_drop_shadows`] before the background. + pub(crate) fn paint_inset_shadows( &mut self, bounds: Bounds, corner_radii: Corners, shadows: &[BoxShadow], - want_inset: bool, ) { self.invalidator.debug_assert_paint(); @@ -3492,57 +3504,31 @@ impl Window { let element_bounds = self.cover_bounds(bounds); let element_corner_radii = corner_radii.scale(scale_factor); for shadow in shadows { - if shadow.inset != want_inset { + if !shadow.inset { continue; } - if shadow.inset { - // CSS inset shadow: the visible shadow is the blurred *complement* of a "hole" - // rect, masked to the element. The hole is the element offset by `offset` and - // shrunk on every side by `spread_radius`. Negative spread expands the hole - // (and therefore shrinks the visible shadow); CSS allows this. - let hole = (bounds + shadow.offset).dilate(-shadow.spread_radius); - // Match CSS: each corner of the hole = max(0, element_corner - spread). - // `Pixels` supports subtraction; clamp at zero so a large spread can't produce - // negative radii (which would be nonsensical and could break the SDF). - let zero = Pixels::ZERO; - let hole_corner_radii = Corners { - top_left: (corner_radii.top_left - shadow.spread_radius).max(zero), - top_right: (corner_radii.top_right - shadow.spread_radius).max(zero), - bottom_right: (corner_radii.bottom_right - shadow.spread_radius).max(zero), - bottom_left: (corner_radii.bottom_left - shadow.spread_radius).max(zero), - }; - self.next_frame.scene.insert_primitive(Shadow { - order: 0, - blur_radius: shadow.blur_radius.scale(scale_factor), - bounds: self.cover_bounds(hole), - content_mask, - corner_radii: hole_corner_radii.scale(scale_factor), - color: shadow.color.opacity(opacity), - element_bounds, - element_corner_radii, - inset: 1, - _pad: 0, - }); - } else { - // CSS drop shadow: shadow rect is the element offset by `offset` and dilated by - // `spread_radius`. The shader inflates this further by `3 * blur_radius` for the - // gaussian tail. - let shadow_bounds = (bounds + shadow.offset).dilate(shadow.spread_radius); - self.next_frame.scene.insert_primitive(Shadow { - order: 0, - blur_radius: shadow.blur_radius.scale(scale_factor), - bounds: self.cover_bounds(shadow_bounds), - content_mask, - corner_radii: corner_radii.scale(scale_factor), - color: shadow.color.opacity(opacity), - // Unused by the shader for drop shadows, but we still pass valid values so - // the GPU struct is well-defined. - element_bounds, - element_corner_radii, - inset: 0, - _pad: 0, - }); - } + let hole = (bounds + shadow.offset).dilate(-shadow.spread_radius); + // Clamp at zero so a large spread can't produce negative radii, which would + // break the SDF in the shader. + let zero = Pixels::ZERO; + let hole_corner_radii = Corners { + top_left: (corner_radii.top_left - shadow.spread_radius).max(zero), + top_right: (corner_radii.top_right - shadow.spread_radius).max(zero), + bottom_right: (corner_radii.bottom_right - shadow.spread_radius).max(zero), + bottom_left: (corner_radii.bottom_left - shadow.spread_radius).max(zero), + }; + self.next_frame.scene.insert_primitive(Shadow { + order: 0, + blur_radius: shadow.blur_radius.scale(scale_factor), + bounds: self.cover_bounds(hole), + content_mask, + corner_radii: hole_corner_radii.scale(scale_factor), + color: shadow.color.opacity(opacity), + element_bounds, + element_corner_radii, + inset: 1, + _pad: 0, + }); } } From bc7a8b54ee0537f948d050d7066f286e4dfea54f Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Tue, 26 May 2026 10:07:00 -0300 Subject: [PATCH 06/11] Drop superflous comments --- crates/gpui/src/scene.rs | 3 --- crates/gpui_macos/src/shaders.metal | 15 +++------------ crates/gpui_wgpu/src/shaders.wgsl | 12 ++++-------- 3 files changed, 7 insertions(+), 23 deletions(-) diff --git a/crates/gpui/src/scene.rs b/crates/gpui/src/scene.rs index de1dcbc34c43ae..2fd2b725fac45a 100644 --- a/crates/gpui/src/scene.rs +++ b/crates/gpui/src/scene.rs @@ -534,9 +534,6 @@ pub struct Shadow { pub element_corner_radii: Corners, /// 0 = drop shadow (rendered outside the element), 1 = inset shadow (rendered inside). pub inset: u32, - /// Explicit trailing padding to match the 8-byte struct alignment WGSL infers from the - /// `vec2` inside `Bounds`. Without this, the Rust struct would be 108 bytes while the - /// WGSL struct would be 112, corrupting every shadow instance after the first. pub _pad: u32, } diff --git a/crates/gpui_macos/src/shaders.metal b/crates/gpui_macos/src/shaders.metal index 824d16ced21320..2b52bb9ecc0476 100644 --- a/crates/gpui_macos/src/shaders.metal +++ b/crates/gpui_macos/src/shaders.metal @@ -468,15 +468,11 @@ vertex ShadowVertexOutput shadow_vertex( float2 unit_vertex = unit_vertices[unit_vertex_id]; Shadow shadow = shadows[shadow_id]; - // The geometry rect we rasterize differs between drop and inset shadows. - // - Drop: shadow.bounds (already offset + spread-dilated by the CPU), inflated by - // 3 * blur_radius so the gaussian tail has room outside the rect. - // - Inset: the element's own bounds. The shadow lives entirely inside the element, - // so the geometry never needs to extend beyond it. Bounds_ScaledPixels bounds; if (shadow.inset != 0u) { bounds = shadow.element_bounds; } else { + // Leave room for the gaussian tail outside the shadow rect. float margin = 3. * shadow.blur_radius; bounds = shadow.bounds; bounds.origin.x -= margin; @@ -523,10 +519,6 @@ fragment float4 shadow_fragment(ShadowFragmentInput input [[stage_in]], } } - // `alpha` is the (blurred) coverage of `shadow.bounds`: - // - For drop shadows, that's the shadow rect itself, so this is the shadow's intensity. - // - For inset shadows, that's the "hole" rect, so we'll invert it below to get the - // blurred *complement* (the dark ring) and then clip it to the element. float alpha; if (shadow.blur_radius == 0.) { float distance = quad_sdf(input.position.xy, shadow.bounds, shadow.corner_radii); @@ -551,10 +543,9 @@ fragment float4 shadow_fragment(ShadowFragmentInput input [[stage_in]], } if (shadow.inset != 0u) { - // Invert: the inset shadow lives *outside* the hole rect. - alpha = 1. - alpha; - // Clip to the element's rounded rect so the shadow never escapes the element. + // The inset shadow is the complement of the (blurred) hole rect, clipped to the element. // `saturate(0.5 - d)` gives a 1-pixel antialiased edge: d <= -0.5 -> 1, d >= 0.5 -> 0. + alpha = 1. - alpha; float element_distance = quad_sdf(input.position.xy, shadow.element_bounds, shadow.element_corner_radii); alpha *= saturate(0.5 - element_distance); diff --git a/crates/gpui_wgpu/src/shaders.wgsl b/crates/gpui_wgpu/src/shaders.wgsl index e66044c914de9b..16ce59c1d100d1 100644 --- a/crates/gpui_wgpu/src/shaders.wgsl +++ b/crates/gpui_wgpu/src/shaders.wgsl @@ -951,10 +951,8 @@ fn fmod(a: f32, b: f32) -> f32 { struct Shadow { order: u32, blur_radius: f32, - // For drop shadows: the shadow rect (element offset + spread-dilated by the CPU). - // For inset shadows: the "hole" rect. + // The shadow rect for drop shadows; the "hole" rect for inset shadows. bounds: Bounds, - // Corner radii of `bounds`. For inset, these are the element's radii minus spread. corner_radii: Corners, content_mask: Bounds, color: Hsla, @@ -984,13 +982,11 @@ fn vs_shadow(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) ins let unit_vertex = vec2(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u)); var shadow = b_shadows[instance_id]; - // The geometry rect differs between drop and inset shadows. - // - Drop: shadow.bounds, inflated by 3 * blur_radius for the gaussian tail. - // - Inset: shadow.element_bounds (the shadow stays inside the element). var geometry: Bounds; if (shadow.inset != 0u) { geometry = shadow.element_bounds; } else { + // Leave room for the gaussian tail outside the shadow rect. let margin = 3.0 * shadow.blur_radius; geometry = shadow.bounds; geometry.origin -= vec2(margin); @@ -1037,8 +1033,8 @@ fn fs_shadow(input: ShadowVarying) -> @location(0) vec4 { } if (shadow.inset != 0u) { - // Invert the rect coverage to get the blurred complement (the dark ring), - // then clip to the element's rounded rect with a 1-pixel AA edge. + // The inset shadow is the complement of the (blurred) hole rect, clipped to the element. + // `saturate(0.5 - d)` gives a 1-pixel antialiased edge: d <= -0.5 -> 1, d >= 0.5 -> 0. alpha = 1.0 - alpha; let element_distance = quad_sdf(input.position.xy, shadow.element_bounds, shadow.element_corner_radii); From 4afdfbe3364ec4c2021210c2df0734ae9c110076 Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Tue, 26 May 2026 10:11:46 -0300 Subject: [PATCH 07/11] Fix missing rename --- crates/gpui/src/style.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/gpui/src/style.rs b/crates/gpui/src/style.rs index d74f7c8fa28ce9..54e09bc37dd994 100644 --- a/crates/gpui/src/style.rs +++ b/crates/gpui/src/style.rs @@ -667,7 +667,7 @@ impl Style { .to_pixels(rem_size) .clamp_radii_for_quad_size(bounds.size); - window.paint_shadows(bounds, corner_radii, &self.box_shadow); + window.paint_drop_shadows(bounds, corner_radii, &self.box_shadow); let background_color = self.background.as_ref().and_then(Fill::color); if background_color.is_some_and(|color| !color.is_transparent()) { From bfe5cfd1350578714e3432e25a1a33de4171aa68 Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Tue, 26 May 2026 10:52:28 -0300 Subject: [PATCH 08/11] Add align comment --- crates/gpui/src/scene.rs | 2 +- crates/gpui/src/window.rs | 4 ++-- crates/gpui_wgpu/src/shaders.wgsl | 5 +---- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/crates/gpui/src/scene.rs b/crates/gpui/src/scene.rs index 2fd2b725fac45a..bc7f5d79eace55 100644 --- a/crates/gpui/src/scene.rs +++ b/crates/gpui/src/scene.rs @@ -534,7 +534,7 @@ pub struct Shadow { pub element_corner_radii: Corners, /// 0 = drop shadow (rendered outside the element), 1 = inset shadow (rendered inside). pub inset: u32, - pub _pad: u32, + pub pad: u32, // align to 8 bytes } impl From for Primitive { diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 8ce2a025e4ac1a..310734dcc6b64d 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -3482,7 +3482,7 @@ impl Window { element_bounds, element_corner_radii, inset: 0, - _pad: 0, + pad: 0, }); } } @@ -3527,7 +3527,7 @@ impl Window { element_bounds, element_corner_radii, inset: 1, - _pad: 0, + pad: 0, }); } } diff --git a/crates/gpui_wgpu/src/shaders.wgsl b/crates/gpui_wgpu/src/shaders.wgsl index 16ce59c1d100d1..c972d766821815 100644 --- a/crates/gpui_wgpu/src/shaders.wgsl +++ b/crates/gpui_wgpu/src/shaders.wgsl @@ -962,10 +962,7 @@ struct Shadow { element_corner_radii: Corners, // 0 = drop shadow, 1 = inset shadow. inset: u32, - // Trailing padding to keep this struct at 112 bytes, matching the Rust + Metal layout. - // Without it the WGSL struct would pad to 112 while the C/Rust struct would be 108, - // corrupting every instance after the first. - _pad: u32, + pad: u32, // align to 8 bytes } @group(1) @binding(0) var b_shadows: array; From 54b20568a85b427caf2fb717b365389aa98c5dd7 Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Tue, 26 May 2026 10:55:14 -0300 Subject: [PATCH 09/11] Update HLSL shader for inset shadows --- crates/gpui_windows/src/shaders.hlsl | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/crates/gpui_windows/src/shaders.hlsl b/crates/gpui_windows/src/shaders.hlsl index d40c7241bd0f23..1b525eb1089d28 100644 --- a/crates/gpui_windows/src/shaders.hlsl +++ b/crates/gpui_windows/src/shaders.hlsl @@ -854,6 +854,10 @@ struct Shadow { Corners corner_radii; Bounds content_mask; Hsla color; + Bounds element_bounds; + Corners element_corner_radii; + uint inset; + uint pad; // align to 8 bytes }; struct ShadowVertexOutput { @@ -875,10 +879,16 @@ ShadowVertexOutput shadow_vertex(uint vertex_id: SV_VertexID, uint shadow_id: SV float2 unit_vertex = float2(float(vertex_id & 1u), 0.5 * float(vertex_id & 2u)); Shadow shadow = shadows[shadow_id]; - float margin = 3.0 * shadow.blur_radius; - Bounds bounds = shadow.bounds; - bounds.origin -= margin; - bounds.size += 2.0 * margin; + Bounds bounds; + if (shadow.inset != 0u) { + bounds = shadow.element_bounds; + } else { + // Leave room for the gaussian tail outside the shadow rect. + float margin = 3.0 * shadow.blur_radius; + bounds = shadow.bounds; + bounds.origin -= margin; + bounds.size += 2.0 * margin; + } float4 device_position = to_device_position(unit_vertex, bounds); float4 clip_distance = distance_from_clip_rect(unit_vertex, bounds, shadow.content_mask); @@ -918,6 +928,15 @@ float4 shadow_fragment(ShadowFragmentInput input): SV_TARGET { y += step; } + if (shadow.inset != 0u) { + // The inset shadow is the complement of the (blurred) hole rect, clipped to the element. + // `saturate(0.5 - d)` gives a 1-pixel antialiased edge: d <= -0.5 -> 1, d >= 0.5 -> 0. + alpha = 1.0 - alpha; + float element_distance = quad_sdf(input.position.xy, shadow.element_bounds, + shadow.element_corner_radii); + alpha *= saturate(0.5 - element_distance); + } + return input.color * float4(1., 1., 1., alpha); } From b5b135f71c61bfe659d69c5c83d686012b66e633 Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Tue, 26 May 2026 11:07:48 -0300 Subject: [PATCH 10/11] Add blur_radius == 0 branch to wgsl and hlsl --- crates/gpui_wgpu/src/shaders.wgsl | 36 ++++++++++++++++------------ crates/gpui_windows/src/shaders.hlsl | 36 ++++++++++++++++------------ 2 files changed, 42 insertions(+), 30 deletions(-) diff --git a/crates/gpui_wgpu/src/shaders.wgsl b/crates/gpui_wgpu/src/shaders.wgsl index c972d766821815..933b88e84d79a9 100644 --- a/crates/gpui_wgpu/src/shaders.wgsl +++ b/crates/gpui_wgpu/src/shaders.wgsl @@ -1012,21 +1012,27 @@ fn fs_shadow(input: ShadowVarying) -> @location(0) vec4 { let corner_radius = pick_corner_radius(center_to_point, shadow.corner_radii); - // The signal is only non-zero in a limited range, so don't waste samples - let low = center_to_point.y - half_size.y; - let high = center_to_point.y + half_size.y; - let start = clamp(-3.0 * shadow.blur_radius, low, high); - let end = clamp(3.0 * shadow.blur_radius, low, high); - - // Accumulate samples (we can get away with surprisingly few samples) - let step = (end - start) / 4.0; - var y = start + step * 0.5; - var alpha = 0.0; - for (var i = 0; i < 4; i += 1) { - let blur = blur_along_x(center_to_point.x, center_to_point.y - y, - shadow.blur_radius, corner_radius, half_size); - alpha += blur * gaussian(y, shadow.blur_radius) * step; - y += step; + var alpha: f32; + if (shadow.blur_radius == 0.0) { + let distance = quad_sdf(input.position.xy, shadow.bounds, shadow.corner_radii); + alpha = saturate(0.5 - distance); + } else { + // The signal is only non-zero in a limited range, so don't waste samples + let low = center_to_point.y - half_size.y; + let high = center_to_point.y + half_size.y; + let start = clamp(-3.0 * shadow.blur_radius, low, high); + let end = clamp(3.0 * shadow.blur_radius, low, high); + + // Accumulate samples (we can get away with surprisingly few samples) + let step = (end - start) / 4.0; + var y = start + step * 0.5; + alpha = 0.0; + for (var i = 0; i < 4; i += 1) { + let blur = blur_along_x(center_to_point.x, center_to_point.y - y, + shadow.blur_radius, corner_radius, half_size); + alpha += blur * gaussian(y, shadow.blur_radius) * step; + y += step; + } } if (shadow.inset != 0u) { diff --git a/crates/gpui_windows/src/shaders.hlsl b/crates/gpui_windows/src/shaders.hlsl index 1b525eb1089d28..89c12489b24cf6 100644 --- a/crates/gpui_windows/src/shaders.hlsl +++ b/crates/gpui_windows/src/shaders.hlsl @@ -911,21 +911,27 @@ float4 shadow_fragment(ShadowFragmentInput input): SV_TARGET { float2 point0 = input.position.xy - center; float corner_radius = pick_corner_radius(point0, shadow.corner_radii); - // The signal is only non-zero in a limited range, so don't waste samples - float low = point0.y - half_size.y; - float high = point0.y + half_size.y; - float start = clamp(-3. * shadow.blur_radius, low, high); - float end = clamp(3. * shadow.blur_radius, low, high); - - // Accumulate samples (we can get away with surprisingly few samples) - float step = (end - start) / 4.; - float y = start + step * 0.5; - float alpha = 0.; - for (int i = 0; i < 4; i++) { - alpha += blur_along_x(point0.x, point0.y - y, shadow.blur_radius, - corner_radius, half_size) * - gaussian(y, shadow.blur_radius) * step; - y += step; + float alpha; + if (shadow.blur_radius == 0.) { + float distance = quad_sdf(input.position.xy, shadow.bounds, shadow.corner_radii); + alpha = saturate(0.5 - distance); + } else { + // The signal is only non-zero in a limited range, so don't waste samples + float low = point0.y - half_size.y; + float high = point0.y + half_size.y; + float start = clamp(-3. * shadow.blur_radius, low, high); + float end = clamp(3. * shadow.blur_radius, low, high); + + // Accumulate samples (we can get away with surprisingly few samples) + float step = (end - start) / 4.; + float y = start + step * 0.5; + alpha = 0.; + for (int i = 0; i < 4; i++) { + alpha += blur_along_x(point0.x, point0.y - y, shadow.blur_radius, + corner_radius, half_size) * + gaussian(y, shadow.blur_radius) * step; + y += step; + } } if (shadow.inset != 0u) { From f9ef8a08b9b1ef968bb36ccae2357d377f0699e1 Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Tue, 26 May 2026 11:21:38 -0300 Subject: [PATCH 11/11] Revert fixture change --- .../disable_cursor_blinking/before.rs | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/crates/agent/src/tools/evals/fixtures/disable_cursor_blinking/before.rs b/crates/agent/src/tools/evals/fixtures/disable_cursor_blinking/before.rs index dfd16a49c853e2..bdf160d8ffe2c6 100644 --- a/crates/agent/src/tools/evals/fixtures/disable_cursor_blinking/before.rs +++ b/crates/agent/src/tools/evals/fixtures/disable_cursor_blinking/before.rs @@ -7703,7 +7703,6 @@ impl Editor { offset: point(px(1.), px(1.)), blur_radius: px(2.), spread_radius: px(0.), - inset: false, }]) .bg(Editor::edit_prediction_line_popover_bg_color(cx)) .border(BORDER_WIDTH) @@ -7838,11 +7837,7 @@ impl Editor { h_flex() .px_0p5() .when(is_platform_style_mac, |parent| parent.gap_0p5()) - .font( - theme_settings::ThemeSettings::get_global(cx) - .buffer_font - .clone(), - ) + .font(theme_settings::ThemeSettings::get_global(cx).buffer_font.clone()) .text_size(TextSize::XSmall.rems(cx)) .child(h_flex().children(ui::render_modifiers( &accept_keystroke.modifiers, @@ -8154,11 +8149,7 @@ impl Editor { .px_2() .child( h_flex() - .font( - theme_settings::ThemeSettings::get_global(cx) - .buffer_font - .clone(), - ) + .font(theme_settings::ThemeSettings::get_global(cx).buffer_font.clone()) .when(is_platform_style_mac, |parent| parent.gap_1()) .child(h_flex().children(ui::render_modifiers( &accept_keystroke.modifiers, @@ -8267,11 +8258,7 @@ impl Editor { .gap_2() .pr_1() .overflow_x_hidden() - .font( - theme_settings::ThemeSettings::get_global(cx) - .buffer_font - .clone(), - ) + .font(theme_settings::ThemeSettings::get_global(cx).buffer_font.clone()) .child(left) .child(preview), )