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 90a8dc5730..16d6e1b269 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 14e5ff4fa8..d00e596c10 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 83c978b853..37ec7b530a 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 2cef54ae61..1915802d08 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 0107624bc8..b2bf126967 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 aecac1fc77..3f9f188bf3 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,