mirror of
https://github.com/penpot/penpot.git
synced 2026-07-23 14:38:12 +00:00
⚡ cache laid-out text paragraphs across renders
This commit is contained in:
parent
113156592a
commit
69581755b4
@ -3,8 +3,8 @@ use crate::{
|
||||
error::Result,
|
||||
math::Rect,
|
||||
shapes::{
|
||||
calculate_text_layout_data, set_paint_fill, ParagraphBuilderGroup, Stroke, StrokeKind,
|
||||
TextContent,
|
||||
calculate_text_layout_data, set_paint_fill, ParagraphBuilderGroup, ParagraphLayout,
|
||||
Stroke, StrokeKind, TextContent,
|
||||
},
|
||||
utils::{get_fallback_fonts, get_font_collection},
|
||||
};
|
||||
@ -250,6 +250,8 @@ fn render_text_on_canvas(
|
||||
fill_inset: Option<f32>,
|
||||
layer_opacity: Option<f32>,
|
||||
) {
|
||||
let use_cache = shadow.is_none() && blur.is_none();
|
||||
|
||||
if let Some(blur_filter) = blur {
|
||||
let mut blur_paint = Paint::default();
|
||||
blur_paint.set_image_filter(blur_filter.clone());
|
||||
@ -260,7 +262,7 @@ fn render_text_on_canvas(
|
||||
if let Some(shadow_paint) = shadow {
|
||||
let layer_rec = SaveLayerRec::default().paint(shadow_paint);
|
||||
canvas.save_layer(&layer_rec);
|
||||
draw_text(canvas, shape, paragraph_builders, layer_opacity);
|
||||
draw_text(canvas, shape, paragraph_builders, layer_opacity, use_cache);
|
||||
canvas.restore();
|
||||
} else if let Some(eps) = fill_inset.filter(|&e| e > 0.0) {
|
||||
if let Some(erode) = skia_safe::image_filters::erode((eps, eps), None, None) {
|
||||
@ -268,13 +270,13 @@ fn render_text_on_canvas(
|
||||
layer_paint.set_image_filter(erode);
|
||||
let layer_rec = SaveLayerRec::default().paint(&layer_paint);
|
||||
canvas.save_layer(&layer_rec);
|
||||
draw_text(canvas, shape, paragraph_builders, layer_opacity);
|
||||
draw_text(canvas, shape, paragraph_builders, layer_opacity, use_cache);
|
||||
canvas.restore();
|
||||
} else {
|
||||
draw_text(canvas, shape, paragraph_builders, layer_opacity);
|
||||
draw_text(canvas, shape, paragraph_builders, layer_opacity, use_cache);
|
||||
}
|
||||
} else {
|
||||
draw_text(canvas, shape, paragraph_builders, layer_opacity);
|
||||
draw_text(canvas, shape, paragraph_builders, layer_opacity, use_cache);
|
||||
}
|
||||
|
||||
if blur.is_some() {
|
||||
@ -284,17 +286,8 @@ fn render_text_on_canvas(
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
/// Lays out and paints paragraph builders without any layer management.
|
||||
fn paint_text(
|
||||
canvas: &Canvas,
|
||||
shape: &Shape,
|
||||
paragraph_builder_groups: &mut [Vec<ParagraphBuilder>],
|
||||
) {
|
||||
let text_content = shape.get_text_content();
|
||||
let layout_info =
|
||||
calculate_text_layout_data(shape, text_content, paragraph_builder_groups, true);
|
||||
|
||||
for para in &layout_info.paragraphs {
|
||||
fn paint_paragraph_layouts(canvas: &Canvas, paragraphs: &[ParagraphLayout]) {
|
||||
for para in paragraphs {
|
||||
para.paragraph.paint(canvas, (para.x, para.y));
|
||||
for deco in ¶.decorations {
|
||||
draw_text_decorations(
|
||||
@ -309,11 +302,53 @@ fn paint_text(
|
||||
}
|
||||
}
|
||||
|
||||
/// `use_cache` is set only by the plain fill path; stroke/mask/shadow pass
|
||||
/// `false` since they build differently painted builders.
|
||||
fn paint_text(
|
||||
canvas: &Canvas,
|
||||
shape: &Shape,
|
||||
paragraph_builder_groups: &mut [Vec<ParagraphBuilder>],
|
||||
use_cache: bool,
|
||||
) {
|
||||
let text_content = shape.get_text_content();
|
||||
|
||||
if use_cache {
|
||||
let selrect = shape.selrect();
|
||||
let key = (
|
||||
text_content.content_version(),
|
||||
selrect.x().to_bits(),
|
||||
selrect.y().to_bits(),
|
||||
selrect.width().to_bits(),
|
||||
selrect.height().to_bits(),
|
||||
shape.vertical_align() as u8,
|
||||
text_content.grow_type() as u8,
|
||||
);
|
||||
|
||||
if let Some(paragraphs) = text_content.layout.cached_render_paragraphs(key) {
|
||||
paint_paragraph_layouts(canvas, ¶graphs);
|
||||
return;
|
||||
}
|
||||
|
||||
let layout_info =
|
||||
calculate_text_layout_data(shape, text_content, paragraph_builder_groups, true);
|
||||
paint_paragraph_layouts(canvas, &layout_info.paragraphs);
|
||||
text_content
|
||||
.layout
|
||||
.store_render_paragraphs(key, layout_info.paragraphs);
|
||||
return;
|
||||
}
|
||||
|
||||
let layout_info =
|
||||
calculate_text_layout_data(shape, text_content, paragraph_builder_groups, true);
|
||||
paint_paragraph_layouts(canvas, &layout_info.paragraphs);
|
||||
}
|
||||
|
||||
fn draw_text(
|
||||
canvas: &Canvas,
|
||||
shape: &Shape,
|
||||
paragraph_builder_groups: &mut [Vec<ParagraphBuilder>],
|
||||
layer_opacity: Option<f32>,
|
||||
use_cache: bool,
|
||||
) {
|
||||
if let Some(opacity) = layer_opacity {
|
||||
let mut opacity_paint = Paint::default();
|
||||
@ -324,7 +359,7 @@ fn draw_text(
|
||||
canvas.save_layer(&SaveLayerRec::default());
|
||||
}
|
||||
|
||||
paint_text(canvas, shape, paragraph_builder_groups);
|
||||
paint_text(canvas, shape, paragraph_builder_groups, use_cache);
|
||||
}
|
||||
|
||||
/// Renders an inner stroke using mask + SrcIn + DstOver layer structure.
|
||||
@ -371,7 +406,7 @@ fn render_inner_stroke_on_canvas(
|
||||
canvas.save_layer(&SaveLayerRec::default());
|
||||
|
||||
// Draw opaque mask (full alpha text shape)
|
||||
paint_text(canvas, shape, mask_builders);
|
||||
paint_text(canvas, shape, mask_builders, false);
|
||||
|
||||
// SrcIn layer — only keeps stroke pixels where mask has alpha
|
||||
let mut src_in_paint = Paint::default();
|
||||
@ -379,7 +414,7 @@ fn render_inner_stroke_on_canvas(
|
||||
canvas.save_layer(&SaveLayerRec::default().paint(&src_in_paint));
|
||||
|
||||
// Draw stroke
|
||||
paint_text(canvas, shape, stroke_builders);
|
||||
paint_text(canvas, shape, stroke_builders, false);
|
||||
|
||||
canvas.restore(); // SrcIn layer
|
||||
canvas.restore(); // mask group layer
|
||||
@ -389,7 +424,7 @@ fn render_inner_stroke_on_canvas(
|
||||
dst_over_paint.set_blend_mode(skia::BlendMode::DstOver);
|
||||
canvas.save_layer(&SaveLayerRec::default().paint(&dst_over_paint));
|
||||
|
||||
paint_text(canvas, shape, fill_builders);
|
||||
paint_text(canvas, shape, fill_builders, false);
|
||||
|
||||
canvas.restore(); // DstOver layer
|
||||
canvas.restore(); // outer layer
|
||||
|
||||
@ -18,7 +18,7 @@ use skia_safe::{
|
||||
Contains,
|
||||
};
|
||||
|
||||
use std::cell::Cell;
|
||||
use std::cell::{Cell, Ref, RefCell};
|
||||
use std::collections::HashSet;
|
||||
|
||||
use super::FontFamily;
|
||||
@ -190,11 +190,23 @@ struct CachedExtrect {
|
||||
bottom: f32,
|
||||
}
|
||||
|
||||
/// (content_version, x, y, width, height, valign, grow_type) as raw bits.
|
||||
pub type RenderTextCacheKey = (u64, u32, u32, u32, u32, u8, u8);
|
||||
|
||||
/// Cached laid-out paragraphs for the plain fill paint path, reused while the
|
||||
/// key is unchanged so re-renders skip Skia paragraph layout.
|
||||
#[derive(Debug)]
|
||||
struct RenderTextCache {
|
||||
key: RenderTextCacheKey,
|
||||
paragraphs: Vec<ParagraphLayout>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TextContentLayout {
|
||||
pub paragraph_builders: Vec<ParagraphBuilderGroup>,
|
||||
pub paragraphs: Vec<Vec<skia::textlayout::Paragraph>>,
|
||||
cached_extrect: Cell<Option<CachedExtrect>>,
|
||||
render_cache: RefCell<Option<RenderTextCache>>,
|
||||
}
|
||||
|
||||
impl Default for TextContentLayout {
|
||||
@ -209,6 +221,7 @@ impl Clone for TextContentLayout {
|
||||
paragraph_builders: vec![],
|
||||
paragraphs: vec![],
|
||||
cached_extrect: Cell::new(None),
|
||||
render_cache: RefCell::new(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -225,6 +238,7 @@ impl TextContentLayout {
|
||||
paragraph_builders: vec![],
|
||||
paragraphs: vec![],
|
||||
cached_extrect: Cell::new(None),
|
||||
render_cache: RefCell::new(None),
|
||||
}
|
||||
}
|
||||
|
||||
@ -236,11 +250,35 @@ impl TextContentLayout {
|
||||
self.paragraph_builders = paragraph_builders;
|
||||
self.paragraphs = paragraphs;
|
||||
self.cached_extrect.set(None);
|
||||
self.render_cache.replace(None);
|
||||
}
|
||||
|
||||
pub fn needs_update(&self) -> bool {
|
||||
self.paragraph_builders.is_empty() || self.paragraphs.is_empty()
|
||||
}
|
||||
|
||||
pub fn cached_render_paragraphs(
|
||||
&self,
|
||||
key: RenderTextCacheKey,
|
||||
) -> Option<Ref<'_, Vec<ParagraphLayout>>> {
|
||||
let cache = self.render_cache.borrow();
|
||||
if cache.as_ref().map(|c| c.key) == Some(key) {
|
||||
Some(Ref::map(cache, |c| {
|
||||
&c.as_ref().expect("checked above").paragraphs
|
||||
}))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn store_render_paragraphs(
|
||||
&self,
|
||||
key: RenderTextCacheKey,
|
||||
paragraphs: Vec<ParagraphLayout>,
|
||||
) {
|
||||
self.render_cache
|
||||
.replace(Some(RenderTextCache { key, paragraphs }));
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@ -873,6 +911,10 @@ impl TextContent {
|
||||
paragraph_height
|
||||
}
|
||||
|
||||
pub fn content_version(&self) -> u64 {
|
||||
self.content_version
|
||||
}
|
||||
|
||||
pub fn needs_update_layout(&self) -> bool {
|
||||
self.layout.needs_update()
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user