Skip to main content

smudge_text/
glyphs.rs

1//! Conversion of a laid out text block into the glyph runs paint operations
2//! carry.
3//!
4//! The conversion turns parley's positioned glyphs into the library's own
5//! [`GlyphRun`], which carries the font as plain bytes so that paint
6//! operations never depend on parley. Each run's ink bounds are measured with
7//! skrifa and cover every glyph's outline plus one logical pixel on every
8//! side for anti-aliasing, which is the area renderers may rely on for damage.
9
10use std::cell::RefCell;
11use std::collections::HashMap;
12
13use parley::FontData;
14use parley::PositionedLayoutItem;
15use skrifa::GlyphId;
16use skrifa::MetadataProvider;
17use skrifa::instance::{LocationRef, Size};
18use smudge_core::geometry::{Color, Point, Rect};
19use smudge_core::paint::{FontBlob, GlyphRun, PositionedGlyph};
20
21use crate::layout::Cut;
22use crate::layout::Ellipsis;
23use crate::layout::TextLayout;
24use crate::layout::Truncation;
25
26thread_local! {
27    /// The font blobs already mapped out of parley's font data, keyed by the
28    /// blob identifier and face index. One entry per font keeps every run of a
29    /// layout sharing the bytes, and keeps the identifier stable so renderers
30    /// can cache glyphs under it.
31    ///
32    /// Entries hold the font bytes alive for the life of the thread. Fonts are
33    /// few and outlive layouts in practice, so nothing collects them.
34    static FONT_BLOBS: RefCell<HashMap<(u64, u32), FontBlob>> = RefCell::new(HashMap::new());
35}
36
37impl TextLayout {
38    /// Converts the laid out text into glyph runs at `origin`, in paint order.
39    ///
40    /// `origin` is where the top left corner of the layout box goes, in the
41    /// caller's paint coordinates, which leave out the scroll offsets of
42    /// enclosing views, so the first line's baseline lies one ascent below
43    /// it. The layout itself carries no color, so `color` is the
44    /// color every returned run paints with, resolved by the caller at paint
45    /// time. Each run carries the font as bytes, the size its style used, the
46    /// given color, and glyphs positioned in those same coordinates. A run
47    /// whose text is empty produces no runs.
48    ///
49    /// # Panics
50    ///
51    /// Panics under the conditions of [`crate::TextSystem::with`], in
52    /// particular when called from inside another `with` call on the same
53    /// thread.
54    pub fn glyph_runs_at(&self, origin: Point, color: Color) -> Vec<GlyphRun> {
55        glyph_runs_with(&self.layout, origin, color, self.truncation.as_ref())
56    }
57}
58
59/// Converts the glyph runs of any laid out text into paint operations' runs.
60///
61/// The conversion is shared with the text input, which paints a parley
62/// [`Layout`](parley::Layout) it drives through the editor rather than a
63/// [`TextLayout`] it laid out itself, and truncates nothing.
64pub(crate) fn glyph_runs<B: parley::Brush>(
65    layout: &parley::Layout<B>,
66    origin: Point,
67    color: Color,
68) -> Vec<GlyphRun> {
69    glyph_runs_with(layout, origin, color, None)
70}
71
72/// Converts the glyph runs of any laid out text into paint operations' runs,
73/// cut to `truncation`.
74///
75/// `truncation` limits the conversion to a truncation's kept lines and cuts
76/// the last of them: the runs before the cut survive whole, the cut run
77/// keeps its leading glyphs, and the ellipsis follows them in a run of its
78/// own, since its font can differ from the cut run's.
79pub(crate) fn glyph_runs_with<B: parley::Brush>(
80    layout: &parley::Layout<B>,
81    origin: Point,
82    color: Color,
83    truncation: Option<&Truncation>,
84) -> Vec<GlyphRun> {
85    let kept_lines = truncation.map_or(usize::MAX, |truncation| truncation.kept_lines);
86    let mut runs = Vec::new();
87    for (line_index, line) in layout.lines().enumerate().take(kept_lines) {
88        let cut = match truncation {
89            Some(truncation) if line_index + 1 == truncation.kept_lines => truncation.cut.as_ref(),
90            _ => None,
91        };
92        let mut run_index = 0usize;
93        for item in line.items() {
94            let PositionedLayoutItem::GlyphRun(run) = item else {
95                continue;
96            };
97            let kept = match cut {
98                Some(cut) if run_index > cut.run => break,
99                Some(cut) if run_index == cut.run => cut.glyphs,
100                _ => usize::MAX,
101            };
102            run_index += 1;
103            if kept == 0 {
104                continue;
105            }
106            let glyphs = run
107                .positioned_glyphs()
108                .take(kept)
109                .map(|glyph| (glyph.id, origin.x + glyph.x, origin.y + glyph.y));
110            runs.push(paint_run(
111                run.run().font(),
112                run.run().font_size(),
113                glyphs,
114                color,
115            ));
116        }
117        if let Some(Cut {
118            x,
119            baseline,
120            ellipsis: Some(ellipsis),
121            ..
122        }) = cut
123        {
124            let y = origin.y + baseline;
125            let dots = (0..ellipsis.dots).map(|dot| {
126                let dot_x = origin.x + x + dot as f32 * ellipsis.advance;
127                (ellipsis.glyph, dot_x, y)
128            });
129            runs.push(paint_run(&ellipsis.font, ellipsis.size, dots, color));
130        }
131    }
132    runs
133}
134
135/// Builds one [`GlyphRun`] out of the glyph identifiers and absolute pen
136/// positions `glyphs` yields, painted with `font` at `size`.
137///
138/// Every glyph's ink is measured with skrifa against that font and size.
139fn paint_run(
140    font: &FontData,
141    size: f32,
142    glyphs: impl Iterator<Item = (u32, f32, f32)>,
143    color: Color,
144) -> GlyphRun {
145    // skrifa reads the font once per run, not once per glyph.
146    let metrics = skrifa::FontRef::from_index(font.data.as_ref(), font.index)
147        .ok()
148        .map(|font| font.glyph_metrics(Size::new(size), LocationRef::default()));
149    let mut painted = Vec::new();
150    let mut ink = Rect::default();
151    for (id, x, y) in glyphs {
152        if let Some(bounds) = metrics
153            .as_ref()
154            .and_then(|metrics| metrics.bounds(GlyphId::new(id)))
155        {
156            // Font coordinates grow up, window coordinates down.
157            ink = ink.union(Rect::new(
158                x + bounds.x_min,
159                y - bounds.y_max,
160                x + bounds.x_max,
161                y - bounds.y_min,
162            ));
163        }
164        painted.push(PositionedGlyph { id, x, y });
165    }
166    GlyphRun {
167        font: font_blob(font),
168        size,
169        color,
170        glyphs: painted,
171        ink_bounds: expand(ink, 1.0),
172    }
173}
174
175/// Resolves the ellipsis glyph truncation appends, measured with the font
176/// and size of the run it ends.
177///
178/// The horizontal ellipsis stands in for the cut text, or three full stops
179/// when the font maps no ellipsis. Returns `None` when the font maps
180/// neither, so truncation looks for the ellipsis elsewhere.
181pub(crate) fn ellipsis_glyph(font: &FontData, font_size: f32) -> Option<Ellipsis> {
182    let face = skrifa::FontRef::from_index(font.data.as_ref(), font.index).ok()?;
183    let charmap = face.charmap();
184    let (glyph, dots) = match charmap.map('\u{2026}') {
185        Some(id) => (id.to_u32(), 1),
186        None => (charmap.map('.')?.to_u32(), 3),
187    };
188    let metrics = face.glyph_metrics(Size::new(font_size), LocationRef::default());
189    let advance = metrics.advance_width(GlyphId::new(glyph)).unwrap_or(0.0);
190    Some(Ellipsis {
191        font: font.clone(),
192        size: font_size,
193        glyph,
194        dots,
195        advance,
196    })
197}
198
199/// Returns the [`FontBlob`] for parley's font data, reusing the blob the font
200/// collection already holds.
201fn font_blob(font: &FontData) -> FontBlob {
202    FONT_BLOBS.with(|cache| {
203        cache
204            .borrow_mut()
205            .entry((font.data.id(), font.index))
206            .or_insert_with(|| {
207                let (data, id) = font.data.clone().into_raw_parts();
208                FontBlob {
209                    id,
210                    data,
211                    index: font.index,
212                }
213            })
214            .clone()
215    })
216}
217
218/// Grows a non-empty rectangle by `by` pixels on every side.
219fn expand(rect: Rect, by: f32) -> Rect {
220    if rect.is_empty() {
221        rect
222    } else {
223        Rect::new(rect.x0 - by, rect.y0 - by, rect.x1 + by, rect.y1 + by)
224    }
225}
226
227#[cfg(test)]
228#[path = "glyphs_tests.rs"]
229mod tests;