parley 0.12.0

Parley provides an API for implementing rich text layout.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
// Copyright 2021 the Parley Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT

use crate::layout::Style;
use crate::layout::data::BreakReason;
use crate::layout::data::{LayoutItemKind, LineData};
use crate::layout::layout::Layout;
use crate::layout::run::Run;
use crate::layout::spacing::EffectiveSpacing;
use crate::style::Brush;
use crate::{BaselineShift, InlineBox, InlineBoxKind};

use core::ops::Range;
use parley_engine::{Atom, Atoms, Glyph};

/// Line in a text layout.
#[derive(Copy, Clone)]
pub struct Line<'a, B: Brush> {
    pub(crate) layout: &'a Layout<B>,
    pub(crate) index: u32,
    pub(crate) data: &'a LineData,
}

impl<'a, B: Brush> Line<'a, B> {
    /// Returns the metrics for the line.
    pub fn metrics(&self) -> &LineMetrics {
        &self.data.metrics
    }

    pub fn break_reason(&self) -> BreakReason {
        self.data.break_reason
    }

    /// Returns the range of text for the line.
    pub fn text_range(&self) -> Range<usize> {
        self.data.text_range.clone()
    }

    /// Returns the number of items in the line.
    pub fn len(&self) -> usize {
        self.data.item_range.len()
    }

    /// Returns `true` if the line is empty.
    pub fn is_empty(&self) -> bool {
        self.data.item_range.is_empty()
    }

    /// Returns the line item at the specified index.
    pub(crate) fn item(&self, index: usize) -> Option<LineItem<'a, B>> {
        let original_index = index;
        let index = self.data.item_range.start + index;
        if index >= self.data.item_range.end {
            return None;
        }
        let item = self.layout.data.line_items.get(index)?;

        Some(match item.kind {
            LayoutItemKind::TextRun => LineItem::Run(Run {
                layout: self.layout,
                line_index: self.index,
                index: original_index as u32,
                shaped_text_run_index: item.index as u32,
                shaped: self.layout.data.shaped_text.runs().get(item.index)?,
                data: self.layout.data.runs.get(item.index)?,
                line_data: Some(item),
            }),
            LayoutItemKind::InlineBox => {
                LineItem::InlineBox(&self.layout.data.inline_boxes.get(item.index)?.inline_box)
            }
        })
    }

    /// Block-axis coordinate of the baseline of the span box generated by `style_index`, taking
    /// its (and its ancestors') `vertical-align` into account.
    pub(crate) fn style_baseline(&self, style_index: u16) -> f32 {
        match self.layout.data.style_metrics.get(usize::from(style_index)) {
            Some(metrics) => self
                .data
                .style_baseline(&self.layout.data.aligned_subtree_offsets, metrics),
            None => self.data.metrics.baseline,
        }
    }

    /// Block-axis coordinate of the top edge of the layout's inline box `index`.
    pub(crate) fn inline_box_top(&self, index: usize) -> f32 {
        let layout_box = &self.layout.data.inline_boxes[index];
        let inline_box = &layout_box.inline_box;
        match inline_box.vertical_align.shift {
            BaselineShift::Top => self.data.metrics.block_min_coord,
            BaselineShift::Bottom => self.data.metrics.block_max_coord - inline_box.height,
            _ => {
                let aligned_subtree_root = self
                    .layout
                    .data
                    .style_metrics
                    .get(usize::from(layout_box.parent_style_index))
                    .map_or(0, |m| m.aligned_subtree_root);
                let ascent = inline_box.baseline.unwrap_or(inline_box.height);
                self.data.metrics.baseline
                    - self.data.aligned_subtree_offset(
                        &self.layout.data.aligned_subtree_offsets,
                        aligned_subtree_root,
                    )
                    - layout_box.baseline_offset
                    - ascent
            }
        }
    }

    /// Returns an iterator over the runs for the line.
    pub fn runs(&self) -> impl Iterator<Item = Run<'a, B>> + 'a + Clone + use<'a, B> {
        self.items_nonpositioned().filter_map(|item| item.run())
    }

    /// Returns an iterator over the non-glyph runs and inline boxes for the line.
    pub(crate) fn items_nonpositioned(
        &self,
    ) -> impl Iterator<Item = LineItem<'a, B>> + Clone + use<'a, B> {
        let copy = self.clone();
        let line_items = &copy.layout.data.line_items[self.data.item_range.clone()];
        line_items
            .iter()
            .enumerate()
            .map(move |(item_index, line_data)| match line_data.kind {
                LayoutItemKind::TextRun => LineItem::Run(Run {
                    layout: copy.layout,
                    line_index: copy.index,
                    index: item_index as u32,
                    shaped_text_run_index: line_data.index as u32,
                    shaped: &copy.layout.data.shaped_text.runs()[line_data.index],
                    data: &copy.layout.data.runs[line_data.index],
                    line_data: Some(line_data),
                }),
                LayoutItemKind::InlineBox => {
                    LineItem::InlineBox(&copy.layout.data.inline_boxes[line_data.index].inline_box)
                }
            })
    }

    /// Returns an iterator over the glyph runs for the line.
    pub fn items(
        &self,
    ) -> impl Iterator<Item = PositionedLayoutItem<'a, B>> + 'a + Clone + use<'a, B> {
        GlyphRunIter {
            line: self.clone(),
            item_index: 0,
            atoms: None,
            offset: 0.,
        }
    }
}

