mirror of
https://github.com/penpot/penpot.git
synced 2025-07-20 10:37:13 +02:00
🔧 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:
parent
2d36a1f3e0
commit
4869373a43
8 changed files with 462 additions and 63 deletions
|
@ -19,9 +19,7 @@ use options::RenderOptions;
|
|||
use surfaces::{SurfaceId, Surfaces};
|
||||
|
||||
use crate::performance;
|
||||
use crate::shapes::{
|
||||
modified_children_ids, Corners, Fill, Shape, StrokeKind, StructureEntry, Type,
|
||||
};
|
||||
use crate::shapes::{modified_children_ids, Corners, Fill, Shape, StructureEntry, Type};
|
||||
use crate::tiles::{self, PendingTiles, TileRect};
|
||||
use crate::uuid::Uuid;
|
||||
use crate::view::Viewbox;
|
||||
|
@ -225,6 +223,14 @@ impl RenderState {
|
|||
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) {
|
||||
let tile_rect = self.get_current_aligned_tile_bounds();
|
||||
self.surfaces.cache_current_tile_texture(
|
||||
|
@ -405,7 +411,13 @@ impl RenderState {
|
|||
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);
|
||||
text::render(self, &shape, ¶graphs, 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, ¶graphs, Some(SurfaceId::Strokes));
|
||||
}
|
||||
|
||||
for stroke in shape.strokes().rev() {
|
||||
let stroke_paragraphs = text_content.get_skia_stroke_paragraphs(
|
||||
|
@ -414,21 +426,15 @@ impl RenderState {
|
|||
self.fonts.font_collection(),
|
||||
);
|
||||
shadows::render_text_drop_shadows(self, &shape, &stroke_paragraphs, antialias);
|
||||
if stroke.kind == StrokeKind::Inner {
|
||||
// Inner strokes must be rendered on the Fills surface because their blend modes
|
||||
// (e.g., SrcATop, DstOver) rely on the text fill already being present underneath.
|
||||
// Rendering them on a separate surface would break this blending and result in incorrect visuals as
|
||||
// black color background.
|
||||
text::render(self, &shape, &stroke_paragraphs, None, None);
|
||||
} else {
|
||||
text::render(
|
||||
self,
|
||||
&shape,
|
||||
&stroke_paragraphs,
|
||||
Some(SurfaceId::Strokes),
|
||||
None,
|
||||
);
|
||||
}
|
||||
strokes::render(
|
||||
self,
|
||||
&shape,
|
||||
stroke,
|
||||
None,
|
||||
None,
|
||||
Some(&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() {
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ use super::{RenderState, SurfaceId};
|
|||
use crate::render::strokes;
|
||||
use crate::render::text::{self};
|
||||
use crate::shapes::{Shadow, Shape, Stroke, Type};
|
||||
use skia_safe::{textlayout::Paragraph, Paint};
|
||||
use skia_safe::{canvas::SaveLayerRec, textlayout::Paragraph, Paint, Path};
|
||||
|
||||
// Fill Shadows
|
||||
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,
|
||||
shape,
|
||||
stroke,
|
||||
Some(SurfaceId::Strokes), // FIXME
|
||||
None,
|
||||
filter.as_ref(),
|
||||
None,
|
||||
antialias,
|
||||
)
|
||||
}
|
||||
|
@ -75,8 +76,9 @@ pub fn render_stroke_inner_shadows(
|
|||
render_state,
|
||||
shape,
|
||||
stroke,
|
||||
Some(SurfaceId::Strokes), // FIXME
|
||||
None,
|
||||
filter.as_ref(),
|
||||
None,
|
||||
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(
|
||||
render_state: &mut RenderState,
|
||||
shape: &Shape,
|
||||
|
@ -102,14 +127,19 @@ pub fn render_text_drop_shadow(
|
|||
antialias: bool,
|
||||
) {
|
||||
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(
|
||||
render_state,
|
||||
shape,
|
||||
paragraphs,
|
||||
Some(SurfaceId::DropShadows),
|
||||
Some(paint),
|
||||
);
|
||||
|
||||
render_state.restore_canvas(SurfaceId::DropShadows);
|
||||
}
|
||||
|
||||
pub fn render_text_inner_shadows(
|
||||
|
@ -131,14 +161,43 @@ pub fn render_text_inner_shadow(
|
|||
antialias: bool,
|
||||
) {
|
||||
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(
|
||||
render_state,
|
||||
shape,
|
||||
paragraphs,
|
||||
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(
|
||||
|
|
|
@ -3,9 +3,10 @@ use std::collections::HashMap;
|
|||
use crate::math::{Matrix, Point, Rect};
|
||||
|
||||
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 crate::render::text::{self};
|
||||
|
||||
// FIXME: See if we can simplify these arguments
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
|
@ -69,6 +70,41 @@ fn draw_stroke_on_circle(
|
|||
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
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn draw_stroke_on_path(
|
||||
|
@ -93,34 +129,15 @@ pub fn draw_stroke_on_path(
|
|||
paint.set_image_filter(filter.clone());
|
||||
}
|
||||
|
||||
// Draw the different kind of strokes for a path requires different strategies:
|
||||
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 => {
|
||||
canvas.save(); // As we are using clear for surfaces we use save and restore here to still be able to clean the full surface
|
||||
canvas.clip_path(&skia_path, skia::ClipOp::Intersect, antialias);
|
||||
canvas.draw_path(&skia_path, &paint);
|
||||
canvas.restore();
|
||||
draw_inner_stroke_path(canvas, &skia_path, &paint, antialias);
|
||||
}
|
||||
// For center stroke we don't need to do anything extra
|
||||
StrokeKind::Center => {
|
||||
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 => {
|
||||
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(&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();
|
||||
draw_outer_stroke_path(canvas, &skia_path, &paint, antialias);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -500,15 +517,13 @@ fn draw_image_stroke_in_container(
|
|||
canvas.restore();
|
||||
}
|
||||
|
||||
/**
|
||||
* This SHOULD be the only public function in this module.
|
||||
*/
|
||||
pub fn render(
|
||||
render_state: &mut RenderState,
|
||||
shape: &Shape,
|
||||
stroke: &Stroke,
|
||||
surface_id: Option<SurfaceId>,
|
||||
shadow: Option<&ImageFilter>,
|
||||
paragraphs: Option<&[Vec<Paragraph>]>,
|
||||
antialias: bool,
|
||||
) {
|
||||
let scale = render_state.get_scale();
|
||||
|
@ -541,6 +556,14 @@ pub fn render(
|
|||
Type::Circle => draw_stroke_on_circle(
|
||||
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(_)) => {
|
||||
if let Some(path) = shape_type.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,26 +1,16 @@
|
|||
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(
|
||||
render_state: &mut RenderState,
|
||||
shape: &Shape,
|
||||
paragraphs: &[Vec<Paragraph>],
|
||||
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
|
||||
.surfaces
|
||||
.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 {
|
||||
let mut offset_y = 0.0;
|
||||
for skia_paragraph in group {
|
||||
|
@ -29,7 +19,47 @@ pub fn render(
|
|||
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);
|
||||
// }
|
||||
|
|
|
@ -910,6 +910,10 @@ impl Shape {
|
|||
pub fn has_fills(&self) -> bool {
|
||||
!self.fills.is_empty()
|
||||
}
|
||||
|
||||
pub fn has_inner_strokes(&self) -> bool {
|
||||
self.strokes.iter().any(|s| s.kind == StrokeKind::Inner)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
|
@ -240,4 +240,28 @@ impl Stroke {
|
|||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
198
render-wasm/src/shapes/text_paths.rs
Normal file
198
render-wasm/src/shapes/text_paths.rs
Normal 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
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue