diff --git a/render-wasm/src/render/text.rs b/render-wasm/src/render/text.rs index ff459481f7..43c8bc664e 100644 --- a/render-wasm/src/render/text.rs +++ b/render-wasm/src/render/text.rs @@ -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, layer_opacity: Option, ) { + 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], -) { - 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], + 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], layer_opacity: Option, + 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 diff --git a/render-wasm/src/shapes/text.rs b/render-wasm/src/shapes/text.rs index 86159ac3df..595d8d2fbc 100644 --- a/render-wasm/src/shapes/text.rs +++ b/render-wasm/src/shapes/text.rs @@ -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, +} + #[derive(Debug)] pub struct TextContentLayout { pub paragraph_builders: Vec, pub paragraphs: Vec>, cached_extrect: Cell>, + render_cache: RefCell>, } 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>> { + 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, + ) { + 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() }