/// Metrics information for a line.
///
/// A line has an inline axis and a block axis. The inline axis runs along a line of a text; the
/// block axis is perpendicular to it. Lines are stacked along the block axis.
#[derive(Copy, Clone, Default, Debug, PartialEq)]
pub struct LineMetrics {
    /// The absolute line height (in layout units).
    pub line_height: f32,

    /// The block-axis coordinate of the line's baseline.
    ///
    /// For horizontal text, this is the baseline's y-coordinate within the layout.
    pub baseline: f32,

    /// Offset for alignment.
    pub offset: f32,

    /// The text indent applied to this line (see [`Layout::set_text_indent`]).
    ///
    /// This isn't part of [`Self::advance`]. After alignment, [`Self::offset`] accounts for it.
    pub indent: f32,

    /// The inline advance of the line's content.
    ///
    /// This includes [`Self::hanging_advance`], but not [`Self::indent`]. This also doesn't include
    /// additional spacing from [justified alignment](crate::Alignment::Justify).
    pub advance: f32,

    /// The portion of [`Self::advance`] that hangs past the line's end edge.
    ///
    /// For LTR paragraphs, this is the right edge, and in RTL paragraphs, it's the left edge.
    //
    // Note: only whitespace hangs at the moment. With something like hanging punctuation, inked
    // content could hang past both the start and end edge. That would likely require splitting this
    // into two fields.
    pub hanging_advance: f32,

    /// The minimum inline-axis coordinate of the line.
    ///
    /// For horizontal text, this would be the left of the line.
    pub inline_min_coord: f32,

    /// The maximum inline-axis coordinate of the line.
    ///
    /// For horizontal text, this would be the right of the line.
    pub inline_max_coord: f32,

    /// The minimum block-axis coordinate of the line box.
    ///
    /// "Line box" is used as in
    /// [CSS 2.2 § 9.4.2](https://www.w3.org/TR/CSS22/visuren.html#line-box). The line box is tall
    /// enough to cover all its inline boxes, but note glyphs may overflow their box, especially
    /// when the typographic line height is small. See also [`Self::content_block_min_coord`].
    ///
    /// For horizontal text, this is the top of the line box.
    pub block_min_coord: f32,

    /// The maximum block-axis coordinate of the line box.
    ///
    /// For horizontal text, this is the bottom of the line box.
    pub block_max_coord: f32,

    /// The minimum block-axis coordinate of the line's content.
    ///
    /// This covers, roughly, the line's glyphs and inline boxes. This does not take into account
    /// typographic leading, but only the typographic ascent and descent. In case of negative
    /// leading, the content block can be larger than the line's block. Glyphs can overflow the
    /// content area: tall diacritics can exceed the area, stacking combining marks can easily
    /// exceed the area, and some fonts will simply have glyphs that exceed this.
    ///
    /// For horizontal text, this would be the top of the line's content.
    pub content_block_min_coord: f32,

    /// The maximum block-axis coordinate of the line's content.
    ///
    /// See [`LineMetrics::content_block_min_coord`] for a caveat about glyphs overflowing the
    /// content area.
    ///
    /// For horizontal text, this would be the bottom of the line's content.
    pub content_block_max_coord: f32,
}

impl LineMetrics {
    /// Returns the size of the line
    pub fn size(&self) -> f32 {
        self.line_height
    }
}

/// A line item and its corresponding data (a run or inline box). Unlike a
/// [`PositionedLayoutItem`], runs are not guaranteed to be split by style.
pub(crate) enum LineItem<'a, B: Brush> {
    Run(Run<'a, B>),
    InlineBox(&'a InlineBox),
}

impl<'a, B: Brush> LineItem<'a, B> {
    pub(crate) fn run(self) -> Option<Run<'a, B>> {
        match self {
            LineItem::Run(run) => Some(run),
            _ => None,
        }
    }
}

