diff --git a/crates/gpui3/build.rs b/crates/gpui3/build.rs index 861534aa6eb75874fa25c1213e0c118bf3ee33aa..c1ad7491d815fa60c696f13ba84df8147a29fa36 100644 --- a/crates/gpui3/build.rs +++ b/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(), diff --git a/crates/gpui3/src/geometry.rs b/crates/gpui3/src/geometry.rs index 478598daba2150ba3cea8c8aee7b0231fa75633a..2616690b089dc48cdfd917ae260a014533c8fa2e 100644 --- a/crates/gpui3/src/geometry.rs +++ b/crates/gpui3/src/geometry.rs @@ -469,6 +469,17 @@ impl Edges { } } +impl Edges { + pub fn scale(&self, factor: f32) -> Edges { + 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 { } impl Corners { - pub fn to_pixels(&self, bounds: Bounds, rem_size: Pixels) -> Corners { - let max = bounds.size.width.max(bounds.size.height) / 2.; + pub fn to_pixels(&self, size: Size, rem_size: Pixels) -> Corners { + 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), diff --git a/crates/gpui3/src/platform/mac/metal_renderer.rs b/crates/gpui3/src/platform/mac/metal_renderer.rs index a018aad7623cdec236d1c97f5176a6f4f5fe65aa..f2e14a04319dfcf2348e3447d2c89172acba7ad6 100644 --- a/crates/gpui3/src/platform/mac/metal_renderer.rs +++ b/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, + 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 as *const _, + ); + + let shadow_bytes_len = mem::size_of::() * 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, diff --git a/crates/gpui3/src/platform/mac/shaders.metal b/crates/gpui3/src/platform/mac/shaders.metal index f71f849438f428e69994e39473088ad13a545185..5e37b48027c3f82d1edd53f0ff038f14a229b60b 100644 --- a/crates/gpui3/src/platform/mac/shaders.metal +++ b/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; +} diff --git a/crates/gpui3/src/scene.rs b/crates/gpui3/src/scene.rs index 8d34d0d3cfdecdff2cad01244cbdaf31a886cba7..6a897a78d29235cde93a2c3bc29d279c45ed0fd8 100644 --- a/crates/gpui3/src/scene.rs +++ b/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, + pub shadows: Vec, pub monochrome_sprites: Vec, pub polychrome_sprites: Vec, } @@ -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>, + shadows: &'a [Shadow], + shadows_start: usize, + shadows_iter: Peekable>, monochrome_sprites: &'a [MonochromeSprite], monochrome_sprites_start: usize, monochrome_sprites_iter: Peekable>, @@ -96,6 +106,10 @@ impl<'a> Iterator for BatchIterator<'a> { fn next(&mut self) -> Option { 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 for Primitive { } } +#[derive(Debug, Clone, Eq, PartialEq)] +#[repr(C)] +pub struct Shadow { + pub order: u32, + pub bounds: Bounds, + pub corner_radii: Corners, + 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 { + Some(self.cmp(other)) + } +} + +impl From for Primitive { + fn from(shadow: Shadow) -> Self { + Primitive::Shadow(shadow) + } +} + #[derive(Clone, Debug, Eq, PartialEq)] #[repr(C)] pub struct MonochromeSprite { diff --git a/crates/gpui3/src/style.rs b/crates/gpui3/src/style.rs index 19c9c31b48baf11f2acd0a9a7ec8956ed39bca2b..299743e695fe2a8c34c288b7f1f367933ac97a21 100644 --- a/crates/gpui3/src/style.rs +++ b/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, + /// Box Shadow of the element + pub box_shadow: Option, + /// TEXT pub text: TextStyleRefinement, } +#[derive(Clone, Debug)] +pub struct BoxShadow { + pub color: Hsla, + pub offset: Point, + 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(), } } diff --git a/crates/gpui3/src/style_helpers.rs b/crates/gpui3/src/style_helpers.rs index 12d9eade058d7bd29f292ec29224f06ae25c7975..109edba90c4a0a17617004b227bf4a53906dc601 100644 --- a/crates/gpui3/src/style_helpers.rs +++ b/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