gpui: Fix overflow_hidden to support clip with border radius (#35083)

Jason Lee created

Release Notes:

- N/A

---

Same case in HTML example:


https://developer.mozilla.org/en-US/play?id=p7FhB3JAhiVfLHAXnsbrn7JYYX%2Byq1gje%2B%2BTZarnXvvjmaAx3NlrXqMAoI35s4zeakShKee6lydHYeHr

```html
<div style="padding: 50px; text-align: center;">
  <div style="overflow: hidden; border-radius: 24px">
    <div style="background: #000; border: 3px solid red; color: #fff; padding: 8px 28px;">
      Let build applications with GPUI.
    </div>
    <div style="background: #333; border: 3px dashed black; color: #fff; padding: 8px 28px;">
      Let build applications with GPUI.
    </div>
  </div>

  <div style="margin-top: 20px; border-radius: 24px">
    <div style="background: #000; color: #fff; padding: 8px 28px;">
      This is not overflow: hidden.
    </div>
  </div>
</div>
```

<img width="610" height="213" alt="image"
src="https://github.com/user-attachments/assets/5f95e263-e52c-414f-8f0c-e6aa04ceb802"
/>

### Before

<img width="912" height="740" alt="image"
src="https://github.com/user-attachments/assets/f09c1936-52fc-4381-9a50-93977e9d64a6"
/>

### After 

```bash
cargo run -p gpui --example content_mask
```

<img width="912" height="740" alt="image"
src="https://github.com/user-attachments/assets/4bde58f3-c850-418d-9dc7-d2245852e7d7"
/> |


- [x] Metal
- [x] Blade
- [x] DirectX
- [x] ContentMask radius must reduce the container border widths.
- [x] The dash border render not correct, when not all side have
borders.

Change summary

crates/editor/src/element.rs                  |  25 +
crates/gpui/examples/content_mask.rs          | 228 +++++++++++++++++++++
crates/gpui/src/elements/list.rs              |  78 ++++--
crates/gpui/src/elements/uniform_list.rs      |   5 
crates/gpui/src/platform/blade/shaders.wgsl   |  60 +++-
crates/gpui/src/platform/mac/shaders.metal    |  24 +
crates/gpui/src/platform/windows/shaders.hlsl |  49 +++-
crates/gpui/src/style.rs                      |  66 +----
crates/gpui/src/window.rs                     |  23 ++
crates/terminal_view/src/terminal_element.rs  |   2 
crates/ui/src/components/scrollbar.rs         |  16 +
11 files changed, 446 insertions(+), 130 deletions(-)

Detailed changes

crates/editor/src/element.rs 🔗

@@ -6025,6 +6025,7 @@ impl EditorElement {
         window.with_content_mask(
             Some(ContentMask {
                 bounds: layout.position_map.text_hitbox.bounds,
+                ..Default::default()
             }),
             |window| {
                 let editor = self.editor.read(cx);
@@ -6967,9 +6968,15 @@ impl EditorElement {
             } else {
                 let mut bounds = layout.hitbox.bounds;
                 bounds.origin.x += layout.gutter_hitbox.bounds.size.width;
-                window.with_content_mask(Some(ContentMask { bounds }), |window| {
-                    block.element.paint(window, cx);
-                })
+                window.with_content_mask(
+                    Some(ContentMask {
+                        bounds,
+                        ..Default::default()
+                    }),
+                    |window| {
+                        block.element.paint(window, cx);
+                    },
+                )
             }
         }
     }
@@ -8270,9 +8277,13 @@ impl Element for EditorElement {
         }
 
         let rem_size = self.rem_size(cx);
+        let content_mask = ContentMask {
+            bounds,
+            ..Default::default()
+        };
         window.with_rem_size(rem_size, |window| {
             window.with_text_style(Some(text_style), |window| {
-                window.with_content_mask(Some(ContentMask { bounds }), |window| {
+                window.with_content_mask(Some(content_mask), |window| {
                     let (mut snapshot, is_read_only) = self.editor.update(cx, |editor, cx| {
                         (editor.snapshot(window, cx), editor.read_only(cx))
                     });
@@ -9380,9 +9391,13 @@ impl Element for EditorElement {
             ..Default::default()
         };
         let rem_size = self.rem_size(cx);
+        let content_mask = ContentMask {
+            bounds,
+            ..Default::default()
+        };
         window.with_rem_size(rem_size, |window| {
             window.with_text_style(Some(text_style), |window| {
-                window.with_content_mask(Some(ContentMask { bounds }), |window| {
+                window.with_content_mask(Some(content_mask), |window| {
                     self.paint_mouse_listeners(layout, window, cx);
                     self.paint_background(layout, window, cx);
                     self.paint_indent_guides(layout, window, cx);

crates/gpui/examples/content_mask.rs 🔗

@@ -0,0 +1,228 @@
+use gpui::{
+    App, Application, Bounds, Context, Window, WindowBounds, WindowOptions, div, prelude::*, px,
+    rgb, size,
+};
+
+struct Example {}
+
+impl Render for Example {
+    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
+        div()
+            .font_family(".SystemUIFont")
+            .flex()
+            .flex_col()
+            .size_full()
+            .p_4()
+            .gap_4()
+            .bg(rgb(0x505050))
+            .justify_center()
+            .items_center()
+            .text_center()
+            .shadow_lg()
+            .text_sm()
+            .text_color(rgb(0xffffff))
+            .child(
+                div()
+                    .overflow_hidden()
+                    .rounded(px(32.))
+                    .border(px(8.))
+                    .border_color(gpui::white())
+                    .text_color(gpui::white())
+                    .child(
+                        div()
+                            .bg(gpui::black())
+                            .py_2()
+                            .px_7()
+                            .border_l_2()
+                            .border_r_2()
+                            .border_b_3()
+                            .border_color(gpui::red())
+                            .child("Let build applications with GPUI"),
+                    )
+                    .child(
+                        div()
+                            .bg(rgb(0x222222))
+                            .text_sm()
+                            .py_1()
+                            .px_7()
+                            .border_l_3()
+                            .border_r_3()
+                            .border_color(gpui::green())
+                            .child("The fast, productive UI framework for Rust"),
+                    )
+                    .child(
+                        div()
+                            .bg(rgb(0x222222))
+                            .w_full()
+                            .flex()
+                            .flex_row()
+                            .text_sm()
+                            .text_color(rgb(0xc0c0c0))
+                            .child(
+                                div()
+                                    .flex_1()
+                                    .p_2()
+                                    .border_3()
+                                    .border_dashed()
+                                    .border_color(gpui::blue())
+                                    .child("Rust"),
+                            )
+                            .child(
+                                div()
+                                    .flex_1()
+                                    .p_2()
+                                    .border_t_3()
+                                    .border_r_3()
+                                    .border_b_3()
+                                    .border_dashed()
+                                    .border_color(gpui::blue())
+                                    .child("GPU Rendering"),
+                            ),
+                    ),
+            )
+            .child(
+                div()
+                    .flex()
+                    .flex_col()
+                    .w(px(320.))
+                    .gap_1()
+                    .overflow_hidden()
+                    .rounded(px(16.))
+                    .child(
+                        div()
+                            .w_full()
+                            .p_2()
+                            .bg(gpui::red())
+                            .child("Clip background"),
+                    ),
+            )
+            .child(
+                div()
+                    .flex()
+                    .flex_col()
+                    .w(px(320.))
+                    .gap_1()
+                    .rounded(px(16.))
+                    .child(
+                        div()
+                            .w_full()
+                            .p_2()
+                            .bg(gpui::yellow())
+                            .text_color(gpui::black())
+                            .child("No content mask"),
+                    ),
+            )
+            .child(
+                div()
+                    .flex()
+                    .flex_col()
+                    .w(px(320.))
+                    .gap_1()
+                    .overflow_hidden()
+                    .rounded(px(16.))
+                    .child(
+                        div()
+                            .w_full()
+                            .p_2()
+                            .border_4()
+                            .border_color(gpui::blue())
+                            .bg(gpui::blue().alpha(0.4))
+                            .child("Clip borders"),
+                    ),
+            )
+            .child(
+                div()
+                    .flex()
+                    .flex_col()
+                    .w(px(320.))
+                    .gap_1()
+                    .overflow_hidden()
+                    .rounded(px(20.))
+                    .child(
+                        div().w_full().border_2().border_color(gpui::black()).child(
+                            div()
+                                .size_full()
+                                .bg(gpui::green().alpha(0.4))
+                                .p_2()
+                                .border_8()
+                                .border_color(gpui::green())
+                                .child("Clip nested elements"),
+                        ),
+                    ),
+            )
+            .child(
+                div()
+                    .flex()
+                    .flex_col()
+                    .w(px(320.))
+                    .gap_1()
+                    .overflow_hidden()
+                    .rounded(px(32.))
+                    .child(
+                        div()
+                            .w_full()
+                            .p_2()
+                            .bg(gpui::black())
+                            .border_2()
+                            .border_dashed()
+                            .rounded_lg()
+                            .border_color(gpui::white())
+                            .child("dash border full and rounded"),
+                    )
+                    .child(
+                        div()
+                            .w_full()
+                            .flex()
+                            .flex_row()
+                            .gap_2()
+                            .child(
+                                div()
+                                    .w_full()
+                                    .p_2()
+                                    .bg(gpui::black())
+                                    .border_x_2()
+                                    .border_dashed()
+                                    .rounded_lg()
+                                    .border_color(gpui::white())
+                                    .child("border x"),
+                            )
+                            .child(
+                                div()
+                                    .w_full()
+                                    .p_2()
+                                    .bg(gpui::black())
+                                    .border_y_2()
+                                    .border_dashed()
+                                    .rounded_lg()
+                                    .border_color(gpui::white())
+                                    .child("border y"),
+                            ),
+                    )
+                    .child(
+                        div()
+                            .w_full()
+                            .p_2()
+                            .bg(gpui::black())
+                            .border_2()
+                            .border_dashed()
+                            .border_color(gpui::white())
+                            .child("border full and no rounded"),
+                    ),
+            )
+    }
+}
+
+fn main() {
+    Application::new().run(|cx: &mut App| {
+        let bounds = Bounds::centered(None, size(px(800.), px(600.)), cx);
+        cx.open_window(
+            WindowOptions {
+                window_bounds: Some(WindowBounds::Windowed(bounds)),
+                ..Default::default()
+            },
+            |_, cx| cx.new(|_| Example {}),
+        )
+        .unwrap();
+        cx.activate(true);
+    });
+}

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

@@ -8,10 +8,10 @@
 //! If all of your elements are the same height, see [`crate::UniformList`] for a simpler API
 
 use crate::{
-    AnyElement, App, AvailableSpace, Bounds, ContentMask, DispatchPhase, Edges, Element, EntityId,
-    FocusHandle, GlobalElementId, Hitbox, HitboxBehavior, InspectorElementId, IntoElement,
-    Overflow, Pixels, Point, ScrollDelta, ScrollWheelEvent, Size, Style, StyleRefinement, Styled,
-    Window, point, px, size,
+    AnyElement, App, AvailableSpace, Bounds, ContentMask, Corners, DispatchPhase, Edges, Element,
+    EntityId, FocusHandle, GlobalElementId, Hitbox, HitboxBehavior, InspectorElementId,
+    IntoElement, Overflow, Pixels, Point, ScrollDelta, ScrollWheelEvent, Size, Style,
+    StyleRefinement, Styled, Window, point, px, size,
 };
 use collections::VecDeque;
 use refineable::Refineable as _;
@@ -705,6 +705,7 @@ impl StateInner {
         &mut self,
         bounds: Bounds<Pixels>,
         padding: Edges<Pixels>,
+        corner_radii: Corners<Pixels>,
         autoscroll: bool,
         render_item: &mut RenderItemFn,
         window: &mut Window,
@@ -728,9 +729,15 @@ impl StateInner {
                 let mut item_origin = bounds.origin + Point::new(px(0.), padding.top);
                 item_origin.y -= layout_response.scroll_top.offset_in_item;
                 for item in &mut layout_response.item_layouts {
-                    window.with_content_mask(Some(ContentMask { bounds }), |window| {
-                        item.element.prepaint_at(item_origin, window, cx);
-                    });
+                    window.with_content_mask(
+                        Some(ContentMask {
+                            bounds,
+                            corner_radii,
+                        }),
+                        |window| {
+                            item.element.prepaint_at(item_origin, window, cx);
+                        },
+                    );
 
                     if let Some(autoscroll_bounds) = window.take_autoscroll()
                         && autoscroll
@@ -952,19 +959,34 @@ impl Element for List {
             state.items = new_items;
         }
 
-        let padding = style
-            .padding
-            .to_pixels(bounds.size.into(), window.rem_size());
-        let layout =
-            match state.prepaint_items(bounds, padding, true, &mut self.render_item, window, cx) {
-                Ok(layout) => layout,
-                Err(autoscroll_request) => {
-                    state.logical_scroll_top = Some(autoscroll_request);
-                    state
-                        .prepaint_items(bounds, padding, false, &mut self.render_item, window, cx)
-                        .unwrap()
-                }
-            };
+        let rem_size = window.rem_size();
+        let padding = style.padding.to_pixels(bounds.size.into(), rem_size);
+        let corner_radii = style.corner_radii.to_pixels(rem_size);
+        let layout = match state.prepaint_items(
+            bounds,
+            padding,
+            corner_radii,
+            true,
+            &mut self.render_item,
+            window,
+            cx,
+        ) {
+            Ok(layout) => layout,
+            Err(autoscroll_request) => {
+                state.logical_scroll_top = Some(autoscroll_request);
+                state
+                    .prepaint_items(
+                        bounds,
+                        padding,
+                        corner_radii,
+                        false,
+                        &mut self.render_item,
+                        window,
+                        cx,
+                    )
+                    .unwrap()
+            }
+        };
 
         state.last_layout_bounds = Some(bounds);
         state.last_padding = Some(padding);
@@ -982,11 +1004,17 @@ impl Element for List {
         cx: &mut App,
     ) {
         let current_view = window.current_view();
-        window.with_content_mask(Some(ContentMask { bounds }), |window| {
-            for item in &mut prepaint.layout.item_layouts {
-                item.element.paint(window, cx);
-            }
-        });
+        window.with_content_mask(
+            Some(ContentMask {
+                bounds,
+                ..Default::default()
+            }),
+            |window| {
+                for item in &mut prepaint.layout.item_layouts {
+                    item.element.paint(window, cx);
+                }
+            },
+        );
 
         let list_state = self.state.clone();
         let height = bounds.size.height;

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

@@ -411,7 +411,10 @@ impl Element for UniformList {
                         (self.render_items)(visible_range.clone(), window, cx)
                     };
 
-                    let content_mask = ContentMask { bounds };
+                    let content_mask = ContentMask {
+                        bounds,
+                        ..Default::default()
+                    };
                     window.with_content_mask(Some(content_mask), |window| {
                         for (mut item, ix) in items.into_iter().zip(visible_range.clone()) {
                             let item_origin = padded_bounds.origin

crates/gpui/src/platform/blade/shaders.wgsl 🔗

@@ -53,6 +53,11 @@ struct Corners {
     bottom_left: f32,
 }
 
+struct ContentMask {
+    bounds: Bounds,
+    corner_radii: Corners,
+}
+
 struct Edges {
     top: f32,
     right: f32,
@@ -440,7 +445,7 @@ struct Quad {
     order: u32,
     border_style: u32,
     bounds: Bounds,
-    content_mask: Bounds,
+    content_mask: ContentMask,
     background: Background,
     border_color: Hsla,
     corner_radii: Corners,
@@ -478,7 +483,7 @@ fn vs_quad(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) insta
     out.background_color1 = gradient.color1;
     out.border_color = hsla_to_rgba(quad.border_color);
     out.quad_id = instance_id;
-    out.clip_distances = distance_from_clip_rect(unit_vertex, quad.bounds, quad.content_mask);
+    out.clip_distances = distance_from_clip_rect(unit_vertex, quad.bounds, quad.content_mask.bounds);
     return out;
 }
 
@@ -491,8 +496,19 @@ fn fs_quad(input: QuadVarying) -> @location(0) vec4<f32> {
 
     let quad = b_quads[input.quad_id];
 
-    let background_color = gradient_color(quad.background, input.position.xy, quad.bounds,
+    // Signed distance field threshold for inclusion of pixels. 0.5 is the
+    // minimum distance between the center of the pixel and the edge.
+    let antialias_threshold = 0.5;
+
+    var background_color = gradient_color(quad.background, input.position.xy, quad.bounds,
         input.background_solid, input.background_color0, input.background_color1);
+    var border_color = input.border_color;
+
+    // Apply content_mask corner radii clipping
+    let clip_sdf = quad_sdf(input.position.xy, quad.content_mask.bounds, quad.content_mask.corner_radii);
+    let clip_alpha = saturate(antialias_threshold - clip_sdf);
+    background_color.a *= clip_alpha;
+    border_color.a *= clip_alpha;
 
     let unrounded = quad.corner_radii.top_left == 0.0 &&
         quad.corner_radii.bottom_left == 0.0 &&
@@ -513,10 +529,6 @@ fn fs_quad(input: QuadVarying) -> @location(0) vec4<f32> {
     let point = input.position.xy - quad.bounds.origin;
     let center_to_point = point - half_size;
 
-    // Signed distance field threshold for inclusion of pixels. 0.5 is the
-    // minimum distance between the center of the pixel and the edge.
-    let antialias_threshold = 0.5;
-
     // Radius of the nearest corner
     let corner_radius = pick_corner_radius(center_to_point, quad.corner_radii);
 
@@ -607,8 +619,6 @@ fn fs_quad(input: QuadVarying) -> @location(0) vec4<f32> {
 
     var color = background_color;
     if (border_sdf < antialias_threshold) {
-        var border_color = input.border_color;
-
         // Dashed border logic when border_style == 1
         if (quad.border_style == 1) {
             // Position along the perimeter in "dash space", where each dash
@@ -644,7 +654,11 @@ fn fs_quad(input: QuadVarying) -> @location(0) vec4<f32> {
                 let is_horizontal =
                         corner_center_to_point.x <
                         corner_center_to_point.y;
-                let border_width = select(border.y, border.x, is_horizontal);
+                var border_width = select(border.y, border.x, is_horizontal);
+                // When border width of some side is 0, we need to use the other side width for dash velocity.
+                if (border_width == 0.0) {
+                    border_width = select(border.x, border.y, is_horizontal);
+                }
                 dash_velocity = dv_numerator / border_width;
                 t = select(point.y, point.x, is_horizontal) * dash_velocity;
                 max_t = select(size.y, size.x, is_horizontal) * dash_velocity;
@@ -856,7 +870,7 @@ struct Shadow {
     blur_radius: f32,
     bounds: Bounds,
     corner_radii: Corners,
-    content_mask: Bounds,
+    content_mask: ContentMask,
     color: Hsla,
 }
 var<storage, read> b_shadows: array<Shadow>;
@@ -884,7 +898,7 @@ fn vs_shadow(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) ins
     out.position = to_device_position(unit_vertex, shadow.bounds);
     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, shadow.bounds, shadow.content_mask.bounds);
     return out;
 }
 
@@ -899,7 +913,6 @@ fn fs_shadow(input: ShadowVarying) -> @location(0) vec4<f32> {
     let half_size = shadow.bounds.size / 2.0;
     let center = shadow.bounds.origin + half_size;
     let center_to_point = input.position.xy - center;
-
     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
@@ -1027,7 +1040,7 @@ struct Underline {
     order: u32,
     pad: u32,
     bounds: Bounds,
-    content_mask: Bounds,
+    content_mask: ContentMask,
     color: Hsla,
     thickness: f32,
     wavy: u32,
@@ -1051,7 +1064,7 @@ fn vs_underline(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index)
     out.position = to_device_position(unit_vertex, underline.bounds);
     out.color = hsla_to_rgba(underline.color);
     out.underline_id = instance_id;
-    out.clip_distances = distance_from_clip_rect(unit_vertex, underline.bounds, underline.content_mask);
+    out.clip_distances = distance_from_clip_rect(unit_vertex, underline.bounds, underline.content_mask.bounds);
     return out;
 }
 
@@ -1093,7 +1106,7 @@ struct MonochromeSprite {
     order: u32,
     pad: u32,
     bounds: Bounds,
-    content_mask: Bounds,
+    content_mask: ContentMask,
     color: Hsla,
     tile: AtlasTile,
     transformation: TransformationMatrix,
@@ -1117,7 +1130,7 @@ fn vs_mono_sprite(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index
 
     out.tile_position = to_tile_position(unit_vertex, sprite.tile);
     out.color = hsla_to_rgba(sprite.color);
-    out.clip_distances = distance_from_clip_rect(unit_vertex, sprite.bounds, sprite.content_mask);
+    out.clip_distances = distance_from_clip_rect(unit_vertex, sprite.bounds, sprite.content_mask.bounds);
     return out;
 }
 
@@ -1139,7 +1152,7 @@ struct PolychromeSprite {
     grayscale: u32,
     opacity: f32,
     bounds: Bounds,
-    content_mask: Bounds,
+    content_mask: ContentMask,
     corner_radii: Corners,
     tile: AtlasTile,
 }
@@ -1161,7 +1174,7 @@ fn vs_poly_sprite(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index
     out.position = to_device_position(unit_vertex, sprite.bounds);
     out.tile_position = to_tile_position(unit_vertex, sprite.tile);
     out.sprite_id = instance_id;
-    out.clip_distances = distance_from_clip_rect(unit_vertex, sprite.bounds, sprite.content_mask);
+    out.clip_distances = distance_from_clip_rect(unit_vertex, sprite.bounds, sprite.content_mask.bounds);
     return out;
 }
 
@@ -1234,3 +1247,12 @@ fn fs_surface(input: SurfaceVarying) -> @location(0) vec4<f32> {
 
     return ycbcr_to_RGB * y_cb_cr;
 }
+
+fn max_corner_radii(a: Corners, b: Corners) -> Corners {
+    return Corners(
+        max(a.top_left, b.top_left),
+        max(a.top_right, b.top_right),
+        max(a.bottom_right, b.bottom_right),
+        max(a.bottom_left, b.bottom_left)
+    );
+}

crates/gpui/src/platform/mac/shaders.metal 🔗

@@ -99,8 +99,21 @@ fragment float4 quad_fragment(QuadFragmentInput input [[stage_in]],
                               constant Quad *quads
                               [[buffer(QuadInputIndex_Quads)]]) {
   Quad quad = quads[input.quad_id];
+
+  // Signed distance field threshold for inclusion of pixels. 0.5 is the
+  // minimum distance between the center of the pixel and the edge.
+  const float antialias_threshold = 0.5;
+
   float4 background_color = fill_color(quad.background, input.position.xy, quad.bounds,
     input.background_solid, input.background_color0, input.background_color1);
+  float4 border_color = input.border_color;
+
+  // Apply content_mask corner radii clipping
+  float clip_sdf = quad_sdf(input.position.xy, quad.content_mask.bounds,
+    quad.content_mask.corner_radii);
+  float clip_alpha = saturate(antialias_threshold - clip_sdf);
+  background_color.a *= clip_alpha;
+  border_color *= clip_alpha;
 
   bool unrounded = quad.corner_radii.top_left == 0.0 &&
     quad.corner_radii.bottom_left == 0.0 &&
@@ -121,10 +134,6 @@ fragment float4 quad_fragment(QuadFragmentInput input [[stage_in]],
   float2 point = input.position.xy - float2(quad.bounds.origin.x, quad.bounds.origin.y);
   float2 center_to_point = point - half_size;
 
-  // Signed distance field threshold for inclusion of pixels. 0.5 is the
-  // minimum distance between the center of the pixel and the edge.
-  const float antialias_threshold = 0.5;
-
   // Radius of the nearest corner
   float corner_radius = pick_corner_radius(center_to_point, quad.corner_radii);
 
@@ -164,7 +173,6 @@ fragment float4 quad_fragment(QuadFragmentInput input [[stage_in]],
     straight_border_inner_corner_to_point.x > 0.0 ||
     straight_border_inner_corner_to_point.y > 0.0;
 
-
   // Whether the point is far enough inside the quad, such that the pixels are
   // not affected by the straight border.
   bool is_within_inner_straight_border =
@@ -208,8 +216,6 @@ fragment float4 quad_fragment(QuadFragmentInput input [[stage_in]],
 
   float4 color = background_color;
   if (border_sdf < antialias_threshold) {
-    float4 border_color = input.border_color;
-
     // Dashed border logic when border_style == 1
     if (quad.border_style == 1) {
       // Position along the perimeter in "dash space", where each dash
@@ -244,6 +250,10 @@ fragment float4 quad_fragment(QuadFragmentInput input [[stage_in]],
         // perimeter. This way each line starts and ends with a dash.
         bool is_horizontal = corner_center_to_point.x < corner_center_to_point.y;
         float border_width = is_horizontal ? border.x : border.y;
+        // When border width of some side is 0, we need to use the other side width for dash velocity.
+        if (border_width == 0.0) {
+            border_width = is_horizontal ? border.y : border.x;
+        }
         dash_velocity = dv_numerator / border_width;
         t = is_horizontal ? point.x : point.y;
         t *= dash_velocity;

crates/gpui/src/platform/windows/shaders.hlsl 🔗

@@ -453,11 +453,16 @@ float quarter_ellipse_sdf(float2 pt, float2 radii) {
 **
 */
 
+struct ContentMask {
+    Bounds bounds;
+    Corners corner_radii;
+};
+
 struct Quad {
     uint order;
     uint border_style;
     Bounds bounds;
-    Bounds content_mask;
+    ContentMask content_mask;
     Background background;
     Hsla border_color;
     Corners corner_radii;
@@ -496,7 +501,7 @@ QuadVertexOutput quad_vertex(uint vertex_id: SV_VertexID, uint quad_id: SV_Insta
         quad.background.solid,
         quad.background.colors
     );
-    float4 clip_distance = distance_from_clip_rect(unit_vertex, quad.bounds, quad.content_mask);
+    float4 clip_distance = distance_from_clip_rect(unit_vertex, quad.bounds, quad.content_mask.bounds);
     float4 border_color = hsla_to_rgba(quad.border_color);
 
     QuadVertexOutput output;
@@ -512,8 +517,21 @@ QuadVertexOutput quad_vertex(uint vertex_id: SV_VertexID, uint quad_id: SV_Insta
 
 float4 quad_fragment(QuadFragmentInput input): SV_Target {
     Quad quad = quads[input.quad_id];
+
+    // Signed distance field threshold for inclusion of pixels. 0.5 is the
+    // minimum distance between the center of the pixel and the edge.
+    const float antialias_threshold = 0.5;
+
     float4 background_color = gradient_color(quad.background, input.position.xy, quad.bounds,
-    input.background_solid, input.background_color0, input.background_color1);
+        input.background_solid, input.background_color0, input.background_color1);
+    float4 border_color = input.border_color;
+
+    // Apply content_mask corner radii clipping
+    float clip_sdf = quad_sdf(input.position.xy, quad.content_mask.bounds,
+        quad.content_mask.corner_radii);
+    float clip_alpha = saturate(antialias_threshold - clip_sdf);
+    background_color.a *= clip_alpha;
+    border_color *= clip_alpha;
 
     bool unrounded = quad.corner_radii.top_left == 0.0 &&
         quad.corner_radii.top_right == 0.0 &&
@@ -534,10 +552,6 @@ float4 quad_fragment(QuadFragmentInput input): SV_Target {
     float2 the_point = input.position.xy - quad.bounds.origin;
     float2 center_to_point = the_point - half_size;
 
-    // Signed distance field threshold for inclusion of pixels. 0.5 is the
-    // minimum distance between the center of the pixel and the edge.
-    const float antialias_threshold = 0.5;
-
     // Radius of the nearest corner
     float corner_radius = pick_corner_radius(center_to_point, quad.corner_radii);
 
@@ -620,7 +634,6 @@ float4 quad_fragment(QuadFragmentInput input): SV_Target {
 
     float4 color = background_color;
     if (border_sdf < antialias_threshold) {
-        float4 border_color = input.border_color;
         // Dashed border logic when border_style == 1
         if (quad.border_style == 1) {
             // Position along the perimeter in "dash space", where each dash
@@ -655,6 +668,10 @@ float4 quad_fragment(QuadFragmentInput input): SV_Target {
                 // perimeter. This way each line starts and ends with a dash.
                 bool is_horizontal = corner_center_to_point.x < corner_center_to_point.y;
                 float border_width = is_horizontal ? border.x : border.y;
+                // When border width of some side is 0, we need to use the other side width for dash velocity.
+                if (border_width == 0.0) {
+                    border_width = is_horizontal ? border.y : border.x;
+                }
                 dash_velocity = dv_numerator / border_width;
                 t = is_horizontal ? the_point.x : the_point.y;
                 t *= dash_velocity;
@@ -805,7 +822,7 @@ struct Shadow {
     float blur_radius;
     Bounds bounds;
     Corners corner_radii;
-    Bounds content_mask;
+    ContentMask content_mask;
     Hsla color;
 };
 
@@ -834,7 +851,7 @@ ShadowVertexOutput shadow_vertex(uint vertex_id: SV_VertexID, uint shadow_id: SV
     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);
+    float4 clip_distance = distance_from_clip_rect(unit_vertex, bounds, shadow.content_mask.bounds);
     float4 color = hsla_to_rgba(shadow.color);
 
     ShadowVertexOutput output;
@@ -987,7 +1004,7 @@ struct Underline {
     uint order;
     uint pad;
     Bounds bounds;
-    Bounds content_mask;
+    ContentMask content_mask;
     Hsla color;
     float thickness;
     uint wavy;
@@ -1013,7 +1030,7 @@ UnderlineVertexOutput underline_vertex(uint vertex_id: SV_VertexID, uint underli
     Underline underline = underlines[underline_id];
     float4 device_position = to_device_position(unit_vertex, underline.bounds);
     float4 clip_distance = distance_from_clip_rect(unit_vertex, underline.bounds,
-                                                    underline.content_mask);
+                                                    underline.content_mask.bounds);
     float4 color = hsla_to_rgba(underline.color);
 
     UnderlineVertexOutput output;
@@ -1061,7 +1078,7 @@ struct MonochromeSprite {
     uint order;
     uint pad;
     Bounds bounds;
-    Bounds content_mask;
+    ContentMask content_mask;
     Hsla color;
     AtlasTile tile;
     TransformationMatrix transformation;
@@ -1088,7 +1105,7 @@ MonochromeSpriteVertexOutput monochrome_sprite_vertex(uint vertex_id: SV_VertexI
     MonochromeSprite sprite = mono_sprites[sprite_id];
     float4 device_position =
         to_device_position_transformed(unit_vertex, sprite.bounds, sprite.transformation);
-    float4 clip_distance = distance_from_clip_rect(unit_vertex, sprite.bounds, sprite.content_mask);
+    float4 clip_distance = distance_from_clip_rect(unit_vertex, sprite.bounds, sprite.content_mask.bounds);
     float2 tile_position = to_tile_position(unit_vertex, sprite.tile);
     float4 color = hsla_to_rgba(sprite.color);
 
@@ -1118,7 +1135,7 @@ struct PolychromeSprite {
     uint grayscale;
     float opacity;
     Bounds bounds;
-    Bounds content_mask;
+    ContentMask content_mask;
     Corners corner_radii;
     AtlasTile tile;
 };
@@ -1143,7 +1160,7 @@ PolychromeSpriteVertexOutput polychrome_sprite_vertex(uint vertex_id: SV_VertexI
     PolychromeSprite sprite = poly_sprites[sprite_id];
     float4 device_position = to_device_position(unit_vertex, sprite.bounds);
     float4 clip_distance = distance_from_clip_rect(unit_vertex, sprite.bounds,
-                                                    sprite.content_mask);
+                                                    sprite.content_mask.bounds);
     float2 tile_position = to_tile_position(unit_vertex, sprite.tile);
 
     PolychromeSpriteVertexOutput output;

crates/gpui/src/style.rs 🔗

@@ -601,7 +601,19 @@ impl Style {
                     (false, false) => Bounds::from_corners(min, max),
                 };
 
-                Some(ContentMask { bounds })
+                let corner_radii = self.corner_radii.to_pixels(rem_size);
+                let border_widths = self.border_widths.to_pixels(rem_size);
+                Some(ContentMask {
+                    bounds: Bounds {
+                        origin: bounds.origin - point(border_widths.left, border_widths.top),
+                        size: bounds.size
+                            + size(
+                                border_widths.left + border_widths.right,
+                                border_widths.top + border_widths.bottom,
+                            ),
+                    },
+                    corner_radii,
+                })
             }
         }
     }
@@ -661,64 +673,16 @@ impl Style {
 
         if self.is_border_visible() {
             let border_widths = self.border_widths.to_pixels(rem_size);
-            let max_border_width = border_widths.max();
-            let max_corner_radius = corner_radii.max();
-
-            let top_bounds = Bounds::from_corners(
-                bounds.origin,
-                bounds.top_right() + point(Pixels::ZERO, max_border_width.max(max_corner_radius)),
-            );
-            let bottom_bounds = Bounds::from_corners(
-                bounds.bottom_left() - point(Pixels::ZERO, max_border_width.max(max_corner_radius)),
-                bounds.bottom_right(),
-            );
-            let left_bounds = Bounds::from_corners(
-                top_bounds.bottom_left(),
-                bottom_bounds.origin + point(max_border_width, Pixels::ZERO),
-            );
-            let right_bounds = Bounds::from_corners(
-                top_bounds.bottom_right() - point(max_border_width, Pixels::ZERO),
-                bottom_bounds.top_right(),
-            );
-
             let mut background = self.border_color.unwrap_or_default();
             background.a = 0.;
-            let quad = quad(
+            window.paint_quad(quad(
                 bounds,
                 corner_radii,
                 background,
                 border_widths,
                 self.border_color.unwrap_or_default(),
                 self.border_style,
-            );
-
-            window.with_content_mask(Some(ContentMask { bounds: top_bounds }), |window| {
-                window.paint_quad(quad.clone());
-            });
-            window.with_content_mask(
-                Some(ContentMask {
-                    bounds: right_bounds,
-                }),
-                |window| {
-                    window.paint_quad(quad.clone());
-                },
-            );
-            window.with_content_mask(
-                Some(ContentMask {
-                    bounds: bottom_bounds,
-                }),
-                |window| {
-                    window.paint_quad(quad.clone());
-                },
-            );
-            window.with_content_mask(
-                Some(ContentMask {
-                    bounds: left_bounds,
-                }),
-                |window| {
-                    window.paint_quad(quad);
-                },
-            );
+            ));
         }
 
         #[cfg(debug_assertions)]

crates/gpui/src/window.rs 🔗

@@ -1283,6 +1283,8 @@ pub(crate) struct DispatchEventResult {
 pub struct ContentMask<P: Clone + Debug + Default + PartialEq> {
     /// The bounds
     pub bounds: Bounds<P>,
+    /// The corner radii of the content mask.
+    pub corner_radii: Corners<P>,
 }
 
 impl ContentMask<Pixels> {
@@ -1290,13 +1292,31 @@ impl ContentMask<Pixels> {
     pub fn scale(&self, factor: f32) -> ContentMask<ScaledPixels> {
         ContentMask {
             bounds: self.bounds.scale(factor),
+            corner_radii: self.corner_radii.scale(factor),
         }
     }
 
     /// Intersect the content mask with the given content mask.
     pub fn intersect(&self, other: &Self) -> Self {
         let bounds = self.bounds.intersect(&other.bounds);
-        ContentMask { bounds }
+        ContentMask {
+            bounds,
+            corner_radii: Corners {
+                top_left: self.corner_radii.top_left.max(other.corner_radii.top_left),
+                top_right: self
+                    .corner_radii
+                    .top_right
+                    .max(other.corner_radii.top_right),
+                bottom_right: self
+                    .corner_radii
+                    .bottom_right
+                    .max(other.corner_radii.bottom_right),
+                bottom_left: self
+                    .corner_radii
+                    .bottom_left
+                    .max(other.corner_radii.bottom_left),
+            },
+        }
     }
 }
 
@@ -2557,6 +2577,7 @@ impl Window {
                     origin: Point::default(),
                     size: self.viewport_size,
                 },
+                ..Default::default()
             })
     }
 

crates/terminal_view/src/terminal_element.rs 🔗

@@ -1184,7 +1184,7 @@ impl Element for TerminalElement {
         cx: &mut App,
     ) {
         let paint_start = Instant::now();
-        window.with_content_mask(Some(ContentMask { bounds }), |window| {
+        window.with_content_mask(Some(ContentMask { bounds, ..Default::default() }), |window| {
             let scroll_top = self.terminal_view.read(cx).scroll_top;
 
             window.paint_quad(fill(bounds, layout.background_color));

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

@@ -303,9 +303,13 @@ impl Element for Scrollbar {
         window: &mut Window,
         _: &mut App,
     ) -> Self::PrepaintState {
-        window.with_content_mask(Some(ContentMask { bounds }), |window| {
-            window.insert_hitbox(bounds, HitboxBehavior::Normal)
-        })
+        window.with_content_mask(
+            Some(ContentMask {
+                bounds,
+                ..Default::default()
+            }),
+            |window| window.insert_hitbox(bounds, HitboxBehavior::Normal),
+        )
     }
 
     fn paint(
@@ -319,7 +323,11 @@ impl Element for Scrollbar {
         cx: &mut App,
     ) {
         const EXTRA_PADDING: Pixels = px(5.0);
-        window.with_content_mask(Some(ContentMask { bounds }), |window| {
+        let content_mask = ContentMask {
+            bounds,
+            ..Default::default()
+        };
+        window.with_content_mask(Some(content_mask), |window| {
             let axis = self.kind;
             let colors = cx.theme().colors();
             let thumb_state = self.state.thumb_state.get();