/// The computed result of an item (glyph run or inline box) within a layout
#[derive(Clone)]
pub enum PositionedLayoutItem<'a, B: Brush> {
    GlyphRun(GlyphRun<'a, B>),
    InlineBox(PositionedInlineBox),
}

/// The computed position of an inline box within a layout
#[derive(Debug, Clone)]
pub struct PositionedInlineBox {
    pub x: f32,
    pub y: f32,
    pub width: f32,
    pub height: f32,
    /// The baseline of the box in pixels, relative to the top of the box (i.e. to [`Self::y`]).
    ///
    /// This is `None` if the box does not specify a baseline, in which case its bottom edge is
    /// aligned with the text baseline.
    pub baseline: Option<f32>,
    pub id: u64,
    pub kind: InlineBoxKind,
}

/// Sequence of fully positioned glyphs with the same style.
#[derive(Clone)]
pub struct GlyphRun<'a, B: Brush> {
    run: Run<'a, B>,
    style_index: u16,
    /// The glyph run's shaped clusters, indexing into the shaped run this [`Run`] belongs to.
    ///
    /// Both bounds are atom boundaries, as glyph runs must span full ligatures.
    shaped_clusters: Range<u32>,
    offset: f32,
    baseline: f32,
    advance: f32,
}

impl<'a, B: Brush> GlyphRun<'a, B> {
    /// Returns the underlying run.
    pub fn run(&self) -> &Run<'a, B> {
        &self.run
    }

    /// Returns the associated style.
    ///
    /// See also [`Self::style_index`].
    pub fn style(&self) -> &Style<B> {
        &self.run.layout.styles()[usize::from(self.style_index)]
    }

    /// Returns the associated style index.
    ///
    /// See also [`Self::style`].
    pub fn style_index(&self) -> u16 {
        self.style_index
    }

    /// Returns the offset to the baseline.
    pub fn baseline(&self) -> f32 {
        self.baseline
    }

    /// Returns the offset to the first glyph along the baseline.
    pub fn offset(&self) -> f32 {
        self.offset
    }

    /// Returns the total advance of the run.
    pub fn advance(&self) -> f32 {
        self.advance
    }

    /// Returns an iterator over the glyphs in the run.
    pub fn glyphs(&'a self) -> impl Iterator<Item = Glyph> + 'a + Clone {
        self.run.glyphs_in(self.shaped_clusters.clone())
    }

    /// Returns an iterator over the fully positioned glyphs in the run.
    pub fn positioned_glyphs(&'a self) -> impl Iterator<Item = Glyph> + 'a + Clone {
        let mut offset = self.offset;
        let baseline = self.baseline;
        self.glyphs().map(move |mut g| {
            g.x += offset;
            g.y += baseline;
            offset += g.advance;
            g
        })
    }
}

/// Peekable iterator over the atoms of a run in visual order, resumable across
/// [`GlyphRunIter::next`] calls.
///
/// This walks the run's shaped data ([`Atom`]s) directly rather than materialising
/// [`Cluster`](crate::Cluster)s and their composed glyph iterators. It computes the same glyph
/// counts and advances as [`Run::glyphs_in`] over the same atoms.
#[derive(Clone)]
struct AtomIter<'a> {
    atoms: Atoms<'a>,
    spacing: EffectiveSpacing,
    is_rtl: bool,
    /// The next atom in visual order, if it has been peeked but not yet consumed.
    peeked: Option<Atom<'a>>,
}

impl<'a> AtomIter<'a> {
    fn new<B: Brush>(run: Run<'a, B>) -> Self {
        let slice = run.line_slice();
        let is_rtl = run.is_rtl();
        Self {
            atoms: if is_rtl {
                slice.atoms_end()
            } else {
                slice.atoms_start()
            },
            spacing: run.line_spacing(),
            is_rtl,
            peeked: None,
        }
    }

