From a89b8f6defdb47942b8a16317d07ee022420ad86 Mon Sep 17 00:00:00 2001 From: Aditya Sharma Date: Wed, 15 Jul 2026 09:28:20 -0700 Subject: [PATCH] fix: support rounded content masks --- crates/gpui/src/elements/list.rs | 4 +- crates/gpui/src/elements/uniform_list.rs | 2 +- crates/gpui/src/scene.rs | 49 ++-- crates/gpui/src/style.rs | 132 ++++++++-- crates/gpui/src/window.rs | 261 ++++++++++++++++++-- crates/gpui_macos/src/metal_renderer.rs | 9 +- crates/gpui_macos/src/shaders.metal | 81 +++++- crates/gpui_wgpu/src/shaders.wgsl | 139 ++++++++--- crates/gpui_wgpu/src/wgpu_renderer.rs | 46 ++-- crates/gpui_windows/src/directx_renderer.rs | 4 +- crates/gpui_windows/src/shaders.hlsl | 73 ++++-- 11 files changed, 656 insertions(+), 144 deletions(-) diff --git a/crates/gpui/src/elements/list.rs b/crates/gpui/src/elements/list.rs index 2fdb8ca..cace219 100644 --- a/crates/gpui/src/elements/list.rs +++ b/crates/gpui/src/elements/list.rs @@ -1284,7 +1284,7 @@ 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| { + window.with_content_mask(Some(ContentMask::new(bounds)), |window| { item.element.prepaint_at(item_origin, window, cx); }); @@ -1595,7 +1595,7 @@ impl Element for List { cx: &mut App, ) { let current_view = window.current_view(); - window.with_content_mask(Some(ContentMask { bounds }), |window| { + window.with_content_mask(Some(ContentMask::new(bounds)), |window| { for item in &mut prepaint.layout.item_layouts { item.element.paint(window, cx); } diff --git a/crates/gpui/src/elements/uniform_list.rs b/crates/gpui/src/elements/uniform_list.rs index fcf1a0a..4b9f78c 100644 --- a/crates/gpui/src/elements/uniform_list.rs +++ b/crates/gpui/src/elements/uniform_list.rs @@ -494,7 +494,7 @@ impl Element for UniformList { (self.render_items)(visible_range.clone(), window, cx) }; - let content_mask = ContentMask { bounds }; + let content_mask = ContentMask::new(bounds); 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 diff --git a/crates/gpui/src/scene.rs b/crates/gpui/src/scene.rs index f383d11..96967b5 100644 --- a/crates/gpui/src/scene.rs +++ b/crates/gpui/src/scene.rs @@ -1142,7 +1142,7 @@ mod tests { order, pad: 0, bounds, - content_mask: ContentMask { bounds }, + content_mask: ContentMask::new(bounds), corner_radii: Corners::all(ScaledPixels(2.0)), blur_radius: ScaledPixels(12.0), source_origin_x: 0.0, @@ -1153,18 +1153,29 @@ mod tests { } } + #[test] + fn content_mask_gpu_layout_matches_shader_storage_contract() { + assert_eq!(align_of::>(), 4); + assert_eq!(size_of::>(), 48); + assert_eq!(offset_of!(ContentMask, bounds), 0); + assert_eq!(offset_of!(ContentMask, rounded_bounds), 16); + assert_eq!(offset_of!(ContentMask, corner_radii), 32); + } + #[test] fn backdrop_blur_gpu_layout_matches_shader_storage_contract() { assert_eq!(align_of::(), 4); - assert_eq!(size_of::(), 80); + assert_eq!(size_of::(), 112); assert_eq!(offset_of!(BackdropBlur, order), 0); assert_eq!(offset_of!(BackdropBlur, bounds), 8); assert_eq!(offset_of!(BackdropBlur, content_mask), 24); - assert_eq!(offset_of!(BackdropBlur, corner_radii), 40); - assert_eq!(offset_of!(BackdropBlur, blur_radius), 56); - assert_eq!(offset_of!(BackdropBlur, source_origin_x), 60); - assert_eq!(offset_of!(BackdropBlur, source_height), 72); - assert_eq!(offset_of!(BackdropBlur, pad2), 76); + assert_eq!(offset_of!(BackdropBlur, corner_radii), 72); + assert_eq!(offset_of!(BackdropBlur, blur_radius), 88); + assert_eq!(offset_of!(BackdropBlur, source_origin_x), 92); + assert_eq!(offset_of!(BackdropBlur, source_origin_y), 96); + assert_eq!(offset_of!(BackdropBlur, source_width), 100); + assert_eq!(offset_of!(BackdropBlur, source_height), 104); + assert_eq!(offset_of!(BackdropBlur, pad2), 108); } #[test] @@ -1178,26 +1189,26 @@ mod tests { #[test] fn underline_gpu_layout_matches_shader_storage_contract() { assert_eq!(align_of::(), 4); - assert_eq!(size_of::(), 64); + assert_eq!(size_of::(), 96); assert_eq!(offset_of!(Underline, order), 0); assert_eq!(offset_of!(Underline, bounds), 8); assert_eq!(offset_of!(Underline, content_mask), 24); - assert_eq!(offset_of!(Underline, color), 40); - assert_eq!(offset_of!(Underline, thickness), 56); - assert_eq!(offset_of!(Underline, wavy), 60); + assert_eq!(offset_of!(Underline, color), 72); + assert_eq!(offset_of!(Underline, thickness), 88); + assert_eq!(offset_of!(Underline, wavy), 92); } #[test] fn polychrome_sprite_gpu_layout_matches_shader_storage_contract() { assert_eq!(align_of::(), 4); - assert_eq!(size_of::(), 96); + assert_eq!(size_of::(), 128); assert_eq!(offset_of!(PolychromeSprite, order), 0); assert_eq!(offset_of!(PolychromeSprite, grayscale), 8); assert_eq!(offset_of!(PolychromeSprite, opacity), 12); assert_eq!(offset_of!(PolychromeSprite, bounds), 16); assert_eq!(offset_of!(PolychromeSprite, content_mask), 32); - assert_eq!(offset_of!(PolychromeSprite, corner_radii), 48); - assert_eq!(offset_of!(PolychromeSprite, tile), 64); + assert_eq!(offset_of!(PolychromeSprite, corner_radii), 80); + assert_eq!(offset_of!(PolychromeSprite, tile), 96); } #[test] @@ -1209,13 +1220,13 @@ mod tests { Quad { order: 4, bounds, - content_mask: ContentMask { bounds }, + content_mask: ContentMask::new(bounds), ..Default::default() }, Quad { order: 1, bounds, - content_mask: ContentMask { bounds }, + content_mask: ContentMask::new(bounds), ..Default::default() }, ]; @@ -1258,7 +1269,7 @@ mod tests { #[test] fn replay_retained_layer_ranges_and_marks_content_clean() { let bounds = test_bounds(); - let content_mask = ContentMask { bounds }; + let content_mask = ContentMask::new(bounds); let mut prev_scene = Scene::default(); prev_scene.insert_primitive(Quad { @@ -1290,7 +1301,7 @@ mod tests { #[test] fn replay_offsets_retained_layer_ranges_after_existing_ops() { let bounds = test_bounds(); - let content_mask = ContentMask { bounds }; + let content_mask = ContentMask::new(bounds); let mut prev_scene = Scene::default(); prev_scene.insert_primitive(Quad { @@ -1333,7 +1344,7 @@ mod tests { blur_radius: ScaledPixels(0.0), bounds, corner_radii: Corners::all(ScaledPixels(0.0)), - content_mask: ContentMask { bounds }, + content_mask: ContentMask::new(bounds), color: Hsla::default(), element_bounds, element_corner_radii: Corners::all(ScaledPixels(0.0)), diff --git a/crates/gpui/src/style.rs b/crates/gpui/src/style.rs index 78d9533..d90c248 100644 --- a/crates/gpui/src/style.rs +++ b/crates/gpui/src/style.rs @@ -649,40 +649,72 @@ impl Style { y: Overflow::Visible, } => None, _ => { + let element_size = bounds.size; let mut min = bounds.origin; let mut max = bounds.bottom_right(); - - if self + let border_widths = self.border_widths.to_pixels(rem_size); + let has_visible_border = self .border_color - .is_some_and(|color| !color.is_transparent()) - { - min.x += self.border_widths.left.to_pixels(rem_size); - max.x -= self.border_widths.right.to_pixels(rem_size); - min.y += self.border_widths.top.to_pixels(rem_size); - max.y -= self.border_widths.bottom.to_pixels(rem_size); + .is_some_and(|color| !color.is_transparent()); + + if has_visible_border { + min.x += border_widths.left; + max.x -= border_widths.right; + min.y += border_widths.top; + max.y -= border_widths.bottom; } - let bounds = match ( + let (bounds, clips_both_axes) = match ( self.overflow.x == Overflow::Visible, self.overflow.y == Overflow::Visible, ) { // x and y both visible (true, true) => return None, // x visible, y hidden - (true, false) => Bounds::from_corners( - point(min.x, bounds.origin.y), - point(max.x, bounds.bottom_right().y), + (true, false) => ( + Bounds::from_corners( + point(bounds.origin.x, min.y), + point(bounds.bottom_right().x, max.y), + ), + false, ), // x hidden, y visible - (false, true) => Bounds::from_corners( - point(bounds.origin.x, min.y), - point(bounds.bottom_right().x, max.y), + (false, true) => ( + Bounds::from_corners( + point(min.x, bounds.origin.y), + point(max.x, bounds.bottom_right().y), + ), + false, ), // both hidden - (false, false) => Bounds::from_corners(min, max), + (false, false) => (Bounds::from_corners(min, max), true), }; - Some(ContentMask { bounds }) + let mut corner_radii = self + .corner_radii + .to_pixels(rem_size) + .clamp_radii_for_quad_size(element_size); + if has_visible_border { + corner_radii.top_left = (corner_radii.top_left + - border_widths.top.max(border_widths.left)) + .max(Pixels::ZERO); + corner_radii.top_right = (corner_radii.top_right + - border_widths.top.max(border_widths.right)) + .max(Pixels::ZERO); + corner_radii.bottom_right = (corner_radii.bottom_right + - border_widths.bottom.max(border_widths.right)) + .max(Pixels::ZERO); + corner_radii.bottom_left = (corner_radii.bottom_left + - border_widths.bottom.max(border_widths.left)) + .max(Pixels::ZERO); + corner_radii = corner_radii.clamp_radii_for_quad_size(bounds.size); + } + + if clips_both_axes && corner_radii != Corners::default() { + Some(ContentMask::rounded(bounds, corner_radii)) + } else { + Some(ContentMask::new(bounds)) + } } } } @@ -1338,10 +1370,74 @@ impl From for taffy::style::Position { #[cfg(test)] mod tests { - use crate::{blue, green, px, red, yellow}; + use crate::{Bounds, blue, green, px, red, size, yellow}; use super::*; + #[test] + fn overflow_mask_preserves_rounded_clip_geometry() { + let bounds = Bounds::new(point(px(10.), px(20.)), size(px(100.), px(80.))); + let style = Style { + overflow: point(Overflow::Hidden, Overflow::Hidden), + corner_radii: Corners::all(px(12.).into()), + ..Default::default() + }; + + assert_eq!( + style.overflow_mask(bounds, px(16.)), + Some(ContentMask::rounded(bounds, Corners::all(px(12.)))) + ); + } + + #[test] + fn overflow_mask_stays_rectangular_when_only_one_axis_is_clipped() { + let bounds = Bounds::new(point(px(10.), px(20.)), size(px(100.), px(80.))); + let style = Style { + overflow: point(Overflow::Hidden, Overflow::Visible), + corner_radii: Corners::all(px(12.).into()), + ..Default::default() + }; + + assert_eq!( + style.overflow_mask(bounds, px(16.)), + Some(ContentMask::new(bounds)) + ); + } + + #[test] + fn overflow_mask_insets_only_the_clipped_axis_for_visible_borders() { + let bounds = Bounds::new(point(px(10.), px(20.)), size(px(100.), px(80.))); + let base_style = Style { + border_widths: Edges::all(px(2.).into()), + border_color: Some(red()), + ..Default::default() + }; + + let x_clipped = Style { + overflow: point(Overflow::Hidden, Overflow::Visible), + ..base_style.clone() + }; + assert_eq!( + x_clipped.overflow_mask(bounds, px(16.)), + Some(ContentMask::new(Bounds::from_corners( + point(px(12.), px(20.)), + point(px(108.), px(100.)), + ))) + ); + + let y_clipped = Style { + overflow: point(Overflow::Visible, Overflow::Hidden), + ..base_style + }; + assert_eq!( + y_clipped.overflow_mask(bounds, px(16.)), + Some(ContentMask::new(Bounds::from_corners( + point(px(10.), px(22.)), + point(px(110.), px(98.)), + ))) + ); + } + #[test] fn box_shadow_builder_sets_css_ordered_fields_and_defaults() { let color = red(); diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 1cd7b3b..3dfd575 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -637,7 +637,7 @@ pub struct Hitbox { pub transform: TransformationMatrix, /// Cached inverse transform from window space into local hitbox space. pub inverse_transform: Option, - /// The content mask when the hitbox was inserted. + /// The content mask in local hitbox space when the hitbox was inserted. pub content_mask: ContentMask, /// Flags that specify hitbox behavior. pub behavior: HitboxBehavior, @@ -679,20 +679,32 @@ impl Hitbox { /// Returns true when this hitbox contains the point in window space. pub fn contains_window_point(&self, point: Point) -> bool { + if !self.bounds.contains(&point) { + return false; + } + + let local_point = if self.transform.is_unit() { + point + } else if let Some(local_point) = self.window_to_local(point) { + local_point + } else { + return false; + }; + if !self - .bounds + .local_bounds .intersect(&self.content_mask.bounds) - .contains(&point) + .contains(&local_point) { return false; } - if self.transform.is_unit() { - return self.local_bounds.contains(&point); - } - - self.window_to_local(point) - .is_some_and(|local| self.local_bounds.contains(&local)) + self.content_mask.corner_radii == Corners::default() + || rounded_rect_contains_point( + self.content_mask.rounded_bounds, + self.content_mask.corner_radii, + local_point, + ) } } @@ -1683,13 +1695,39 @@ pub struct DispatchEventResult { } /// Indicates which region of the window is visible. Content falling outside of this mask will not be -/// rendered. Currently, only rectangular content masks are supported, but we give the mask its own type -/// to leave room to support more complex shapes in the future. +/// rendered. +/// +/// The rectangular `bounds` are always applied. One rounded clip can be preserved in addition to +/// those bounds so nested rectangular overflow masks do not discard an ancestor's corner radii. #[derive(Clone, Debug, Default, PartialEq, Eq)] #[repr(C)] pub struct ContentMask { - /// The bounds + /// The rectangular clip bounds. pub bounds: Bounds

, + /// Bounds for the rounded clip geometry. + pub rounded_bounds: Bounds

, + /// Corner radii for `rounded_bounds`. Zero radii disable the rounded clip. + pub corner_radii: Corners

, +} + +impl ContentMask

{ + /// Create a rectangular content mask. + pub fn new(bounds: Bounds

) -> Self { + Self { + rounded_bounds: bounds.clone(), + bounds, + corner_radii: Corners::default(), + } + } + + /// Create a content mask with rounded clip geometry. + pub fn rounded(bounds: Bounds

, corner_radii: Corners

) -> Self { + Self { + rounded_bounds: bounds.clone(), + bounds, + corner_radii, + } + } } impl ContentMask { @@ -1697,14 +1735,106 @@ impl ContentMask { pub fn scale(&self, factor: f32) -> ContentMask { ContentMask { bounds: self.bounds.scale(factor), + rounded_bounds: self.rounded_bounds.scale(factor), + corner_radii: self.corner_radii.scale(factor), } } /// Intersect the content mask with the given content mask. + /// + /// Rectangular bounds intersect exactly. When both masks are rounded, the inner clip is kept + /// if its complete rectangular bounds fit inside the ancestor's rounded geometry. Otherwise, + /// the existing ancestor clip (`other`) is retained; content masks currently carry one rounded + /// geometry. pub fn intersect(&self, other: &Self) -> Self { let bounds = self.bounds.intersect(&other.bounds); - ContentMask { bounds } + let self_is_rounded = self.corner_radii != Corners::default(); + let other_is_rounded = other.corner_radii != Corners::default(); + let (rounded_bounds, corner_radii) = if self_is_rounded + && (!other_is_rounded + || rounded_rect_contains_bounds( + other.rounded_bounds, + other.corner_radii, + self.bounds, + )) { + (self.rounded_bounds, self.corner_radii) + } else if other_is_rounded { + (other.rounded_bounds, other.corner_radii) + } else { + (bounds, Corners::default()) + }; + + ContentMask { + bounds, + rounded_bounds, + corner_radii, + } + } +} + +fn rounded_rect_contains_bounds( + rounded_bounds: Bounds, + corner_radii: Corners, + bounds: Bounds, +) -> bool { + [ + bounds.origin, + bounds.top_right(), + bounds.bottom_right(), + bounds.bottom_left(), + ] + .into_iter() + .all(|point| rounded_rect_contains_point(rounded_bounds, corner_radii, point)) +} + +fn rounded_rect_contains_point( + bounds: Bounds, + corner_radii: Corners, + point: Point, +) -> bool { + let x = (point.x - bounds.origin.x).0; + let y = (point.y - bounds.origin.y).0; + let width = bounds.size.width.0; + let height = bounds.size.height.0; + + if x < 0. || y < 0. || x > width || y > height { + return false; + } + + let inside_corner = |x: f32, y: f32, center_x: f32, center_y: f32, radius: Pixels| { + let radius = radius.0; + let dx = x - center_x; + let dy = y - center_y; + dx * dx + dy * dy <= radius * radius + }; + + let top_left = corner_radii.top_left; + if x < top_left.0 && y < top_left.0 { + return inside_corner(x, y, top_left.0, top_left.0, top_left); } + + let top_right = corner_radii.top_right; + if x > width - top_right.0 && y < top_right.0 { + return inside_corner(x, y, width - top_right.0, top_right.0, top_right); + } + + let bottom_right = corner_radii.bottom_right; + if x > width - bottom_right.0 && y > height - bottom_right.0 { + return inside_corner( + x, + y, + width - bottom_right.0, + height - bottom_right.0, + bottom_right, + ); + } + + let bottom_left = corner_radii.bottom_left; + if x < bottom_left.0 && y > height - bottom_left.0 { + return inside_corner(x, y, bottom_left.0, height - bottom_left.0, bottom_left); + } + + true } impl Window { @@ -3256,15 +3386,12 @@ impl Window { /// Obtain the current content mask. This method should only be called during element drawing. pub fn content_mask(&self) -> ContentMask { self.invalidator.debug_assert_paint_or_prepaint(); - self.content_mask_stack - .last() - .cloned() - .unwrap_or_else(|| ContentMask { - bounds: Bounds { - origin: Point::default(), - size: self.viewport_size, - }, + self.content_mask_stack.last().cloned().unwrap_or_else(|| { + ContentMask::new(Bounds { + origin: Point::default(), + size: self.viewport_size, }) + }) } /// Provide elements in the called function with a new namespace in which their identifiers must be unique. @@ -4235,9 +4362,7 @@ impl Window { local_bounds, transform, inverse_transform: transform.try_inverse(), - content_mask: ContentMask { - bounds: transformed_bounds(content_mask.bounds, transform), - }, + content_mask, behavior, }; self.next_frame.hitboxes.push(hitbox.clone()); @@ -6139,10 +6264,96 @@ impl From<&'static core::panic::Location<'static>> for ElementId { #[cfg(test)] mod bounds_change_tests { + use super::{Hitbox, HitboxId}; use crate::{ - Bounds, Empty, Modifiers, Point, TestAppContext, VisualTestContext, point, px, size, + Bounds, ContentMask, Corners, Empty, HitboxBehavior, Modifiers, Point, TestAppContext, + TransformationMatrix, VisualTestContext, point, px, size, }; + #[test] + fn content_mask_intersection_preserves_rounded_ancestor() { + let ancestor_bounds = Bounds::new(point(px(0.), px(0.)), size(px(100.), px(80.))); + let child_bounds = Bounds::new(point(px(10.), px(5.)), size(px(60.), px(40.))); + let ancestor = ContentMask::rounded(ancestor_bounds, Corners::all(px(12.))); + let child = ContentMask::new(child_bounds); + + assert_eq!( + child.intersect(&ancestor), + ContentMask { + bounds: child_bounds, + rounded_bounds: ancestor_bounds, + corner_radii: Corners::all(px(12.)), + } + ); + } + + #[test] + fn content_mask_intersection_keeps_contained_rounded_child() { + let ancestor_bounds = Bounds::new(point(px(0.), px(0.)), size(px(100.), px(80.))); + let child_bounds = Bounds::new(point(px(8.), px(8.)), size(px(60.), px(40.))); + let ancestor = ContentMask::rounded(ancestor_bounds, Corners::all(px(12.))); + let child = ContentMask::rounded(child_bounds, Corners::all(px(16.))); + + assert_eq!( + child.intersect(&ancestor), + ContentMask { + bounds: child_bounds, + rounded_bounds: child_bounds, + corner_radii: Corners::all(px(16.)), + } + ); + } + + #[test] + fn hitbox_rejects_points_clipped_by_rounded_content_mask() { + let bounds = Bounds::new(point(px(0.), px(0.)), size(px(100.), px(80.))); + let transform = TransformationMatrix::unit(); + let hitbox = Hitbox { + id: HitboxId(0), + bounds, + local_bounds: bounds, + transform, + inverse_transform: Some(transform), + content_mask: ContentMask::rounded(bounds, Corners::all(px(16.))), + behavior: HitboxBehavior::Normal, + }; + + assert!(!hitbox.contains_window_point(point(px(1.), px(1.)))); + assert!(hitbox.contains_window_point(point(px(16.), px(16.)))); + } + + #[test] + fn transformed_hitbox_tests_rounded_mask_in_local_space() { + let bounds = Bounds::new(point(px(0.), px(0.)), size(px(100.), px(80.))); + let transform = TransformationMatrix::unit().scale(size(2., 2.)); + let hitbox = Hitbox { + id: HitboxId(0), + bounds: super::transformed_bounds(bounds, transform), + local_bounds: bounds, + transform, + inverse_transform: transform.try_inverse(), + content_mask: ContentMask::rounded(bounds, Corners::all(px(16.))), + behavior: HitboxBehavior::Normal, + }; + + assert!(!hitbox.contains_window_point(point(px(5.), px(5.)))); + assert!(hitbox.contains_window_point(point(px(32.), px(32.)))); + } + + #[test] + fn content_mask_scale_scales_rounded_geometry() { + let bounds = Bounds::new(point(px(2.), px(4.)), size(px(20.), px(10.))); + let mask = ContentMask::rounded(bounds, Corners::all(px(3.))); + let scaled = mask.scale(2.); + + assert_eq!(scaled.bounds.origin.x.0, 4.); + assert_eq!(scaled.bounds.origin.y.0, 8.); + assert_eq!(scaled.bounds.size.width.0, 40.); + assert_eq!(scaled.bounds.size.height.0, 20.); + assert_eq!(scaled.rounded_bounds, scaled.bounds); + assert_eq!(scaled.corner_radii, Corners::all(crate::ScaledPixels(6.))); + } + #[gpui::test] fn bounds_changed_refreshes_mouse_position_from_platform(cx: &mut TestAppContext) { let window = cx.add_window(|_, _| Empty); diff --git a/crates/gpui_macos/src/metal_renderer.rs b/crates/gpui_macos/src/metal_renderer.rs index d862fc1..3ae77fd 100644 --- a/crates/gpui_macos/src/metal_renderer.rs +++ b/crates/gpui_macos/src/metal_renderer.rs @@ -221,6 +221,7 @@ pub struct PathRasterizationVertex { pub st_position: Point, pub color: Background, pub bounds: Bounds, + pub content_mask: ContentMask, pub scratch_bounds: Bounds, pub texture_size: Size, } @@ -1378,6 +1379,7 @@ impl MetalRenderer { origin: Point, ) { Self::localize_bounds(&mut content_mask.bounds, origin); + Self::localize_bounds(&mut content_mask.rounded_bounds, origin); } fn localize_transformation( @@ -2185,6 +2187,7 @@ impl MetalRenderer { st_position: v.st_position, color: path.color, bounds: path.bounds.intersect(&path.content_mask.bounds), + content_mask: path.content_mask.clone(), scratch_bounds: scratch_bounds.bounds, texture_size: scratch_bounds.texture_size, })); @@ -3151,7 +3154,7 @@ mod tests { content_revision: 1.into(), content_dirty: true, bounds, - content_mask: ContentMask { bounds }, + content_mask: ContentMask::new(bounds), transform: TransformationMatrix::unit(), opacity: 1.0, paint_range: 0..scene.paint_operation_count(), @@ -3169,7 +3172,7 @@ mod tests { order: 0, pad: 0, bounds, - content_mask: ContentMask { bounds }, + content_mask: ContentMask::new(bounds), corner_radii: Corners::all(ScaledPixels(8.0)), blur_radius: ScaledPixels(16.0), source_origin_x: 0.0, @@ -3191,7 +3194,7 @@ mod tests { let scene = scene_with_retained_primitive(Quad { order: 0, bounds, - content_mask: ContentMask { bounds }, + content_mask: ContentMask::new(bounds), ..Default::default() }); diff --git a/crates/gpui_macos/src/shaders.metal b/crates/gpui_macos/src/shaders.metal index e20f443..897a7b2 100644 --- a/crates/gpui_macos/src/shaders.metal +++ b/crates/gpui_macos/src/shaders.metal @@ -44,6 +44,19 @@ struct GradientColor { }; GradientColor prepare_fill_color(uint tag, uint color_space, Hsla solid, Hsla color0, Hsla color1); +float content_mask_alpha(float2 position, Bounds_ScaledPixels rounded_bounds, + Corners_ScaledPixels corner_radii) { + bool has_rounded_clip = corner_radii.top_left != 0.0 || + corner_radii.top_right != 0.0 || + corner_radii.bottom_right != 0.0 || + corner_radii.bottom_left != 0.0; + if (!has_rounded_clip) { + return 1.0; + } + + return saturate(0.5 - quad_sdf(position, rounded_bounds, corner_radii)); +} + struct QuadVertexOutput { uint quad_id [[flat]]; float4 position [[position]]; @@ -247,6 +260,8 @@ fragment float4 backdrop_blur_fragment( float4 color = backdrop_texture.sample(blur_sampler, uv); float distance = quad_sdf(input.position.xy, blur.bounds, blur.corner_radii); float alpha = clamp(0.5 - distance, 0.0, 1.0); + alpha *= content_mask_alpha(input.position.xy, blur.content_mask.rounded_bounds, + blur.content_mask.corner_radii); return color * alpha; } @@ -288,6 +303,9 @@ fragment float4 quad_fragment(QuadFragmentInput input [[stage_in]], constant Quad *quads [[buffer(QuadInputIndex_Quads)]]) { Quad quad = quads[input.quad_id]; + float mask_alpha = content_mask_alpha(input.position.xy, + quad.content_mask.rounded_bounds, + quad.content_mask.corner_radii); float4 background_color = fill_color(quad.background, input.position.xy, quad.bounds, input.background_solid, input.background_color0, input.background_color1); @@ -302,7 +320,7 @@ fragment float4 quad_fragment(QuadFragmentInput input [[stage_in]], quad.border_widths.right == 0.0 && quad.border_widths.bottom == 0.0 && unrounded) { - return background_color; + return background_color * float4(1.0, 1.0, 1.0, mask_alpha); } float2 size = float2(quad.bounds.size.width, quad.bounds.size.height); @@ -362,7 +380,7 @@ fragment float4 quad_fragment(QuadFragmentInput input [[stage_in]], // Fast path for points that must be part of the background if (is_within_inner_straight_border && !is_near_rounded_corner) { - return background_color; + return background_color * float4(1.0, 1.0, 1.0, mask_alpha); } // Signed distance of the point to the outside edge of the quad's border @@ -580,7 +598,8 @@ fragment float4 quad_fragment(QuadFragmentInput input [[stage_in]], saturate(antialias_threshold - inner_sdf)); } - return color * float4(1.0, 1.0, 1.0, saturate(antialias_threshold - outer_sdf)); + return color * float4(1.0, 1.0, 1.0, + saturate(antialias_threshold - outer_sdf) * mask_alpha); } // Returns the dash velocity of a corner given the dash velocity of the two @@ -736,6 +755,9 @@ fragment float4 shadow_fragment(ShadowFragmentInput input [[stage_in]], alpha *= saturate(0.5 - element_distance); } + alpha *= content_mask_alpha(input.position.xy, + shadow.content_mask.rounded_bounds, + shadow.content_mask.corner_radii); return input.color * float4(1., 1., 1., alpha); } @@ -779,6 +801,9 @@ fragment float4 underline_fragment(UnderlineFragmentInput input [[stage_in]], const float WAVE_HEIGHT_RATIO = 0.8; Underline underline = underlines[input.underline_id]; + float mask_alpha = content_mask_alpha(input.position.xy, + underline.content_mask.rounded_bounds, + underline.content_mask.corner_radii); if (underline.wavy) { float half_thickness = underline.thickness * 0.5; float2 origin = @@ -797,9 +822,9 @@ fragment float4 underline_fragment(UnderlineFragmentInput input [[stage_in]], float distance_from_bottom_border = distance_in_pixels + half_thickness; float alpha = saturate( 0.5 - max(-distance_from_bottom_border, distance_from_top_border)); - return input.color * float4(1., 1., 1., alpha); + return input.color * float4(1., 1., 1., alpha * mask_alpha); } else { - return input.color; + return input.color * float4(1., 1., 1., mask_alpha); } } @@ -807,6 +832,7 @@ struct MonochromeSpriteVertexOutput { float4 position [[position]]; float2 tile_position; float4 color [[flat]]; + uint sprite_id [[flat]]; float4 clip_distance; }; @@ -814,6 +840,7 @@ struct MonochromeSpriteFragmentInput { float4 position [[position]]; float2 tile_position; float4 color [[flat]]; + uint sprite_id [[flat]]; float4 clip_distance; }; @@ -837,6 +864,7 @@ vertex MonochromeSpriteVertexOutput monochrome_sprite_vertex( device_position, tile_position, color, + sprite_id, {clip_distance.x, clip_distance.y, clip_distance.z, clip_distance.w}}; } @@ -852,8 +880,11 @@ fragment float4 monochrome_sprite_fragment( min_filter::linear); float4 sample = atlas_texture.sample(atlas_texture_sampler, input.tile_position); + MonochromeSprite sprite = sprites[input.sprite_id]; float4 color = input.color; - color.a *= sample.a; + color.a *= sample.a * content_mask_alpha(input.position.xy, + sprite.content_mask.rounded_bounds, + sprite.content_mask.corner_radii); return color; } @@ -912,7 +943,10 @@ fragment float4 polychrome_sprite_fragment( color.g = grayscale; color.b = grayscale; } - color.a *= sprite.opacity * saturate(0.5 - distance); + color.a *= sprite.opacity * saturate(0.5 - distance) * + content_mask_alpha(input.position.xy, + sprite.content_mask.rounded_bounds, + sprite.content_mask.corner_radii); return color; } @@ -997,7 +1031,10 @@ fragment float4 path_rasterization_fragment( gradient_color.color0, gradient_color.color1 ); - return float4(color.rgb * color.a * alpha, alpha * color.a); + float mask_alpha = content_mask_alpha(input.screen_position, + v.content_mask.rounded_bounds, + v.content_mask.corner_radii); + return float4(color.rgb * color.a * alpha, alpha * color.a) * mask_alpha; } struct PathSpriteVertexOutput { @@ -1110,18 +1147,26 @@ fragment float4 retained_layer_fragment( constant RetainedLayerSprite *layer [[buffer(RetainedLayerInputIndex_Layer)]] ) { constexpr sampler layer_texture_sampler(mag_filter::linear, min_filter::linear); - return layer_texture.sample(layer_texture_sampler, input.texture_coords) * layer[0].opacity; + float mask_alpha = content_mask_alpha(input.position.xy, + layer[0].content_mask.rounded_bounds, + layer[0].content_mask.corner_radii); + return layer_texture.sample(layer_texture_sampler, input.texture_coords) * + layer[0].opacity * mask_alpha; } struct SurfaceVertexOutput { float4 position [[position]]; float2 texture_position; + float4 content_mask_rounded_bounds [[flat]]; + float4 content_mask_corner_radii [[flat]]; float clip_distance [[clip_distance]][4]; }; struct SurfaceFragmentInput { float4 position [[position]]; float2 texture_position; + float4 content_mask_rounded_bounds [[flat]]; + float4 content_mask_corner_radii [[flat]]; }; vertex SurfaceVertexOutput surface_vertex( @@ -1141,9 +1186,15 @@ vertex SurfaceVertexOutput surface_vertex( // We are going to copy the whole texture, so the texture position corresponds // to the current vertex of the unit triangle. float2 texture_position = unit_vertex; + Bounds_ScaledPixels rounded_bounds = surface.content_mask.rounded_bounds; + Corners_ScaledPixels corner_radii = surface.content_mask.corner_radii; return SurfaceVertexOutput{ device_position, texture_position, + {rounded_bounds.origin.x, rounded_bounds.origin.y, + rounded_bounds.size.width, rounded_bounds.size.height}, + {corner_radii.top_left, corner_radii.top_right, + corner_radii.bottom_right, corner_radii.bottom_left}, {clip_distance.x, clip_distance.y, clip_distance.z, clip_distance.w}}; } @@ -1162,7 +1213,17 @@ fragment float4 surface_fragment(SurfaceFragmentInput input [[stage_in]], y_texture.sample(texture_sampler, input.texture_position).r, cb_cr_texture.sample(texture_sampler, input.texture_position).rg, 1.0); - return ycbcrToRGBTransform * ycbcr; + Bounds_ScaledPixels rounded_bounds = { + {input.content_mask_rounded_bounds.x, input.content_mask_rounded_bounds.y}, + {input.content_mask_rounded_bounds.z, input.content_mask_rounded_bounds.w}}; + Corners_ScaledPixels corner_radii = { + input.content_mask_corner_radii.x, + input.content_mask_corner_radii.y, + input.content_mask_corner_radii.z, + input.content_mask_corner_radii.w}; + float4 color = ycbcrToRGBTransform * ycbcr; + color.a *= content_mask_alpha(input.position.xy, rounded_bounds, corner_radii); + return color; } float4 hsla_to_rgba(Hsla hsla) { diff --git a/crates/gpui_wgpu/src/shaders.wgsl b/crates/gpui_wgpu/src/shaders.wgsl index 1f9d962..0d51140 100644 --- a/crates/gpui_wgpu/src/shaders.wgsl +++ b/crates/gpui_wgpu/src/shaders.wgsl @@ -112,6 +112,12 @@ struct Corners { bottom_left: f32, } +struct ContentMask { + bounds: Bounds, + rounded_bounds: Bounds, + corner_radii: Corners, +} + struct Edges { top: f32, right: f32, @@ -387,6 +393,24 @@ fn quad_sdf_impl(corner_center_to_point: vec2, corner_radius: f32) -> f32 { } } +fn content_mask_alpha(position: vec2, mask: ContentMask) -> f32 { + let has_rounded_clip = mask.corner_radii.top_left != 0.0 || + mask.corner_radii.top_right != 0.0 || + mask.corner_radii.bottom_right != 0.0 || + mask.corner_radii.bottom_left != 0.0; + if (!has_rounded_clip) { + return 1.0; + } + + return saturate(0.5 - quad_sdf(position, mask.rounded_bounds, mask.corner_radii)); +} + +fn apply_content_mask(color: vec4, position: vec2, mask: ContentMask) -> vec4 { + let alpha = content_mask_alpha(position, mask); + let color_multiplier = select(1.0, alpha, globals.premultiplied_alpha != 0u); + return vec4(color.rgb * color_multiplier, color.a * alpha); +} + // Abstract away the final color transformation based on the // target alpha compositing mode. fn blend_color(color: vec4, alpha_factor: f32) -> vec4 { @@ -521,7 +545,7 @@ struct Quad { order: u32, border_style: u32, bounds: Bounds, - content_mask: Bounds, + content_mask: ContentMask, background: Background, border_color: Hsla, corner_radii: Corners, @@ -559,7 +583,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; } @@ -586,7 +610,11 @@ fn fs_quad(input: QuadVarying) -> @location(0) vec4 { quad.border_widths.right == 0.0 && quad.border_widths.bottom == 0.0 && unrounded) { - return blend_color(background_color, 1.0); + return apply_content_mask( + blend_color(background_color, 1.0), + input.position.xy, + quad.content_mask, + ); } let size = quad.bounds.size; @@ -652,7 +680,11 @@ fn fs_quad(input: QuadVarying) -> @location(0) vec4 { // However, that might negatively impact performance in the case of // reasonable sizes for rounded corners. if (is_within_inner_straight_border && !is_near_rounded_corner) { - return blend_color(background_color, 1.0); + return apply_content_mask( + blend_color(background_color, 1.0), + input.position.xy, + quad.content_mask, + ); } // Signed distance of the point to the outside edge of the quad's border. It @@ -891,7 +923,11 @@ fn fs_quad(input: QuadVarying) -> @location(0) vec4 { saturate(antialias_threshold - inner_sdf)); } - return blend_color(color, saturate(antialias_threshold - outer_sdf)); + return apply_content_mask( + blend_color(color, saturate(antialias_threshold - outer_sdf)), + input.position.xy, + quad.content_mask, + ); } // Returns the dash velocity of a corner given the dash velocity of the two @@ -955,7 +991,7 @@ struct Shadow { // The shadow rect for drop shadows; the "hole" rect for inset shadows. bounds: Bounds, corner_radii: Corners, - content_mask: Bounds, + content_mask: ContentMask, 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. @@ -995,7 +1031,7 @@ fn vs_shadow(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) ins 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, geometry, shadow.content_mask); + out.clip_distances = distance_from_clip_rect(unit_vertex, geometry, shadow.content_mask.bounds); return out; } @@ -1045,7 +1081,11 @@ fn fs_shadow(input: ShadowVarying) -> @location(0) vec4 { alpha *= saturate(0.5 - element_distance); } - return blend_color(input.color, alpha); + return apply_content_mask( + blend_color(input.color, alpha), + input.position.xy, + shadow.content_mask, + ); } // --- backdrop blurs --- // @@ -1054,7 +1094,7 @@ struct BackdropBlur { order: u32, pad: u32, bounds: Bounds, - content_mask: Bounds, + content_mask: ContentMask, corner_radii: Corners, blur_radius: f32, source_origin_x: f32, @@ -1088,7 +1128,7 @@ fn vs_backdrop_blur(@builtin(vertex_index) vertex_id: u32, @builtin(instance_ind var out = BackdropBlurVarying(); out.position = to_device_position(unit_vertex, padded_bounds); out.blur_id = instance_id; - out.clip_distances = distance_from_clip_rect(unit_vertex_original, blur.bounds, blur.content_mask); + out.clip_distances = distance_from_clip_rect(unit_vertex_original, blur.bounds, blur.content_mask.bounds); return out; } @@ -1137,9 +1177,13 @@ fn fs_backdrop_blur(input: BackdropBlurVarying) -> @location(0) vec4 { let blurred_rgb = linear_to_srgb(accum_linear / safe_alpha); let distance = quad_sdf(input.position.xy, blur.bounds, blur.corner_radii); - let mask_alpha = saturate(0.5 - distance); - let alpha = (accum_alpha / max(weight_sum, 0.0001)) * mask_alpha; - return blend_color(vec4(blurred_rgb, 1.0), alpha); + let surface_alpha = saturate(0.5 - distance); + let alpha = (accum_alpha / max(weight_sum, 0.0001)) * surface_alpha; + return apply_content_mask( + blend_color(vec4(blurred_rgb, 1.0), alpha), + input.position.xy, + blur.content_mask, + ); } // --- path rasterization --- // @@ -1149,6 +1193,7 @@ struct PathRasterizationVertex { st_position: vec2, color: Background, bounds: Bounds, + content_mask: ContentMask, scratch_bounds: Bounds, texture_size: vec2, } @@ -1209,7 +1254,8 @@ fn fs_path_rasterization(input: PathRasterizationVarying) -> @location(0) vec4(color.rgb * color.a * alpha, color.a * alpha); + return vec4(color.rgb * color.a * alpha, color.a * alpha) * + content_mask_alpha(input.screen_position, v.content_mask); } // --- paths --- // @@ -1253,7 +1299,7 @@ fn fs_path(input: PathVarying) -> @location(0) vec4 { struct RetainedLayerSprite { bounds: Bounds, - content_mask: Bounds, + content_mask: ContentMask, transformation: TransformationMatrix, opacity: f32, pad0: f32, @@ -1267,6 +1313,7 @@ struct RetainedLayerVarying { @location(0) texture_coords: vec2, @location(1) clip_distances: vec4, @location(2) @interpolate(flat) opacity: f32, + @location(3) @interpolate(flat) layer_id: u32, } @vertex @@ -1277,8 +1324,9 @@ fn vs_retained_layer(@builtin(vertex_index) vertex_id: u32, @builtin(instance_in var out = RetainedLayerVarying(); out.position = to_device_position_transformed(unit_vertex, layer.bounds, layer.transformation); out.texture_coords = unit_vertex; - out.clip_distances = distance_from_clip_rect_transformed(unit_vertex, layer.bounds, layer.content_mask, layer.transformation); + out.clip_distances = distance_from_clip_rect_transformed(unit_vertex, layer.bounds, layer.content_mask.bounds, layer.transformation); out.opacity = layer.opacity; + out.layer_id = instance_id; return out; } @@ -1291,7 +1339,12 @@ fn fs_retained_layer(input: RetainedLayerVarying) -> @location(0) vec4 { let sample = textureSample(t_sprite, s_sprite, input.texture_coords); let alpha = sample.a * input.opacity; let color_multiplier = select(1.0, input.opacity, globals.premultiplied_alpha != 0u); - return vec4(sample.rgb * color_multiplier, alpha); + let layer = b_retained_layers[input.layer_id]; + return apply_content_mask( + vec4(sample.rgb * color_multiplier, alpha), + input.position.xy, + layer.content_mask, + ); } // --- underlines --- // @@ -1300,7 +1353,7 @@ struct Underline { order: u32, pad: u32, bounds: Bounds, - content_mask: Bounds, + content_mask: ContentMask, color: Hsla, thickness: f32, wavy: u32, @@ -1324,7 +1377,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; } @@ -1339,9 +1392,10 @@ fn fs_underline(input: UnderlineVarying) -> @location(0) vec4 { } let underline = b_underlines[input.underline_id]; + let mask_alpha = content_mask_alpha(input.position.xy, underline.content_mask); if (underline.wavy == 0u) { - return blend_color(input.color, input.color.a); + return blend_color(input.color, mask_alpha); } let half_thickness = underline.thickness * 0.5; @@ -1357,7 +1411,7 @@ fn fs_underline(input: UnderlineVarying) -> @location(0) vec4 { let distance_from_top_border = distance_in_pixels - half_thickness; let distance_from_bottom_border = distance_in_pixels + half_thickness; let alpha = saturate(0.5 - max(-distance_from_bottom_border, distance_from_top_border)); - return blend_color(input.color, alpha * input.color.a); + return blend_color(input.color, alpha * mask_alpha); } // --- monochrome sprites --- // @@ -1366,7 +1420,7 @@ struct MonochromeSprite { order: u32, pad: u32, bounds: Bounds, - content_mask: Bounds, + content_mask: ContentMask, color: Hsla, tile: AtlasTile, transformation: TransformationMatrix, @@ -1377,6 +1431,7 @@ struct MonoSpriteVarying { @builtin(position) position: vec4, @location(0) tile_position: vec2, @location(1) @interpolate(flat) color: vec4, + @location(2) @interpolate(flat) sprite_id: u32, @location(3) clip_distances: vec4, } @@ -1390,7 +1445,8 @@ 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_transformed(unit_vertex, sprite.bounds, sprite.content_mask, sprite.transformation); + out.sprite_id = instance_id; + out.clip_distances = distance_from_clip_rect_transformed(unit_vertex, sprite.bounds, sprite.content_mask.bounds, sprite.transformation); return out; } @@ -1404,7 +1460,11 @@ fn fs_mono_sprite(input: MonoSpriteVarying) -> @location(0) vec4 { return vec4(0.0); } - return blend_color(input.color, alpha_corrected); + let sprite = b_mono_sprites[input.sprite_id]; + return blend_color( + input.color, + alpha_corrected * content_mask_alpha(input.position.xy, sprite.content_mask), + ); } // --- polychrome sprites --- // @@ -1415,7 +1475,7 @@ struct PolychromeSprite { grayscale: u32, opacity: f32, bounds: Bounds, - content_mask: Bounds, + content_mask: ContentMask, corner_radii: Corners, tile: AtlasTile, } @@ -1437,7 +1497,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; } @@ -1457,14 +1517,18 @@ fn fs_poly_sprite(input: PolySpriteVarying) -> @location(0) vec4 { let grayscale = dot(color.rgb, GRAYSCALE_FACTORS); color = vec4(vec3(grayscale), sample.a); } - return blend_color(color, sprite.opacity * saturate(0.5 - distance)); + return blend_color( + color, + sprite.opacity * saturate(0.5 - distance) * + content_mask_alpha(input.position.xy, sprite.content_mask), + ); } // --- surfaces --- // struct SurfaceParams { bounds: Bounds, - content_mask: Bounds, + content_mask: ContentMask, } @group(1) @binding(0) var surface_locals: SurfaceParams; @@ -1492,7 +1556,7 @@ fn vs_surface(@builtin(vertex_index) vertex_id: u32) -> SurfaceVarying { var out = SurfaceVarying(); out.position = to_device_position(unit_vertex, surface_locals.bounds); out.texture_position = unit_vertex; - out.clip_distances = distance_from_clip_rect(unit_vertex, surface_locals.bounds, surface_locals.content_mask); + out.clip_distances = distance_from_clip_rect(unit_vertex, surface_locals.bounds, surface_locals.content_mask.bounds); return out; } @@ -1508,7 +1572,11 @@ fn fs_surface(input: SurfaceVarying) -> @location(0) vec4 { textureSampleLevel(t_cb_cr, s_surface, input.texture_position, 0.0).rg, 1.0); - return ycbcr_to_RGB * y_cb_cr; + return apply_content_mask( + ycbcr_to_RGB * y_cb_cr, + input.position.xy, + surface_locals.content_mask, + ); } // --- subpixel sprites --- // @@ -1517,7 +1585,7 @@ struct SubpixelSprite { order: u32, pad: u32, bounds: Bounds, - content_mask: Bounds, + content_mask: ContentMask, color: Hsla, tile: AtlasTile, transformation: TransformationMatrix, @@ -1528,6 +1596,7 @@ struct SubpixelSpriteOutput { @builtin(position) position: vec4, @location(0) tile_position: vec2, @location(1) @interpolate(flat) color: vec4, + @location(2) @interpolate(flat) sprite_id: u32, @location(3) clip_distances: vec4, } @@ -1545,7 +1614,8 @@ fn vs_subpixel_sprite(@builtin(vertex_index) vertex_id: u32, @builtin(instance_i out.position = to_device_position_transformed(unit_vertex, sprite.bounds, sprite.transformation); out.tile_position = to_tile_position(unit_vertex, sprite.tile); out.color = hsla_to_rgba(sprite.color); - out.clip_distances = distance_from_clip_rect_transformed(unit_vertex, sprite.bounds, sprite.content_mask, sprite.transformation); + out.sprite_id = instance_id; + out.clip_distances = distance_from_clip_rect_transformed(unit_vertex, sprite.bounds, sprite.content_mask.bounds, sprite.transformation); return out; } @@ -1562,8 +1632,11 @@ fn fs_subpixel_sprite(input: SubpixelSpriteOutput) -> SubpixelSpriteFragmentOutp return SubpixelSpriteFragmentOutput(vec4(0.0), vec4(0.0)); } + let sprite = b_subpixel_sprites[input.sprite_id]; + let mask_alpha = content_mask_alpha(input.position.xy, sprite.content_mask); + var out = SubpixelSpriteFragmentOutput(); out.foreground = vec4(input.color.rgb, 1.0); - out.alpha = vec4(input.color.a * alpha_corrected, 1.0); + out.alpha = vec4(input.color.a * alpha_corrected * mask_alpha, 1.0); return out; } diff --git a/crates/gpui_wgpu/src/wgpu_renderer.rs b/crates/gpui_wgpu/src/wgpu_renderer.rs index 5cd1929..3ecdba6 100644 --- a/crates/gpui_wgpu/src/wgpu_renderer.rs +++ b/crates/gpui_wgpu/src/wgpu_renderer.rs @@ -106,11 +106,19 @@ impl From> for PodBounds { } } +#[repr(C)] +#[derive(Clone, Copy, Pod, Zeroable)] +struct PodContentMask { + bounds: PodBounds, + rounded_bounds: PodBounds, + corner_radii: [f32; 4], +} + #[repr(C)] #[derive(Clone, Copy, Pod, Zeroable)] struct SurfaceParams { bounds: PodBounds, - content_mask: PodBounds, + content_mask: PodContentMask, } #[derive(Clone, Debug)] @@ -148,6 +156,7 @@ struct PathRasterizationVertex { st_position: Point, color: Background, bounds: Bounds, + content_mask: ContentMask, scratch_bounds: Bounds, texture_size: [f32; 2], } @@ -1763,49 +1772,57 @@ impl WgpuRenderer { fn localize_scene(scene: &mut Scene, origin: Point) { for quad in &mut scene.quads { quad.bounds = quad.bounds - origin; - quad.content_mask.bounds = quad.content_mask.bounds - origin; + Self::localize_content_mask(&mut quad.content_mask, origin); } for shadow in &mut scene.shadows { shadow.bounds = shadow.bounds - origin; shadow.element_bounds = shadow.element_bounds - origin; - shadow.content_mask.bounds = shadow.content_mask.bounds - origin; + Self::localize_content_mask(&mut shadow.content_mask, origin); } for blur in &mut scene.backdrop_blurs { blur.bounds = blur.bounds - origin; - blur.content_mask.bounds = blur.content_mask.bounds - origin; + Self::localize_content_mask(&mut blur.content_mask, origin); } for path in &mut scene.paths { path.bounds = path.bounds - origin; - path.content_mask.bounds = path.content_mask.bounds - origin; + Self::localize_content_mask(&mut path.content_mask, origin); for vertex in &mut path.vertices { vertex.xy_position -= origin; - vertex.content_mask.bounds = vertex.content_mask.bounds - origin; + Self::localize_content_mask(&mut vertex.content_mask, origin); } } for underline in &mut scene.underlines { underline.bounds = underline.bounds - origin; - underline.content_mask.bounds = underline.content_mask.bounds - origin; + Self::localize_content_mask(&mut underline.content_mask, origin); } for sprite in &mut scene.monochrome_sprites { sprite.bounds = sprite.bounds - origin; - sprite.content_mask.bounds = sprite.content_mask.bounds - origin; + Self::localize_content_mask(&mut sprite.content_mask, origin); sprite.transformation = Self::localize_transform(sprite.transformation, origin); } for sprite in &mut scene.subpixel_sprites { sprite.bounds = sprite.bounds - origin; - sprite.content_mask.bounds = sprite.content_mask.bounds - origin; + Self::localize_content_mask(&mut sprite.content_mask, origin); sprite.transformation = Self::localize_transform(sprite.transformation, origin); } for sprite in &mut scene.polychrome_sprites { sprite.bounds = sprite.bounds - origin; - sprite.content_mask.bounds = sprite.content_mask.bounds - origin; + Self::localize_content_mask(&mut sprite.content_mask, origin); } for surface in &mut scene.surfaces { surface.bounds = surface.bounds - origin; - surface.content_mask.bounds = surface.content_mask.bounds - origin; + Self::localize_content_mask(&mut surface.content_mask, origin); } } + fn localize_content_mask( + content_mask: &mut ContentMask, + origin: Point, + ) { + content_mask.bounds = content_mask.bounds - origin; + content_mask.rounded_bounds = content_mask.rounded_bounds - origin; + } + fn localize_transform( transform: TransformationMatrix, origin: Point, @@ -2694,6 +2711,7 @@ impl WgpuRenderer { st_position: v.st_position, color: path.color, bounds, + content_mask: path.content_mask.clone(), scratch_bounds: scratch_bounds.bounds, texture_size: [ scratch_bounds.texture_size.width.0 as f32, @@ -2967,7 +2985,7 @@ mod tests { content_revision: 1.into(), content_dirty: true, bounds, - content_mask: ContentMask { bounds }, + content_mask: ContentMask::new(bounds), transform: TransformationMatrix::unit(), opacity: 1.0, paint_range: 0..scene.paint_operation_count(), @@ -3078,7 +3096,7 @@ mod tests { order: 0, pad: 0, bounds, - content_mask: ContentMask { bounds }, + content_mask: ContentMask::new(bounds), corner_radii: Corners::all(ScaledPixels(8.0)), blur_radius: ScaledPixels(16.0), source_origin_x: 0.0, @@ -3103,7 +3121,7 @@ mod tests { let (scene, layer) = scene_with_retained_primitive(Quad { order: 0, bounds, - content_mask: ContentMask { bounds }, + content_mask: ContentMask::new(bounds), ..Default::default() }); diff --git a/crates/gpui_windows/src/directx_renderer.rs b/crates/gpui_windows/src/directx_renderer.rs index 557bc40..f45df17 100644 --- a/crates/gpui_windows/src/directx_renderer.rs +++ b/crates/gpui_windows/src/directx_renderer.rs @@ -1403,6 +1403,7 @@ impl DirectXRenderer { st_position: v.st_position, color: path.color, bounds: path.clipped_bounds(), + content_mask: path.content_mask.clone(), scratch_bounds: scratch_bounds.bounds, texture_size: [ scratch_bounds.texture_size.width.0 as f32, @@ -2410,13 +2411,14 @@ impl PipelineState { } } -#[derive(Clone, Copy)] +#[derive(Clone)] #[repr(C)] struct PathRasterizationSprite { xy_position: Point, st_position: Point, color: Background, bounds: Bounds, + content_mask: ContentMask, scratch_bounds: Bounds, texture_size: [f32; 2], } diff --git a/crates/gpui_windows/src/shaders.hlsl b/crates/gpui_windows/src/shaders.hlsl index 2997695..0117c5a 100644 --- a/crates/gpui_windows/src/shaders.hlsl +++ b/crates/gpui_windows/src/shaders.hlsl @@ -29,6 +29,12 @@ struct Corners { float bottom_left; }; +struct ContentMask { + Bounds bounds; + Bounds rounded_bounds; + Corners corner_radii; +}; + struct Edges { float top; float right; @@ -309,6 +315,20 @@ float quad_sdf(float2 pt, Bounds bounds, Corners corner_radii) { return quad_sdf_impl(corner_center_to_point, corner_radius); } +float content_mask_alpha(float2 position, ContentMask content_mask) { + bool has_rounded_clip = content_mask.corner_radii.top_left != 0.0 || + content_mask.corner_radii.top_right != 0.0 || + content_mask.corner_radii.bottom_right != 0.0 || + content_mask.corner_radii.bottom_left != 0.0; + if (!has_rounded_clip) { + return 1.0; + } + + float distance = quad_sdf(position, content_mask.rounded_bounds, + content_mask.corner_radii); + return saturate(0.5 - distance); +} + GradientColor prepare_gradient_color(uint tag, uint color_space, Hsla solid, LinearColorStop colors[2]) { GradientColor output; if (tag == 0 || tag == 2 || tag == 3) { @@ -495,7 +515,7 @@ struct Quad { uint order; uint border_style; Bounds bounds; - Bounds content_mask; + ContentMask content_mask; Background background; Hsla border_color; Corners corner_radii; @@ -533,7 +553,7 @@ struct BackdropBlur { uint order; uint pad; Bounds bounds; - Bounds content_mask; + ContentMask content_mask; Corners corner_radii; float blur_radius; float source_origin_x; @@ -659,7 +679,7 @@ BackdropBlurVertexOutput backdrop_blur_vertex(uint vertex_id: SV_VertexID, uint float2 padded_size = blur.bounds.size + float2(pad * 2.0, pad * 2.0); float2 unit_vertex_original = (unit_vertex * padded_size - float2(pad, pad)) / blur.bounds.size; float4 device_position = to_device_position(unit_vertex, padded_bounds); - float4 clip_distance = distance_from_clip_rect(unit_vertex_original, blur.bounds, blur.content_mask); + float4 clip_distance = distance_from_clip_rect(unit_vertex_original, blur.bounds, blur.content_mask.bounds); BackdropBlurVertexOutput output; output.position = device_position; @@ -677,6 +697,7 @@ float4 backdrop_blur_fragment(BackdropBlurFragmentInput input) : SV_Target { float distance = quad_sdf(input.position.xy, blur.bounds, blur.corner_radii); float alpha = saturate(0.5 - distance); + alpha *= content_mask_alpha(input.position.xy, blur.content_mask); return color * alpha; } @@ -691,7 +712,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; @@ -709,6 +730,7 @@ float4 quad_fragment(QuadFragmentInput input): SV_Target { Quad quad = quads[input.quad_id]; float4 background_color = gradient_color(quad.background, input.position.xy, quad.bounds, input.background_solid, input.background_color0, input.background_color1); + float mask_alpha = content_mask_alpha(input.position.xy, quad.content_mask); bool unrounded = quad.corner_radii.top_left == 0.0 && quad.corner_radii.top_right == 0.0 && @@ -721,6 +743,7 @@ float4 quad_fragment(QuadFragmentInput input): SV_Target { quad.border_widths.right == 0.0 && quad.border_widths.bottom == 0.0 && unrounded) { + background_color.a *= mask_alpha; return background_color; } @@ -780,6 +803,7 @@ float4 quad_fragment(QuadFragmentInput input): SV_Target { // Fast path for points that must be part of the background if (is_within_inner_straight_border && !is_near_rounded_corner) { + background_color.a *= mask_alpha; return background_color; } @@ -993,7 +1017,8 @@ float4 quad_fragment(QuadFragmentInput input): SV_Target { saturate(antialias_threshold - inner_sdf)); } - return color * float4(1.0, 1.0, 1.0, saturate(antialias_threshold - outer_sdf)); + return color * float4(1.0, 1.0, 1.0, + saturate(antialias_threshold - outer_sdf) * mask_alpha); } /* @@ -1007,7 +1032,7 @@ struct Shadow { float blur_radius; Bounds bounds; Corners corner_radii; - Bounds content_mask; + ContentMask content_mask; Hsla color; Bounds element_bounds; Corners element_corner_radii; @@ -1046,7 +1071,7 @@ ShadowVertexOutput shadow_vertex(uint vertex_id: SV_VertexID, uint shadow_id: SV } 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; @@ -1098,6 +1123,7 @@ float4 shadow_fragment(ShadowFragmentInput input): SV_TARGET { alpha *= saturate(0.5 - element_distance); } + alpha *= content_mask_alpha(input.position.xy, shadow.content_mask); return input.color * float4(1., 1., 1., alpha); } @@ -1112,6 +1138,7 @@ struct PathRasterizationSprite { float2 st_position; Background color; Bounds bounds; + ContentMask content_mask; Bounds scratch_bounds; float2 texture_size; }; @@ -1171,7 +1198,8 @@ float4 path_rasterization_fragment(PathFragmentInput input): SV_Target { float4 color = gradient_color(background, input.screen_position, bounds, gradient.solid, gradient.color0, gradient.color1); - return float4(color.rgb * color.a * alpha, alpha * color.a); + float mask_alpha = content_mask_alpha(input.screen_position, sprite.content_mask); + return float4(color.rgb * color.a * alpha, alpha * color.a) * mask_alpha; } /* @@ -1223,7 +1251,7 @@ struct Underline { uint order; uint pad; Bounds bounds; - Bounds content_mask; + ContentMask content_mask; Hsla color; float thickness; uint wavy; @@ -1249,7 +1277,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; @@ -1265,6 +1293,7 @@ float4 underline_fragment(UnderlineFragmentInput input): SV_Target { const float WAVE_HEIGHT_RATIO = 0.8; Underline underline = underlines[input.underline_id]; + float mask_alpha = content_mask_alpha(input.position.xy, underline.content_mask); if (underline.wavy) { float half_thickness = underline.thickness * 0.5; float2 origin = underline.bounds.origin; @@ -1281,9 +1310,9 @@ float4 underline_fragment(UnderlineFragmentInput input): SV_Target { float distance_from_bottom_border = distance_in_pixels + half_thickness; float alpha = saturate( 0.5 - max(-distance_from_bottom_border, distance_from_top_border)); - return input.color * float4(1., 1., 1., alpha); + return input.color * float4(1., 1., 1., alpha * mask_alpha); } else { - return input.color; + return input.color * float4(1., 1., 1., mask_alpha); } } @@ -1297,13 +1326,14 @@ struct MonochromeSprite { uint order; uint pad; Bounds bounds; - Bounds content_mask; + ContentMask content_mask; Hsla color; AtlasTile tile; TransformationMatrix transformation; }; struct MonochromeSpriteVertexOutput { + nointerpolation uint sprite_id: TEXCOORD0; float4 position: SV_Position; float2 tile_position: POSITION; nointerpolation float4 color: COLOR; @@ -1311,6 +1341,7 @@ struct MonochromeSpriteVertexOutput { }; struct MonochromeSpriteFragmentInput { + nointerpolation uint sprite_id: TEXCOORD0; float4 position: SV_Position; float2 tile_position: POSITION; nointerpolation float4 color: COLOR; @@ -1324,11 +1355,12 @@ 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_transformed(unit_vertex, sprite.bounds, sprite.content_mask, sprite.transformation); + float4 clip_distance = distance_from_clip_rect_transformed(unit_vertex, sprite.bounds, sprite.content_mask.bounds, sprite.transformation); float2 tile_position = to_tile_position(unit_vertex, sprite.tile); float4 color = hsla_to_rgba(sprite.color); MonochromeSpriteVertexOutput output; + output.sprite_id = sprite_id; output.position = device_position; output.tile_position = tile_position; output.color = color; @@ -1337,9 +1369,11 @@ MonochromeSpriteVertexOutput monochrome_sprite_vertex(uint vertex_id: SV_VertexI } float4 monochrome_sprite_fragment(MonochromeSpriteFragmentInput input): SV_Target { + MonochromeSprite sprite = mono_sprites[input.sprite_id]; float sample = t_sprite.Sample(s_sprite, input.tile_position).r; float alpha_corrected = apply_contrast_and_gamma_correction(sample, input.color.rgb, grayscale_enhanced_contrast, gamma_ratios); - return float4(input.color.rgb, input.color.a * alpha_corrected); + float mask_alpha = content_mask_alpha(input.position.xy, sprite.content_mask); + return float4(input.color.rgb, input.color.a * alpha_corrected * mask_alpha); } MonochromeSpriteVertexOutput subpixel_sprite_vertex(uint vertex_id: SV_VertexID, uint sprite_id: SV_InstanceID) { @@ -1347,15 +1381,17 @@ MonochromeSpriteVertexOutput subpixel_sprite_vertex(uint vertex_id: SV_VertexID, } SubpixelSpriteFragmentOutput subpixel_sprite_fragment(MonochromeSpriteFragmentInput input) { + MonochromeSprite sprite = mono_sprites[input.sprite_id]; float3 sample = t_sprite.Sample(s_sprite, input.tile_position).rgb; if (is_bgr) { sample = sample.bgr; } float3 alpha_corrected = apply_contrast_and_gamma_correction3(sample, input.color.rgb, subpixel_enhanced_contrast, gamma_ratios); + float mask_alpha = content_mask_alpha(input.position.xy, sprite.content_mask); SubpixelSpriteFragmentOutput output; output.foreground = float4(input.color.rgb, 1.0f); - output.alpha = float4(input.color.a * alpha_corrected, 1.0f); + output.alpha = float4(input.color.a * alpha_corrected * mask_alpha, 1.0f); return output; } @@ -1371,7 +1407,7 @@ struct PolychromeSprite { uint grayscale; float opacity; Bounds bounds; - Bounds content_mask; + ContentMask content_mask; Corners corner_radii; AtlasTile tile; }; @@ -1396,7 +1432,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; @@ -1418,5 +1454,6 @@ float4 polychrome_sprite_fragment(PolychromeSpriteFragmentInput input): SV_Targe color = float4(grayscale, sample.a); } color.a *= sprite.opacity * saturate(0.5 - distance); + color.a *= content_mask_alpha(input.position.xy, sprite.content_mask); return color; }