From 5f376011227dd3a29a434089fc85a77419d63a02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Torr=C3=B3?= Date: Tue, 2 Sep 2025 12:56:07 +0200 Subject: [PATCH] :bug: Fix different fonts on texts shadows (#7214) * :bug: Fix different fonts on texts shadows * :wrench: Refactor text rendering and move text-decoration logic outside * :wrench: Use transparency correctly --- render-wasm/src/render.rs | 91 ++++++-- render-wasm/src/render/shadows.rs | 46 +--- render-wasm/src/render/strokes.rs | 2 - render-wasm/src/render/text.rs | 334 ++++++++++++++------------- render-wasm/src/shapes.rs | 28 +++ render-wasm/src/shapes/modifiers.rs | 4 +- render-wasm/src/shapes/text.rs | 161 +++++++++++-- render-wasm/src/shapes/text_paths.rs | 2 +- render-wasm/src/wasm/text.rs | 6 +- 9 files changed, 421 insertions(+), 253 deletions(-) diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index 8b37332e4c..f4e48e05a7 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -537,32 +537,58 @@ impl RenderState { }); let text_content = text_content.new_bounds(shape.selrect()); - let mut paragraphs = text_content.to_paragraphs( - shape.image_filter(1.).as_ref(), - shape.mask_filter(1.).as_ref(), - ); + let drop_shadows = shape.drop_shadow_paints(); + let inner_shadows = shape.inner_shadow_paints(); + let blur_filter = shape.image_filter(1.); + let blur_mask = shape.mask_filter(1.); + let mut paragraphs = + text_content.to_paragraphs(blur_filter.as_ref(), blur_mask.as_ref(), None); - if !shape.has_visible_strokes() { - shadows::render_text_drop_shadows(self, &shape, &mut paragraphs, antialias); + // Render all drop shadows if there are no visible strokes + if !shape.has_visible_strokes() && !drop_shadows.is_empty() { + for drop_shadow in &drop_shadows { + let mut paragraphs_with_drop_shadows = text_content.to_paragraphs( + blur_filter.as_ref(), + blur_mask.as_ref(), + Some(drop_shadow), + ); + shadows::render_text_drop_shadows( + self, + &shape, + &mut paragraphs_with_drop_shadows, + ); + } } let count_inner_strokes = shape.count_visible_inner_strokes(); - text::render(self, &shape, &mut paragraphs, None, None); - + text::render(self, &shape, &mut paragraphs, None); for stroke in shape.visible_strokes().rev() { + for drop_shadow in &drop_shadows { + let mut stroke_paragraphs_with_drop_shadows = text_content + .to_stroke_paragraphs( + stroke, + &shape.selrect(), + blur_filter.as_ref(), + blur_mask.as_ref(), + Some(drop_shadow), + count_inner_strokes, + ); + shadows::render_text_drop_shadows( + self, + &shape, + &mut stroke_paragraphs_with_drop_shadows, + ); + } + let mut stroke_paragraphs = text_content.to_stroke_paragraphs( stroke, &shape.selrect(), - shape.image_filter(1.).as_ref(), - shape.mask_filter(1.).as_ref(), + blur_filter.as_ref(), + blur_mask.as_ref(), + None, count_inner_strokes, ); - shadows::render_text_drop_shadows( - self, - &shape, - &mut stroke_paragraphs, - antialias, - ); + strokes::render( self, &shape, @@ -571,17 +597,38 @@ impl RenderState { None, Some(&mut stroke_paragraphs), antialias, - None, + ); + + for inner_shadow in &inner_shadows { + let mut stroke_paragraphs_with_inner_shadows = text_content + .to_stroke_paragraphs( + stroke, + &shape.selrect(), + blur_filter.as_ref(), + blur_mask.as_ref(), + Some(inner_shadow), + count_inner_strokes, + ); + shadows::render_text_inner_shadows( + self, + &shape, + &mut stroke_paragraphs_with_inner_shadows, + ); + } + } + + for inner_shadow in &inner_shadows { + let mut paragraphs_with_inner_shadows = text_content.to_paragraphs( + blur_filter.as_ref(), + blur_mask.as_ref(), + Some(inner_shadow), ); shadows::render_text_inner_shadows( self, &shape, - &mut stroke_paragraphs, - antialias, + &mut paragraphs_with_inner_shadows, ); } - - shadows::render_text_inner_shadows(self, &shape, &mut paragraphs, antialias); } _ => { let surface_ids = SurfaceId::Strokes as u32 @@ -630,7 +677,7 @@ impl RenderState { shadows::render_stroke_drop_shadows(self, shape, stroke, antialias); //In clipped content strokes are drawn over the contained elements in a subsequent step if !shape.clip() { - strokes::render(self, shape, stroke, None, None, None, antialias, None); + strokes::render(self, shape, stroke, None, None, None, antialias); } shadows::render_stroke_inner_shadows(self, shape, stroke, antialias); } diff --git a/render-wasm/src/render/shadows.rs b/render-wasm/src/render/shadows.rs index d0f7e2af00..f97cd81c3b 100644 --- a/render-wasm/src/render/shadows.rs +++ b/render-wasm/src/render/shadows.rs @@ -59,7 +59,6 @@ pub fn render_stroke_drop_shadows( filter.as_ref(), None, antialias, - None, ) } } @@ -82,7 +81,6 @@ pub fn render_stroke_inner_shadows( filter.as_ref(), None, antialias, - None, ) } } @@ -92,11 +90,13 @@ pub fn render_text_drop_shadows( render_state: &mut RenderState, shape: &Shape, paragraphs: &mut [Vec], - antialias: bool, ) { - for shadow in shape.drop_shadows().rev().filter(|s| !s.hidden()) { - render_text_drop_shadow(render_state, shape, shadow, paragraphs, antialias); - } + text::render( + render_state, + shape, + paragraphs, + Some(SurfaceId::DropShadows), + ); } // Render text paths (unused) @@ -122,50 +122,16 @@ pub fn render_text_path_stroke_drop_shadows( } } -pub fn render_text_drop_shadow( - render_state: &mut RenderState, - shape: &Shape, - shadow: &Shadow, - paragraphs: &mut [Vec], - antialias: bool, -) { - let paint = shadow.get_drop_shadow_paint(antialias, shape.image_filter(1.).as_ref()); - - text::render( - render_state, - shape, - paragraphs, - Some(SurfaceId::DropShadows), - Some(&paint), - ); -} - pub fn render_text_inner_shadows( render_state: &mut RenderState, shape: &Shape, paragraphs: &mut [Vec], - antialias: bool, ) { - for shadow in shape.inner_shadows().rev().filter(|s| !s.hidden()) { - render_text_inner_shadow(render_state, shape, shadow, paragraphs, antialias); - } -} - -pub fn render_text_inner_shadow( - render_state: &mut RenderState, - shape: &Shape, - shadow: &Shadow, - paragraphs: &mut [Vec], - antialias: bool, -) { - let paint = shadow.get_inner_shadow_paint(antialias, shape.image_filter(1.).as_ref()); - text::render( render_state, shape, paragraphs, Some(SurfaceId::InnerShadows), - Some(&paint), ); } diff --git a/render-wasm/src/render/strokes.rs b/render-wasm/src/render/strokes.rs index 778f42977d..3f0d455424 100644 --- a/render-wasm/src/render/strokes.rs +++ b/render-wasm/src/render/strokes.rs @@ -521,7 +521,6 @@ pub fn render( shadow: Option<&ImageFilter>, paragraphs: Option<&mut Vec>>, antialias: bool, - paint: Option<&skia::Paint>, ) { let scale = render_state.get_scale(); let canvas = render_state @@ -571,7 +570,6 @@ pub fn render( shape, paragraphs.expect("Text shapes should have paragraphs"), Some(SurfaceId::Strokes), - paint, ); } shape_type @ (Type::Path(_) | Type::Bool(_)) => { diff --git a/render-wasm/src/render/text.rs b/render-wasm/src/render/text.rs index 3c263e8c8c..593dd008ff 100644 --- a/render-wasm/src/render/text.rs +++ b/render-wasm/src/render/text.rs @@ -1,16 +1,18 @@ use super::{RenderState, Shape, SurfaceId}; use crate::shapes::VerticalAlign; -use crate::utils::get_font_collection; -use skia_safe::{textlayout::ParagraphBuilder, Paint, Path}; +use skia_safe::{ + canvas::SaveLayerRec, textlayout::LineMetrics, textlayout::Paragraph, + textlayout::ParagraphBuilder, textlayout::RectHeightStyle, textlayout::RectWidthStyle, + textlayout::StyleMetrics, textlayout::TextDecoration, textlayout::TextStyle, Canvas, Paint, + Path, +}; pub fn render( render_state: &mut RenderState, shape: &Shape, paragraphs: &mut [Vec], surface_id: Option, - paint: Option<&Paint>, ) { - let fonts = get_font_collection(); let canvas = render_state .surfaces .canvas(surface_id.unwrap_or(SurfaceId::Fills)); @@ -31,189 +33,33 @@ pub fn render( _ => 0.0, }; - let layer_rec = skia_safe::canvas::SaveLayerRec::default(); + let layer_rec = SaveLayerRec::default(); canvas.save_layer(&layer_rec); for group in paragraphs { let mut group_offset_y = global_offset_y; let group_len = group.len(); - for (index, builder) in group.iter_mut().enumerate() { + for builder in group.iter_mut() { let mut skia_paragraph = builder.build(); - - if paint.is_some() && index == 0 { - let text = builder.get_text().to_string(); - let mut paragraph_builder = - ParagraphBuilder::new(&builder.get_paragraph_style(), fonts); - let mut text_style: skia_safe::Handle<_> = builder.peek_style(); - text_style.set_foreground_paint(paint.unwrap()); - paragraph_builder.reset(); - paragraph_builder.push_style(&text_style); - paragraph_builder.add_text(&text); - skia_paragraph = paragraph_builder.build(); - } else if paint.is_some() && index > 0 { - continue; - } - skia_paragraph.layout(paragraph_width); let paragraph_height = skia_paragraph.height(); let xy = (shape.selrect().x(), shape.selrect().y() + group_offset_y); skia_paragraph.paint(canvas, xy); for line_metrics in skia_paragraph.get_line_metrics().iter() { - let style_metrics: Vec<_> = line_metrics - .get_style_metrics(line_metrics.start_index..line_metrics.end_index) - .into_iter() - .collect(); - - let mut current_x_offset = 0.0; - let total_chars = line_metrics.end_index - line_metrics.start_index; - let line_start_offset = line_metrics.left as f32; - - if total_chars == 0 || style_metrics.is_empty() { - continue; - } - - let line_baseline = xy.1 + line_metrics.baseline as f32; - let full_text = builder.get_text(); - - // 1. Caculate text decoration for line - let mut max_underline_thickness: f32 = 0.0; - let mut underline_y = None; - let mut max_strike_thickness: f32 = 0.0; - let mut strike_y = None; - for (_style_start, style_metric) in style_metrics.iter() { - let font_metrics = style_metric.font_metrics; - let font_size = font_metrics - .cap_height - .abs() - .max(font_metrics.x_height.abs()); - let min_thickness = (font_size * 0.06).max(1.0); - let thickness = font_metrics - .underline_thickness() - .unwrap_or(1.0) - .max(min_thickness); - if style_metric.text_style.decoration().ty - == skia_safe::textlayout::TextDecoration::UNDERLINE - { - let y = - line_baseline + font_metrics.underline_position().unwrap_or(thickness); - max_underline_thickness = max_underline_thickness.max(thickness); - underline_y = Some(y); - } - if style_metric.text_style.decoration().ty - == skia_safe::textlayout::TextDecoration::LINE_THROUGH - { - let y = line_baseline - + font_metrics - .strikeout_position() - .unwrap_or(-font_metrics.cap_height / 2.0); - max_strike_thickness = max_strike_thickness.max(thickness); - strike_y = Some(y); - } - } - - // 2. Draw decorations per segment - for (i, (style_start, style_metric)) in style_metrics.iter().enumerate() { - let text_style = style_metric.text_style; - let style_end = style_metrics - .get(i + 1) - .map(|(next_i, _)| *next_i) - .unwrap_or(line_metrics.end_index); - - let seg_start = (*style_start).max(line_metrics.start_index); - let seg_end = style_end.min(line_metrics.end_index); - if seg_start >= seg_end { - continue; - } - - let start_byte = full_text - .char_indices() - .nth(seg_start) - .map(|(i, _)| i) - .unwrap_or(0); - let end_byte = full_text - .char_indices() - .nth(seg_end) - .map(|(i, _)| i) - .unwrap_or(full_text.len()); - let segment_text = &full_text[start_byte..end_byte]; - - let rects = skia_paragraph.get_rects_for_range( - seg_start..seg_end, - skia_safe::textlayout::RectHeightStyle::Tight, - skia_safe::textlayout::RectWidthStyle::Tight, - ); - let (segment_width, actual_x_offset) = if !rects.is_empty() { - let total_width: f32 = rects.iter().map(|r| r.rect.width()).sum(); - let skia_x_offset = rects - .first() - .map(|r| r.rect.left - line_start_offset) - .unwrap_or(0.0); - (total_width, skia_x_offset) - } else { - let font = skia_paragraph.get_font_at(seg_start); - let measured_width = font.measure_text(segment_text, None).0; - (measured_width, current_x_offset) - }; - - // Underline - if text_style.decoration().ty - == skia_safe::textlayout::TextDecoration::UNDERLINE - { - if let Some(y) = underline_y { - let thickness = max_underline_thickness; - let text_left = xy.0 + line_start_offset + actual_x_offset; - let text_width = segment_width; - let r = skia_safe::Rect::new( - text_left, - y - thickness / 2.0, - text_left + text_width, - y + thickness / 2.0, - ); - let mut decoration_paint = text_style.foreground(); - decoration_paint.set_anti_alias(true); - canvas.draw_rect(r, &decoration_paint); - } - } - // Strikethrough - if text_style.decoration().ty - == skia_safe::textlayout::TextDecoration::LINE_THROUGH - { - if let Some(y) = strike_y { - let thickness = max_strike_thickness; - let text_left = xy.0 + line_start_offset + actual_x_offset; - let text_width = segment_width; - let r = skia_safe::Rect::new( - text_left, - y - thickness / 2.0, - text_left + text_width, - y + thickness / 2.0, - ); - let mut decoration_paint = text_style.foreground(); - decoration_paint.set_anti_alias(true); - canvas.draw_rect(r, &decoration_paint); - } - } - current_x_offset += segment_width; - } + render_text_decoration(canvas, &skia_paragraph, builder, line_metrics, xy); } - // Only increment group_offset_y for regular paragraphs (single element groups) - // For stroke groups (multiple elements), keep same offset for blending if group_len == 1 { group_offset_y += paragraph_height; } - // For stroke groups (group_len > 1), don't increment group_offset_y within the group - // This ensures all stroke variants render at the same position for proper blending } - // For stroke groups (multiple elements), increment global_offset_y once per group if group_len > 1 { let mut first_paragraph = group[0].build(); first_paragraph.layout(paragraph_width); global_offset_y += first_paragraph.height(); } else { - // For regular paragraphs, global_offset_y was already incremented inside the loop global_offset_y = group_offset_y; } } @@ -221,6 +67,168 @@ pub fn render( canvas.restore(); } +fn draw_text_decorations( + canvas: &Canvas, + text_style: &TextStyle, + y: Option, + thickness: f32, + text_left: f32, + text_width: f32, +) { + if let Some(y) = y { + let r = skia_safe::Rect::new( + text_left, + y - thickness / 2.0, + text_left + text_width, + y + thickness / 2.0, + ); + let mut decoration_paint = text_style.foreground(); + decoration_paint.set_anti_alias(true); + canvas.draw_rect(r, &decoration_paint); + } +} + +fn calculate_decoration_metrics( + style_metrics: &Vec<(usize, &StyleMetrics)>, + line_baseline: f32, +) -> (f32, Option, f32, Option) { + let mut max_underline_thickness: f32 = 0.0; + let mut underline_y = None; + let mut max_strike_thickness: f32 = 0.0; + let mut strike_y = None; + for (_style_start, style_metric) in style_metrics.iter() { + let font_metrics = style_metric.font_metrics; + let font_size = font_metrics + .cap_height + .abs() + .max(font_metrics.x_height.abs()); + let min_thickness = (font_size * 0.06).max(1.0); + let thickness = font_metrics + .underline_thickness() + .unwrap_or(1.0) + .max(min_thickness); + if style_metric.text_style.decoration().ty == TextDecoration::UNDERLINE { + let y = line_baseline + font_metrics.underline_position().unwrap_or(thickness); + max_underline_thickness = max_underline_thickness.max(thickness); + underline_y = Some(y); + } + if style_metric.text_style.decoration().ty == TextDecoration::LINE_THROUGH { + let y = line_baseline + + font_metrics + .strikeout_position() + .unwrap_or(-font_metrics.cap_height / 2.0); + max_strike_thickness = max_strike_thickness.max(thickness); + strike_y = Some(y); + } + } + ( + max_underline_thickness, + underline_y, + max_strike_thickness, + strike_y, + ) +} + +fn render_text_decoration( + canvas: &Canvas, + skia_paragraph: &Paragraph, + builder: &mut ParagraphBuilder, + line_metrics: &LineMetrics, + xy: (f32, f32), +) { + let style_metrics: Vec<_> = line_metrics + .get_style_metrics(line_metrics.start_index..line_metrics.end_index) + .into_iter() + .collect(); + + let mut current_x_offset = 0.0; + let total_chars = line_metrics.end_index - line_metrics.start_index; + let line_start_offset = line_metrics.left as f32; + + if total_chars == 0 || style_metrics.is_empty() { + return; + } + + let line_baseline = xy.1 + line_metrics.baseline as f32; + let full_text = builder.get_text(); + + // Calculate decoration metrics + let (max_underline_thickness, underline_y, max_strike_thickness, strike_y) = + calculate_decoration_metrics(&style_metrics, line_baseline); + + // Draw decorations per segment (text leaf) + for (i, (style_start, style_metric)) in style_metrics.iter().enumerate() { + let text_style = &style_metric.text_style; + let style_end = style_metrics + .get(i + 1) + .map(|(next_i, _)| *next_i) + .unwrap_or(line_metrics.end_index); + + let seg_start = (*style_start).max(line_metrics.start_index); + let seg_end = style_end.min(line_metrics.end_index); + if seg_start >= seg_end { + continue; + } + + let start_byte = full_text + .char_indices() + .nth(seg_start) + .map(|(i, _)| i) + .unwrap_or(0); + let end_byte = full_text + .char_indices() + .nth(seg_end) + .map(|(i, _)| i) + .unwrap_or(full_text.len()); + let segment_text = &full_text[start_byte..end_byte]; + + let rects = skia_paragraph.get_rects_for_range( + seg_start..seg_end, + RectHeightStyle::Tight, + RectWidthStyle::Tight, + ); + let (segment_width, actual_x_offset) = if !rects.is_empty() { + let total_width: f32 = rects.iter().map(|r| r.rect.width()).sum(); + let skia_x_offset = rects + .first() + .map(|r| r.rect.left - line_start_offset) + .unwrap_or(0.0); + (total_width, skia_x_offset) + } else { + let font = skia_paragraph.get_font_at(seg_start); + let measured_width = font.measure_text(segment_text, None).0; + (measured_width, current_x_offset) + }; + + let text_left = xy.0 + line_start_offset + actual_x_offset; + let text_width = segment_width; + + // Underline + if text_style.decoration().ty == TextDecoration::UNDERLINE { + draw_text_decorations( + canvas, + text_style, + underline_y, + max_underline_thickness, + text_left, + text_width, + ); + } + // Strikethrough + if text_style.decoration().ty == TextDecoration::LINE_THROUGH { + draw_text_decorations( + canvas, + text_style, + strike_y, + max_strike_thickness, + text_left, + text_width, + ); + } + current_x_offset += segment_width; + } +} + fn calculate_total_paragraphs_height(paragraphs: &mut [ParagraphBuilder], width: f32) -> f32 { paragraphs .iter_mut() diff --git a/render-wasm/src/shapes.rs b/render-wasm/src/shapes.rs index 1b7a5d922c..5cbc214185 100644 --- a/render-wasm/src/shapes.rs +++ b/render-wasm/src/shapes.rs @@ -1152,6 +1152,34 @@ impl Shape { self.children_ids(include_hidden) } } + + pub fn drop_shadow_paints(&self) -> Vec { + let drop_shadows: Vec<&crate::shapes::shadows::Shadow> = + self.drop_shadows().filter(|s| !s.hidden()).collect(); + drop_shadows + .into_iter() + .map(|shadow| { + let mut paint = skia_safe::Paint::default(); + let filter = shadow.get_drop_shadow_filter(); + paint.set_image_filter(filter); + paint + }) + .collect() + } + + pub fn inner_shadow_paints(&self) -> Vec { + let inner_shadows: Vec<&crate::shapes::shadows::Shadow> = + self.inner_shadows().filter(|s| !s.hidden()).collect(); + inner_shadows + .into_iter() + .map(|shadow| { + let mut paint = skia_safe::Paint::default(); + let filter = shadow.get_inner_shadow_filter(); + paint.set_image_filter(filter); + paint + }) + .collect() + } } #[cfg(test)] diff --git a/render-wasm/src/shapes/modifiers.rs b/render-wasm/src/shapes/modifiers.rs index e90b8e2d0c..4c8e00682f 100644 --- a/render-wasm/src/shapes/modifiers.rs +++ b/render-wasm/src/shapes/modifiers.rs @@ -200,7 +200,7 @@ fn propagate_transform( match content.grow_type() { GrowType::AutoHeight => { let paragraph_width = shape_bounds_after.width(); - let mut paragraphs = content.to_paragraphs(None, None); + let mut paragraphs = content.to_paragraphs(None, None, None); let height = auto_height(&mut paragraphs, paragraph_width); let resize_transform = math::resize_matrix( &shape_bounds_after, @@ -213,7 +213,7 @@ fn propagate_transform( } GrowType::AutoWidth => { let paragraph_width = content.get_width(); - let mut paragraphs = content.to_paragraphs(None, None); + let mut paragraphs = content.to_paragraphs(None, None, None); let height = auto_height(&mut paragraphs, paragraph_width); let resize_transform = math::resize_matrix( &shape_bounds_after, diff --git a/render-wasm/src/shapes/text.rs b/render-wasm/src/shapes/text.rs index 6cbfbd3052..825a3c24e7 100644 --- a/render-wasm/src/shapes/text.rs +++ b/render-wasm/src/shapes/text.rs @@ -2,6 +2,7 @@ use crate::{ math::{Matrix, Rect}, render::{default_font, filters::compose_filters, DEFAULT_EMOJI_FONT}, }; + use skia_safe::{ self as skia, paint::Paint, @@ -111,6 +112,7 @@ impl TextContent { &self, blur: Option<&ImageFilter>, blur_mask: Option<&MaskFilter>, + shadow: Option<&Paint>, ) -> Vec> { let fonts = get_font_collection(); let fallback_fonts = get_fallback_fonts(); @@ -120,7 +122,8 @@ impl TextContent { let paragraph_style = paragraph.paragraph_to_style(); let mut builder = ParagraphBuilder::new(¶graph_style, fonts); for leaf in ¶graph.children { - let text_style = leaf.to_style(&self.bounds, fallback_fonts, blur, blur_mask); + let text_style = + leaf.to_style(&self.bounds, fallback_fonts, blur, blur_mask, shadow); let text = leaf.apply_text_transform(); builder.push_style(&text_style); builder.add_text(&text); @@ -137,6 +140,7 @@ impl TextContent { bounds: &Rect, blur: Option<&ImageFilter>, blur_mask: Option<&MaskFilter>, + shadow: Option<&Paint>, count_inner_strokes: usize, ) -> Vec> { let fallback_fonts = get_fallback_fonts(); @@ -152,14 +156,26 @@ impl TextContent { if let Some(blur_mask) = blur_mask { text_paint.set_mask_filter(blur_mask.clone()); } - let stroke_paints = get_text_stroke_paints( - stroke, - bounds, - &text_paint, - blur, - blur_mask, - count_inner_strokes, - ); + + let stroke_paints = if shadow.is_some() { + get_text_stroke_paints_with_shadows( + stroke, + blur, + blur_mask, + shadow, + leaf.is_transparent(), + ) + } else { + get_text_stroke_paints( + stroke, + bounds, + &text_paint, + blur, + blur_mask, + count_inner_strokes, + ) + }; + let text: String = leaf.apply_text_transform(); for (paint_idx, stroke_paint) in stroke_paints.iter().enumerate() { @@ -169,7 +185,7 @@ impl TextContent { }); let stroke_paint = stroke_paint.clone(); let stroke_style = - leaf.to_stroke_style(&stroke_paint, fallback_fonts, blur, blur_mask); + leaf.to_stroke_style(&stroke_paint, fallback_fonts, blur, blur_mask, None); builder.push_style(&stroke_style); builder.add_text(&text); } @@ -187,7 +203,7 @@ impl TextContent { pub fn get_width(&self) -> f32 { if self.grow_type() == GrowType::AutoWidth { - let temp_paragraphs = self.to_paragraphs(None, None); + let temp_paragraphs = self.to_paragraphs(None, None, None); let mut temp_paragraphs = temp_paragraphs; auto_width(&mut temp_paragraphs, f32::MAX).ceil() } else { @@ -205,7 +221,7 @@ impl TextContent { pub fn visual_bounds(&self) -> (f32, f32) { let paragraph_width = self.get_width(); - let mut paragraphs = self.to_paragraphs(None, None); + let mut paragraphs = self.to_paragraphs(None, None, None); let paragraph_height = auto_height(&mut paragraphs, paragraph_width); (paragraph_width, paragraph_height) } @@ -408,15 +424,24 @@ impl TextLeaf { fallback_fonts: &HashSet, _blur: Option<&ImageFilter>, blur_mask: Option<&MaskFilter>, + shadow: Option<&Paint>, ) -> skia::textlayout::TextStyle { let mut style = skia::textlayout::TextStyle::default(); - let mut paint = merge_fills(&self.fills, *content_bounds); - if let Some(blur_mask) = blur_mask { - paint.set_mask_filter(blur_mask.clone()); + if shadow.is_some() { + let paint = shadow.unwrap().clone(); + style.set_foreground_paint(&paint); + } else { + let paint = merge_fills(&self.fills, *content_bounds); + style.set_foreground_paint(&paint); + } + + if let Some(blur_mask) = blur_mask { + let mut paint = skia::Paint::default(); + paint.set_mask_filter(blur_mask.clone()); + style.set_foreground_paint(&paint); } - style.set_foreground_paint(&paint); style.set_font_size(self.font_size); style.set_letter_spacing(self.letter_spacing); style.set_half_leading(false); @@ -450,8 +475,9 @@ impl TextLeaf { fallback_fonts: &HashSet, blur: Option<&ImageFilter>, blur_mask: Option<&MaskFilter>, + shadow: Option<&Paint>, ) -> skia::textlayout::TextStyle { - let mut style = self.to_style(&Rect::default(), fallback_fonts, blur, blur_mask); + let mut style = self.to_style(&Rect::default(), fallback_fonts, blur, blur_mask, shadow); style.set_foreground_paint(stroke_paint); style.set_font_size(self.font_size); style.set_letter_spacing(self.letter_spacing); @@ -492,6 +518,16 @@ impl TextLeaf { pub fn scale_content(&mut self, value: f32) { self.font_size *= value; } + + pub fn is_transparent(&self) -> bool { + if self.fills.is_empty() { + return true; + } + self.fills.iter().all(|fill| match fill { + shapes::Fill::Solid(shapes::SolidColor(color)) => color.a() == 0, + _ => false, + }) + } } const RAW_PARAGRAPH_DATA_SIZE: usize = std::mem::size_of::(); @@ -734,6 +770,94 @@ pub fn auto_height(paragraphs: &mut [Vec], width: f32) -> f32 }) } +fn get_text_stroke_paints_with_shadows( + stroke: &Stroke, + blur: Option<&ImageFilter>, + blur_mask: Option<&MaskFilter>, + shadow: Option<&Paint>, + is_transparent: bool, +) -> Vec { + let mut paints = Vec::new(); + + match stroke.kind { + StrokeKind::Inner => { + let mut paint = skia_safe::Paint::default(); + paint.set_style(skia::PaintStyle::Fill); + paint.set_anti_alias(true); + + if let Some(blur) = blur { + paint.set_image_filter(blur.clone()); + } + + if let Some(shadow) = shadow { + paint.set_image_filter(shadow.image_filter()); + } + + paints.push(paint.clone()); + + if is_transparent { + let image_filter = skia_safe::image_filters::erode( + (stroke.width, stroke.width), + paint.image_filter(), + None, + ); + paint.set_image_filter(image_filter); + paint.set_blend_mode(skia::BlendMode::DstOut); + paints.push(paint.clone()); + } + } + StrokeKind::Center => { + let mut paint = skia_safe::Paint::default(); + paint.set_anti_alias(true); + paint.set_stroke_width(stroke.width); + + if let Some(blur) = blur { + paint.set_image_filter(blur.clone()); + } + + if let Some(shadow) = shadow { + paint.set_image_filter(shadow.image_filter()); + } + + if is_transparent { + paint.set_style(skia::PaintStyle::Stroke); + } else { + paint.set_style(skia::PaintStyle::StrokeAndFill); + } + + paints.push(paint); + } + StrokeKind::Outer => { + let mut paint = skia_safe::Paint::default(); + paint.set_style(skia::PaintStyle::StrokeAndFill); + paint.set_anti_alias(true); + paint.set_stroke_width(stroke.width * 2.0); + + if let Some(blur_mask) = blur_mask { + paint.set_mask_filter(blur_mask.clone()); + } + + if let Some(shadow) = shadow { + paint.set_image_filter(shadow.image_filter()); + } + + paints.push(paint.clone()); + + if is_transparent { + let image_filter = skia_safe::image_filters::erode( + (stroke.width, stroke.width), + paint.image_filter(), + None, + ); + paint.set_image_filter(image_filter); + paint.set_blend_mode(skia::BlendMode::DstOut); + paints.push(paint.clone()); + } + } + } + paints +} + fn get_text_stroke_paints( stroke: &Stroke, bounds: &Rect, @@ -819,9 +943,6 @@ fn get_text_stroke_paints( paint.set_blend_mode(skia::BlendMode::Clear); paint.set_color(skia::Color::TRANSPARENT); paint.set_anti_alias(true); - if let Some(blur_mask) = blur_mask { - paint.set_mask_filter(blur_mask.clone()); - } paints.push(paint); } } diff --git a/render-wasm/src/shapes/text_paths.rs b/render-wasm/src/shapes/text_paths.rs index 11c987055e..9c12b86a83 100644 --- a/render-wasm/src/shapes/text_paths.rs +++ b/render-wasm/src/shapes/text_paths.rs @@ -20,7 +20,7 @@ impl TextPaths { let mut paths = Vec::new(); let mut offset_y = self.bounds.y(); - let mut paragraphs = self.to_paragraphs(None, None); + let mut paragraphs = self.to_paragraphs(None, None, None); for paragraphs in paragraphs.iter_mut() { for paragraph_builder in paragraphs.iter_mut() { diff --git a/render-wasm/src/wasm/text.rs b/render-wasm/src/wasm/text.rs index 06096bf0e8..28976c66ff 100644 --- a/render-wasm/src/wasm/text.rs +++ b/render-wasm/src/wasm/text.rs @@ -45,7 +45,7 @@ pub extern "C" fn get_text_dimensions() -> *mut u8 { if let Type::Text(content) = &shape.shape_type { // 1. Reset Paragraphs let paragraph_width = content.get_width(); - let mut paragraphs = content.to_paragraphs(None, None); + let mut paragraphs = content.to_paragraphs(None, None, None); let built_paragraphs = build_paragraphs_with_width(&mut paragraphs, paragraph_width); // 2. Max Width Calculation @@ -57,12 +57,12 @@ pub extern "C" fn get_text_dimensions() -> *mut u8 { // 3. Width and Height Calculation match content.grow_type() { GrowType::AutoHeight => { - let mut paragraph_height = content.to_paragraphs(None, None); + let mut paragraph_height = content.to_paragraphs(None, None, None); height = auto_height(&mut paragraph_height, paragraph_width).ceil(); } GrowType::AutoWidth => { width = paragraph_width; - let mut paragraph_height = content.to_paragraphs(None, None); + let mut paragraph_height = content.to_paragraphs(None, None, None); height = auto_height(&mut paragraph_height, paragraph_width).ceil(); } GrowType::Fixed => {}