diff --git a/frontend/playwright/ui/render-wasm-specs/texts.spec.js-snapshots/Renders-a-file-with-emoji-and-text-decoration-1.png b/frontend/playwright/ui/render-wasm-specs/texts.spec.js-snapshots/Renders-a-file-with-emoji-and-text-decoration-1.png index 39cd658325..3fbabb451d 100644 Binary files a/frontend/playwright/ui/render-wasm-specs/texts.spec.js-snapshots/Renders-a-file-with-emoji-and-text-decoration-1.png and b/frontend/playwright/ui/render-wasm-specs/texts.spec.js-snapshots/Renders-a-file-with-emoji-and-text-decoration-1.png differ diff --git a/frontend/playwright/ui/render-wasm-specs/texts.spec.js-snapshots/Renders-a-file-with-styled-texts-1.png b/frontend/playwright/ui/render-wasm-specs/texts.spec.js-snapshots/Renders-a-file-with-styled-texts-1.png index a49d49445c..0fc974b205 100644 Binary files a/frontend/playwright/ui/render-wasm-specs/texts.spec.js-snapshots/Renders-a-file-with-styled-texts-1.png and b/frontend/playwright/ui/render-wasm-specs/texts.spec.js-snapshots/Renders-a-file-with-styled-texts-1.png differ diff --git a/frontend/playwright/ui/render-wasm-specs/texts.spec.js-snapshots/Renders-a-file-with-text-decoration-1.png b/frontend/playwright/ui/render-wasm-specs/texts.spec.js-snapshots/Renders-a-file-with-text-decoration-1.png index e49e489e27..2d83914dae 100644 Binary files a/frontend/playwright/ui/render-wasm-specs/texts.spec.js-snapshots/Renders-a-file-with-text-decoration-1.png and b/frontend/playwright/ui/render-wasm-specs/texts.spec.js-snapshots/Renders-a-file-with-text-decoration-1.png differ diff --git a/frontend/playwright/ui/render-wasm-specs/texts.spec.js-snapshots/Renders-a-file-with-texts-with-different-alignments-1.png b/frontend/playwright/ui/render-wasm-specs/texts.spec.js-snapshots/Renders-a-file-with-texts-with-different-alignments-1.png index 19b96e2375..5585141830 100644 Binary files a/frontend/playwright/ui/render-wasm-specs/texts.spec.js-snapshots/Renders-a-file-with-texts-with-different-alignments-1.png and b/frontend/playwright/ui/render-wasm-specs/texts.spec.js-snapshots/Renders-a-file-with-texts-with-different-alignments-1.png differ diff --git a/frontend/playwright/ui/render-wasm-specs/texts.spec.js-snapshots/Renders-a-file-with-texts-with-emoji-and-different-symbols-1.png b/frontend/playwright/ui/render-wasm-specs/texts.spec.js-snapshots/Renders-a-file-with-texts-with-emoji-and-different-symbols-1.png index 47cad22636..8d0980ca21 100644 Binary files a/frontend/playwright/ui/render-wasm-specs/texts.spec.js-snapshots/Renders-a-file-with-texts-with-emoji-and-different-symbols-1.png and b/frontend/playwright/ui/render-wasm-specs/texts.spec.js-snapshots/Renders-a-file-with-texts-with-emoji-and-different-symbols-1.png differ diff --git a/render-wasm/src/render/text.rs b/render-wasm/src/render/text.rs index cf597fbf8d..3c263e8c8c 100644 --- a/render-wasm/src/render/text.rs +++ b/render-wasm/src/render/text.rs @@ -1,7 +1,7 @@ use super::{RenderState, Shape, SurfaceId}; use crate::shapes::VerticalAlign; use crate::utils::get_font_collection; -use skia_safe::{textlayout::ParagraphBuilder, FontMetrics, Paint, Path}; +use skia_safe::{textlayout::ParagraphBuilder, Paint, Path}; pub fn render( render_state: &mut RenderState, @@ -33,7 +33,6 @@ pub fn render( let layer_rec = skia_safe::canvas::SaveLayerRec::default(); canvas.save_layer(&layer_rec); - for group in paragraphs { let mut group_offset_y = global_offset_y; let group_len = group.len(); @@ -67,53 +66,132 @@ pub fn render( .collect(); let mut current_x_offset = 0.0; - let total_line_width = line_metrics.width as f32; let total_chars = line_metrics.end_index - line_metrics.start_index; - - // Calculate line's actual start position considering text alignment - // let paragraph_width = shape.bounds().width(); let line_start_offset = line_metrics.left as f32; - // No text decoration for empty lines if total_chars == 0 || style_metrics.is_empty() { continue; } - for (i, (index, style_metric)) in style_metrics.iter().enumerate() { - let text_style = style_metric.text_style; + 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 next_index = style_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 char_count = next_index - index; - let segment_width = if total_chars > 0 { - (char_count as f32 / total_chars as f32) * total_line_width + + 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 { - char_count as f32 * font_metrics.avg_char_width + 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::NO_DECORATION + == skia_safe::textlayout::TextDecoration::UNDERLINE { - let decoration_type = text_style.decoration().ty; - let text_left = xy.0 + line_start_offset + current_x_offset; - let text_top = - xy.1 + line_metrics.baseline as f32 - line_metrics.ascent as f32; - let text_width = segment_width; - let line_height = line_metrics.height as f32; - - let r = calculate_text_decoration_rect( - decoration_type, - font_metrics, - text_left, - text_top, - text_width, - line_height, - ); - - if let Some(decoration_rect) = r { - let decoration_paint = text_style.foreground(); - canvas.draw_rect(decoration_rect, &decoration_paint); + 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; @@ -143,38 +221,6 @@ pub fn render( canvas.restore(); } -pub fn calculate_text_decoration_rect( - decoration: skia_safe::textlayout::TextDecoration, - font_metrics: FontMetrics, - blob_left: f32, - blob_offset_y: f32, - text_width: f32, - blob_height: f32, -) -> Option { - let thickness = font_metrics.underline_thickness().unwrap_or(1.0); - match decoration { - skia_safe::textlayout::TextDecoration::LINE_THROUGH => { - let line_position = blob_height / 2.0; - Some(skia_safe::Rect::new( - blob_left, - blob_offset_y + line_position - thickness / 2.0, - blob_left + text_width, - blob_offset_y + line_position + thickness / 2.0, - )) - } - skia_safe::textlayout::TextDecoration::UNDERLINE => { - let underline_y = blob_offset_y + blob_height - thickness; - Some(skia_safe::Rect::new( - blob_left, - underline_y, - blob_left + text_width, - underline_y + thickness, - )) - } - _ => None, - } -} - fn calculate_total_paragraphs_height(paragraphs: &mut [ParagraphBuilder], width: f32) -> f32 { paragraphs .iter_mut() diff --git a/render-wasm/src/shapes/text.rs b/render-wasm/src/shapes/text.rs index 10d53023be..6cbfbd3052 100644 --- a/render-wasm/src/shapes/text.rs +++ b/render-wasm/src/shapes/text.rs @@ -120,8 +120,7 @@ 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(paragraph, &self.bounds, fallback_fonts, blur, blur_mask); + let text_style = leaf.to_style(&self.bounds, fallback_fonts, blur, blur_mask); let text = leaf.apply_text_transform(); builder.push_style(&text_style); builder.add_text(&text); @@ -169,13 +168,8 @@ impl TextContent { ParagraphBuilder::new(¶graph_style, fonts) }); let stroke_paint = stroke_paint.clone(); - let stroke_style = leaf.to_stroke_style( - paragraph, - &stroke_paint, - fallback_fonts, - blur, - blur_mask, - ); + let stroke_style = + leaf.to_stroke_style(&stroke_paint, fallback_fonts, blur, blur_mask); builder.push_style(&stroke_style); builder.add_text(&text); } @@ -320,14 +314,12 @@ impl Paragraph { 3 => skia::textlayout::TextAlign::Justify, _ => skia::textlayout::TextAlign::Left, }); - style.set_height(self.line_height); style.set_text_direction(match self.text_direction { 0 => skia::textlayout::TextDirection::LTR, 1 => skia::textlayout::TextDirection::RTL, _ => skia::textlayout::TextDirection::LTR, }); - // Force minimum line height for empty lines using strut style if !self.children.is_empty() { let reference_child = self .children @@ -336,13 +328,21 @@ impl Paragraph { .unwrap_or(&self.children[0]); let mut strut_style = skia::textlayout::StrutStyle::default(); + let line_height = self.line_height.max(1.0); strut_style.set_font_size(reference_child.font_size); - strut_style.set_height(self.line_height); + strut_style.set_height(line_height); strut_style.set_height_override(true); - strut_style.set_half_leading(false); - strut_style.set_leading(0.0); + strut_style.set_half_leading(true); strut_style.set_strut_enabled(true); strut_style.set_force_strut_height(true); + + let font_families = vec![ + reference_child.serialized_font_family(), + default_font(), + DEFAULT_EMOJI_FONT.to_string(), + ]; + strut_style.set_font_families(&font_families); + style.set_strut_style(strut_style); } @@ -404,7 +404,6 @@ impl TextLeaf { pub fn to_style( &self, - paragraph: &Paragraph, content_bounds: &Rect, fallback_fonts: &HashSet, _blur: Option<&ImageFilter>, @@ -420,8 +419,6 @@ impl TextLeaf { style.set_foreground_paint(&paint); style.set_font_size(self.font_size); style.set_letter_spacing(self.letter_spacing); - style.set_height(paragraph.line_height); - style.set_height_override(true); style.set_half_leading(false); style.set_decoration_type(match self.text_decoration { @@ -449,13 +446,12 @@ impl TextLeaf { pub fn to_stroke_style( &self, - paragraph: &Paragraph, stroke_paint: &Paint, fallback_fonts: &HashSet, blur: Option<&ImageFilter>, blur_mask: Option<&MaskFilter>, ) -> skia::textlayout::TextStyle { - let mut style = self.to_style(paragraph, &Rect::default(), fallback_fonts, blur, blur_mask); + let mut style = self.to_style(&Rect::default(), fallback_fonts, blur, blur_mask); style.set_foreground_paint(stroke_paint); style.set_font_size(self.font_size); style.set_letter_spacing(self.letter_spacing);