Skip to main content

ferroui_pretext/
pretext_layout_shaping.rs

1//! Prepared glyph output, safe range slicing and exact-line shaping fallback.
2
3use std::cell::RefCell;
4use std::collections::HashMap;
5use std::rc::{Rc, Weak};
6
7use crate::pretext_layout_line_text::{
8    build_line_text_from_range, line_has_discretionary_hyphen, segment_grapheme_offsets,
9};
10use crate::*;
11
12#[derive(Clone, Debug, PartialEq)]
13pub struct ShapedLayoutLine {
14    pub shaped_run: Rc<PretextShapedRun>,
15    pub width: f64,
16    pub start: LayoutCursor,
17    pub end: LayoutCursor,
18}
19
20#[derive(Clone, Debug, PartialEq)]
21pub struct ShapedRichInlineFragment {
22    pub item_index: usize,
23    pub shaped_run: Rc<PretextShapedRun>,
24    pub gap_before: f64,
25    pub occupied_width: f64,
26    pub start: LayoutCursor,
27    pub end: LayoutCursor,
28}
29
30#[derive(Clone, Debug, PartialEq)]
31pub struct ShapedRichInlineLine {
32    pub fragments: Vec<ShapedRichInlineFragment>,
33    pub width: f64,
34    pub end: RichInlineCursor,
35}
36
37/// A prepared text object and its shaping snapshot. It retains its backend, so
38/// clearing or replacing an engine's backend cannot invalidate retained lines.
39pub struct PreparedShapedText {
40    prepared: Rc<PreparedTextWithSegments>,
41    shared: Rc<PreparedShapingData>,
42}
43
44// This payload never owns the prepared document: the engine may retain it while
45// the source exists, without a reference cycle through the public wrapper.
46struct PreparedShapingData {
47    direction: PretextTextDirection,
48    text: String,
49    shaped_run: Rc<PretextShapedRun>,
50    grapheme_boundary_text_indexes: Vec<Vec<usize>>,
51    text_utf16_length: usize,
52    state: Rc<ShaperState>,
53    line_runs: RefCell<HashMap<(LayoutCursor, LayoutCursor), Rc<PretextShapedRun>>>,
54}
55
56pub(crate) struct PreparedShapedCacheEntry {
57    prepared: Weak<PreparedTextWithSegments>,
58    shared: Rc<PreparedShapingData>,
59    wrapper: Weak<PreparedShapedText>,
60}
61
62impl PreparedShapedText {
63    pub fn prepared(&self) -> &Rc<PreparedTextWithSegments> {
64        &self.prepared
65    }
66    pub fn direction(&self) -> PretextTextDirection {
67        self.shared.direction
68    }
69    pub fn options(&self) -> PretextShapeOptions {
70        PretextShapeOptions {
71            direction: self.shared.direction,
72        }
73    }
74    pub fn text(&self) -> &str {
75        &self.shared.text
76    }
77    pub fn shaped_run(&self) -> &Rc<PretextShapedRun> {
78        &self.shared.shaped_run
79    }
80
81    fn get_or_create_line_run(
82        &self,
83        start: LayoutCursor,
84        end: LayoutCursor,
85    ) -> Result<Rc<PretextShapedRun>, PretextError> {
86        validate_range(&self.prepared, start, end)?;
87        let key = (start, end);
88        if let Some(cached) = self.shared.line_runs.borrow().get(&key) {
89            return Ok(cached.clone());
90        }
91        let shaped = if can_slice_prepared_shaped_run(&self.prepared, start, end) {
92            Rc::new(slice_shaped_run(
93                &self.shared.shaped_run,
94                self.text_index(start),
95                self.text_index(end),
96            ))
97        } else {
98            let text = build_line_text_from_range(&self.prepared, start, end, false);
99            self.shared.state.shape_text(&text, self.options())?
100        };
101        // Backend code above may re-enter the caller; no map borrow is held.
102        Ok(self
103            .shared
104            .line_runs
105            .borrow_mut()
106            .entry(key)
107            .or_insert(shaped)
108            .clone())
109    }
110
111    fn text_index(&self, cursor: LayoutCursor) -> usize {
112        self.shared
113            .grapheme_boundary_text_indexes
114            .get(cursor.segment_index)
115            .and_then(|boundaries| {
116                boundaries.get(
117                    cursor
118                        .grapheme_index
119                        .min(boundaries.len().saturating_sub(1)),
120                )
121            })
122            .copied()
123            .unwrap_or(self.shared.text_utf16_length)
124    }
125}
126
127impl PretextLayout {
128    pub fn shape_text(
129        &self,
130        text: &str,
131        font: &str,
132        options: PretextShapeOptions,
133    ) -> Result<Rc<PretextShapedRun>, PretextError> {
134        self.get_shaper_state(font)?.shape_text(text, options)
135    }
136
137    /// Convenience for hosts that can continue without glyph output.
138    /// Use [`Self::shape_text`] when the precise backend error is needed.
139    pub fn try_shape_text(
140        &self,
141        text: &str,
142        font: &str,
143        options: PretextShapeOptions,
144    ) -> Option<Rc<PretextShapedRun>> {
145        self.shape_text(text, font, options).ok()
146    }
147
148    pub fn shape_prepared_text(
149        &self,
150        prepared: Rc<PreparedTextWithSegments>,
151        options: PretextShapeOptions,
152    ) -> Result<Rc<PreparedShapedText>, PretextError> {
153        let key = (Rc::as_ptr(&prepared) as usize, options.direction);
154        let cached = {
155            let cache = self.prepared_shaped.borrow();
156            cache.get(&key).and_then(|entry| {
157                entry
158                    .prepared
159                    .upgrade()
160                    .filter(|source| Rc::ptr_eq(source, &prepared))
161                    .map(|_| (entry.shared.clone(), entry.wrapper.upgrade()))
162            })
163        };
164        if let Some((shared, wrapper)) = cached {
165            if let Some(wrapper) = wrapper {
166                return Ok(wrapper);
167            }
168            let wrapper = Rc::new(PreparedShapedText { prepared, shared });
169            if let Some(entry) = self.prepared_shaped.borrow_mut().get_mut(&key) {
170                entry.wrapper = Rc::downgrade(&wrapper);
171            }
172            return Ok(wrapper);
173        }
174        let generation = self.cache_generation.get();
175        let state = self.get_shaper_state(prepared.font())?;
176        let (text, grapheme_boundary_text_indexes, text_utf16_length) =
177            build_prepared_shaping_text(&prepared);
178        let shaped_run = state.shape_text(&text, options)?;
179        let shared = Rc::new(PreparedShapingData {
180            direction: options.direction,
181            text,
182            shaped_run,
183            grapheme_boundary_text_indexes,
184            text_utf16_length,
185            state,
186            line_runs: RefCell::new(HashMap::new()),
187        });
188        let shaped = Rc::new(PreparedShapedText { prepared, shared });
189        if generation == self.cache_generation.get() {
190            let retired = {
191                let mut cache = self.prepared_shaped.borrow_mut();
192                let dead_keys: Vec<_> = cache
193                    .iter()
194                    .filter(|(_, value)| value.prepared.strong_count() == 0)
195                    .map(|(key, _)| *key)
196                    .collect();
197                let mut retired: Vec<_> = dead_keys
198                    .iter()
199                    .filter_map(|key| cache.remove(key))
200                    .collect();
201                retired.extend(cache.insert(
202                    key,
203                    PreparedShapedCacheEntry {
204                        prepared: Rc::downgrade(&shaped.prepared),
205                        shared: shaped.shared.clone(),
206                        wrapper: Rc::downgrade(&shaped),
207                    },
208                ));
209                retired
210            };
211            // Dropping a cache payload can release a user-provided backend.
212            drop(retired);
213        }
214        Ok(shaped)
215    }
216
217    pub fn layout_next_shaped_line(
218        prepared: &PreparedShapedText,
219        start: LayoutCursor,
220        max_width: f64,
221    ) -> Result<Option<ShapedLayoutLine>, PretextError> {
222        Self::layout_next_line_range(&prepared.prepared, start, max_width)
223            .map(|line| Self::materialize_shaped_line_range(prepared, &line))
224            .transpose()
225    }
226
227    pub fn materialize_shaped_line_range(
228        prepared: &PreparedShapedText,
229        line: &LayoutLineRange,
230    ) -> Result<ShapedLayoutLine, PretextError> {
231        Ok(ShapedLayoutLine {
232            shaped_run: prepared.get_or_create_line_run(line.start, line.end)?,
233            width: line.width,
234            start: line.start,
235            end: line.end,
236        })
237    }
238
239    /// Convenience overload for callers retaining only their prepared metrics.
240    pub fn materialize_shaped_line_range_with_options(
241        &self,
242        prepared: Rc<PreparedTextWithSegments>,
243        line: &LayoutLineRange,
244        options: PretextShapeOptions,
245    ) -> Result<ShapedLayoutLine, PretextError> {
246        let shaped = self.shape_prepared_text(prepared, options)?;
247        Self::materialize_shaped_line_range(&shaped, line)
248    }
249
250    pub fn materialize_shaped_rich_inline_line_range(
251        &self,
252        prepared: &PreparedRichInline,
253        line: &RichInlineLineRange,
254        options: PretextShapeOptions,
255    ) -> Result<ShapedRichInlineLine, PretextError> {
256        let mut fragments = Vec::with_capacity(line.fragments.len());
257        for fragment in &line.fragments {
258            let item = prepared.prepared_item(fragment.item_index).ok_or_else(|| {
259                PretextError::InvalidInput("rich-inline fragment refers to a missing item".into())
260            })?;
261            let shaped = self.shape_prepared_text(item.clone(), options)?;
262            fragments.push(ShapedRichInlineFragment {
263                item_index: fragment.item_index,
264                shaped_run: shaped.get_or_create_line_run(fragment.start, fragment.end)?,
265                gap_before: fragment.gap_before,
266                occupied_width: fragment.occupied_width,
267                start: fragment.start,
268                end: fragment.end,
269            });
270        }
271        Ok(ShapedRichInlineLine {
272            fragments,
273            width: line.width,
274            end: line.end,
275        })
276    }
277
278    fn get_shaper_state(&self, font: &str) -> Result<Rc<ShaperState>, PretextError> {
279        if let Some(cached) = self.shaper_states.borrow().get(font) {
280            return Ok(cached.clone());
281        }
282        let generation = self.cache_generation.get();
283        let factory = self.text_shaper_factory()?;
284        let state = Rc::new(ShaperState {
285            shaper: factory.create_shaper(font)?,
286            cache: RefCell::new(HashMap::new()),
287        });
288        if generation == self.cache_generation.get() {
289            let displaced = self
290                .shaper_states
291                .borrow_mut()
292                .insert(font.to_owned(), state.clone());
293            drop(displaced);
294        }
295        Ok(state)
296    }
297}
298
299pub(crate) struct ShaperState {
300    shaper: Rc<dyn IPretextTextShaper>,
301    // Nested by direction to allow borrowed &str lookup with no key allocation
302    // on cache hits.
303    cache: RefCell<HashMap<PretextTextDirection, HashMap<String, Rc<PretextShapedRun>>>>,
304}
305
306impl ShaperState {
307    fn shape_text(
308        &self,
309        text: &str,
310        options: PretextShapeOptions,
311    ) -> Result<Rc<PretextShapedRun>, PretextError> {
312        if let Some(cached) = self
313            .cache
314            .borrow()
315            .get(&options.direction)
316            .and_then(|cache| cache.get(text))
317        {
318            return Ok(cached.clone());
319        }
320        let shaped = Rc::new(self.shaper.shape_text(text, options)?);
321        Ok(self
322            .cache
323            .borrow_mut()
324            .entry(options.direction)
325            .or_default()
326            .entry(text.into())
327            .or_insert(shaped)
328            .clone())
329    }
330}
331
332fn build_prepared_shaping_text(
333    prepared: &PreparedTextWithSegments,
334) -> (String, Vec<Vec<usize>>, usize) {
335    let capacity = prepared
336        .segments
337        .iter()
338        .enumerate()
339        .filter(|(index, _)| !is_invisible_break_segment(prepared, *index))
340        .map(|(_, text)| text.len())
341        .sum();
342    let mut text = String::with_capacity(capacity);
343    let mut boundaries = Vec::with_capacity(prepared.segments.len());
344    let mut utf16_length = 0;
345    for (index, segment) in prepared.segments.iter().enumerate() {
346        let offsets = segment_grapheme_offsets(prepared, index);
347        let mut segment_boundaries = vec![utf16_length; offsets.len()];
348        if !is_invisible_break_segment(prepared, index) {
349            for (grapheme_index, pair) in offsets.windows(2).enumerate() {
350                segment_boundaries[grapheme_index] = utf16_length;
351                let grapheme = &segment[pair[0]..pair[1]];
352                text.push_str(grapheme);
353                utf16_length += grapheme.encode_utf16().count();
354            }
355            if let Some(end) = segment_boundaries.last_mut() {
356                *end = utf16_length;
357            }
358        }
359        boundaries.push(segment_boundaries);
360    }
361    (text, boundaries, utf16_length)
362}
363
364fn validate_range(
365    prepared: &PreparedTextWithSegments,
366    start: LayoutCursor,
367    end: LayoutCursor,
368) -> Result<(), PretextError> {
369    let valid = |cursor: LayoutCursor| {
370        if cursor.segment_index == prepared.segments.len() {
371            cursor.grapheme_index == 0
372        } else {
373            cursor.segment_index < prepared.segments.len()
374                && cursor.grapheme_index
375                    < segment_grapheme_offsets(prepared, cursor.segment_index).len()
376        }
377    };
378    if start > end || !valid(start) || !valid(end) {
379        Err(PretextError::InvalidInput(
380            "shaped line cursor is outside the prepared text".into(),
381        ))
382    } else {
383        Ok(())
384    }
385}
386
387fn can_slice_prepared_shaped_run(
388    prepared: &PreparedTextWithSegments,
389    start: LayoutCursor,
390    end: LayoutCursor,
391) -> bool {
392    !line_has_discretionary_hyphen(prepared, start, end.segment_index)
393        && is_shaping_safe_boundary(prepared, start)
394        && is_shaping_safe_boundary(prepared, end)
395}
396
397fn is_shaping_safe_boundary(prepared: &PreparedTextWithSegments, cursor: LayoutCursor) -> bool {
398    if cursor.segment_index >= prepared.segments.len() {
399        return true;
400    }
401    if cursor.grapheme_index == 0 {
402        return !is_invisible_break_boundary(prepared, cursor.segment_index);
403    }
404    cursor.grapheme_index
405        >= segment_grapheme_offsets(prepared, cursor.segment_index)
406            .len()
407            .saturating_sub(1)
408        && !is_invisible_break_boundary(prepared, cursor.segment_index + 1)
409}
410
411fn is_invisible_break_boundary(prepared: &PreparedTextWithSegments, index: usize) -> bool {
412    is_invisible_break_segment(prepared, index)
413        || index
414            .checked_sub(1)
415            .is_some_and(|previous| is_invisible_break_segment(prepared, previous))
416}
417
418fn is_invisible_break_segment(prepared: &PreparedTextWithSegments, index: usize) -> bool {
419    matches!(
420        prepared.kinds.get(index),
421        Some(
422            SegmentBreakKind::ZeroWidthBreak
423                | SegmentBreakKind::SoftHyphen
424                | SegmentBreakKind::HardBreak
425        )
426    )
427}
428
429fn slice_shaped_run(source: &PretextShapedRun, start: usize, end: usize) -> PretextShapedRun {
430    let mut glyphs = Vec::new();
431    let mut font_map = HashMap::new();
432    let mut font_runs: Vec<PretextShapedFontRun> = Vec::new();
433    let mut origin = None;
434    let mut advance_x = 0.0;
435    let mut advance_y = 0.0;
436    for glyph in source
437        .glyphs()
438        .iter()
439        .filter(|glyph| glyph.cluster >= start && glyph.cluster < end)
440    {
441        let (origin_x, origin_y) = *origin.get_or_insert((glyph.x, glyph.y));
442        let mapped_index = *font_map.entry(glyph.font_run_index).or_insert_with(|| {
443            let index = font_runs.len();
444            let font = source
445                .font_runs()
446                .iter()
447                .find(|run| run.index == glyph.font_run_index)
448                .or_else(|| source.font_runs().first());
449            font_runs.push(PretextShapedFontRun {
450                index,
451                font: font.map(|run| run.font.clone()).unwrap_or_default(),
452                first_glyph_index: glyphs.len(),
453                glyph_count: 0,
454            });
455            index
456        });
457        glyphs.push(PretextShapedGlyph {
458            cluster: glyph.cluster - start,
459            x: glyph.x - origin_x,
460            y: glyph.y - origin_y,
461            font_run_index: mapped_index,
462            ..*glyph
463        });
464        font_runs[mapped_index].glyph_count += 1;
465        advance_x += glyph.x_advance;
466        advance_y += glyph.y_advance;
467    }
468    PretextShapedRun::new(source.kind(), glyphs, font_runs, advance_x, advance_y)
469}