🔧 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

@ -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(

View file

@ -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);
}
}
}
}

View file

@ -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);
// }