From cbd5c1344a9ffeacb2283683f7c771b65434b494 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Tue, 1 Sep 2026 11:32:49 +0200 Subject: [PATCH] :recycle: Share text layout paragraphs across modifier clones Store Skia paragraphs in Rc so TextContentLayout::clone keeps the cached layout for rotate/pan modifiers. Add layout.clear() and treat needs_update as paragraphs-empty only. --- render-wasm/src/shapes/text.rs | 40 +++++++++++++++++++++++----- render-wasm/src/state/text_editor.rs | 9 +++---- render-wasm/src/wasm/text_editor.rs | 9 +++---- 3 files changed, 39 insertions(+), 19 deletions(-) diff --git a/render-wasm/src/shapes/text.rs b/render-wasm/src/shapes/text.rs index 3bc29e96fd..a4b8411afa 100644 --- a/render-wasm/src/shapes/text.rs +++ b/render-wasm/src/shapes/text.rs @@ -23,6 +23,7 @@ use skia_safe::{ use std::cell::Cell; use std::collections::HashSet; +use std::rc::Rc; use super::FontFamily; use crate::math::Point; @@ -196,7 +197,9 @@ struct CachedExtrect { #[derive(Debug)] pub struct TextContentLayout { pub paragraph_builders: Vec, - pub paragraphs: Vec>, + /// Shared across shape clones (e.g. modifier transforms) so rotation/pan + /// can paint without rebuilding Skia layout. Cleared builders on clone are OK. + pub paragraphs: Rc>>, cached_extrect: Cell>, } @@ -210,8 +213,8 @@ impl Clone for TextContentLayout { fn clone(&self) -> Self { Self { paragraph_builders: vec![], - paragraphs: vec![], - cached_extrect: Cell::new(None), + paragraphs: Rc::clone(&self.paragraphs), + cached_extrect: Cell::new(self.cached_extrect.get()), } } } @@ -226,7 +229,7 @@ impl TextContentLayout { pub fn new() -> Self { Self { paragraph_builders: vec![], - paragraphs: vec![], + paragraphs: Rc::new(Vec::new()), cached_extrect: Cell::new(None), } } @@ -237,12 +240,18 @@ impl TextContentLayout { paragraphs: Vec>, ) { self.paragraph_builders = paragraph_builders; - self.paragraphs = paragraphs; + self.paragraphs = Rc::new(paragraphs); + self.cached_extrect.set(None); + } + + pub fn clear(&mut self) { + self.paragraph_builders.clear(); + self.paragraphs = Rc::new(Vec::new()); self.cached_extrect.set(None); } pub fn needs_update(&self) -> bool { - self.paragraph_builders.is_empty() || self.paragraphs.is_empty() + self.paragraphs.is_empty() } } @@ -471,7 +480,7 @@ impl TextContent { let mut has_lines = false; let mut y_accum = base_y + vertical_offset; - for group in paragraphs { + for group in paragraphs.iter() { if let Some(paragraph) = group.first() { let line_metrics = paragraph.get_line_metrics(); for line in &line_metrics { @@ -1871,4 +1880,21 @@ mod tests { assert_eq!(para.char_utf16_len_at(1), 2); assert_eq!(para.char_utf16_len_at(2), 1); } + + #[test] + fn layout_clone_shares_skia_paragraphs() { + let mut layout = TextContentLayout::new(); + layout.paragraphs = Rc::new(vec![vec![]]); + let cloned = layout.clone(); + assert!(Rc::ptr_eq(&layout.paragraphs, &cloned.paragraphs)); + assert!(cloned.paragraph_builders.is_empty()); + } + + #[test] + fn layout_clear_empties_paragraphs() { + let mut layout = TextContentLayout::new(); + layout.paragraphs = Rc::new(vec![vec![]]); + layout.clear(); + assert!(layout.needs_update()); + } } diff --git a/render-wasm/src/state/text_editor.rs b/render-wasm/src/state/text_editor.rs index ecb792f775..0c14c69ae0 100644 --- a/render-wasm/src/state/text_editor.rs +++ b/render-wasm/src/state/text_editor.rs @@ -796,8 +796,7 @@ impl TextEditorState { } } - text_content.layout.paragraphs.clear(); - text_content.layout.paragraph_builders.clear(); + text_content.layout.clear(); self.reset_blink(); self.push_event(TextEditorEvent::ContentChanged); @@ -822,8 +821,7 @@ impl TextEditorState { self.selection.set_caret(clamped); } - text_content.layout.paragraphs.clear(); - text_content.layout.paragraph_builders.clear(); + text_content.layout.clear(); self.reset_blink(); self.push_event(TextEditorEvent::ContentChanged); @@ -844,8 +842,7 @@ impl TextEditorState { self.selection.set_caret(new_cursor); } - text_content.layout.paragraphs.clear(); - text_content.layout.paragraph_builders.clear(); + text_content.layout.clear(); self.reset_blink(); self.push_event(TextEditorEvent::ContentChanged); diff --git a/render-wasm/src/wasm/text_editor.rs b/render-wasm/src/wasm/text_editor.rs index 7f0e75465f..995722895b 100644 --- a/render-wasm/src/wasm/text_editor.rs +++ b/render-wasm/src/wasm/text_editor.rs @@ -396,8 +396,7 @@ pub extern "C" fn text_editor_composition_end() -> Result<()> { get_text_editor_state().selection.set_caret(new_cursor); } - text_content.layout.paragraphs.clear(); - text_content.layout.paragraph_builders.clear(); + text_content.layout.clear(); get_text_editor_state().reset_blink(); get_text_editor_state().push_event(crate::state::TextEditorEvent::ContentChanged); @@ -448,8 +447,7 @@ pub extern "C" fn text_editor_composition_update() -> Result<()> { let cursor = get_text_editor_state().selection.focus; text_helpers::insert_text_with_newlines(text_content, &cursor, &text); - text_content.layout.paragraphs.clear(); - text_content.layout.paragraph_builders.clear(); + text_content.layout.clear(); get_text_editor_state().reset_blink(); get_text_editor_state().push_event(crate::state::TextEditorEvent::ContentChanged); @@ -517,8 +515,7 @@ pub extern "C" fn text_editor_insert_text() -> Result<()> { get_text_editor_state().selection.set_caret(new_cursor); } - text_content.layout.paragraphs.clear(); - text_content.layout.paragraph_builders.clear(); + text_content.layout.clear(); get_text_editor_state().reset_blink(); get_text_editor_state().push_event(TextEditorEvent::ContentChanged);