From ccfc1ce387f0078979798137fff286a32160e454 Mon Sep 17 00:00:00 2001 From: Sunli Date: Thu, 9 Oct 2025 20:53:36 +0800 Subject: [PATCH] gpui: Fix drawing rotated SVGs (#33288) Fixes: https://github.com/longbridge/gpui-component/issues/994 1. When SVG is rotated, incorrect graphics are drawn. For example: the original aspect ratio of the SVG is 1:1, if the bounds used to render the SVG are 400x200 (aspect ratio 2:1), [here](https://github.com/zed-industries/zed/blob/21f985a018f7cca9c0fb7f5b7a87555486ab9db5/crates/gpui/src/svg_renderer.rs#L91) the width is used as the scaling factor, causing the rendered SVG to only have half the height. This PR ensures the complete SVG image is always rendered. 2. The clipping region has no transformation applied, I added a function called `distance_from_clip_rect_transformed` in the shader. 3. Fixed `monochrome_sprite_fragment` in `shader.metal` not applying clipping region. ### Before: https://github.com/user-attachments/assets/8f93ac36-281e-4837-96cd-c308bfbf92d1 ### After: https://github.com/user-attachments/assets/f52b67a6-4cb9-4d6c-b759-bbb91b59c1cf Release Notes: - N/A --------- Co-authored-by: Jason Lee --- crates/gpui/examples/animation.rs | 85 +++++++++++++------ crates/gpui/src/platform/blade/shaders.wgsl | 8 +- crates/gpui/src/platform/mac/shaders.metal | 30 ++++++- crates/gpui/src/platform/windows/shaders.hlsl | 8 +- crates/gpui/src/svg_renderer.rs | 30 ++++--- crates/gpui/src/window.rs | 19 +++-- 6 files changed, 130 insertions(+), 50 deletions(-) diff --git a/crates/gpui/examples/animation.rs b/crates/gpui/examples/animation.rs index 90a8dc57302c91723fc7b2b1927e2ec7c0c8e81d..16d6e1b269975f61316fa35880d5d3924790fed1 100644 --- a/crates/gpui/examples/animation.rs +++ b/crates/gpui/examples/animation.rs @@ -3,8 +3,8 @@ use std::time::Duration; use anyhow::Result; use gpui::{ Animation, AnimationExt as _, App, Application, AssetSource, Bounds, Context, SharedString, - Transformation, Window, WindowBounds, WindowOptions, black, bounce, div, ease_in_out, - percentage, prelude::*, px, rgb, size, svg, + Transformation, Window, WindowBounds, WindowOptions, bounce, div, ease_in_out, percentage, + prelude::*, px, size, svg, }; struct Assets {} @@ -37,37 +37,66 @@ struct AnimationExample {} impl Render for AnimationExample { fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - div().flex().flex_col().size_full().justify_around().child( - div().flex().flex_row().w_full().justify_around().child( + div() + .flex() + .flex_col() + .size_full() + .bg(gpui::white()) + .text_color(gpui::black()) + .justify_around() + .child( div() .flex() - .bg(rgb(0x2e7d32)) - .size(px(300.0)) - .justify_center() - .items_center() - .shadow_lg() - .text_xl() - .text_color(black()) - .child("hello") + .flex_col() + .size_full() + .justify_around() .child( - svg() - .size_8() - .path(ARROW_CIRCLE_SVG) - .text_color(black()) - .with_animation( - "image_circle", - Animation::new(Duration::from_secs(2)) - .repeat() - .with_easing(bounce(ease_in_out)), - |svg, delta| { - svg.with_transformation(Transformation::rotate(percentage( - delta, - ))) - }, + div() + .id("content") + .flex() + .flex_col() + .h(px(150.)) + .overflow_y_scroll() + .w_full() + .flex_1() + .justify_center() + .items_center() + .text_xl() + .gap_4() + .child("Hello Animation") + .child( + svg() + .size_20() + .overflow_hidden() + .path(ARROW_CIRCLE_SVG) + .text_color(gpui::black()) + .with_animation( + "image_circle", + Animation::new(Duration::from_secs(2)) + .repeat() + .with_easing(bounce(ease_in_out)), + |svg, delta| { + svg.with_transformation(Transformation::rotate( + percentage(delta), + )) + }, + ), ), + ) + .child( + div() + .flex() + .h(px(64.)) + .w_full() + .p_2() + .justify_center() + .items_center() + .border_t_1() + .border_color(gpui::black().opacity(0.1)) + .bg(gpui::black().opacity(0.05)) + .child("Other Panel"), ), - ), - ) + ) } } diff --git a/crates/gpui/src/platform/blade/shaders.wgsl b/crates/gpui/src/platform/blade/shaders.wgsl index 14e5ff4fa8e9c12c6f26f6b0c2f5895e10e6b2d8..d00e596c1043c4df3ed7cf1e38943496fcebbda2 100644 --- a/crates/gpui/src/platform/blade/shaders.wgsl +++ b/crates/gpui/src/platform/blade/shaders.wgsl @@ -172,6 +172,12 @@ fn distance_from_clip_rect(unit_vertex: vec2, bounds: Bounds, clip_bounds: return distance_from_clip_rect_impl(position, clip_bounds); } +fn distance_from_clip_rect_transformed(unit_vertex: vec2, bounds: Bounds, clip_bounds: Bounds, transform: TransformationMatrix) -> vec4 { + let position = unit_vertex * vec2(bounds.size) + bounds.origin; + let transformed = transpose(transform.rotation_scale) * position + transform.translation; + return distance_from_clip_rect_impl(transformed, clip_bounds); +} + // https://gamedev.stackexchange.com/questions/92015/optimized-linear-to-srgb-glsl fn srgb_to_linear(srgb: vec3) -> vec3 { let cutoff = srgb < vec3(0.04045); @@ -1150,7 +1156,7 @@ fn vs_mono_sprite(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index out.tile_position = to_tile_position(unit_vertex, sprite.tile); out.color = hsla_to_rgba(sprite.color); - out.clip_distances = distance_from_clip_rect(unit_vertex, sprite.bounds, sprite.content_mask); + out.clip_distances = distance_from_clip_rect_transformed(unit_vertex, sprite.bounds, sprite.content_mask, sprite.transformation); return out; } diff --git a/crates/gpui/src/platform/mac/shaders.metal b/crates/gpui/src/platform/mac/shaders.metal index 83c978b853443d5c612f514625f94b6d6725be8a..37ec7b530a9cbdf562c179ee10cc4c82af07f0d2 100644 --- a/crates/gpui/src/platform/mac/shaders.metal +++ b/crates/gpui/src/platform/mac/shaders.metal @@ -18,6 +18,8 @@ float2 to_tile_position(float2 unit_vertex, AtlasTile tile, constant Size_DevicePixels *atlas_size); float4 distance_from_clip_rect(float2 unit_vertex, Bounds_ScaledPixels bounds, Bounds_ScaledPixels clip_bounds); +float4 distance_from_clip_rect_transformed(float2 unit_vertex, Bounds_ScaledPixels bounds, + Bounds_ScaledPixels clip_bounds, TransformationMatrix transformation); float corner_dash_velocity(float dv1, float dv2); float dash_alpha(float t, float period, float length, float dash_velocity, float antialias_threshold); @@ -599,13 +601,14 @@ struct MonochromeSpriteVertexOutput { float4 position [[position]]; float2 tile_position; float4 color [[flat]]; - float clip_distance [[clip_distance]][4]; + float4 clip_distance; }; struct MonochromeSpriteFragmentInput { float4 position [[position]]; float2 tile_position; float4 color [[flat]]; + float4 clip_distance; }; vertex MonochromeSpriteVertexOutput monochrome_sprite_vertex( @@ -620,8 +623,8 @@ vertex MonochromeSpriteVertexOutput monochrome_sprite_vertex( MonochromeSprite sprite = sprites[sprite_id]; float4 device_position = to_device_position_transformed(unit_vertex, sprite.bounds, sprite.transformation, viewport_size); - float4 clip_distance = distance_from_clip_rect(unit_vertex, sprite.bounds, - sprite.content_mask.bounds); + 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, atlas_size); float4 color = hsla_to_rgba(sprite.color); return MonochromeSpriteVertexOutput{ @@ -635,6 +638,10 @@ fragment float4 monochrome_sprite_fragment( MonochromeSpriteFragmentInput input [[stage_in]], constant MonochromeSprite *sprites [[buffer(SpriteInputIndex_Sprites)]], texture2d atlas_texture [[texture(SpriteInputIndex_AtlasTexture)]]) { + if (any(input.clip_distance < float4(0.0))) { + return float4(0.0); + } + constexpr sampler atlas_texture_sampler(mag_filter::linear, min_filter::linear); float4 sample = @@ -1096,6 +1103,23 @@ float4 distance_from_clip_rect(float2 unit_vertex, Bounds_ScaledPixels bounds, clip_bounds.origin.y + clip_bounds.size.height - position.y); } +float4 distance_from_clip_rect_transformed(float2 unit_vertex, Bounds_ScaledPixels bounds, + Bounds_ScaledPixels clip_bounds, TransformationMatrix transformation) { + float2 position = + unit_vertex * float2(bounds.size.width, bounds.size.height) + + float2(bounds.origin.x, bounds.origin.y); + float2 transformed_position = float2(0, 0); + transformed_position[0] = position[0] * transformation.rotation_scale[0][0] + position[1] * transformation.rotation_scale[0][1]; + transformed_position[1] = position[0] * transformation.rotation_scale[1][0] + position[1] * transformation.rotation_scale[1][1]; + transformed_position[0] += transformation.translation[0]; + transformed_position[1] += transformation.translation[1]; + + return float4(transformed_position.x - clip_bounds.origin.x, + clip_bounds.origin.x + clip_bounds.size.width - transformed_position.x, + transformed_position.y - clip_bounds.origin.y, + clip_bounds.origin.y + clip_bounds.size.height - transformed_position.y); +} + float4 over(float4 below, float4 above) { float4 result; float alpha = above.a + below.a * (1.0 - above.a); diff --git a/crates/gpui/src/platform/windows/shaders.hlsl b/crates/gpui/src/platform/windows/shaders.hlsl index 2cef54ae6166e313795eb42210b5f07c1bc378fc..1915802d08d8c22c9bfc893f087bd61d0a1de331 100644 --- a/crates/gpui/src/platform/windows/shaders.hlsl +++ b/crates/gpui/src/platform/windows/shaders.hlsl @@ -107,6 +107,12 @@ float4 distance_from_clip_rect(float2 unit_vertex, Bounds bounds, Bounds clip_bo return distance_from_clip_rect_impl(position, clip_bounds); } +float4 distance_from_clip_rect_transformed(float2 unit_vertex, Bounds bounds, Bounds clip_bounds, TransformationMatrix transformation) { + float2 position = unit_vertex * bounds.size + bounds.origin; + float2 transformed = mul(position, transformation.rotation_scale) + transformation.translation; + return distance_from_clip_rect_impl(transformed, clip_bounds); +} + // Convert linear RGB to sRGB float3 linear_to_srgb(float3 color) { return pow(color, float3(2.2, 2.2, 2.2)); @@ -1088,7 +1094,7 @@ MonochromeSpriteVertexOutput monochrome_sprite_vertex(uint vertex_id: SV_VertexI MonochromeSprite sprite = mono_sprites[sprite_id]; float4 device_position = to_device_position_transformed(unit_vertex, sprite.bounds, sprite.transformation); - float4 clip_distance = distance_from_clip_rect(unit_vertex, sprite.bounds, sprite.content_mask); + float4 clip_distance = distance_from_clip_rect_transformed(unit_vertex, sprite.bounds, sprite.content_mask, sprite.transformation); float2 tile_position = to_tile_position(unit_vertex, sprite.tile); float4 color = hsla_to_rgba(sprite.color); diff --git a/crates/gpui/src/svg_renderer.rs b/crates/gpui/src/svg_renderer.rs index 0107624bc8d0e6a26c6acc4a085cbddc7e14c4c5..b2bf126967cd0c533eb6faac8c168508fe5c1d34 100644 --- a/crates/gpui/src/svg_renderer.rs +++ b/crates/gpui/src/svg_renderer.rs @@ -54,7 +54,10 @@ impl SvgRenderer { } } - pub(crate) fn render(&self, params: &RenderSvgParams) -> Result>> { + pub(crate) fn render( + &self, + params: &RenderSvgParams, + ) -> Result, Vec)>> { anyhow::ensure!(!params.size.is_zero(), "can't render at a zero size"); // Load the tree. @@ -65,30 +68,33 @@ impl SvgRenderer { let pixmap = self.render_pixmap(&bytes, SvgSize::Size(params.size))?; // Convert the pixmap's pixels into an alpha mask. + let size = Size::new( + DevicePixels(pixmap.width() as i32), + DevicePixels(pixmap.height() as i32), + ); let alpha_mask = pixmap .pixels() .iter() .map(|p| p.alpha()) .collect::>(); - Ok(Some(alpha_mask)) + Ok(Some((size, alpha_mask))) } pub fn render_pixmap(&self, bytes: &[u8], size: SvgSize) -> Result { let tree = usvg::Tree::from_data(bytes, &self.usvg_options)?; - - let size = match size { - SvgSize::Size(size) => size, - SvgSize::ScaleFactor(scale) => crate::size( - DevicePixels((tree.size().width() * scale) as i32), - DevicePixels((tree.size().height() * scale) as i32), - ), + let svg_size = tree.size(); + let scale = match size { + SvgSize::Size(size) => size.width.0 as f32 / svg_size.width(), + SvgSize::ScaleFactor(scale) => scale, }; // Render the SVG to a pixmap with the specified width and height. - let mut pixmap = resvg::tiny_skia::Pixmap::new(size.width.into(), size.height.into()) - .ok_or(usvg::Error::InvalidSize)?; + let mut pixmap = resvg::tiny_skia::Pixmap::new( + (svg_size.width() * scale) as u32, + (svg_size.height() * scale) as u32, + ) + .ok_or(usvg::Error::InvalidSize)?; - let scale = size.width.0 as f32 / tree.size().width(); let transform = resvg::tiny_skia::Transform::from_scale(scale, scale); resvg::render(&tree, transform, &mut pixmap.as_mut()); diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index aecac1fc770e56990dbf6ac4118d835f25d5766e..3f9f188bf34da29973e96d40463e6db99e396a00 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -3082,22 +3082,31 @@ impl Window { let Some(tile) = self.sprite_atlas .get_or_insert_with(¶ms.clone().into(), &mut || { - let Some(bytes) = cx.svg_renderer.render(¶ms)? else { + let Some((size, bytes)) = cx.svg_renderer.render(¶ms)? else { return Ok(None); }; - Ok(Some((params.size, Cow::Owned(bytes)))) + Ok(Some((size, Cow::Owned(bytes)))) })? else { return Ok(()); }; let content_mask = self.content_mask().scale(scale_factor); + let svg_bounds = Bounds { + origin: bounds.center() + - Point::new( + ScaledPixels(tile.bounds.size.width.0 as f32 / SMOOTH_SVG_SCALE_FACTOR / 2.), + ScaledPixels(tile.bounds.size.height.0 as f32 / SMOOTH_SVG_SCALE_FACTOR / 2.), + ), + size: tile + .bounds + .size + .map(|value| ScaledPixels(value.0 as f32 / SMOOTH_SVG_SCALE_FACTOR)), + }; self.next_frame.scene.insert_primitive(MonochromeSprite { order: 0, pad: 0, - bounds: bounds - .map_origin(|origin| origin.floor()) - .map_size(|size| size.ceil()), + bounds: svg_bounds, content_mask, color: color.opacity(element_opacity), tile,