From a722a33503a33408999c0858ef76133395b33b5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Torr=C3=B3?= Date: Thu, 30 Jul 2026 12:11:53 +0200 Subject: [PATCH] :bug: Fix gradients and text selection (#10941) * :bug: Add missing text fills * :sparkles: Keep text leaf selected while editing --- render-wasm/src/render/text_editor.rs | 8 +++-- render-wasm/src/shapes/fills.rs | 31 ++++++++++++++++ render-wasm/src/state/text_editor.rs | 6 +++- render-wasm/src/wasm/fills.rs | 50 +++++++++++++++++++++----- render-wasm/src/wasm/fills/gradient.rs | 35 ++++++++++++++++++ render-wasm/src/wasm/fills/image.rs | 23 ++++++++++++ 6 files changed, 140 insertions(+), 13 deletions(-) diff --git a/render-wasm/src/render/text_editor.rs b/render-wasm/src/render/text_editor.rs index 91d1093d85..8ed53bb436 100644 --- a/render-wasm/src/render/text_editor.rs +++ b/render-wasm/src/render/text_editor.rs @@ -12,7 +12,9 @@ pub fn render_overlay( editor_state: &TextEditorState, shape: &Shape, ) { - if !editor_state.has_focus { + let has_selection = editor_state.selection.is_selection(); + + if !editor_state.has_focus && !has_selection { return; } @@ -25,12 +27,12 @@ pub fn render_overlay( canvas.scale((zoom, zoom)); canvas.translate((-viewbox.area.left, -viewbox.area.top)); - if editor_state.selection.is_selection() { + if has_selection { // With an active selection there is no blinking caret (the caret is one // end of the selection); drawing it would make it toggle on top of the // highlight while the selection is held. render_selection(canvas, editor_state, text_content, shape); - } else if editor_state.cursor_visible { + } else if editor_state.has_focus && editor_state.cursor_visible { render_cursor(canvas, zoom, options.dpr, editor_state, text_content, shape); } diff --git a/render-wasm/src/shapes/fills.rs b/render-wasm/src/shapes/fills.rs index 87da3881bf..15d5dc09f7 100644 --- a/render-wasm/src/shapes/fills.rs +++ b/render-wasm/src/shapes/fills.rs @@ -42,6 +42,29 @@ impl Gradient { self.offsets.extend(offsets); } + pub fn start(&self) -> (f32, f32) { + self.start + } + + pub fn end(&self) -> (f32, f32) { + self.end + } + + pub fn opacity(&self) -> u8 { + self.opacity + } + + pub fn width(&self) -> f32 { + self.width + } + + pub fn stops(&self) -> impl Iterator + '_ { + self.colors + .iter() + .copied() + .zip(self.offsets.iter().copied()) + } + pub fn to_linear_shader(&self, rect: &Rect) -> Option { let start = ( rect.left + self.start.0 * rect.width(), @@ -126,6 +149,14 @@ impl ImageFill { pub fn keep_aspect_ratio(&self) -> bool { self.keep_aspect_ratio } + + pub fn width(&self) -> i32 { + self.width + } + + pub fn height(&self) -> i32 { + self.height + } } #[derive(Debug, Clone, PartialEq, Copy)] diff --git a/render-wasm/src/state/text_editor.rs b/render-wasm/src/state/text_editor.rs index 7ae4421246..3e46f77116 100644 --- a/render-wasm/src/state/text_editor.rs +++ b/render-wasm/src/state/text_editor.rs @@ -385,11 +385,15 @@ impl TextEditorState { } pub fn focus(&mut self, shape_id: Uuid) { + let same_shape = self.active_shape_id == Some(shape_id); + self.has_focus = true; self.active_shape_id = Some(shape_id); self.cursor_visible = true; self.last_blink_time_ms = 0.0; - self.selection.reset(); + if !same_shape { + self.selection.reset(); + } self.is_pointer_selection_active = false; self.is_overtype_mode = false; self.pending_events.clear(); diff --git a/render-wasm/src/wasm/fills.rs b/render-wasm/src/wasm/fills.rs index 2e652b11b1..c0a8d6850d 100644 --- a/render-wasm/src/wasm/fills.rs +++ b/render-wasm/src/wasm/fills.rs @@ -48,15 +48,15 @@ impl TryFrom<&shapes::Fill> for RawFillData { | (color.b() as u32), })) } - shapes::Fill::LinearGradient(_) => { - Err("LinearGradient serialization is not implemented".to_string()) - } - shapes::Fill::RadialGradient(_) => { - Err("RadialGradient serialization is not implemented".to_string()) - } - shapes::Fill::Image(_) => { - Err("Image fill serialization is not implemented".to_string()) - } + shapes::Fill::LinearGradient(linear_gradient) => Ok(RawFillData::Linear( + gradient::RawGradientData::from(linear_gradient), + )), + shapes::Fill::RadialGradient(radial_gradient) => Ok(RawFillData::Radial( + gradient::RawGradientData::from(radial_gradient), + )), + shapes::Fill::Image(image_fill) => Ok(RawFillData::Image( + image::RawImageFillData::from(image_fill), + )), } } } @@ -155,4 +155,36 @@ mod tests { RawFillData::Solid(solid::RawSolidData { color: 0xfffabada }) ); } + + #[test] + fn test_gradient_fill_round_trip() { + let gradient = shapes::Gradient::new( + (0.0, 0.5), + (1.0, 0.5), + 0x80, + 0.25, + &[ + (shapes::Color::from(0xfffabada), 0.0), + (shapes::Color::from(0xff00ff00), 1.0), + ], + ); + + let fill = shapes::Fill::LinearGradient(gradient.clone()); + let raw_fill = RawFillData::try_from(&fill).expect("gradient must be serializable"); + let bytes = <[u8; RAW_FILL_DATA_SIZE]>::from(raw_fill); + + assert_eq!(bytes[0], 0x01); + assert_eq!(shapes::Fill::from(RawFillData::from(bytes)), fill); + } + + #[test] + fn test_image_fill_round_trip() { + let image_fill = shapes::ImageFill::new(crate::uuid::Uuid::nil(), 0x80, 300, 200, true); + let fill = shapes::Fill::Image(image_fill); + let raw_fill = RawFillData::try_from(&fill).expect("image fill must be serializable"); + let bytes = <[u8; RAW_FILL_DATA_SIZE]>::from(raw_fill); + + assert_eq!(bytes[0], 0x03); + assert_eq!(shapes::Fill::from(RawFillData::from(bytes)), fill); + } } diff --git a/render-wasm/src/wasm/fills/gradient.rs b/render-wasm/src/wasm/fills/gradient.rs index c656cf197e..f36bcd70eb 100644 --- a/render-wasm/src/wasm/fills/gradient.rs +++ b/render-wasm/src/wasm/fills/gradient.rs @@ -44,6 +44,41 @@ impl RawStopData { } } +impl From<&Gradient> for RawGradientData { + fn from(gradient: &Gradient) -> Self { + let mut stops = [RawStopData { + color: 0, + offset: 0.0, + }; MAX_GRADIENT_STOPS]; + + let mut stop_count: u8 = 0; + for (index, (color, offset)) in gradient.stops().take(MAX_GRADIENT_STOPS).enumerate() { + stops[index] = RawStopData { + color: ((color.a() as u32) << 24) + | ((color.r() as u32) << 16) + | ((color.g() as u32) << 8) + | (color.b() as u32), + offset, + }; + stop_count += 1; + } + + let (start_x, start_y) = gradient.start(); + let (end_x, end_y) = gradient.end(); + + RawGradientData { + start_x, + start_y, + end_x, + end_y, + opacity: gradient.opacity(), + width: gradient.width(), + stop_count, + stops, + } + } +} + impl From for Gradient { fn from(raw_gradient: RawGradientData) -> Self { let stops = raw_gradient diff --git a/render-wasm/src/wasm/fills/image.rs b/render-wasm/src/wasm/fills/image.rs index 778b80bf92..ce14511220 100644 --- a/render-wasm/src/wasm/fills/image.rs +++ b/render-wasm/src/wasm/fills/image.rs @@ -48,6 +48,29 @@ pub struct RawImageFillData { height: i32, } +impl From<&ImageFill> for RawImageFillData { + fn from(image_fill: &ImageFill) -> Self { + let id = image_fill.id(); + let (a, b, c, d) = crate::utils::uuid_to_u32_quartet(&id); + let flags = if image_fill.keep_aspect_ratio() { + FLAG_KEEP_ASPECT_RATIO + } else { + 0 + }; + + Self { + a, + b, + c, + d, + opacity: image_fill.opacity(), + flags, + width: image_fill.width(), + height: image_fill.height(), + } + } +} + impl From for ImageFill { fn from(value: RawImageFillData) -> Self { let id = uuid_from_u32_quartet(value.a, value.b, value.c, value.d);