Skip to main content

frust_text/
layout.rs

1//! [`TextLayout`]: a laid-out, line-broken, aligned block of text.
2//!
3//! Wraps the immutable result of a [`crate::TextContext::layout`] pass and
4//! caches its measured size for the widget layout phase.
5
6use std::cell::RefCell;
7use std::ops::Range;
8
9use kurbo::{Affine, Point, Size};
10use peniko::Brush;
11
12use frust_scene::GlyphRun;
13
14/// A source-text byte range plus rendered width for one line of a
15/// [`TextLayout`], used only by [`crate::context::TextContext::layout_bounded`]'s
16/// truncation walk. `pub(crate)` — no `parley::layout::Line` leaks past
17/// [`TextLayout::line_info`].
18pub(crate) struct LineInfo {
19    /// The line's text range in the *original* source string passed to
20    /// [`crate::TextContext::layout`]/`layout_bounded` — parley's
21    /// line-breaker assigns each line a contiguous, non-overlapping span, so
22    /// concatenating every line's range in order reconstructs the source.
23    pub range: Range<usize>,
24    /// The line's rendered advance, excluding trailing whitespace (which
25    /// collapses at the line edge and would otherwise make a legitimately
26    /// fitting line look like it overflows).
27    pub width: f32,
28}
29
30/// A finished text layout: positioned glyph runs plus a cached measured size.
31///
32/// Produced by [`crate::TextContext::layout`]. It borrows nothing from the
33/// context, so it can outlive the layout pass and be measured or converted to
34/// scene runs on demand.
35pub struct TextLayout {
36    layout: parley::Layout<Brush>,
37    size: Size,
38    /// Memoized origin-independent glyph runs, built lazily on the first
39    /// [`to_scene_runs`](Self::to_scene_runs) call and reused across every
40    /// later call while this layout is retained. The only
41    /// origin-dependent field of a [`GlyphRun`] is its `transform`, so a paint
42    /// at any origin clones these base runs and re-translates rather than
43    /// re-walking the parley layout (font matching / `positioned_glyphs`) every
44    /// frame. Retaining the [`TextLayout`] across frames — which the
45    /// [`Text`](../../frust_widgets/struct.TextWidget.html) widget now does when
46    /// content/style/width are unchanged — is what makes this reuse effective.
47    base_runs: RefCell<Option<Vec<GlyphRun>>>,
48}
49
50impl TextLayout {
51    /// Wraps a finished parley layout, caching its measured size.
52    pub(crate) fn new(layout: parley::Layout<Brush>) -> Self {
53        let size = Size::new(layout.width() as f64, layout.height() as f64);
54        Self {
55            layout,
56            size,
57            base_runs: RefCell::new(None),
58        }
59    }
60
61    /// The measured size (width x height) of the laid-out block.
62    ///
63    /// Width is the longest line's advance (bounded by the layout's
64    /// `max_width` when one was given); height is the sum of line heights.
65    pub fn size(&self) -> Size {
66        self.size
67    }
68
69    /// Converts the layout into [`frust_scene::GlyphRun`]s placed at `origin`.
70    ///
71    /// See [`crate::convert`] for the coordinate convention. The origin-invariant
72    /// run construction (the parley walk) is memoized on first call and reused
73    /// across frames (see [`base_runs`](Self::base_runs)); each call re-applies
74    /// only the cheap `origin` translation onto the cached runs' transforms.
75    ///
76    /// A caller supplying its own `run.brush` after this call (a gradient
77    /// foreground, say) must resolve that brush's geometry relative to
78    /// `origin` too — never as an already-window-space point. The render
79    /// backends apply a `GlyphRun`'s `transform` to its active paint, not
80    /// just its glyph outlines (vello's `Scene::draw_glyphs(..).transform(t)`
81    /// composes `t` with the brush the same way it composes `t` with the
82    /// glyphs; `vello_cpu`'s `set_transform` likewise precedes `set_paint`),
83    /// so a brush baked with `origin` already added gets it added a second
84    /// time at paint time. Every other [`frust_scene::Command`] variant's
85    /// `origin`/position argument does *not* auto-translate its brush this
86    /// way (see the `frust-material` gradient-button decoration's own
87    /// `PaintScene` doc note) — this contract is specific to
88    /// [`frust_scene::Command::GlyphRun`].
89    pub fn to_scene_runs(&self, origin: Point) -> Vec<GlyphRun> {
90        let mut memo = self.base_runs.borrow_mut();
91        let base = memo.get_or_insert_with(|| {
92            crate::convert::layout_to_scene_runs(&self.layout, Point::ORIGIN)
93        });
94        let transform = Affine::translate((origin.x, origin.y));
95        base.iter()
96            .map(|run| GlyphRun {
97                transform,
98                ..run.clone()
99            })
100            .collect()
101    }
102
103    /// The number of lines in this layout — `0` only for an entirely empty
104    /// layout with no lines at all. `pub(crate)` — the `max_lines`/overflow
105    /// truncation walk in [`crate::context`] is the sole caller.
106    pub(crate) fn line_count(&self) -> usize {
107        self.layout.len()
108    }
109
110    /// The source text range and rendered (trailing-whitespace-excluded)
111    /// width of line `index`, or `None` if out of bounds. `pub(crate)` — see
112    /// [`Self::line_count`].
113    pub(crate) fn line_info(&self, index: usize) -> Option<LineInfo> {
114        let line = self.layout.get(index)?;
115        let metrics = line.metrics();
116        Some(LineInfo {
117            range: line.text_range(),
118            width: (metrics.advance - metrics.trailing_whitespace).max(0.0),
119        })
120    }
121}