WIP

Antonio Scandurra created

Change summary

crates/gpui3/build.rs                           |   2 
crates/gpui3/src/geometry.rs                    |  15 ++
crates/gpui3/src/platform/mac/metal_renderer.rs |  87 ++++++++++++++
crates/gpui3/src/platform/mac/shaders.metal     | 109 +++++++++++++++++++
crates/gpui3/src/scene.rs                       |  61 ++++++++++
crates/gpui3/src/style.rs                       |  43 ++++++
crates/gpui3/src/style_helpers.rs               |  57 +++++++++
crates/storybook2/src/collab_panel.rs           |   3 
8 files changed, 366 insertions(+), 11 deletions(-)

Detailed changes

crates/gpui3/build.rs 🔗

@@ -52,6 +52,8 @@ fn generate_shader_bindings() -> PathBuf {
         "AtlasTile".into(),
         "QuadInputIndex".into(),
         "Quad".into(),
+        "ShadowInputIndex".into(),
+        "Shadow".into(),
         "SpriteInputIndex".into(),
         "MonochromeSprite".into(),
         "PolychromeSprite".into(),

crates/gpui3/src/geometry.rs 🔗

@@ -469,6 +469,17 @@ impl Edges<AbsoluteLength> {
     }
 }
 
+impl Edges<Pixels> {
+    pub fn scale(&self, factor: f32) -> Edges<ScaledPixels> {
+        Edges {
+            top: self.top.scale(factor),
+            right: self.right.scale(factor),
+            bottom: self.bottom.scale(factor),
+            left: self.left.scale(factor),
+        }
+    }
+}
+
 #[derive(Refineable, Clone, Default, Debug, Eq, PartialEq)]
 #[refineable(debug)]
 #[repr(C)]
