🔧 Add methods to render text as path (#6624)

* 🔧 Refactor text strokes drawing

* 🔧 Add text to path methods for future usage

* 📚 Add text as paths internal documentation
This commit is contained in:
Elena Torró 2025-06-16 13:37:29 +02:00 committed by GitHub
parent 2d36a1f3e0
commit 4869373a43
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 462 additions and 63 deletions

View file

@ -28,6 +28,18 @@ Penpot offers font handling for both Google Fonts and custom fonts, using Skia
- **Fallback Mechanism:** Skia requires explicit font data for proper Unicode rendering, so we cannot rely on browser fallback as with SVG. We detect the language used and automatically add the appropriate Noto Sans font as a fallback. If the users selected fonts cannot render the text, Skias fallback mechanism will try the next available font in the list. - **Fallback Mechanism:** Skia requires explicit font data for proper Unicode rendering, so we cannot rely on browser fallback as with SVG. We detect the language used and automatically add the appropriate Noto Sans font as a fallback. If the users selected fonts cannot render the text, Skias fallback mechanism will try the next available font in the list.
- **Emoji Support:** For emoji characters, we use Noto Color Emoji by default. Ideally in the future, users will be able to select custom emoji fonts instead of Noto Sans as default, as the code is ready for this scenario. - **Emoji Support:** For emoji characters, we use Noto Color Emoji by default. Ideally in the future, users will be able to select custom emoji fonts instead of Noto Sans as default, as the code is ready for this scenario.
## Texts as Paths
In Skia, it's possible to render text as paths in different ways. However, to preserve paragraph properties, we need to convert text nodes to `TextBlob`, then convert them to `Path`, and finally render them. This is necessary to ensure each piece of text is rendered in its correct position within the paragraph layout. We achieve this by using **Line Metrics** and **Style Metrics**, which allow us to iterate through each text element and read its dimensions, position, and style properties.
This feature is not currently activated, but it is explained step by step in [render-wasm/src/shapes/text_paths.rs](/render-wasm/src/shapes/text_paths.rs).
1. Get the paragraph and set the layout width. This is important because the rest of the metrics depend on setting the layout correctly.
2. Iterate through each line in the paragraph. Paragraph text style is retrieved through `LineMetrics`, which is why we go line by line.
3. Get the styles present in the line for each text leaf. `StyleMetrics` contain the style for individual text leaves, which we convert to `TextBlob` and then to `Path`.
4. Finally, `text::render` should paint all the paths on the canvas.
## References ## References
- [Noto: A typeface for the world](https://fonts.google.com/noto) - [Noto: A typeface for the world](https://fonts.google.com/noto)

View file

@ -19,9 +19,7 @@ use options::RenderOptions;
use surfaces::{SurfaceId, Surfaces}; use surfaces::{SurfaceId, Surfaces};
use crate::performance; use crate::performance;
use crate::shapes::{ use crate::shapes::{modified_children_ids, Corners, Fill, Shape, StructureEntry, Type};
modified_children_ids, Corners, Fill, Shape, StrokeKind, StructureEntry, Type,
};
use crate::tiles::{self, PendingTiles, TileRect}; use crate::tiles::{self, PendingTiles, TileRect};
use crate::uuid::Uuid; use crate::uuid::Uuid;
use crate::view::Viewbox; use crate::view::Viewbox;
@ -225,6 +223,14 @@ impl RenderState {
self.surfaces.reset(self.background_color); self.surfaces.reset(self.background_color);
} }
pub fn get_canvas_at(&mut self, surface_id: SurfaceId) -> &skia::Canvas {
self.surfaces.canvas(surface_id)
}
pub fn restore_canvas(&mut self, surface_id: SurfaceId) {
self.surfaces.canvas(surface_id).restore();
}
pub fn apply_render_to_final_canvas(&mut self, rect: skia::Rect) { pub fn apply_render_to_final_canvas(&mut self, rect: skia::Rect) {
let tile_rect = self.get_current_aligned_tile_bounds(); let tile_rect = self.get_current_aligned_tile_bounds();
self.surfaces.cache_current_tile_texture( self.surfaces.cache_current_tile_texture(
@ -405,7 +411,13 @@ impl RenderState {
let paragraphs = text_content.get_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);
if shape.has_inner_strokes() {
// Inner strokes paints need the text fill to apply correctly their blend modes
// (e.g., SrcATop, DstOver)
text::render(self, &shape, &paragraphs, Some(SurfaceId::Strokes));
}
for stroke in shape.strokes().rev() { for stroke in shape.strokes().rev() {
let stroke_paragraphs = text_content.get_skia_stroke_paragraphs( let stroke_paragraphs = text_content.get_skia_stroke_paragraphs(
@ -414,21 +426,15 @@ impl RenderState {
self.fonts.font_collection(), self.fonts.font_collection(),
); );
shadows::render_text_drop_shadows(self, &shape, &stroke_paragraphs, antialias); shadows::render_text_drop_shadows(self, &shape, &stroke_paragraphs, antialias);
if stroke.kind == StrokeKind::Inner { strokes::render(
// Inner strokes must be rendered on the Fills surface because their blend modes self,
// (e.g., SrcATop, DstOver) rely on the text fill already being present underneath. &shape,
// Rendering them on a separate surface would break this blending and result in incorrect visuals as stroke,
// black color background. None,
text::render(self, &shape, &stroke_paragraphs, None, None); None,
} else { Some(&stroke_paragraphs),
text::render( antialias,
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, &stroke_paragraphs, antialias);
} }
@ -458,7 +464,7 @@ impl RenderState {
for stroke in shape.strokes().rev() { for stroke in shape.strokes().rev() {
shadows::render_stroke_drop_shadows(self, &shape, stroke, antialias); shadows::render_stroke_drop_shadows(self, &shape, stroke, antialias);
strokes::render(self, &shape, stroke, None, None, antialias); strokes::render(self, &shape, stroke, None, None, None, antialias);
shadows::render_stroke_inner_shadows(self, &shape, stroke, antialias); shadows::render_stroke_inner_shadows(self, &shape, stroke, antialias);
} }

View file

@ -2,7 +2,7 @@ use super::{RenderState, SurfaceId};
use crate::render::strokes; use crate::render::strokes;
use crate::render::text::{self}; use crate::render::text::{self};
use crate::shapes::{Shadow, Shape, Stroke, Type}; use crate::shapes::{Shadow, Shape, Stroke, Type};
use skia_safe::{textlayout::Paragraph, Paint}; use skia_safe::{canvas::SaveLayerRec, textlayout::Paragraph, Paint, Path};
// Fill Shadows // Fill Shadows
pub fn render_fill_drop_shadows(render_state: &mut RenderState, shape: &Shape, antialias: bool) { pub fn render_fill_drop_shadows(render_state: &mut RenderState, shape: &Shape, antialias: bool) {
@ -54,8 +54,9 @@ pub fn render_stroke_drop_shadows(
render_state, render_state,
shape, shape,
stroke, stroke,
Some(SurfaceId::Strokes), // FIXME None,
filter.as_ref(), filter.as_ref(),
None,
antialias, antialias,
) )
} }
@ -75,8 +76,9 @@ pub fn render_stroke_inner_shadows(
render_state, render_state,
shape, shape,
stroke, stroke,
Some(SurfaceId::Strokes), // FIXME None,
filter.as_ref(), filter.as_ref(),
None,
antialias, antialias,
) )
} }
@ -94,6 +96,29 @@ pub fn render_text_drop_shadows(
} }
} }
// Render text paths (unused)
#[allow(dead_code)]
pub fn render_text_path_stroke_drop_shadows(
render_state: &mut RenderState,
shape: &Shape,
paths: &Vec<(Path, Paint)>,
stroke: &Stroke,
antialias: bool,
) {
for shadow in shape.drop_shadows().rev().filter(|s| !s.hidden()) {
let stroke_shadow = shadow.get_drop_shadow_filter();
strokes::render_text_paths(
render_state,
shape,
stroke,
paths,
Some(SurfaceId::DropShadows),
stroke_shadow.as_ref(),
antialias,
);
}
}
pub fn render_text_drop_shadow( pub fn render_text_drop_shadow(
render_state: &mut RenderState, render_state: &mut RenderState,
shape: &Shape, shape: &Shape,
@ -102,14 +127,19 @@ pub fn render_text_drop_shadow(
antialias: bool, antialias: bool,
) { ) {
let paint = shadow.get_drop_shadow_paint(antialias); let paint = shadow.get_drop_shadow_paint(antialias);
let mask_paint = paint.clone();
let mask = SaveLayerRec::default().paint(&mask_paint);
let canvas = render_state.get_canvas_at(SurfaceId::DropShadows);
canvas.save_layer(&mask);
text::render( text::render(
render_state, render_state,
shape, shape,
paragraphs, paragraphs,
Some(SurfaceId::DropShadows), Some(SurfaceId::DropShadows),
Some(paint),
); );
render_state.restore_canvas(SurfaceId::DropShadows);
} }
pub fn render_text_inner_shadows( pub fn render_text_inner_shadows(
@ -131,14 +161,43 @@ pub fn render_text_inner_shadow(
antialias: bool, antialias: bool,
) { ) {
let paint = shadow.get_inner_shadow_paint(antialias); let paint = shadow.get_inner_shadow_paint(antialias);
let mask_paint = paint.clone();
let mask = SaveLayerRec::default().paint(&mask_paint);
let canvas = render_state.get_canvas_at(SurfaceId::InnerShadows);
canvas.save_layer(&mask);
text::render( text::render(
render_state, render_state,
shape, shape,
paragraphs, paragraphs,
Some(SurfaceId::InnerShadows), Some(SurfaceId::InnerShadows),
Some(paint),
); );
render_state.restore_canvas(SurfaceId::InnerShadows);
}
// Render text paths (unused)
#[allow(dead_code)]
pub fn render_text_path_stroke_inner_shadows(
render_state: &mut RenderState,
shape: &Shape,
paths: &Vec<(Path, Paint)>,
stroke: &Stroke,
antialias: bool,
) {
for shadow in shape.inner_shadows().rev().filter(|s| !s.hidden()) {
let stroke_shadow = shadow.get_inner_shadow_filter();
strokes::render_text_paths(
render_state,
shape,
stroke,
paths,
Some(SurfaceId::InnerShadows),
stroke_shadow.as_ref(),
antialias,
);
}
} }
fn render_shadow_paint( fn render_shadow_paint(

View file

@ -3,9 +3,10 @@ use std::collections::HashMap;
use crate::math::{Matrix, Point, Rect}; use crate::math::{Matrix, Point, Rect};
use crate::shapes::{Corners, Fill, ImageFill, Path, Shape, Stroke, StrokeCap, StrokeKind, Type}; use crate::shapes::{Corners, Fill, ImageFill, Path, Shape, Stroke, StrokeCap, StrokeKind, Type};
use skia_safe::{self as skia, ImageFilter, RRect}; use skia_safe::{self as skia, textlayout::Paragraph, ImageFilter, RRect};
use super::{RenderState, SurfaceId}; use super::{RenderState, SurfaceId};
use crate::render::text::{self};
// FIXME: See if we can simplify these arguments // FIXME: See if we can simplify these arguments
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
@ -69,6 +70,41 @@ fn draw_stroke_on_circle(
canvas.draw_oval(stroke_rect, &paint); canvas.draw_oval(stroke_rect, &paint);
} }
fn draw_outer_stroke_path(
canvas: &skia::Canvas,
path: &skia::Path,
paint: &skia::Paint,
antialias: bool,
) {
let mut outer_paint = skia::Paint::default();
outer_paint.set_blend_mode(skia::BlendMode::SrcOver);
outer_paint.set_anti_alias(antialias);
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&outer_paint);
canvas.save_layer(&layer_rec);
canvas.draw_path(path, paint);
let mut clear_paint = skia::Paint::default();
clear_paint.set_blend_mode(skia::BlendMode::Clear);
clear_paint.set_anti_alias(antialias);
canvas.draw_path(path, &clear_paint);
canvas.restore();
}
// For inner stroke we draw a center stroke (with double width) and clip to the original path (that way the extra outer stroke is removed)
fn draw_inner_stroke_path(
canvas: &skia::Canvas,
path: &skia::Path,
paint: &skia::Paint,
antialias: bool,
) {
canvas.save();
canvas.clip_path(path, skia::ClipOp::Intersect, antialias);
canvas.draw_path(path, paint);
canvas.restore();
}
// For outer stroke we draw a center stroke (with double width) and use another path with blend mode clear to remove the inner stroke added
// FIXME: See if we can simplify these arguments // FIXME: See if we can simplify these arguments
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub fn draw_stroke_on_path( pub fn draw_stroke_on_path(
@ -93,34 +129,15 @@ pub fn draw_stroke_on_path(
paint.set_image_filter(filter.clone()); paint.set_image_filter(filter.clone());
} }
// Draw the different kind of strokes for a path requires different strategies:
match stroke.render_kind(is_open) { match stroke.render_kind(is_open) {
// For inner stroke we draw a center stroke (with double width) and clip to the original path (that way the extra outer stroke is removed)
StrokeKind::Inner => { StrokeKind::Inner => {
canvas.save(); // As we are using clear for surfaces we use save and restore here to still be able to clean the full surface draw_inner_stroke_path(canvas, &skia_path, &paint, antialias);
canvas.clip_path(&skia_path, skia::ClipOp::Intersect, antialias);
canvas.draw_path(&skia_path, &paint);
canvas.restore();
} }
// For center stroke we don't need to do anything extra
StrokeKind::Center => { StrokeKind::Center => {
canvas.draw_path(&skia_path, &paint); canvas.draw_path(&skia_path, &paint);
} }
// For outer stroke we draw a center stroke (with double width) and use another path with blend mode clear to remove the inner stroke added
StrokeKind::Outer => { StrokeKind::Outer => {
let mut outer_paint = skia::Paint::default(); draw_outer_stroke_path(canvas, &skia_path, &paint, antialias);
outer_paint.set_blend_mode(skia::BlendMode::SrcOver);
outer_paint.set_anti_alias(antialias);
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&outer_paint);
canvas.save_layer(&layer_rec);
canvas.draw_path(&skia_path, &paint);
let mut clear_paint = skia::Paint::default();
clear_paint.set_blend_mode(skia::BlendMode::Clear);
clear_paint.set_anti_alias(antialias);
canvas.draw_path(&skia_path, &clear_paint);
canvas.restore();
} }
} }
@ -500,15 +517,13 @@ fn draw_image_stroke_in_container(
canvas.restore(); canvas.restore();
} }
/**
* This SHOULD be the only public function in this module.
*/
pub fn render( pub fn render(
render_state: &mut RenderState, render_state: &mut RenderState,
shape: &Shape, shape: &Shape,
stroke: &Stroke, stroke: &Stroke,
surface_id: Option<SurfaceId>, surface_id: Option<SurfaceId>,
shadow: Option<&ImageFilter>, shadow: Option<&ImageFilter>,
paragraphs: Option<&[Vec<Paragraph>]>,
antialias: bool, antialias: bool,
) { ) {
let scale = render_state.get_scale(); let scale = render_state.get_scale();
@ -541,6 +556,14 @@ pub fn render(
Type::Circle => draw_stroke_on_circle( Type::Circle => draw_stroke_on_circle(
canvas, stroke, &selrect, &selrect, svg_attrs, scale, shadow, antialias, canvas, stroke, &selrect, &selrect, svg_attrs, scale, shadow, antialias,
), ),
Type::Text(_) => {
text::render(
render_state,
shape,
paragraphs.expect("Text shapes should have paragraphs"),
Some(SurfaceId::Strokes),
);
}
shape_type @ (Type::Path(_) | Type::Bool(_)) => { shape_type @ (Type::Path(_) | Type::Bool(_)) => {
if let Some(path) = shape_type.path() { if let Some(path) = shape_type.path() {
draw_stroke_on_path( draw_stroke_on_path(
@ -560,3 +583,46 @@ pub fn render(
} }
} }
} }
// Render text paths (unused)
#[allow(dead_code)]
pub fn render_text_paths(
render_state: &mut RenderState,
shape: &Shape,
stroke: &Stroke,
paths: &Vec<(skia::Path, skia::Paint)>,
surface_id: Option<SurfaceId>,
shadow: Option<&ImageFilter>,
antialias: bool,
) {
let scale = render_state.get_scale();
let canvas = render_state
.surfaces
.canvas(surface_id.unwrap_or(SurfaceId::Strokes));
let selrect = &shape.selrect;
let svg_attrs = &shape.svg_attrs;
let mut paint: skia_safe::Handle<_> =
stroke.to_text_stroked_paint(false, selrect, svg_attrs, scale, antialias);
if let Some(filter) = shadow {
paint.set_image_filter(filter.clone());
}
match stroke.render_kind(false) {
StrokeKind::Inner => {
for (path, _) in paths {
draw_inner_stroke_path(canvas, path, &paint, antialias);
}
}
StrokeKind::Center => {
for (path, _) in paths {
canvas.draw_path(path, &paint);
}
}
StrokeKind::Outer => {
for (path, _) in paths {
draw_outer_stroke_path(canvas, path, &paint, antialias);
}
}
}
}

View file

@ -1,26 +1,16 @@
use super::{RenderState, Shape, SurfaceId}; use super::{RenderState, Shape, SurfaceId};
use skia_safe::{self as skia, canvas::SaveLayerRec, textlayout::Paragraph}; use skia_safe::{textlayout::Paragraph, Paint, Path};
pub fn render( pub fn render(
render_state: &mut RenderState, render_state: &mut RenderState,
shape: &Shape, shape: &Shape,
paragraphs: &[Vec<Paragraph>], paragraphs: &[Vec<Paragraph>],
surface_id: Option<SurfaceId>, surface_id: Option<SurfaceId>,
paint: Option<skia::Paint>,
) { ) {
let use_save_layer = paint.is_some();
let mask_paint = paint.unwrap_or_default();
let mask = SaveLayerRec::default().paint(&mask_paint);
let canvas = render_state let canvas = render_state
.surfaces .surfaces
.canvas(surface_id.unwrap_or(SurfaceId::Fills)); .canvas(surface_id.unwrap_or(SurfaceId::Fills));
// Skip save_layer when no custom paint is provided to avoid isolating content unnecessarily.
// This ensures inner strokes and fills can blend correctly on the same surface.
if use_save_layer {
canvas.save_layer(&mask);
}
for group in paragraphs { for group in paragraphs {
let mut offset_y = 0.0; let mut offset_y = 0.0;
for skia_paragraph in group { for skia_paragraph in group {
@ -29,7 +19,47 @@ pub fn render(
offset_y += skia_paragraph.height(); offset_y += skia_paragraph.height();
} }
} }
if use_save_layer { }
canvas.restore();
// Render text paths (unused)
#[allow(dead_code)]
pub fn render_as_path(
render_state: &mut RenderState,
paths: &Vec<(Path, Paint)>,
surface_id: Option<SurfaceId>,
) {
let canvas = render_state
.surfaces
.canvas(surface_id.unwrap_or(SurfaceId::Fills));
for (path, paint) in paths {
// Note: path can be empty
canvas.draw_path(path, paint);
} }
} }
// How to use it?
// Type::Text(text_content) => {
// self.surfaces
// .apply_mut(&[SurfaceId::Fills, SurfaceId::Strokes], |s| {
// s.canvas().concat(&matrix);
// });
// let text_content = text_content.new_bounds(shape.selrect());
// let paths = text_content.get_paths(antialias);
// shadows::render_text_drop_shadows(self, &shape, &paths, antialias);
// text::render(self, &paths, None, None);
// for stroke in shape.strokes().rev() {
// shadows::render_text_path_stroke_drop_shadows(
// self, &shape, &paths, stroke, antialias,
// );
// strokes::render_text_paths(self, &shape, stroke, &paths, None, None, antialias);
// shadows::render_text_path_stroke_inner_shadows(
// self, &shape, &paths, stroke, antialias,
// );
// }
// shadows::render_text_inner_shadows(self, &shape, &paths, antialias);
// }

View file

@ -910,6 +910,10 @@ impl Shape {
pub fn has_fills(&self) -> bool { pub fn has_fills(&self) -> bool {
!self.fills.is_empty() !self.fills.is_empty()
} }
pub fn has_inner_strokes(&self) -> bool {
self.strokes.iter().any(|s| s.kind == StrokeKind::Inner)
}
} }
/* /*

View file

@ -240,4 +240,28 @@ impl Stroke {
paint paint
} }
// Render text paths (unused)
#[allow(dead_code)]
pub fn to_text_stroked_paint(
&self,
is_open: bool,
rect: &Rect,
svg_attrs: &HashMap<String, String>,
scale: f32,
antialias: bool,
) -> skia::Paint {
let mut paint = self.to_paint(rect, svg_attrs, scale, antialias);
match self.render_kind(is_open) {
StrokeKind::Inner => {
paint.set_stroke_width(2. * paint.stroke_width());
}
StrokeKind::Center => {}
StrokeKind::Outer => {
paint.set_stroke_width(2. * paint.stroke_width());
}
}
paint
}
} }

View file

@ -0,0 +1,198 @@
use crate::shapes::text::TextContent;
use skia_safe::{self as skia, textlayout::ParagraphBuilder, Path, Paint};
use std::ops::Deref;
pub struct TextPaths(TextContent);
// Note: This class is not being currently used.
// It's an example of how to convert texts to paths
#[allow(dead_code)]
impl TextPaths {
pub fn new(content: TextContent) -> Self {
Self(content)
}
pub fn get_skia_paragraphs(&self) -> Vec<ParagraphBuilder> {
let mut paragraphs = self.to_paragraphs();
self.collect_paragraphs(&mut paragraphs);
paragraphs
}
pub fn get_paths(&self, antialias: bool) -> Vec<(skia::Path, skia::Paint)> {
let mut paths = Vec::new();
let mut offset_y = self.bounds.y();
let mut paragraphs = self.get_skia_paragraphs();
for paragraph_builder in paragraphs.iter_mut() {
// 1. Get paragraph and set the width layout
let mut skia_paragraph = paragraph_builder.build();
let text = paragraph_builder.get_text();
let paragraph_width = self.bounds.width();
skia_paragraph.layout(paragraph_width);
let mut line_offset_y = offset_y;
// 2. Iterate through each line in the paragraph
for line_metrics in skia_paragraph.get_line_metrics() {
let line_baseline = line_metrics.baseline as f32;
let start = line_metrics.start_index;
let end = line_metrics.end_index;
// 3. Get styles present in line for each text leaf
let style_metrics = line_metrics.get_style_metrics(start..end);
let mut offset_x = 0.0;
for (i, (start_index, style_metric)) in style_metrics.iter().enumerate() {
let end_index = style_metrics.get(i + 1).map_or(end, |next| next.0);
let start_byte = text
.char_indices()
.nth(*start_index)
.map(|(i, _)| i)
.unwrap_or(0);
let end_byte = text
.char_indices()
.nth(end_index)
.map(|(i, _)| i)
.unwrap_or(text.len());
let leaf_text = &text[start_byte..end_byte];
let font = skia_paragraph.get_font_at(*start_index);
let blob_offset_x = self.bounds.x() + line_metrics.left as f32 + offset_x;
let blob_offset_y = line_offset_y;
// 4. Get the path for each text leaf
if let Some((text_path, paint)) = self.generate_text_path(
leaf_text,
&font,
blob_offset_x,
blob_offset_y,
style_metric,
antialias,
) {
let text_width = font.measure_text(leaf_text, None).0;
offset_x += text_width;
paths.push((text_path, paint));
}
}
line_offset_y = offset_y + line_baseline;
}
offset_y += skia_paragraph.height();
}
paths
}
fn generate_text_path(
&self,
leaf_text: &str,
font: &skia::Font,
blob_offset_x: f32,
blob_offset_y: f32,
style_metric: &skia::textlayout::StyleMetrics,
antialias: bool,
) -> Option<(skia::Path, skia::Paint)> {
// Convert text to path, including text decoration
// TextBlob might be empty and, in this case, we return None
// This is used to avoid rendering empty paths, but we can
// revisit this logic later
if let Some((text_blob_path, text_blob_bounds)) =
Self::get_text_blob_path(leaf_text, font, blob_offset_x, blob_offset_y)
{
let mut text_path = text_blob_path.clone();
let text_width = font.measure_text(leaf_text, None).0;
let decoration = style_metric.text_style.decoration();
let font_metrics = style_metric.font_metrics;
let blob_left = blob_offset_x;
let blob_top = blob_offset_y;
let blob_height = text_blob_bounds.height();
if let Some(decoration_rect) = self.calculate_text_decoration_rect(
decoration.ty,
font_metrics,
blob_left,
blob_top,
text_width,
blob_height,
) {
text_path.add_rect(decoration_rect, None);
}
let mut paint = style_metric.text_style.foreground();
paint.set_anti_alias(antialias);
return Some((text_path, paint));
}
None
}
fn calculate_text_decoration_rect(
&self,
decoration: skia::textlayout::TextDecoration,
font_metrics: FontMetrics,
blob_left: f32,
blob_offset_y: f32,
text_width: f32,
blob_height: f32,
) -> Option<Rect> {
match decoration {
skia::textlayout::TextDecoration::LINE_THROUGH => {
let underline_thickness = font_metrics.underline_thickness().unwrap_or(0.0);
let underline_position = blob_height / 2.0;
Some(Rect::new(
blob_left,
blob_offset_y + underline_position - underline_thickness / 2.0,
blob_left + text_width,
blob_offset_y + underline_position + underline_thickness / 2.0,
))
}
skia::textlayout::TextDecoration::UNDERLINE => {
let underline_thickness = font_metrics.underline_thickness().unwrap_or(0.0);
let underline_position = blob_height - underline_thickness;
Some(Rect::new(
blob_left,
blob_offset_y + underline_position - underline_thickness / 2.0,
blob_left + text_width,
blob_offset_y + underline_position + underline_thickness / 2.0,
))
}
_ => None,
}
}
fn get_text_blob_path(
leaf_text: &str,
font: &skia::Font,
blob_offset_x: f32,
blob_offset_y: f32,
) -> Option<(skia::Path, skia::Rect)> {
with_state!(state, {
let utf16_text = leaf_text.encode_utf16().collect::<Vec<u16>>();
let text = unsafe { skia_safe::as_utf16_unchecked(&utf16_text) };
let emoji_font = state.render_state.fonts().get_emoji_font(font.size());
let use_font = emoji_font.as_ref().unwrap_or(font);
if let Some(mut text_blob) = TextBlob::from_text(text, use_font) {
let path = SkiaParagraph::get_path(&mut text_blob);
let d = Point::new(blob_offset_x, blob_offset_y);
let offset_path = path.with_offset(d);
let bounds = text_blob.bounds();
return Some((offset_path, *bounds));
}
});
None
}
}
impl Deref for TextPaths {
type Target = TextContent;
fn deref(&self) -> &Self::Target {
&self.0
}
}