From 61eb2f4a195cfa3c8441367df4521a20358ba18d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Torr=C3=B3?= Date: Wed, 7 May 2025 17:28:36 +0200 Subject: [PATCH] :tada: Add text solid strokes (#6384) * :tada: Add text strokes * :wrench: Minor refactor --- render-wasm/src/render.rs | 19 ++++++- render-wasm/src/render/shadows.rs | 8 +-- render-wasm/src/render/text.rs | 14 +++-- render-wasm/src/shapes.rs | 56 ++++++++++++++++++- render-wasm/src/shapes/text.rs | 92 +++++++++++++++++++++++++++---- 5 files changed, 166 insertions(+), 23 deletions(-) diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index 9c28628b87..aaf825d356 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -350,15 +350,32 @@ impl RenderState { } } } + Type::Text(text_content) => { self.surfaces.apply_mut(&[SurfaceId::Fills], |s| { s.canvas().concat(&matrix); }); - let paragraphs = text_content.to_skia_paragraphs(&self.fonts.font_collection()); + let paragraphs = text_content.get_skia_paragraphs(&self.fonts.font_collection()); shadows::render_text_drop_shadows(self, &shape, ¶graphs, antialias); text::render(self, &shape, ¶graphs, None, None); + + for stroke in shape.strokes().rev() { + let stroke_paints = shape.get_text_stroke_paint(&stroke); + let stroke_paragraphs = text_content + .get_skia_stroke_paragraphs(&self.fonts.font_collection(), &stroke_paints); + shadows::render_text_drop_shadows(self, &shape, &stroke_paragraphs, antialias); + text::render( + self, + &shape, + &stroke_paragraphs, + Some(SurfaceId::Strokes), + None, + ); + shadows::render_text_inner_shadows(self, &shape, &stroke_paragraphs, antialias); + } + shadows::render_text_inner_shadows(self, &shape, ¶graphs, antialias); } _ => { diff --git a/render-wasm/src/render/shadows.rs b/render-wasm/src/render/shadows.rs index d7a9e637e6..5f4b80a3d4 100644 --- a/render-wasm/src/render/shadows.rs +++ b/render-wasm/src/render/shadows.rs @@ -86,7 +86,7 @@ pub fn render_stroke_inner_shadows( pub fn render_text_drop_shadows( render_state: &mut RenderState, shape: &Shape, - paragraphs: &[Paragraph], + paragraphs: &[Vec], antialias: bool, ) { for shadow in shape.drop_shadows().rev().filter(|s| !s.hidden()) { @@ -98,7 +98,7 @@ pub fn render_text_drop_shadow( render_state: &mut RenderState, shape: &Shape, shadow: &Shadow, - paragraphs: &[Paragraph], + paragraphs: &[Vec], antialias: bool, ) { let paint = &shadow.get_drop_shadow_paint(antialias); @@ -115,7 +115,7 @@ pub fn render_text_drop_shadow( pub fn render_text_inner_shadows( render_state: &mut RenderState, shape: &Shape, - paragraphs: &[Paragraph], + paragraphs: &[Vec], antialias: bool, ) { for shadow in shape.inner_shadows().rev().filter(|s| !s.hidden()) { @@ -127,7 +127,7 @@ pub fn render_text_inner_shadow( render_state: &mut RenderState, shape: &Shape, shadow: &Shadow, - paragraphs: &[Paragraph], + paragraphs: &[Vec], antialias: bool, ) { let paint = &shadow.get_inner_shadow_paint(antialias); diff --git a/render-wasm/src/render/text.rs b/render-wasm/src/render/text.rs index 9ef6cdb1d2..a52e13c66d 100644 --- a/render-wasm/src/render/text.rs +++ b/render-wasm/src/render/text.rs @@ -4,11 +4,10 @@ use skia_safe::{self as skia, canvas::SaveLayerRec, paint, textlayout::Paragraph pub fn render( render_state: &mut RenderState, shape: &Shape, - paragraphs: &[Paragraph], + paragraphs: &[Vec], surface_id: Option, paint: Option<&paint::Paint>, ) { - let mut offset_y = 0.0; let default_paint = skia::Paint::default(); let mask = SaveLayerRec::default().paint(&paint.unwrap_or(&default_paint)); let canvas = render_state @@ -16,10 +15,13 @@ pub fn render( .canvas(surface_id.unwrap_or(SurfaceId::Fills)); canvas.save_layer(&mask); - for skia_paragraph in paragraphs { - let xy = (shape.selrect().x(), shape.selrect.y() + offset_y); - skia_paragraph.paint(canvas, xy); - offset_y += skia_paragraph.height(); + for group in paragraphs { + let mut offset_y = 0.0; + for skia_paragraph in group { + let xy = (shape.selrect().x(), shape.selrect.y() + offset_y); + skia_paragraph.paint(canvas, xy); + offset_y += skia_paragraph.height(); + } } canvas.restore(); } diff --git a/render-wasm/src/shapes.rs b/render-wasm/src/shapes.rs index caf9056c82..5e27a14570 100644 --- a/render-wasm/src/shapes.rs +++ b/render-wasm/src/shapes.rs @@ -1,4 +1,4 @@ -use skia_safe::{self as skia}; +use skia_safe::{self as skia, paint::Paint}; use crate::render::BlendMode; use crate::uuid::Uuid; @@ -811,6 +811,60 @@ impl Shape { pub fn has_fills(&self) -> bool { !self.fills.is_empty() } + + pub fn get_text_stroke_paint(&self, stroke: &Stroke) -> Vec { + let mut paints = Vec::new(); + + match stroke.kind { + StrokeKind::InnerStroke => { + let mut paint = skia::Paint::default(); + paint.set_blend_mode(skia::BlendMode::DstOver); + paint.set_anti_alias(true); + paints.push(paint); + + let mut paint = skia::Paint::default(); + paint.set_style(skia::PaintStyle::Stroke); + paint.set_blend_mode(skia::BlendMode::SrcATop); + paint.set_anti_alias(true); + paint.set_stroke_width(stroke.width * 2.0); + paint.set_color(match &stroke.fill { + Fill::Solid(color) => *color, + _ => Color::BLACK, + }); + paints.push(paint); + } + StrokeKind::CenterStroke => { + let mut paint = skia::Paint::default(); + paint.set_style(skia::PaintStyle::Stroke); + paint.set_anti_alias(true); + paint.set_stroke_width(stroke.width); + paint.set_color(match &stroke.fill { + Fill::Solid(color) => *color, + _ => Color::BLACK, + }); + paints.push(paint); + } + StrokeKind::OuterStroke => { + let mut paint = skia::Paint::default(); + paint.set_style(skia::PaintStyle::Stroke); + paint.set_blend_mode(skia::BlendMode::DstOver); + paint.set_anti_alias(true); + paint.set_stroke_width(stroke.width * 2.0); + paint.set_color(match &stroke.fill { + Fill::Solid(color) => *color, + _ => Color::BLACK, + }); + paints.push(paint); + + let mut paint = skia::Paint::default(); + paint.set_blend_mode(skia::BlendMode::Clear); + paint.set_anti_alias(true); + paints.push(paint); + } + } + + paints + } } /* diff --git a/render-wasm/src/shapes/text.rs b/render-wasm/src/shapes/text.rs index 0e338f62ef..6ae677a936 100644 --- a/render-wasm/src/shapes/text.rs +++ b/render-wasm/src/shapes/text.rs @@ -4,6 +4,7 @@ use crate::{ }; use skia_safe::{ self as skia, + paint::Paint, textlayout::{FontCollection, ParagraphBuilder, ParagraphStyle}, }; @@ -47,32 +48,90 @@ impl TextContent { self.paragraphs.push(paragraph); } - pub fn to_paragraphs(&self, fonts: &FontCollection) -> Vec { - self.paragraphs + pub fn to_paragraphs(&self, fonts: &FontCollection) -> Vec> { + let mut paragraph_group = Vec::new(); + let paragraphs = self + .paragraphs .iter() .map(|p| { let paragraph_style = p.paragraph_to_style(); let mut builder = ParagraphBuilder::new(¶graph_style, fonts); for leaf in &p.children { - let text_style = leaf.to_style(&p); + let text_style = leaf.to_style(p); let text = leaf.apply_text_transform(p.text_transform); - builder.push_style(&text_style); builder.add_text(&text); builder.pop(); } builder.build() }) + .collect(); + paragraph_group.push(paragraphs); + paragraph_group + } + + pub fn to_stroke_paragraphs( + &self, + fonts: &FontCollection, + stroke_paints: &Vec, + ) -> Vec> { + let mut paragraph_group = Vec::new(); + + for stroke_paint in stroke_paints { + let mut stroke_paragraphs = Vec::new(); + for paragraph in &self.paragraphs { + let paragraph_style = paragraph.paragraph_to_style(); + let mut builder = ParagraphBuilder::new(¶graph_style, fonts); + for leaf in ¶graph.children { + let stroke_style = leaf.to_stroke_style(paragraph, &stroke_paint); + let text: String = leaf.apply_text_transform(paragraph.text_transform); + builder.push_style(&stroke_style); + builder.add_text(&text); + let p = builder.build(); + stroke_paragraphs.push(p); + } + builder.reset(); + } + paragraph_group.push(stroke_paragraphs); + } + paragraph_group + } + + pub fn get_skia_paragraphs( + &self, + fonts: &FontCollection, + ) -> Vec> { + self.to_paragraphs(fonts) + .into_iter() + .map(|group| { + group + .into_iter() + .map(|mut paragraph| { + paragraph.layout(self.width()); + paragraph + }) + .collect() + }) .collect() } - pub fn to_skia_paragraphs(&self, fonts: &FontCollection) -> Vec { - let mut paragraphs = Vec::new(); - for mut skia_paragraph in self.to_paragraphs(fonts) { - skia_paragraph.layout(self.width()); - paragraphs.push(skia_paragraph); - } - paragraphs + pub fn get_skia_stroke_paragraphs( + &self, + fonts: &FontCollection, + paints: &Vec, + ) -> Vec> { + self.to_stroke_paragraphs(fonts, paints) + .into_iter() + .map(|group| { + group + .into_iter() + .map(|mut paragraph| { + paragraph.layout(self.width()); + paragraph + }) + .collect() + }) + .collect() } } @@ -216,6 +275,7 @@ impl TextLeaf { 3 => skia::textlayout::TextDecoration::OVERLINE, _ => skia::textlayout::TextDecoration::NO_DECORATION, }); + style.set_font_families(&[ self.serialized_font_family(), default_font(), @@ -225,6 +285,16 @@ impl TextLeaf { style } + pub fn to_stroke_style( + &self, + paragraph: &Paragraph, + stroke_paint: &Paint, + ) -> skia::textlayout::TextStyle { + let mut style = self.to_style(paragraph); + style.set_foreground_paint(stroke_paint); + style + } + fn serialized_font_family(&self) -> String { format!("{}", self.font_family) }