Skip to content
35 changes: 26 additions & 9 deletions parley/src/layout/data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,7 @@ impl<B: Brush> LayoutData<B> {
}
for atom in slice.atoms_start() {
let character = &atom.characters()[0];
let whitespace = character.info.whitespace();
let boundary = character.info.boundary();
let style = &self.styles[character.style_index as usize];
let prev_text_wrap_mode = text_wrap_mode;
Expand All @@ -369,16 +370,32 @@ impl<B: Brush> LayoutData<B> {
let trailing_whitespace = whitespace_advance(prev_atom);
min_width = min_width.max(running_min_width - trailing_whitespace);
running_min_width = 0.0;
if boundary == Boundary::Mandatory {
max_width = max_width.max(running_max_width - trailing_whitespace);
running_max_width = 0.0;
}
}
let advance = spacing.atom_advance(&atom);
running_min_width += advance;
running_max_width += advance;
if !is_rtl {
prev_atom = Some((character.info.whitespace(), advance));

// Handle `Whitespace::Newline` rather than relying on `Boundary::Mandatory`,
// because `Boundary::Mandatory` is only set on the character *following* a break,
// at which point it is too late to handle inline boxes between the line break
// and the following character.
//
// This function doesn't have special handling for CRLF because two linebreaks
// immediately following each other are equivalent to one linebreak for the purpose
// of width calculation.
if whitespace == Whitespace::Newline {
let trailing_whitespace = whitespace_advance(prev_atom);
min_width = min_width.max(running_min_width - trailing_whitespace);
max_width = max_width.max(running_max_width - trailing_whitespace);
running_min_width = 0.0;
running_max_width = 0.0;
if !is_rtl {
prev_atom = None;
}
} else {
let advance = spacing.atom_advance(&atom);
running_min_width += advance;
running_max_width += advance;
if !is_rtl {
prev_atom = Some((whitespace, advance));
}
}
}
let trailing_whitespace = whitespace_advance(prev_atom);
Expand Down
120 changes: 99 additions & 21 deletions parley/src/layout/line.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ 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::{InlineBox, InlineBoxKind};

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

/// Line in a text layout.
#[derive(Copy, Clone)]
Expand Down Expand Up @@ -107,6 +108,7 @@ impl<'a, B: Brush> Line<'a, B> {
GlyphRunIter {
line: self.clone(),
item_index: 0,
atoms: None,
glyph_start: 0,
offset: 0.,
}
Expand Down Expand Up @@ -296,10 +298,79 @@ impl<'a, B: Brush> GlyphRun<'a, B> {
}
}

/// 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>>,
glyph_start: usize,
offset: f32,
}
Expand Down Expand Up @@ -333,27 +404,32 @@ impl<'a, B: Brush> Iterator for GlyphRunIter<'a, B> {
}));
}
LineItem::Run(run) => {
// TODO: this is taking the glyphs and style indices from `parley`'s `Cluster`,
// which means style indices are taken from the atom's first character. We could
// get the style index from `parley_engine`'s `ShapedCluster` instead, which
// would be somewhat finer-grained.
let mut glyphs = run
.visual_clusters()
.flat_map(|c| {
let style_index = c.style_index();
c.glyphs().map(move |glyph| (glyph, style_index))
})
.skip(self.glyph_start);

if let Some((first_glyph, first_style_index)) = glyphs.next() {
let mut advance = first_glyph.advance;
let mut glyph_count = 1;
for (glyph, _) in
glyphs.take_while(|(_, style_index)| *style_index == first_style_index)
{
glyph_count += 1;
advance += glyph.advance;
// 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;
let mut style_index: Option<u16> = 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;
if style_index.is_some_and(|s| s != atom_style_index) {
break;
}
style_index = Some(atom_style_index);
glyph_count += atom_glyph_count;
advance += atom_advance;
}
atoms.consume();
}

if let Some(first_style_index) = style_index {
let glyph_start = self.glyph_start;
self.glyph_start += glyph_count;
let offset = self.offset;
Expand All @@ -370,6 +446,8 @@ impl<'a, B: Brush> Iterator for GlyphRunIter<'a, B> {
advance,
}));
}
// Any atoms left have no glyphs and thus nothing to yield.
self.atoms = None;
self.item_index += 1;
self.glyph_start = 0;
}
Expand Down
6 changes: 5 additions & 1 deletion parley_bench/benches/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,17 @@

use tango_bench::tango_benchmarks;

use parley_bench::benches::{defaults, repeated_justification, spacing, styled};
use parley_bench::benches::{
defaults, iterate_styled_items, long_line, repeated_justification, spacing, styled,
};
use parley_bench::fontique_benches::system_fonts_init;

tango_benchmarks!(
defaults(),
styled(),
iterate_styled_items(),
spacing(),
repeated_justification(),
long_line(),
system_fonts_init()
);
Loading