Allow passing a corner radius and borders to rendered images

Antonio Scandurra and Nathan Sobo created

Co-Authored-By: Nathan Sobo <nathan@zed.dev>

Change summary

gpui/src/color.rs                           |   4 
gpui/src/elements/image.rs                  |  30 +++++-
gpui/src/platform/mac/renderer.rs           |   8 +
gpui/src/platform/mac/shaders/shaders.h     |   6 +
gpui/src/platform/mac/shaders/shaders.metal | 101 ++++++++++++----------
gpui/src/scene.rs                           |   2 
6 files changed, 101 insertions(+), 50 deletions(-)

Detailed changes

gpui/src/color.rs 🔗

@@ -29,6 +29,10 @@ impl Color {
         Self(ColorU::white())
     }
 
+    pub fn red() -> Self {
+        Self(ColorU::from_u32(0xff0000ff))
+    }
+
     pub fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
         Self(ColorU::new(r, g, b, a))
     }

gpui/src/elements/image.rs 🔗

@@ -1,16 +1,34 @@
 use crate::{
     geometry::{rect::RectF, vector::Vector2F},
     json::{json, ToJson},
-    scene, DebugContext, Element, Event, EventContext, ImageData, LayoutContext, PaintContext,
-    SizeConstraint,
+    scene, Border, DebugContext, Element, Event, EventContext, ImageData, LayoutContext,
+    PaintContext, SizeConstraint,
 };
 use std::sync::Arc;
 
-pub struct Image(Arc<ImageData>);
+pub struct Image {
+    data: Arc<ImageData>,
+    border: Border,
+    corner_radius: f32,
+}
 
 impl Image {
     pub fn new(data: Arc<ImageData>) -> Self {
-        Self(data)
+        Self {
+            data,
+            border: Default::default(),
+            corner_radius: Default::default(),
+        }
+    }
+
+    pub fn with_corner_radius(mut self, corner_radius: f32) -> Self {
+        self.corner_radius = corner_radius;
+        self
+    }
+
+    pub fn with_border(mut self, border: Border) -> Self {
+        self.border = border;
+        self
     }
 }
 
@@ -35,7 +53,9 @@ impl Element for Image {
     ) -> Self::PaintState {
         cx.scene.push_image(scene::Image {
             bounds,
-            data: self.0.clone(),
+            border: self.border,
+            corner_radius: self.corner_radius,
+            data: self.data.clone(),
         });
     }
 

gpui/src/platform/mac/renderer.rs 🔗

@@ -657,6 +657,8 @@ impl Renderer {
         for image in images {
             let origin = image.bounds.origin() * scale_factor;
             let target_size = image.bounds.size() * scale_factor;
+            let corner_radius = image.corner_radius * scale_factor;
+            let border_width = image.border.width * scale_factor;
             let (alloc_id, atlas_bounds) = self
                 .prev_rendered_images
                 .remove(&image.data.id)
@@ -675,6 +677,12 @@ impl Renderer {
                     target_size: target_size.to_float2(),
                     source_size: atlas_bounds.size().to_float2(),
                     atlas_origin: atlas_bounds.origin().to_float2(),
+                    border_top: border_width * (image.border.top as usize as f32),
+                    border_right: border_width * (image.border.right as usize as f32),
+                    border_bottom: border_width * (image.border.bottom as usize as f32),
+                    border_left: border_width * (image.border.left as usize as f32),
+                    border_color: image.border.color.to_uchar4(),
+                    corner_radius,
                 });
         }
 

gpui/src/platform/mac/shaders/shaders.h 🔗

@@ -97,4 +97,10 @@ typedef struct
     vector_float2 target_size;
     vector_float2 source_size;
     vector_float2 atlas_origin;
+    float border_top;
+    float border_right;
+    float border_bottom;
+    float border_left;
+    vector_uchar4 border_color;
+    float corner_radius;
 } GPUIImage;

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

@@ -34,46 +34,19 @@ float blur_along_x(float x, float y, float sigma, float corner, float2 halfSize)
 
 struct QuadFragmentInput {
     float4 position [[position]];
-    vector_float2 origin;
-    vector_float2 size;
-    vector_uchar4 background_color;
+    float2 atlas_position; // only used in the image shader
+    float2 origin;
+    float2 size;
+    float4 background_color;
     float border_top;
     float border_right;
     float border_bottom;
     float border_left;
-    vector_uchar4 border_color;
+    float4 border_color;
     float corner_radius;
 };
 