@@ -480,8 +491,8 @@ pub struct Corners<T: Clone + Debug> {
 }
 
 impl Corners<AbsoluteLength> {
-    pub fn to_pixels(&self, bounds: Bounds<Pixels>, rem_size: Pixels) -> Corners<Pixels> {
-        let max = bounds.size.width.max(bounds.size.height) / 2.;
+    pub fn to_pixels(&self, size: Size<Pixels>, rem_size: Pixels) -> Corners<Pixels> {
+        let max = size.width.max(size.height) / 2.;
         Corners {
             top_left: self.top_left.to_pixels(rem_size).min(max),
             top_right: self.top_right.to_pixels(rem_size).min(max),

crates/gpui3/src/platform/mac/metal_renderer.rs 🔗

@@ -1,6 +1,6 @@
 use crate::{
     point, size, AtlasTextureId, DevicePixels, MetalAtlas, MonochromeSprite, PolychromeSprite,
-    Quad, Scene, Size,
+    Quad, Scene, Shadow, Size,
 };
 use cocoa::{
     base::{NO, YES},
@@ -18,6 +18,7 @@ pub struct MetalRenderer {
     layer: metal::MetalLayer,
     command_queue: CommandQueue,
     quads_pipeline_state: metal::RenderPipelineState,
+    shadows_pipeline_state: metal::RenderPipelineState,
     monochrome_sprites_pipeline_state: metal::RenderPipelineState,
     polychrome_sprites_pipeline_state: metal::RenderPipelineState,
     unit_vertices: metal::Buffer,
@@ -90,6 +91,14 @@ impl MetalRenderer {
             "quad_fragment",
             PIXEL_FORMAT,
         );
+        let shadows_pipeline_state = build_pipeline_state(
+            &device,
+            &library,
+            "shadows",
+            "shadow_vertex",
+            "shadow_fragment",
+            PIXEL_FORMAT,
+        );
         let monochrome_sprites_pipeline_state = build_pipeline_state(
             &device,
             &library,
@@ -114,6 +123,7 @@ impl MetalRenderer {
             layer,
             command_queue,
             quads_pipeline_state,
+            shadows_pipeline_state,
             monochrome_sprites_pipeline_state,
             polychrome_sprites_pipeline_state,
             unit_vertices,
@@ -183,6 +193,14 @@ impl MetalRenderer {
                             command_encoder,
                         );
                     }
+                    crate::PrimitiveBatch::Shadows(shadows) => {
+                        self.draw_shadows(
+                            shadows,
+                            &mut instance_offset,
+                            viewport_size,
+                            command_encoder,
+                        );
+                    }
                     crate::PrimitiveBatch::MonochromeSprites {
                         texture_id,
                         sprites,
@@ -279,6 +297,66 @@ impl MetalRenderer {
         *offset = next_offset;
     }
 
+    fn draw_shadows(
+        &mut self,
+        shadows: &[Shadow],
+        offset: &mut usize,
+        viewport_size: Size<DevicePixels>,
+        command_encoder: &metal::RenderCommandEncoderRef,
+    ) {
+        if shadows.is_empty() {
+            return;
+        }
+        align_offset(offset);
+
+        command_encoder.set_render_pipeline_state(&self.shadows_pipeline_state);
+        command_encoder.set_vertex_buffer(
+            ShadowInputIndex::Vertices as u64,
+            Some(&self.unit_vertices),
+            0,
+        );
+        command_encoder.set_vertex_buffer(
+            ShadowInputIndex::Shadows as u64,
+            Some(&self.instances),
+            *offset as u64,
+        );
+        command_encoder.set_fragment_buffer(
+            ShadowInputIndex::Shadows as u64,
+            Some(&self.instances),
+            *offset as u64,
+        );
+
+        command_encoder.set_vertex_bytes(
+            ShadowInputIndex::ViewportSize as u64,
+            mem::size_of_val(&viewport_size) as u64,
+            &viewport_size as *const Size<DevicePixels> as *const _,
+        );
+
+        let shadow_bytes_len = mem::size_of::<Shadow>() * shadows.len();
+        let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) };
+        unsafe {
+            ptr::copy_nonoverlapping(
+                shadows.as_ptr() as *const u8,
+                buffer_contents,
+                shadow_bytes_len,
+            );
+        }
+
+        let next_offset = *offset + shadow_bytes_len;
+        assert!(
+            next_offset <= INSTANCE_BUFFER_SIZE,
+            "instance buffer exhausted"
+        );
+
+        command_encoder.draw_primitives_instanced(
+            metal::MTLPrimitiveType::Triangle,
+            0,
+            6,
+            shadows.len() as u64,
+        );
+        *offset = next_offset;
+    }
+
     fn draw_monochrome_sprites(
         &mut self,
         texture_id: AtlasTextureId,
@@ -469,6 +547,13 @@ enum QuadInputIndex {
     ViewportSize = 2,
 }
 
+#[repr(C)]
+enum ShadowInputIndex {
+    Vertices = 0,
+    Shadows = 1,
+    ViewportSize = 2,
+}
+
 #[repr(C)]
 enum SpriteInputIndex {
     Vertices = 0,

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

@@ -11,6 +11,9 @@ float2 to_tile_position(float2 unit_vertex, AtlasTile tile,
                         constant Size_DevicePixels *atlas_size);
 float quad_sdf(float2 point, Bounds_ScaledPixels bounds,
                Corners_ScaledPixels corner_radii);
+float gaussian(float x, float sigma);
+float2 erf(float2 x);
+float blur_along_x(float x, float y, float sigma, float corner, float2 half_size);
 
 struct QuadVertexOutput {
   float4 position [[position]];
@@ -110,6 +113,91 @@ fragment float4 quad_fragment(QuadVertexOutput input [[stage_in]],
   return color * float4(1., 1., 1., saturate(0.5 - distance));
 }
 
+struct ShadowVertexOutput {
+  float4 position [[position]];
+  float4 color [[flat]];
+  uint shadow_id [[flat]];
+};
+
+vertex ShadowVertexOutput shadow_vertex(
+    uint unit_vertex_id [[vertex_id]],
+    uint shadow_id [[instance_id]],
+    constant float2 *unit_vertices [[buffer(ShadowInputIndex_Vertices)]],
+    constant Shadow *shadows [[buffer(ShadowInputIndex_Shadows)]],
+    constant Size_DevicePixels *viewport_size [[buffer(ShadowInputIndex_ViewportSize)]]
+) {
+    float2 unit_vertex = unit_vertices[unit_vertex_id];
+    Shadow shadow = shadows[shadow_id];
+
+    float margin = (3. * shadow.blur_radius) + shadow.spread_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;
+
+    float4 device_position = to_device_position(unit_vertex, bounds, bounds, viewport_size);
+    float4 color = hsla_to_rgba(shadow.color);
+
+    return ShadowVertexOutput {
+        device_position,
+        color,
+        shadow_id,
+    };
+}
+
+fragment float4 shadow_fragment(
+    ShadowVertexOutput input [[stage_in]],
+    constant Shadow *shadows [[buffer(ShadowInputIndex_Shadows)]]
+) {
+    Shadow shadow = shadows[input.shadow_id];
+
+    float2 origin = float2(
+        shadow.bounds.origin.x - shadow.spread_radius,
+        shadow.bounds.origin.y - shadow.spread_radius
+    );
+    float2 size = float2(
+        shadow.bounds.size.width + shadow.spread_radius * 2.,
+        shadow.bounds.size.height + shadow.spread_radius * 2.
+    );
+    float2 half_size = size / 2.;
+    float2 center = origin + half_size;
+    float2 point = input.position.xy - center;
+    float corner_radius;
+    if (point.x < 0.) {
+        if (point.y < 0.) {
+            corner_radius = shadow.corner_radii.top_left;
+        } else {
+            corner_radius = shadow.corner_radii.bottom_left;
+        }
+    } else {
+        if (point.y < 0.) {
+            corner_radius = shadow.corner_radii.top_right;
+        } else {
+            corner_radius = shadow.corner_radii.bottom_right;
+        }
+    }
+
+    // The signal is only non-zero in a limited range, so don't waste samples
+    float low = point.y - half_size.y;
+    float high = point.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(point.x, point.y - y, shadow.blur_radius, corner_radius, half_size) * gaussian(y, shadow.blur_radius) * step;
+        y += step;
+    }
+
+    return input.color * float4(1., 1., 1., alpha);
+}
+
 struct MonochromeSpriteVertexOutput {
   float4 position [[position]];
   float2 tile_position;
@@ -308,3 +396,24 @@ float quad_sdf(float2 point, Bounds_ScaledPixels bounds,
 
   return distance;
 }
+
+// A standard gaussian function, used for weighting samples
+float gaussian(float x, float sigma) {
+    return exp(-(x * x) / (2. * sigma * sigma)) / (sqrt(2. * M_PI_F) * sigma);
+}
+
+// This approximates the error function, needed for the gaussian integral
+float2 erf(float2 x) {
+    float2 s = sign(x);
+    float2 a = abs(x);
+    x = 1. + (0.278393 + (0.230389 + 0.078108 * (a * a)) * a) * a;
+    x *= x;
+    return s - s / (x * x);
+}
+
+float blur_along_x(float x, float y, float sigma, float corner, float2 half_size) {
+    float delta = min(half_size.y - corner - abs(y), 0.);
+    float curved = half_size.x - corner + sqrt(max(0., corner * corner - delta * delta));
+    float2 integral = 0.5 + 0.5 * erf((x + float2(-curved, curved)) * (sqrt(0.5) / sigma));
+    return integral.y - integral.x;
+}

crates/gpui3/src/scene.rs 🔗

@@ -38,6 +38,9 @@ impl Scene {
             Primitive::Quad(quad) => {
                 layer.quads.push(quad);
             }
+            Primitive::Shadow(shadow) => {
+                layer.shadows.push(shadow);
+            }
             Primitive::MonochromeSprite(sprite) => {
                 layer.monochrome_sprites.push(sprite);
             }
@@ -55,6 +58,7 @@ impl Scene {
 #[derive(Debug, Default)]
 pub(crate) struct SceneLayer {
     pub quads: Vec<Quad>,
+    pub shadows: Vec<Shadow>,
     pub monochrome_sprites: Vec<MonochromeSprite>,
     pub polychrome_sprites: Vec<PolychromeSprite>,
 }
@@ -68,6 +72,9 @@ impl SceneLayer {
             quads: &self.quads,
             quads_start: 0,
             quads_iter: self.quads.iter().peekable(),
+            shadows: &self.shadows,
+            shadows_start: 0,
+            shadows_iter: self.shadows.iter().peekable(),
             monochrome_sprites: &self.monochrome_sprites,
             monochrome_sprites_start: 0,
             monochrome_sprites_iter: self.monochrome_sprites.iter().peekable(),
@@ -82,6 +89,9 @@ struct BatchIterator<'a> {
     quads: &'a [Quad],
     quads_start: usize,
     quads_iter: Peekable<slice::Iter<'a, Quad>>,
+    shadows: &'a [Shadow],
+    shadows_start: usize,
+    shadows_iter: Peekable<slice::Iter<'a, Shadow>>,
     monochrome_sprites: &'a [MonochromeSprite],
     monochrome_sprites_start: usize,
     monochrome_sprites_iter: Peekable<slice::Iter<'a, MonochromeSprite>>,
@@ -96,6 +106,10 @@ impl<'a> Iterator for BatchIterator<'a> {
     fn next(&mut self) -> Option<Self::Item> {
         let mut kinds_and_orders = [
             (PrimitiveKind::Quad, self.quads_iter.peek().map(|q| q.order)),
+            (
+                PrimitiveKind::Shadow,
+                self.shadows_iter.peek().map(|s| s.order),
+            ),
             (
                 PrimitiveKind::MonochromeSprite,
                 self.monochrome_sprites_iter.peek().map(|s| s.order),
@@ -127,6 +141,19 @@ impl<'a> Iterator for BatchIterator<'a> {
                 self.quads_start = quads_end;
                 Some(PrimitiveBatch::Quads(&self.quads[quads_start..quads_end]))
             }
+            PrimitiveKind::Shadow => {
+                let shadows_start = self.shadows_start;
+                let shadows_end = shadows_start
+                    + self
+                        .shadows_iter
+                        .by_ref()
+                        .take_while(|shadow| shadow.order <= max_order)
+                        .count();
+                self.shadows_start = shadows_end;
+                Some(PrimitiveBatch::Shadows(
+                    &self.shadows[shadows_start..shadows_end],
+                ))
+            }
             PrimitiveKind::MonochromeSprite => {
                 let texture_id = self.monochrome_sprites_iter.peek().unwrap().tile.texture_id;
                 let sprites_start = self.monochrome_sprites_start;
@@ -168,6 +195,7 @@ impl<'a> Iterator for BatchIterator<'a> {
 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
 pub enum PrimitiveKind {
     Quad,
+    Shadow,
     MonochromeSprite,
     PolychromeSprite,
 }
@@ -175,12 +203,15 @@ pub enum PrimitiveKind {
 #[derive(Clone, Debug)]
 pub enum Primitive {
     Quad(Quad),
+    Shadow(Shadow),
     MonochromeSprite(MonochromeSprite),
     PolychromeSprite(PolychromeSprite),
 }
 
+#[derive(Debug)]
 pub(crate) enum PrimitiveBatch<'a> {
     Quads(&'a [Quad]),
+    Shadows(&'a [Shadow]),
     MonochromeSprites {
         texture_id: AtlasTextureId,
         sprites: &'a [MonochromeSprite],
@@ -221,6 +252,36 @@ impl From<Quad> for Primitive {
     }
 }
 
+#[derive(Debug, Clone, Eq, PartialEq)]
+#[repr(C)]
+pub struct Shadow {
+    pub order: u32,
+    pub bounds: Bounds<ScaledPixels>,
+    pub corner_radii: Corners<ScaledPixels>,
+    pub content_mask: ScaledContentMask,
+    pub color: Hsla,
+    pub blur_radius: ScaledPixels,
+    pub spread_radius: ScaledPixels,
+}
+
+impl Ord for Shadow {
+    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
+        self.order.cmp(&other.order)
+    }
+}
+
+impl PartialOrd for Shadow {
+    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
+        Some(self.cmp(other))
+    }
+}
+
+impl From<Shadow> for Primitive {
+    fn from(shadow: Shadow) -> Self {
+        Primitive::Shadow(shadow)
+    }
+}
+
 #[derive(Clone, Debug, Eq, PartialEq)]
 #[repr(C)]
 pub struct MonochromeSprite {

crates/gpui3/src/style.rs 🔗

@@ -1,7 +1,7 @@
 use crate::{
     phi, point, rems, AbsoluteLength, BorrowAppContext, BorrowWindow, Bounds, ContentMask, Corners,
     CornersRefinement, DefiniteLength, Edges, EdgesRefinement, Font, FontFeatures, FontStyle,
-    FontWeight, Hsla, Length, Pixels, Point, PointRefinement, Quad, Rems, Result, RunStyle,
+    FontWeight, Hsla, Length, Pixels, Point, PointRefinement, Quad, Rems, Result, RunStyle, Shadow,
     SharedString, Size, SizeRefinement, ViewContext, WindowContext,
 };
 use refineable::Refineable;
@@ -89,10 +89,21 @@ pub struct Style {
     #[refineable]
     pub corner_radii: Corners<AbsoluteLength>,
 
+    /// Box Shadow of the element
+    pub box_shadow: Option<BoxShadow>,
+
     /// TEXT
     pub text: TextStyleRefinement,
 }
 
+#[derive(Clone, Debug)]
+pub struct BoxShadow {
+    pub color: Hsla,
+    pub offset: Point<Pixels>,
+    pub blur_radius: Pixels,
+    pub spread_radius: Pixels,
+}
+
 #[derive(Refineable, Clone, Debug)]
 #[refineable(debug)]
 pub struct TextStyle {
@@ -233,6 +244,28 @@ impl Style {
         let rem_size = cx.rem_size();
         let scale = cx.scale_factor();
 
+        if let Some(shadow) = self.box_shadow.as_ref() {
+            let layer_id = cx.current_layer_id();
+            let content_mask = cx.content_mask();
+            let mut shadow_bounds = bounds;
+            shadow_bounds.origin += shadow.offset;
+            cx.scene().insert(
+                layer_id,
+                Shadow {
+                    order,
+                    bounds: shadow_bounds.scale(scale),
+                    content_mask: content_mask.scale(scale),
+                    corner_radii: self
+                        .corner_radii
+                        .to_pixels(bounds.size, rem_size)
+                        .scale(scale),
+                    color: shadow.color,
+                    blur_radius: shadow.blur_radius.scale(scale),
+                    spread_radius: shadow.spread_radius.scale(scale),
+                },
+            );
+        }
+
         let background_color = self.fill.as_ref().and_then(Fill::color);
         if background_color.is_some() || self.is_border_visible() {
             let layer_id = cx.current_layer_id();
@@ -247,10 +280,9 @@ impl Style {
                     border_color: self.border_color.unwrap_or_default(),
                     corner_radii: self
                         .corner_radii
-                        .map(|length| length.to_pixels(rem_size).scale(scale)),
-                    border_widths: self
-                        .border_widths
-                        .map(|length| length.to_pixels(rem_size).scale(scale)),
+                        .to_pixels(bounds.size, rem_size)
+                        .scale(scale),
+                    border_widths: self.border_widths.to_pixels(rem_size).scale(scale),
                 },
             );
         }
@@ -296,6 +328,7 @@ impl Default for Style {
             fill: None,
             border_color: None,
             corner_radii: Corners::default(),
+            box_shadow: None,
             text: TextStyleRefinement::default(),
         }
     }

crates/gpui3/src/style_helpers.rs 🔗

@@ -1,6 +1,7 @@
 use crate::{
-    self as gpui3, relative, rems, AlignItems, Display, Fill, FlexDirection, Hsla, JustifyContent,
-    Length, Position, SharedString, Style, StyleRefinement, Styled, TextStyleRefinement,
+    self as gpui3, hsla, point, px, relative, rems, AlignItems, BoxShadow, Display, Fill,
+    FlexDirection, Hsla, JustifyContent, Length, Position, SharedString, Style, StyleRefinement,
+    Styled, TextStyleRefinement,
 };
 
 pub trait StyleHelpers: Styled<Style = Style> {
@@ -213,6 +214,58 @@ pub trait StyleHelpers: Styled<Style = Style> {
         self
     }
 
+    fn shadow(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        self
+    }
+
+    fn shadow_sm(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        self.declared_style().box_shadow = Some(BoxShadow {
+            color: hsla(0., 0., 0., 1.),
+            offset: point(px(0.), px(0.)),
+            blur_radius: px(1.),
+            spread_radius: px(0.),
+        });
+        self
+    }
+
+    fn shadow_md(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        todo!();
+        self
+    }
+
+    fn shadow_lg(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        todo!();
+        self
+    }
+
+    fn shadow_xl(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        todo!();
+        self
+    }
+
+    fn shadow_2xl(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        todo!();
+        self
+    }
+
     fn text_style(&mut self) -> &mut Option<TextStyleRefinement> {
         let style: &mut StyleRefinement = self.declared_style();
         &mut style.text

crates/storybook2/src/collab_panel.rs 🔗

@@ -168,7 +168,8 @@ impl CollabPanel {
                             .uri(avatar_uri)
                             .size_3p5()
                             .rounded_full()
-                            .fill(theme.middle.positive.default.foreground),
+                            .fill(theme.middle.positive.default.foreground)
+                            .shadow_sm(),
                     )
                     .child(label),
             )