🎉 Add text solid strokes (#6384)

* 🎉 Add text strokes

* 🔧 Minor refactor
This commit is contained in:
Elena Torró 2025-05-07 17:28:36 +02:00 committed by GitHub
parent 44bf276c49
commit 61eb2f4a19
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 166 additions and 23 deletions

View file

@ -350,15 +350,32 @@ impl RenderState {
} }
} }
} }
Type::Text(text_content) => { Type::Text(text_content) => {
self.surfaces.apply_mut(&[SurfaceId::Fills], |s| { self.surfaces.apply_mut(&[SurfaceId::Fills], |s| {
s.canvas().concat(&matrix); 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, &paragraphs, antialias); shadows::render_text_drop_shadows(self, &shape, &paragraphs, antialias);
text::render(self, &shape, &paragraphs, None, None); text::render(self, &shape, &paragraphs, 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, &paragraphs, antialias); shadows::render_text_inner_shadows(self, &shape, &paragraphs, antialias);
} }
_ => { _ => {

View file

@ -86,7 +86,7 @@ pub fn render_stroke_inner_shadows(
pub fn render_text_drop_shadows( pub fn render_text_drop_shadows(
render_state: &mut RenderState, render_state: &mut RenderState,
shape: &Shape, shape: &Shape,
paragraphs: &[Paragraph], paragraphs: &[Vec<Paragraph>],
antialias: bool, antialias: bool,
) { ) {
for shadow in shape.drop_shadows().rev().filter(|s| !s.hidden()) { 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, render_state: &mut RenderState,
shape: &Shape, shape: &Shape,
shadow: &Shadow, shadow: &Shadow,
paragraphs: &[Paragraph], paragraphs: &[Vec<Paragraph>],
antialias: bool, antialias: bool,
) { ) {
let paint = &shadow.get_drop_shadow_paint(antialias); let paint = &shadow.get_drop_shadow_paint(antialias);
@ -115,7 +115,7 @@ pub fn render_text_drop_shadow(
pub fn render_text_inner_shadows( pub fn render_text_inner_shadows(
render_state: &mut RenderState, render_state: &mut RenderState,
shape: &Shape, shape: &Shape,
paragraphs: &[Paragraph], paragraphs: &[Vec<Paragraph>],
antialias: bool, antialias: bool,
) { ) {
for shadow in shape.inner_shadows().rev().filter(|s| !s.hidden()) { 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, render_state: &mut RenderState,
shape: &Shape, shape: &Shape,
shadow: &Shadow, shadow: &Shadow,
paragraphs: &[Paragraph], paragraphs: &[Vec<Paragraph>],
antialias: bool, antialias: bool,
) { ) {
let paint = &shadow.get_inner_shadow_paint(antialias); let paint = &shadow.get_inner_shadow_paint(antialias);

View file

@ -4,11 +4,10 @@ use skia_safe::{self as skia, canvas::SaveLayerRec, paint, textlayout::Paragraph
pub fn render( pub fn render(
render_state: &mut RenderState, render_state: &mut RenderState,
shape: &Shape, shape: &Shape,
paragraphs: &[Paragraph], paragraphs: &[Vec<Paragraph>],
surface_id: Option<SurfaceId>, surface_id: Option<SurfaceId>,
paint: Option<&paint::Paint>, paint: Option<&paint::Paint>,
) { ) {
let mut offset_y = 0.0;
let default_paint = skia::Paint::default(); let default_paint = skia::Paint::default();
let mask = SaveLayerRec::default().paint(&paint.unwrap_or(&default_paint)); let mask = SaveLayerRec::default().paint(&paint.unwrap_or(&default_paint));
let canvas = render_state let canvas = render_state
@ -16,10 +15,13 @@ pub fn render(
.canvas(surface_id.unwrap_or(SurfaceId::Fills)); .canvas(surface_id.unwrap_or(SurfaceId::Fills));
canvas.save_layer(&mask); canvas.save_layer(&mask);
for skia_paragraph in paragraphs { 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); let xy = (shape.selrect().x(), shape.selrect.y() + offset_y);
skia_paragraph.paint(canvas, xy); skia_paragraph.paint(canvas, xy);
offset_y += skia_paragraph.height(); offset_y += skia_paragraph.height();
} }
}
canvas.restore(); canvas.restore();
} }

View file

@ -1,4 +1,4 @@
use skia_safe::{self as skia}; use skia_safe::{self as skia, paint::Paint};
use crate::render::BlendMode; use crate::render::BlendMode;
use crate::uuid::Uuid; use crate::uuid::Uuid;
@ -811,6 +811,60 @@ impl Shape {
pub fn has_fills(&self) -> bool { pub fn has_fills(&self) -> bool {
!self.fills.is_empty() !self.fills.is_empty()
} }
pub fn get_text_stroke_paint(&self, stroke: &Stroke) -> Vec<Paint> {
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
}
} }
/* /*

View file

@ -4,6 +4,7 @@ use crate::{
}; };
use skia_safe::{ use skia_safe::{
self as skia, self as skia,
paint::Paint,
textlayout::{FontCollection, ParagraphBuilder, ParagraphStyle}, textlayout::{FontCollection, ParagraphBuilder, ParagraphStyle},
}; };
@ -47,32 +48,90 @@ impl TextContent {
self.paragraphs.push(paragraph); self.paragraphs.push(paragraph);
} }
pub fn to_paragraphs(&self, fonts: &FontCollection) -> Vec<skia::textlayout::Paragraph> { pub fn to_paragraphs(&self, fonts: &FontCollection) -> Vec<Vec<skia::textlayout::Paragraph>> {
self.paragraphs let mut paragraph_group = Vec::new();
let paragraphs = self
.paragraphs
.iter() .iter()
.map(|p| { .map(|p| {
let paragraph_style = p.paragraph_to_style(); let paragraph_style = p.paragraph_to_style();
let mut builder = ParagraphBuilder::new(&paragraph_style, fonts); let mut builder = ParagraphBuilder::new(&paragraph_style, fonts);
for leaf in &p.children { 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); let text = leaf.apply_text_transform(p.text_transform);
builder.push_style(&text_style); builder.push_style(&text_style);
builder.add_text(&text); builder.add_text(&text);
builder.pop(); builder.pop();
} }
builder.build() builder.build()
}) })
.collect();
paragraph_group.push(paragraphs);
paragraph_group
}
pub fn to_stroke_paragraphs(
&self,
fonts: &FontCollection,
stroke_paints: &Vec<Paint>,
) -> Vec<Vec<skia::textlayout::Paragraph>> {
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(&paragraph_style, fonts);
for leaf in &paragraph.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<Vec<skia::textlayout::Paragraph>> {
self.to_paragraphs(fonts)
.into_iter()
.map(|group| {
group
.into_iter()
.map(|mut paragraph| {
paragraph.layout(self.width());
paragraph
})
.collect()
})
.collect() .collect()
} }
pub fn to_skia_paragraphs(&self, fonts: &FontCollection) -> Vec<skia::textlayout::Paragraph> { pub fn get_skia_stroke_paragraphs(
let mut paragraphs = Vec::new(); &self,
for mut skia_paragraph in self.to_paragraphs(fonts) { fonts: &FontCollection,
skia_paragraph.layout(self.width()); paints: &Vec<Paint>,
paragraphs.push(skia_paragraph); ) -> Vec<Vec<skia::textlayout::Paragraph>> {
} self.to_stroke_paragraphs(fonts, paints)
paragraphs .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, 3 => skia::textlayout::TextDecoration::OVERLINE,
_ => skia::textlayout::TextDecoration::NO_DECORATION, _ => skia::textlayout::TextDecoration::NO_DECORATION,
}); });
style.set_font_families(&[ style.set_font_families(&[
self.serialized_font_family(), self.serialized_font_family(),
default_font(), default_font(),
@ -225,6 +285,16 @@ impl TextLeaf {
style 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 { fn serialized_font_family(&self) -> String {
format!("{}", self.font_family) format!("{}", self.font_family)
} }