-vertex QuadFragmentInput quad_vertex(
-    uint unit_vertex_id [[vertex_id]],
-    uint quad_id [[instance_id]],
-    constant float2 *unit_vertices [[buffer(GPUIQuadInputIndexVertices)]],
-    constant GPUIQuad *quads [[buffer(GPUIQuadInputIndexQuads)]],
-    constant GPUIUniforms *uniforms [[buffer(GPUIQuadInputIndexUniforms)]]
-) {
-    float2 unit_vertex = unit_vertices[unit_vertex_id];
-    GPUIQuad quad = quads[quad_id];
-    float2 position = unit_vertex * quad.size + quad.origin;
-    float4 device_position = to_device_position(position, uniforms->viewport_size);
-
-    return QuadFragmentInput {
-        device_position,
-        quad.origin,
-        quad.size,
-        quad.background_color,
-        quad.border_top,
-        quad.border_right,
-        quad.border_bottom,
-        quad.border_left,
-        quad.border_color,
-        quad.corner_radius,
-    };
-}
-
-fragment float4 quad_fragment(
-    QuadFragmentInput input [[stage_in]]
-) {
+float4 quad_sdf(QuadFragmentInput input) {
     float2 half_size = input.size / 2.;
     float2 center = input.origin + half_size;
     float2 center_to_point = input.position.xy - center;
@@ -95,12 +68,12 @@ fragment float4 quad_fragment(
 
     float4 color;
     if (border_width == 0.) {
-        color = coloru_to_colorf(input.background_color);
+        color = input.background_color;
     } else {
         float inset_distance = distance + border_width;
         color = mix(
-            coloru_to_colorf(input.border_color),
-            coloru_to_colorf(input.background_color),
+            input.border_color,
+            input.background_color,
             saturate(0.5 - inset_distance)
         );
     }
@@ -109,6 +82,39 @@ fragment float4 quad_fragment(
     return coverage * color;
 }
 
+vertex QuadFragmentInput quad_vertex(
+    uint unit_vertex_id [[vertex_id]],
+    uint quad_id [[instance_id]],
+    constant float2 *unit_vertices [[buffer(GPUIQuadInputIndexVertices)]],
+    constant GPUIQuad *quads [[buffer(GPUIQuadInputIndexQuads)]],
+    constant GPUIUniforms *uniforms [[buffer(GPUIQuadInputIndexUniforms)]]
+) {
+    float2 unit_vertex = unit_vertices[unit_vertex_id];
+    GPUIQuad quad = quads[quad_id];
+    float2 position = unit_vertex * quad.size + quad.origin;
+    float4 device_position = to_device_position(position, uniforms->viewport_size);
+
+    return QuadFragmentInput {
+        device_position,
+        float2(0., 0.),
+        quad.origin,
+        quad.size,
+        coloru_to_colorf(quad.background_color),
+        quad.border_top,
+        quad.border_right,
+        quad.border_bottom,
+        quad.border_left,
+        coloru_to_colorf(quad.border_color),
+        quad.corner_radius,
+    };
+}
+
+fragment float4 quad_fragment(
+    QuadFragmentInput input [[stage_in]]
+) {
+    return quad_sdf(input);
+}
+
 struct ShadowFragmentInput {
     float4 position [[position]];
     vector_float2 origin;
@@ -217,12 +223,7 @@ fragment float4 sprite_fragment(
     return color;
 }
 
-struct ImageFragmentInput {
-    float4 position [[position]];
-    float2 atlas_position;
-};
-
-vertex ImageFragmentInput image_vertex(
+vertex QuadFragmentInput image_vertex(
     uint unit_vertex_id [[vertex_id]],
     uint image_id [[instance_id]],
     constant float2 *unit_vertices [[buffer(GPUIImageVertexInputIndexVertices)]],
@@ -236,18 +237,28 @@ vertex ImageFragmentInput image_vertex(
     float4 device_position = to_device_position(position, *viewport_size);
     float2 atlas_position = (unit_vertex * image.source_size + image.atlas_origin) / *atlas_size;
 
-    return ImageFragmentInput {
+    return QuadFragmentInput {
         device_position,
         atlas_position,
+        image.origin,
+        image.target_size,
+        float4(0.),
+        image.border_top,
+        image.border_right,
+        image.border_bottom,
+        image.border_left,
+        coloru_to_colorf(image.border_color),
+        image.corner_radius,
     };
 }
 
 fragment float4 image_fragment(
-    ImageFragmentInput input [[stage_in]],
+    QuadFragmentInput input [[stage_in]],
     texture2d<float> atlas [[ texture(GPUIImageFragmentInputIndexAtlas) ]]
 ) {
     constexpr sampler atlas_sampler(mag_filter::linear, min_filter::linear);
-    return atlas.sample(atlas_sampler, input.atlas_position);
+    input.background_color = atlas.sample(atlas_sampler, input.atlas_position);
+    return quad_sdf(input);
 }
 
 struct PathAtlasVertexOutput {

gpui/src/scene.rs 🔗

@@ -128,6 +128,8 @@ pub struct PathVertex {
 
 pub struct Image {
     pub bounds: RectF,
+    pub border: Border,
+    pub corner_radius: f32,
     pub data: Arc<ImageData>,
 }