    #[inline]
    fn peek(&mut self) -> Option<Atom<'a>> {
        if self.peeked.is_none() {
            self.peeked = if self.is_rtl {
                self.atoms.prev()
            } else {
                self.atoms.next()
            };
        }
        self.peeked
    }

    /// Consume the atom returned by [`Self::peek`], so the next peek moves on.
    #[inline]
    fn consume(&mut self) {
        self.peeked = None;
    }

    /// The number of glyphs in `atom` and the sum of their advances (including spacing gaps),
    /// matching what [`Run::glyphs_in`] yields for the atom's clusters.
    #[inline]
    fn measure(&self, atom: &Atom<'a>) -> (usize, f32) {
        let glyph_count: usize = atom
            .shaped_clusters()
            .iter()
            .map(|cluster| usize::from(cluster.glyph_len()))
            .sum();
        let mut advance = atom.advance();
        if glyph_count != 0 && !self.spacing.is_zero() {
            advance += self.spacing.gaps(atom).total();
        }
        (glyph_count, advance)
    }
}

#[derive(Clone)]
struct GlyphRunIter<'a, B: Brush> {
    line: Line<'a, B>,
    item_index: usize,
    /// Iterator over the visual atoms of the run at `item_index`, positioned at the first atom
    /// not yet yielded as part of a glyph run. `None` before entering a run.
    atoms: Option<AtomIter<'a>>,
    offset: f32,
}

impl<'a, B: Brush> Iterator for GlyphRunIter<'a, B> {
    type Item = PositionedLayoutItem<'a, B>;

    fn next(&mut self) -> Option<Self::Item> {
        loop {
            let item = self.line.item(self.item_index)?;
            match item {
                LineItem::InlineBox(inline_box) => {
                    let x = self.offset
                        + self.line.data.metrics.inline_min_coord
                        + self.line.data.metrics.offset;
                    let box_index = self.line.layout.data.line_items
                        [self.line.data.item_range.start + self.item_index]
                        .index;
                    let y = self.line.inline_box_top(box_index);

                    self.item_index += 1;
                    if inline_box.kind == InlineBoxKind::InFlow {
                        self.offset += inline_box.width;
                    }
                    return Some(PositionedLayoutItem::InlineBox(PositionedInlineBox {
                        x,
                        y,
                        width: inline_box.width,
                        height: inline_box.height,
                        baseline: inline_box.baseline,
                        id: inline_box.id,
                        kind: inline_box.kind,
                    }));
                }
                LineItem::Run(run) => {
                    // TODO: style indices are taken from the atom's first character, matching
                    // `Cluster::style_index`. We could get the style index from `parley_engine`'s
                    // `ShapedCluster` instead, which would be somewhat finer-grained.
                    let atoms = self.atoms.get_or_insert_with(|| AtomIter::new(run));

                    // Styles are uniform within an atom, so glyph runs always end on atom
                    // boundaries and the iterator only needs to resume at atom granularity.
                    // Atoms without glyphs don't take part.
                    let mut advance = 0.0;
                    let mut glyph_count = 0;
                    // The style index and shaped cluster range of the glyph run being accumulated.
                    let mut glyph_run: Option<(u16, Range<u32>)> = None;
                    while let Some(atom) = atoms.peek() {
                        let (atom_glyph_count, atom_advance) = atoms.measure(&atom);
                        if atom_glyph_count != 0 {
                            let atom_style_index = atom.characters()[0].style_index;
                            let atom_clusters = atom.shaped_clusters_range();
                            match &mut glyph_run {
                                Some((style_index, shaped_clusters)) => {
                                    if *style_index != atom_style_index {
                                        break;
                                    }
                                    // In case the run is RTL, atoms are visited in reverse logical
                                    // order, so just grow both sides of the range.
                                    shaped_clusters.start =
                                        shaped_clusters.start.min(atom_clusters.start);
                                    shaped_clusters.end =
                                        shaped_clusters.end.max(atom_clusters.end);
                                }
                                None => glyph_run = Some((atom_style_index, atom_clusters)),
                            }
                            glyph_count += atom_glyph_count;
                            advance += atom_advance;
                        }
                        atoms.consume();
                    }

                    if let Some((style_index, shaped_clusters)) = glyph_run {
                        debug_assert_eq!(
                            run.glyphs_in(shaped_clusters.clone()).count(),
                            glyph_count,
                            "The calculated shaped cluster range of this glyph run must hold exactly the number of glyphs we counted above."
                        );
                        let offset = self.offset;
                        self.offset += advance;
                        return Some(PositionedLayoutItem::GlyphRun(GlyphRun {
                            run,
                            style_index,
                            shaped_clusters,
                            offset: offset
                                + self.line.data.metrics.inline_min_coord
                                + self.line.data.metrics.offset,
                            baseline: self.line.style_baseline(style_index),
                            advance,
                        }));
                    }
                    // Any atoms left have no glyphs and thus nothing to yield.
                    self.atoms = None;
                    self.item_index += 1;
                }
            }
        }
    }
}