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}