Skip to main content

ferroui_pretext/
pretext_layout_preparation.rs

1use std::cell::OnceCell;
2use std::rc::Rc;
3
4use crate::pretext_layout_analysis::{analyze_tokens, contains_cjk, AnalysisToken};
5use crate::pretext_layout_bidi::compute_segment_levels;
6use crate::pretext_layout_measurement::{
7    build_base_cjk_units, get_engine_profile, merge_keep_all_text_units, EngineProfile, FontState,
8    MeasuredSegment,
9};
10use crate::{
11    PrepareOptions, PrepareProfile, PreparedLineChunk, PreparedText, PreparedTextWithSegments,
12    PretextError, PretextLayout, SegmentBreakKind, WordBreakMode,
13};
14
15struct PreparedContent {
16    base: PreparedText,
17    segments: Vec<String>,
18    segment_levels: Option<Vec<i8>>,
19}
20
21impl PretextLayout {
22    /// Measures normalized text once for repeated allocation-free layout.
23    pub fn prepare(
24        &self,
25        text: &str,
26        font: &str,
27        options: PrepareOptions,
28    ) -> Result<PreparedText, PretextError> {
29        Ok(self.prepare_core(text, font, options, false)?.base)
30    }
31
32    /// Retains normalized source segments in addition to reusable metrics.
33    pub fn prepare_with_segments(
34        &self,
35        text: &str,
36        font: &str,
37        options: PrepareOptions,
38    ) -> Result<Rc<PreparedTextWithSegments>, PretextError> {
39        let content = self.prepare_core(text, font, options, true)?;
40        let grapheme_offsets = (0..content.segments.len())
41            .map(|_| OnceCell::new())
42            .collect();
43        Ok(Rc::new(PreparedTextWithSegments {
44            base: content.base,
45            segments: content.segments,
46            segment_levels: content.segment_levels,
47            grapheme_offsets,
48        }))
49    }
50
51    /// Profiles analysis and measurement using a caller-supplied monotonic clock
52    /// measured in milliseconds. This works with native clocks and browser
53    /// `performance.now()` without introducing a platform dependency.
54    pub fn profile_prepare(
55        &self,
56        text: &str,
57        font: &str,
58        options: PrepareOptions,
59        mut now_ms: impl FnMut() -> f64,
60    ) -> Result<PrepareProfile, PretextError> {
61        let start = now_ms();
62        let locale = self.locale();
63        let tokens = analyze_tokens(
64            text,
65            options.white_space,
66            options.word_break,
67            locale.as_deref(),
68        );
69        let analysis_end = now_ms();
70        let state = self.get_font_state(font)?;
71        let mut prepared_segments = 0;
72        let mut breakable_segments = 0;
73        for token in &tokens {
74            visit_prepared_segments(
75                token,
76                &state,
77                get_engine_profile(),
78                options.word_break,
79                |segment| {
80                    prepared_segments += 1;
81                    if segment.is_breakable_run
82                        && segment
83                            .breakable_widths
84                            .as_ref()
85                            .is_some_and(|widths| widths.len() > 1)
86                    {
87                        breakable_segments += 1;
88                    }
89                },
90            )?;
91        }
92        let end = now_ms();
93        if !start.is_finite()
94            || !analysis_end.is_finite()
95            || !end.is_finite()
96            || analysis_end < start
97            || end < analysis_end
98        {
99            return Err(PretextError::InvalidInput(
100                "profiling clock must return finite, monotonic milliseconds".into(),
101            ));
102        }
103        Ok(PrepareProfile {
104            analysis_ms: analysis_end - start,
105            measure_ms: end - analysis_end,
106            total_ms: end - start,
107            analysis_segments: tokens.len(),
108            prepared_segments,
109            breakable_segments,
110        })
111    }
112
113    fn prepare_core(
114        &self,
115        text: &str,
116        font: &str,
117        options: PrepareOptions,
118        include_segments: bool,
119    ) -> Result<PreparedContent, PretextError> {
120        let state = self.get_font_state(font)?;
121        let locale = self.locale();
122        let tokens = analyze_tokens(
123            text,
124            options.white_space,
125            options.word_break,
126            locale.as_deref(),
127        );
128        let capacity = tokens.len();
129        let mut base = PreparedText {
130            font: font.to_owned(),
131            white_space: options.white_space,
132            widths: Vec::with_capacity(capacity),
133            line_end_fit_advances: Vec::with_capacity(capacity),
134            line_end_paint_advances: Vec::with_capacity(capacity),
135            kinds: Vec::with_capacity(capacity),
136            breakable_widths: Vec::with_capacity(capacity),
137            breakable_prefix_widths: Vec::with_capacity(capacity),
138            discretionary_hyphen_width: state.hyphen_width,
139            tab_stop_advance: state.tab_stop_advance,
140            chunks: Vec::new(),
141            simple_line_walk_fast_path: true,
142        };
143        let mut segments = if include_segments {
144            Vec::with_capacity(capacity)
145        } else {
146            Vec::new()
147        };
148        let mut normalized = String::new();
149        let mut starts = Vec::new();
150        let mut utf16_length = 0;
151        for token in &tokens {
152            visit_prepared_segments(
153                token,
154                &state,
155                get_engine_profile(),
156                options.word_break,
157                |segment| {
158                    base.simple_line_walk_fast_path &= matches!(
159                        segment.kind,
160                        SegmentBreakKind::Text
161                            | SegmentBreakKind::Space
162                            | SegmentBreakKind::ZeroWidthBreak
163                    );
164                    base.widths.push(segment.width);
165                    base.line_end_fit_advances
166                        .push(get_line_end_fit_advance(&segment, state.hyphen_width));
167                    base.line_end_paint_advances
168                        .push(get_line_end_paint_advance(&segment, state.hyphen_width));
169                    base.kinds.push(segment.kind);
170                    base.breakable_widths.push(segment.breakable_widths);
171                    base.breakable_prefix_widths
172                        .push(segment.breakable_prefix_widths);
173                    if include_segments {
174                        starts.push(utf16_length);
175                        utf16_length += segment.text.encode_utf16().count();
176                        normalized.push_str(&segment.text);
177                        segments.push(segment.text);
178                    }
179                },
180            )?;
181        }
182        base.chunks = build_chunks(&base.kinds);
183        base.simple_line_walk_fast_path &= base.chunks.len() <= 1;
184        let segment_levels = if include_segments {
185            compute_segment_levels(&normalized, &starts)
186        } else {
187            None
188        };
189        Ok(PreparedContent {
190            base,
191            segments,
192            segment_levels,
193        })
194    }
195
196    pub(crate) fn get_font_state(&self, font: &str) -> Result<Rc<FontState>, PretextError> {
197        let cached = self.font_states.borrow().get(font).cloned();
198        if let Some(cached) = cached {
199            return Ok(cached);
200        }
201        let generation = self.cache_generation.get();
202        let factory = self.text_measurer_factory();
203        if !factory.is_supported() {
204            return Err(PretextError::Backend(format!(
205                "text measurement backend '{}' is unsupported",
206                factory.name()
207            )));
208        }
209        let state = Rc::new(FontState::new(factory.create(font)?)?);
210        // Backend callbacks can replace the configured factory or clear caches.
211        // The current preparation owns its snapshot, but must not repopulate a
212        // newer generation with a stale backend.
213        if self.cache_generation.get() == generation {
214            let displaced = self
215                .font_states
216                .borrow_mut()
217                .insert(font.to_owned(), state.clone());
218            // A reentrant factory may have populated this same font. Release
219            // the cache borrow before the displaced backend's destructor runs.
220            drop(displaced);
221        }
222        Ok(state)
223    }
224}
225
226fn visit_prepared_segments(
227    token: &AnalysisToken,
228    state: &FontState,
229    profile: EngineProfile,
230    word_break: WordBreakMode,
231    mut on_segment: impl FnMut(MeasuredSegment),
232) -> Result<(), PretextError> {
233    if matches!(
234        token.kind,
235        SegmentBreakKind::HardBreak
236            | SegmentBreakKind::ZeroWidthBreak
237            | SegmentBreakKind::SoftHyphen
238            | SegmentBreakKind::Tab
239    ) {
240        on_segment(MeasuredSegment {
241            text: token.text.clone(),
242            kind: token.kind,
243            is_breakable_run: false,
244            width: 0.0,
245            breakable_widths: None,
246            breakable_prefix_widths: None,
247        });
248        return Ok(());
249    }
250    if token.kind == SegmentBreakKind::Text && contains_cjk(&token.text) {
251        let units = build_base_cjk_units(&token.text, profile.carry_cjk_after_closing_quote);
252        let units = if word_break == WordBreakMode::KeepAll {
253            merge_keep_all_text_units(units)
254        } else {
255            units
256        };
257        for unit in units {
258            let is_breakable_run = word_break == WordBreakMode::KeepAll || !contains_cjk(&unit);
259            on_segment(state.measure_segment(&unit, token.kind, is_breakable_run)?);
260        }
261    } else {
262        let is_breakable_run = token.kind == SegmentBreakKind::Text
263            && token.is_word_like
264            && token.text.encode_utf16().count() > 1;
265        on_segment(state.measure_segment(&token.text, token.kind, is_breakable_run)?);
266    }
267    Ok(())
268}
269
270fn get_line_end_fit_advance(segment: &MeasuredSegment, hyphen_width: f64) -> f64 {
271    match segment.kind {
272        SegmentBreakKind::Space
273        | SegmentBreakKind::PreservedSpace
274        | SegmentBreakKind::ZeroWidthBreak
275        | SegmentBreakKind::Tab
276        | SegmentBreakKind::HardBreak => 0.0,
277        SegmentBreakKind::SoftHyphen => hyphen_width,
278        _ => segment.width,
279    }
280}
281
282fn get_line_end_paint_advance(segment: &MeasuredSegment, hyphen_width: f64) -> f64 {
283    match segment.kind {
284        SegmentBreakKind::Space
285        | SegmentBreakKind::ZeroWidthBreak
286        | SegmentBreakKind::Tab
287        | SegmentBreakKind::HardBreak => 0.0,
288        SegmentBreakKind::SoftHyphen => hyphen_width,
289        _ => segment.width,
290    }
291}
292
293fn build_chunks(kinds: &[SegmentBreakKind]) -> Vec<PreparedLineChunk> {
294    let mut chunks = Vec::new();
295    let mut start = 0;
296    for (index, &kind) in kinds.iter().enumerate() {
297        if kind == SegmentBreakKind::HardBreak {
298            chunks.push(PreparedLineChunk {
299                start_segment_index: start,
300                end_segment_index: index,
301                consumed_end_segment_index: index + 1,
302            });
303            start = index + 1;
304        }
305    }
306    if start < kinds.len() {
307        chunks.push(PreparedLineChunk {
308            start_segment_index: start,
309            end_segment_index: kinds.len(),
310            consumed_end_segment_index: kinds.len(),
311        });
312    }
313    chunks
314}