Skip to main content

ferroui_base/media/text_formatting/
text_run_cache.rs

1use std::cell::RefCell;
2use std::collections::HashMap;
3use std::rc::Rc;
4
5use crate::media::text_formatting::{ShapedTextRun, TextRun};
6use crate::media::FlowDirection;
7
8/// Caches shaped text runs and bidi processing results to avoid redundant
9/// shaping when only the paragraph width constraint changes (e.g., between
10/// Measure and Arrange).
11///
12/// Uses an inline single-entry store for the common case of a single
13/// paragraph, and only promotes to a dictionary when multiple entries are
14/// added.
15///
16/// This API is in preview and subject to change without deprecation.
17#[derive(Default)]
18pub struct TextRunCache {
19    state: RefCell<State>,
20}
21
22#[derive(Default)]
23struct State {
24    // Single-entry inline store (avoids a map allocation for the common single-paragraph case).
25    single: Option<(i32, CachedShapingResult)>,
26    // Multi-entry store (only allocated when 2+ distinct keys are added).
27    entries: Option<HashMap<i32, CachedShapingResult>>,
28}
29
30impl TextRunCache {
31    pub fn new() -> Self {
32        Self::default()
33    }
34
35    /// Invalidates all cached entries and disposes their shaped buffers.
36    pub fn invalidate(&self) {
37        let mut state = self.state.borrow_mut();
38
39        if let Some((_, value)) = state.single.take() {
40            Self::dispose_cached_runs(&value);
41            return;
42        }
43
44        let Some(entries) = &mut state.entries else {
45            return;
46        };
47
48        for entry in entries.values() {
49            Self::dispose_cached_runs(entry);
50        }
51
52        entries.clear();
53    }
54
55    /// Invalidates all cached entries at or after the specified text source index.
56    pub fn invalidate_from(&self, text_source_index: i32) {
57        let mut state = self.state.borrow_mut();
58
59        if let Some((key, value)) = &state.single {
60            if *key >= text_source_index {
61                Self::dispose_cached_runs(value);
62                state.single = None;
63            }
64
65            return;
66        }
67
68        let Some(entries) = &mut state.entries else {
69            return;
70        };
71
72        entries.retain(|key, result| {
73            if *key >= text_source_index {
74                Self::dispose_cached_runs(result);
75                false
76            } else {
77                true
78            }
79        });
80    }
81
82    /// Tries to retrieve cached shaped runs for the given text source index.
83    pub(crate) fn try_get_shaped_runs(&self, first_text_source_index: i32) -> Option<CachedShapingResult> {
84        let state = self.state.borrow();
85
86        if let Some((key, value)) = &state.single {
87            if *key == first_text_source_index {
88                return Some(value.clone());
89            }
90        }
91
92        state.entries.as_ref().and_then(|entries| entries.get(&first_text_source_index).cloned())
93    }
94
95    /// Adds shaped runs to the cache for the given text source index. The cache takes
96    /// its own reference to each [`ShapedTextRun`]; the caller retains its
97    /// original references unchanged.
98    pub(crate) fn add(&self, first_text_source_index: i32, result: CachedShapingResult) {
99        Self::add_ref_shaped_runs(&result.shaped_runs);
100
101        let mut state = self.state.borrow_mut();
102
103        if let Some(entries) = &mut state.entries {
104            if let Some(existing) = entries.insert(first_text_source_index, result) {
105                Self::dispose_cached_runs(&existing);
106            }
107
108            return;
109        }
110
111        match state.single.take() {
112            None => {
113                state.single = Some((first_text_source_index, result));
114            }
115            Some((single_key, single_value)) if single_key == first_text_source_index => {
116                Self::dispose_cached_runs(&single_value);
117                state.single = Some((first_text_source_index, result));
118            }
119            Some((single_key, single_value)) => {
120                // Second distinct key: promote to dictionary.
121                let mut entries = HashMap::new();
122                entries.insert(single_key, single_value);
123                entries.insert(first_text_source_index, result);
124                state.entries = Some(entries);
125            }
126        }
127    }
128
129    fn add_ref_shaped_runs(runs: &[Rc<dyn TextRun>]) {
130        for run in runs {
131            if let Some(shaped) = run.downcast_ref::<ShapedTextRun>() {
132                shaped.add_reference();
133            }
134        }
135    }
136
137    /// Releases the cache.
138    pub fn dispose(&self) {
139        self.invalidate();
140        self.state.borrow_mut().entries = None;
141    }
142
143    fn dispose_cached_runs(result: &CachedShapingResult) {
144        for run in result.shaped_runs.iter() {
145            if let Some(shaped) = run.downcast_ref::<ShapedTextRun>() {
146                shaped.dispose();
147            }
148        }
149    }
150
151    /// Whether the multi-entry store has been allocated.
152    #[cfg(test)]
153    pub(crate) fn has_entries_map(&self) -> bool {
154        self.state.borrow().entries.is_some()
155    }
156}
157
158/// Stores the result of text shaping for a paragraph segment starting at a
159/// given text source index.
160#[derive(Clone)]
161pub(crate) struct CachedShapingResult {
162    /// The shaped text runs (output of the shaping step).
163    pub shaped_runs: Rc<[Rc<dyn TextRun>]>,
164    /// The resolved flow direction for the paragraph.
165    pub resolved_flow_direction: FlowDirection,
166    /// The end of line marker, if any.
167    pub text_end_of_line: Option<Rc<dyn TextRun>>,
168    /// The total text source length consumed.
169    pub text_source_length: i32,
170}
171
172impl CachedShapingResult {
173    pub fn new(
174        shaped_runs: Rc<[Rc<dyn TextRun>]>,
175        resolved_flow_direction: FlowDirection,
176        text_end_of_line: Option<Rc<dyn TextRun>>,
177        text_source_length: i32,
178    ) -> Self {
179        Self { shaped_runs, resolved_flow_direction, text_end_of_line, text_source_length }
180    }
181}