Skip to main content

densa_paragraph/
paragraph.rs

1use std::ops::Range;
2
3use densa_text::{Font, GlyphId, GlyphRun, PositionedGlyph};
4use parley::{Alignment, AlignmentOptions, FontData, Layout, PositionedLayoutItem};
5
6use crate::style::{check_width, ParagraphError, TextAlign};
7use densa_core::Color;
8
9/// One laid-out line, in pixels relative to the paragraph's top-left corner.
10#[derive(Debug, Clone, PartialEq)]
11pub struct LineBox {
12    /// Baseline, a whole pixel.
13    pub baseline: f32,
14    /// Top of the line box.
15    pub top: f32,
16    /// Bottom of the line box.
17    pub bottom: f32,
18    /// Left edge of the line's content after alignment.
19    pub left: f32,
20    /// Advance of the line's content.
21    pub width: f32,
22    /// Byte range of the text on this line.
23    pub text_range: Range<usize>,
24}
25
26/// Identifies a font face by its bytes (address and length) and face index.
27///
28/// Not by parley's blob id: the same bytes registered twice, or reloaded under a new blob,
29/// are still one face, sharing one `densa_text::Font` and its cached glyphs. Holders keep
30/// the bytes alive (each `Font` owns an `Arc` of them), so an address is never reused for
31/// other data while its key is in use.
32pub(crate) type FaceKey = (usize, usize, u32);
33
34/// The [`FaceKey`] of `bytes` at face `index`.
35pub(crate) fn bytes_key(bytes: &[u8], index: u32) -> FaceKey {
36    (bytes.as_ptr() as usize, bytes.len(), index)
37}
38
39/// The [`FaceKey`] of a face parley chose.
40pub(crate) fn face_key(data: &FontData) -> FaceKey {
41    bytes_key(data.data.data(), data.index)
42}
43
44/// A font face a paragraph uses.
45#[derive(Debug)]
46pub(crate) struct Face {
47    pub(crate) key: FaceKey,
48    pub(crate) font: Font,
49}
50
51#[derive(Debug, Clone, Copy)]
52struct RunRecord {
53    face: usize,
54    px: f32,
55    color: Color,
56    glyphs: (u32, u32),
57}
58
59/// Shaped, line-broken and aligned text, ready to draw as [`GlyphRun`]s.
60///
61/// Built by `TextSystem::paragraph`. Changing the width ([`reflow`](Self::reflow)) re-breaks
62/// the shaped text without shaping it again; changing the text or styles needs a new
63/// paragraph. Iterating [`runs`](Self::runs) never allocates, so drawing a paragraph every
64/// frame is allocation-free.
65///
66/// Not yet supported: variable-font axes and synthetic bold/italic (glyphs are drawn from
67/// the face's default instance), color glyphs (emoji draw as coverage masks).
68pub struct Paragraph {
69    layout: Layout<Color>,
70    text_len: usize,
71    align: TextAlign,
72    max_width: Option<f32>,
73    faces: Vec<Face>,
74    glyphs: Vec<PositionedGlyph>,
75    runs: Vec<RunRecord>,
76    lines: Vec<LineBox>,
77}
78
79impl std::fmt::Debug for Paragraph {
80    // Hand-written: parley's layout does not implement Debug.
81    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82        f.debug_struct("Paragraph")
83            .field("align", &self.align)
84            .field("max_width", &self.max_width)
85            .field("faces", &self.faces.len())
86            .field("glyphs", &self.glyphs.len())
87            .field("runs", &self.runs.len())
88            .field("lines", &self.lines)
89            .finish_non_exhaustive()
90    }
91}
92
93impl Paragraph {
94    /// Breaks and aligns `layout` (of a `text_len`-byte text), resolving each font face it
95    /// uses once through `resolve`.
96    pub(crate) fn new(
97        layout: Layout<Color>,
98        text_len: usize,
99        align: TextAlign,
100        max_width: Option<f32>,
101        resolve: &mut dyn FnMut(&FontData) -> Option<Font>,
102    ) -> Result<Self, ParagraphError> {
103        let mut paragraph = Self {
104            layout,
105            text_len,
106            align,
107            max_width,
108            faces: Vec::new(),
109            glyphs: Vec::new(),
110            runs: Vec::new(),
111            lines: Vec::new(),
112        };
113        paragraph.break_and_flatten(resolve)?;
114        Ok(paragraph)
115    }
116
117    /// The parley layout, for selection geometry.
118    pub(crate) fn layout(&self) -> &Layout<Color> {
119        &self.layout
120    }
121
122    /// Length of the laid-out text in bytes.
123    pub(crate) fn text_len(&self) -> usize {
124        self.text_len
125    }
126
127    /// Width of the widest line, in pixels.
128    pub fn width(&self) -> f32 {
129        self.layout.width()
130    }
131
132    /// Height of all lines, in pixels: the bottom of the last line box.
133    ///
134    /// Taken from the (pixel-snapped) line boxes rather than parley's unsnapped total, so
135    /// stacking paragraphs by height never drifts from where their lines are drawn.
136    pub fn height(&self) -> f32 {
137        self.lines.last().map_or(0.0, |line| line.bottom)
138    }
139
140    /// The width lines wrap at; `None` if they only break at newlines.
141    pub fn max_width(&self) -> Option<f32> {
142        self.max_width
143    }
144
145    /// The lines, top to bottom.
146    pub fn lines(&self) -> &[LineBox] {
147        &self.lines
148    }
149
150    /// Glyphs to draw, in all runs.
151    pub fn glyph_count(&self) -> usize {
152        self.glyphs.len()
153    }
154
155    /// The glyph runs to draw, positioned relative to the paragraph's top-left corner.
156    pub fn runs(&self) -> impl ExactSizeIterator<Item = GlyphRun<'_>> + '_ {
157        self.runs.iter().map(|run| {
158            let (start, end) = run.glyphs;
159            GlyphRun {
160                font: &self.faces[run.face].font,
161                px: run.px,
162                color: run.color,
163                glyphs: &self.glyphs[start as usize..end as usize],
164            }
165        })
166    }
167
168    /// Re-breaks and re-aligns the lines for a new `max_width` (`None`: no wrapping),
169    /// without shaping again.
170    ///
171    /// # Errors
172    /// [`ParagraphError::InvalidWidth`] if `max_width` is negative or not finite.
173    pub fn reflow(&mut self, max_width: Option<f32>) -> Result<(), ParagraphError> {
174        self.max_width = check_width(max_width)?;
175        // Re-breaking reuses the shaped runs, so every face is known already.
176        self.break_and_flatten(&mut |_| None)
177    }
178
179    fn break_and_flatten(
180        &mut self,
181        resolve: &mut dyn FnMut(&FontData) -> Option<Font>,
182    ) -> Result<(), ParagraphError> {
183        self.layout.break_all_lines(self.max_width);
184        let options = AlignmentOptions::default();
185        self.layout.align(alignment(self.align), options);
186
187        self.glyphs.clear();
188        self.runs.clear();
189        self.lines.clear();
190        for line in self.layout.lines() {
191            let metrics = line.metrics();
192            self.lines.push(LineBox {
193                baseline: metrics.baseline,
194                top: metrics.block_min_coord,
195                bottom: metrics.block_max_coord,
196                left: metrics.offset,
197                width: metrics.advance,
198                // Clamped: parley gives an empty text's single line the range 0..1.
199                text_range: clamp(line.text_range(), self.text_len),
200            });
201            for item in line.items() {
202                let PositionedLayoutItem::GlyphRun(run) = item else {
203                    continue; // Inline boxes are not supported yet.
204                };
205                let data = run.run().font();
206                let key = face_key(data);
207                let face = match self.faces.iter().position(|f| f.key == key) {
208                    Some(face) => face,
209                    None => {
210                        // Parley read this face to shape with it, so this only fails if the
211                        // data cannot be parsed for drawing: an error, not a silent gap.
212                        let font = resolve(data).ok_or(ParagraphError::UnreadableFont)?;
213                        self.faces.push(Face { key, font });
214                        self.faces.len() - 1
215                    }
216                };
217                // Lossless: a paragraph holds far fewer than 2^32 glyphs.
218                let start = self.glyphs.len() as u32;
219                self.glyphs
220                    .extend(run.positioned_glyphs().map(|g| PositionedGlyph {
221                        id: GlyphId(g.id),
222                        x: g.x,
223                        y: g.y,
224                    }));
225                let end = self.glyphs.len() as u32;
226                self.runs.push(RunRecord {
227                    face,
228                    px: run.run().font_size(),
229                    color: run.style().brush,
230                    glyphs: (start, end),
231                });
232            }
233        }
234        Ok(())
235    }
236}
237
238fn clamp(range: Range<usize>, len: usize) -> Range<usize> {
239    range.start.min(len)..range.end.min(len)
240}
241
242fn alignment(align: TextAlign) -> Alignment {
243    match align {
244        TextAlign::Start => Alignment::Start,
245        TextAlign::End => Alignment::End,
246        TextAlign::Left => Alignment::Left,
247        TextAlign::Center => Alignment::Center,
248        TextAlign::Right => Alignment::Right,
249        TextAlign::Justify => Alignment::Justify,
250    }
251}