From e6ced0783f38fd3e3155645df3a6f60bd9aa4bfb Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Thu, 1 Oct 2026 18:45:38 +0000 Subject: [PATCH 01/11] line_break: make LineBoxMetrics Copy with an append-only subtree history Move the extents of the non-root aligned subtrees (those rooted at a `vertical-align: top | bottom` box) out of `LineBoxMetrics` into a `SubtreeHistory` on `BreakerState`, so that saving a line-breaking opportunity no longer clones a `SmallVec`. The history is strictly append-only: growing a subtree pushes its new extents. A saved opportunity records the history's length and reverting truncates to it, the same way `contributed` is already handled. --- parley/src/layout/line_break.rs | 276 +++++++++++++----- ..._aligned_subtrees_grow_across_breaks-0.png | Bin 0 -> 6767 bytes parley_tests/tests/lines.rs | 73 +++++ 3 files changed, 276 insertions(+), 73 deletions(-) create mode 100644 parley_tests/snapshots/lines_aligned_subtrees_grow_across_breaks-0.png diff --git a/parley/src/layout/line_break.rs b/parley/src/layout/line_break.rs index b9c1a3ad1..f76685105 100644 --- a/parley/src/layout/line_break.rs +++ b/parley/src/layout/line_break.rs @@ -12,7 +12,9 @@ use parlance::BidiLevel; use crate::layout::data::{AlignedSubtreeOffset, run_box_metrics}; use crate::layout::spacing::{EffectiveSpacing, Justification, is_word_separator}; -use crate::layout::style_metrics::{InlineBoxPlacement, StyleMetrics, inline_box_placement}; +use crate::layout::style_metrics::{ + BoxMetrics, InlineBoxPlacement, StyleMetrics, inline_box_placement, +}; use crate::layout::whitespace::atom_hanging_advance; use crate::layout::{ BreakReason, Layout, LayoutData, LayoutItem, LayoutItemKind, LineData, LineItemData, @@ -27,7 +29,6 @@ use crate::{ use core::ops::Range; use parley_engine::Atom; use parley_engine::shape::{Character, Whitespace}; -use smallvec::SmallVec; #[derive(Default)] struct LineLayout { @@ -102,16 +103,18 @@ impl LineState { /// parent-relative `vertical-align` are first aligned to each other relative to the baseline of /// their parent span; each chain of such boxes forms an [aligned subtree], rooted at the root span /// box or at a box with `vertical-align: top | bottom`. Each subtree's extents are tracked -/// relative to its own baseline in [`Self::subtrees`]; the subtrees are only positioned against -/// each other once the line is complete (see `BreakLines::finish_line`). +/// relative to its own baseline: the root subtree's in [`Self::root`], the others' in +/// [`SubtreeHistory`]. The subtrees are only positioned against each other once the line is +/// complete (see `BreakLines::finish_line`). /// See . /// /// [aligned subtree]: crate::layout::style_metrics#aligned-subtrees -#[derive(Clone, Debug)] +#[derive(Clone, Copy, Debug)] struct LineBoxMetrics { - /// Extents of each aligned subtree with content on this line. The first entry is always the - /// root subtree (root style index `0`). - subtrees: SmallVec<[SubtreeExtents; 2]>, + /// Extents of the root aligned subtree. + root: SubtreeExtents, + /// Height of the tallest non-root aligned subtree (see [`SubtreeHistory`]). + non_root_height: f32, /// Height of the tallest `vertical-align: top` inline box, which is positioned against the /// line box rather than a baseline. line_relative_top_height: f32, @@ -125,7 +128,7 @@ struct LineBoxMetrics { } /// Extents of one aligned subtree on the current line, measured from the subtree's own baseline. -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, PartialEq)] struct SubtreeExtents { /// Style index of the subtree root: `0` for the root span box, otherwise a style with /// `vertical-align: top | bottom`. @@ -145,7 +148,7 @@ struct SubtreeExtents { content_box: Extents, } -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, PartialEq)] struct Extents { /// The space over the baseline. over: f32, @@ -202,15 +205,84 @@ impl SubtreeExtents { content_box: Extents::default(), } } + + /// Grow to include a box whose baseline is `baseline_offset` above the subtree's baseline. + #[inline(always)] + fn add(&mut self, baseline_offset: f32, metrics: BoxMetrics) { + self.line_box + .add(baseline_offset, metrics.over, metrics.under); + self.content_box + .add(baseline_offset, metrics.ascent, metrics.descent); + } +} + +/// The extents of the non-root [aligned subtrees] on the current line, i.e. those rooted at a +/// box with `vertical-align: top | bottom`. Empty for lines without such content. +/// +/// Reverting to a saved line-breaking opportunity has to restore these extents to what they were +/// at that opportunity. So that saving an opportunity doesn't have to copy them, this is an +/// append-only history: growing a subtree pushes its new extents instead of overwriting the old +/// ones. Hence: +/// +/// - the current extents of a subtree are the *last* entry with its root, and +/// - truncating to an earlier [length](Self::len) restores the extents as of that time. +/// +/// [aligned subtrees]: crate::layout::style_metrics#aligned-subtrees +#[derive(Clone, Default)] +struct SubtreeHistory { + entries: Vec, +} + +impl SubtreeHistory { + /// The current extents of the subtree rooted at `root`, if it has content on the line. + fn get(&self, root: u16) -> Option { + self.entries.iter().rev().find(|s| s.root == root).copied() + } + + /// The current extents of every subtree with content on the line. + fn iter(&self) -> impl Iterator { + let is_current = |(i, s): &(usize, &SubtreeExtents)| { + let newer = &self.entries[i + 1..]; + newer.iter().all(|n| n.root != s.root) + }; + self.entries + .iter() + .enumerate() + .filter(is_current) + .map(|(_, s)| *s) + } + + /// Grow the subtree rooted at `root` to include a box whose baseline is `baseline_offset` + /// above the subtree's baseline. Returns the subtree's new extents. + fn grow(&mut self, root: u16, baseline_offset: f32, metrics: BoxMetrics) -> SubtreeExtents { + let old = self.get(root); + let mut new = old.unwrap_or(SubtreeExtents::new(root)); + new.add(baseline_offset, metrics); + if old != Some(new) { + self.entries.push(new); + } + new + } + + fn len(&self) -> usize { + self.entries.len() + } + + fn truncate(&mut self, len: usize) { + self.entries.truncate(len); + } + + fn clear(&mut self) { + self.entries.clear(); + } } impl Default for LineBoxMetrics { /// An empty line containing only the root aligned subtree, without a strut. fn default() -> Self { - let mut subtrees = SmallVec::new(); - subtrees.push(SubtreeExtents::new(0)); Self { - subtrees, + root: SubtreeExtents::new(0), + non_root_height: 0., line_relative_top_height: 0., line_relative_bottom_height: 0., has_content: false, @@ -222,51 +294,34 @@ impl Default for LineBoxMetrics { impl LineBoxMetrics { /// Reset to an empty line. fn reset(&mut self) { - let Self { - subtrees, - line_relative_top_height, - line_relative_bottom_height, - has_content, - last_text, - } = Self::default(); - self.subtrees.clear(); - self.subtrees.extend(subtrees); - self.line_relative_top_height = line_relative_top_height; - self.line_relative_bottom_height = line_relative_bottom_height; - self.has_content = has_content; - self.last_text = last_text; - } - - /// The extents of the root aligned subtree. - fn root(&self) -> &SubtreeExtents { - &self.subtrees[0] + *self = Self::default(); } + /// Add a box whose baseline is `baseline_offset` above the baseline of the aligned subtree + /// rooted at `aligned_subtree`. #[inline] - fn subtree_mut(&mut self, root: u16) -> &mut SubtreeExtents { - if root == 0 { - return &mut self.subtrees[0]; + fn add_box( + &mut self, + aligned_subtree: u16, + baseline_offset: f32, + metrics: BoxMetrics, + subtrees: &mut SubtreeHistory, + ) { + if aligned_subtree == 0 { + self.root.add(baseline_offset, metrics); + } else { + let subtree = subtrees.grow(aligned_subtree, baseline_offset, metrics); + self.non_root_height = self.non_root_height.max(subtree.line_box.height()); } - let index = match self.subtrees.iter().position(|s| s.root == root) { - Some(index) => index, - None => { - self.subtrees.push(SubtreeExtents::new(root)); - self.subtrees.len() - 1 - } - }; - &mut self.subtrees[index] } /// The line height seen so far. #[inline] fn line_height(&self) -> f32 { - let mut height = self.root().line_box.height(); - if self.subtrees.len() > 1 { - for subtree in &self.subtrees[1..] { - height = height.max(subtree.line_box.height()); - } - } - height + self.root + .line_box + .height() + .max(self.non_root_height) .max(self.line_relative_top_height) .max(self.line_relative_bottom_height) } @@ -278,6 +333,7 @@ impl LineBoxMetrics { style_index: u16, style_metrics: &[StyleMetrics], contributed: &mut Vec, + subtrees: &mut SubtreeHistory, ) { let mut index = style_index; while !contributed.contains(&index) { @@ -285,13 +341,18 @@ impl LineBoxMetrics { let Some(metrics) = style_metrics.get(usize::from(index)) else { return; }; - let subtree = self.subtree_mut(metrics.aligned_subtree); - subtree - .line_box - .add(metrics.baseline_offset, metrics.over, metrics.under); - subtree - .content_box - .add(metrics.baseline_offset, metrics.ascent, metrics.descent); + let span_box = BoxMetrics { + ascent: metrics.ascent, + descent: metrics.descent, + over: metrics.over, + under: metrics.under, + }; + self.add_box( + metrics.aligned_subtree, + metrics.baseline_offset, + span_box, + subtrees, + ); if index == 0 { return; } @@ -319,6 +380,7 @@ impl LineBoxMetrics { characters: &[Character], data: &LayoutData, contributed: &mut Vec, + subtrees: &mut SubtreeHistory, ) { self.has_content = true; // Consecutive atoms almost always come from the same run and style, whose boxes are then @@ -334,6 +396,7 @@ impl LineBoxMetrics { characters, data, contributed, + subtrees, ); } @@ -349,16 +412,22 @@ impl LineBoxMetrics { characters: &[Character], data: &LayoutData, contributed: &mut Vec, + subtrees: &mut SubtreeHistory, ) { self.last_text = (item_idx, style_index); if contributed.last() != Some(&style_index) { - self.add_style(style_index, &data.style_metrics, contributed); + self.add_style(style_index, &data.style_metrics, contributed, subtrees); } if data.runs[run_idx].has_mixed_style_atoms { // Add the spans of all the atom's other styles. for character in characters.iter().skip(1) { if character.style_index != style_index { - self.add_style(character.style_index, &data.style_metrics, contributed); + self.add_style( + character.style_index, + &data.style_metrics, + contributed, + subtrees, + ); } } } @@ -374,13 +443,7 @@ impl LineBoxMetrics { .style_metrics .get(style) .map_or((0., 0), |m| (m.baseline_offset, m.aligned_subtree)); - let subtree = self.subtree_mut(aligned_subtree); - subtree - .line_box - .add(baseline_offset, run_box.over, run_box.under); - subtree - .content_box - .add(baseline_offset, run_box.ascent, run_box.descent); + self.add_box(aligned_subtree, baseline_offset, run_box, subtrees); } /// Add an inline box extending `ascent` above and `descent` below a baseline that is @@ -391,6 +454,7 @@ impl LineBoxMetrics { baseline_offset: f32, ascent: f32, descent: f32, + subtrees: &mut SubtreeHistory, ) { // Inline box extents are exact box sizes supplied by the caller, not font metrics; // rounding them would change the space the box reserves relative to its height. @@ -399,9 +463,13 @@ impl LineBoxMetrics { if (ascent != 0. || descent != 0.) && ascent.is_finite() && descent.is_finite() { self.has_content = true; } - let subtree = self.subtree_mut(aligned_subtree); - subtree.line_box.add(baseline_offset, ascent, descent); - subtree.content_box.add(baseline_offset, ascent, descent); + let inline_box = BoxMetrics { + ascent, + descent, + over: ascent, + under: descent, + }; + self.add_box(aligned_subtree, baseline_offset, inline_box, subtrees); } /// Add an inline box with `vertical-align: top | bottom`, which only constrains the line @@ -427,6 +495,8 @@ struct PrevBoundaryState { state: LineState, /// Length of [`BreakerState::contributed`] at this opportunity. contributed_len: usize, + /// Length of [`BreakerState::subtrees`] at this opportunity. + subtrees_len: usize, } /// Reason that the line breaker has yielded control flow @@ -536,6 +606,9 @@ pub struct BreakerState { /// [`LineBoxMetrics::add_style`]). Lives here rather than in [`LineState`] so that saving a /// line-breaking opportunity only records its length; reverting truncates it back. contributed: Vec, + /// Extents of the non-root aligned subtrees on the current line. Like [`Self::contributed`], + /// saving a line-breaking opportunity only records its length. + subtrees: SubtreeHistory, // Saved breaker states for reverting to a previously encountered line-breaking opportunity /// Saved breaker state for the last non-emergency line-breaking opportunity @@ -559,6 +632,7 @@ impl Default for BreakerState { line_max_height: f32::MAX, line: LineState::default(), contributed: Vec::new(), + subtrees: SubtreeHistory::default(), prev_boundary: None, emergency_boundary: None, } @@ -593,6 +667,7 @@ impl BreakerState { atom.characters(), data, &mut self.contributed, + &mut self.subtrees, ); self.update_max_height_exceeded(); } @@ -606,7 +681,9 @@ impl BreakerState { self.item_idx += 1; self.line.items.end += 1; self.line.x = next_x; - self.line.box_metrics.add_inline_box(0, 0., ascent, descent); + self.line + .box_metrics + .add_inline_box(0, 0., ascent, descent, &mut self.subtrees); self.update_max_height_exceeded(); } @@ -634,6 +711,7 @@ impl BreakerState { placement.baseline_offset, placement.ascent, placement.descent, + &mut self.subtrees, ); } self.update_max_height_exceeded(); @@ -648,6 +726,7 @@ impl BreakerState { cluster_idx: self.cluster_idx, state: self.line.clone(), contributed_len: self.contributed.len(), + subtrees_len: self.subtrees.len(), }); } @@ -660,6 +739,7 @@ impl BreakerState { cluster_idx: self.cluster_idx, state: self.line.clone(), contributed_len: self.contributed.len(), + subtrees_len: self.subtrees.len(), }); } @@ -670,6 +750,7 @@ impl BreakerState { self.cluster_idx = prev_state.cluster_idx; self.line = prev_state.state; self.contributed.truncate(prev_state.contributed_len); + self.subtrees.truncate(prev_state.subtrees_len); } #[inline(always)] @@ -771,10 +852,12 @@ impl<'a, B: Brush> BreakLines<'a, B> { let state = &mut self.state; state.line.reset(); state.contributed.clear(); + state.subtrees.clear(); state.line.box_metrics.add_style( 0, &self.layout.data.style_metrics, &mut state.contributed, + &mut state.subtrees, ); state.update_max_height_exceeded(); } @@ -1425,6 +1508,7 @@ impl<'a, B: Brush> BreakLines<'a, B> { &[], &self.layout.data, &mut self.state.contributed, + &mut self.state.subtrees, ); line.metrics.line_height = self.state.line.box_metrics.line_height(); self.lines.line_items.push(LineItemData { @@ -1448,14 +1532,14 @@ impl<'a, B: Brush> BreakLines<'a, B> { (Extents::default().or_zero(), Extents::default().or_zero()) } else { ( - box_metrics.root().line_box.or_zero(), - box_metrics.root().content_box.or_zero(), + box_metrics.root.line_box.or_zero(), + box_metrics.root.content_box.or_zero(), ) }; let mut top_height = box_metrics.line_relative_top_height; let mut bottom_height = box_metrics.line_relative_bottom_height; - for subtree in &box_metrics.subtrees[1..] { + for subtree in self.state.subtrees.iter() { let height = subtree.line_box.height(); match self.layout.data.styles[usize::from(subtree.root)] .vertical_align @@ -1476,7 +1560,7 @@ impl<'a, B: Brush> BreakLines<'a, B> { let offsets = &mut self.lines.aligned_subtree_offsets; line.aligned_subtree_offsets.start = offsets.len() as u32; - for subtree in &box_metrics.subtrees[1..] { + for subtree in self.state.subtrees.iter() { let extents = subtree.line_box.or_zero(); let offset = match self.layout.data.styles[usize::from(subtree.root)] .vertical_align @@ -1918,3 +2002,49 @@ fn reorder_line_items(runs: &mut [LineItemData]) { } } } + +#[cfg(test)] +mod tests { + use super::{BoxMetrics, BreakerState}; + + #[test] + fn subtree_extents_restored_at_break_opportunities() { + let metrics = BoxMetrics { + ascent: 6., + descent: 2., + over: 6., + under: 2., + }; + for emergency in [false, true] { + let mut state = BreakerState::default(); + let add_box = |state: &mut BreakerState, baseline_offset| { + state + .line + .box_metrics + .add_box(1, baseline_offset, metrics, &mut state.subtrees); + }; + let subtree_height = + |state: &BreakerState| state.subtrees.get(1).unwrap().line_box.height(); + + add_box(&mut state, 0.); + state.mark_line_break_opportunity(); + add_box(&mut state, 4.); + state.mark_emergency_break_opportunity(); + add_box(&mut state, -4.); + assert_eq!(state.line.box_metrics.line_height(), 16.); + assert_eq!(subtree_height(&state), 16.); + assert_eq!(state.subtrees.iter().count(), 1); + + let boundary = if emergency { + state.emergency_boundary.take().unwrap() + } else { + state.prev_boundary.take().unwrap() + }; + state.reset_to(boundary); + let expected = if emergency { 12. } else { 8. }; + assert_eq!(state.line.box_metrics.line_height(), expected); + assert_eq!(subtree_height(&state), expected); + assert_eq!(state.subtrees.iter().count(), 1); + } + } +} diff --git a/parley_tests/snapshots/lines_aligned_subtrees_grow_across_breaks-0.png b/parley_tests/snapshots/lines_aligned_subtrees_grow_across_breaks-0.png new file mode 100644 index 0000000000000000000000000000000000000000..086cf2a47a1a21e239f4e98ff69f7f3d5f1d88ee GIT binary patch literal 6767 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 SH{X3E;pu7`YE-M)hyM@a@P4ZR literal 0 HcmV?d00001 diff --git a/parley_tests/tests/lines.rs b/parley_tests/tests/lines.rs index 0431236fc..c39cc260d 100644 --- a/parley_tests/tests/lines.rs +++ b/parley_tests/tests/lines.rs @@ -763,3 +763,76 @@ fn lines_revert_restores_line_height() { let heights: Vec = layout.lines().map(|l| l.metrics().line_height).collect(); assert_eq!(heights, [16.0, 64.0]); } + +/// Like [`lines_revert_restores_line_height`], but the reverted content is in a +/// `vertical-align: top` span, i.e. in a non-root aligned subtree. +#[test] +fn lines_revert_restores_aligned_subtree_line_height() { + let mut env = TestEnv::new(test_name!(), None); + + let text = "aaa BBB"; + let mut builder = env.ranged_builder(text); + builder.push(StyleProperty::FontSize(64.0), 4..7); + builder.push(StyleProperty::VerticalAlign(VerticalAlign::TOP), 4..7); + let mut layout = builder.build(text); + + layout.break_all_lines(Some(95.0)); + layout.align(Alignment::Start, AlignmentOptions::default()); + + assert_eq!(layout.len(), 2); + let heights: Vec = layout.lines().map(|l| l.metrics().line_height).collect(); + assert_eq!(heights, [16.0, 64.0]); +} + +/// A `top` and a `bottom` aligned subtree spanning several words each, whose extents grow word +/// by word across line-breaking opportunities and reverts, on lines with both of them. +#[test] +fn lines_aligned_subtrees_grow_across_breaks() { + let mut env = TestEnv::new(test_name!(), None); + + let mut builder = env.tree_builder(); + for (i, align) in [ + VerticalAlign::TOP, + VerticalAlign::BOTTOM, + VerticalAlign::TOP, + VerticalAlign::BOTTOM, + ] + .into_iter() + .enumerate() + { + builder.push_style_modification_span(&[StyleProperty::VerticalAlign(align)]); + for (j, word) in ["aa ", "bbb ", "cc ", "ddd ", "ee "] + .into_iter() + .enumerate() + { + let size = 12.0 + 4.0 * ((i + j) % 4) as f32; + // `vertical-align` is inherited, so reset it to keep the word in the outer subtree. + builder.push_style_modification_span(&[ + StyleProperty::FontSize(size), + StyleProperty::VerticalAlign(VerticalAlign::BASELINE), + ]); + builder.push_text(word); + builder.pop_style_span(); + } + builder.pop_style_span(); + } + let (mut layout, _) = builder.build(); + layout.break_all_lines(Some(130.0)); + layout.align(Alignment::Start, AlignmentOptions::default()); + + let metrics: Vec<(f32, f32)> = layout + .lines() + .map(|l| (l.metrics().line_height, l.metrics().baseline)) + .collect(); + assert_eq!( + metrics, + [ + (24.0, 13.0), + (24.0, 45.0), + (24.0, 61.0), + (24.0, 89.0), + (24.0, 117.0) + ] + ); + env.check_layout_snapshot(&layout); +} From 51af4719f94266873a3464473439a90fb170585d Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Thu, 1 Oct 2026 23:35:31 +0000 Subject: [PATCH 02/11] line_break: collect the current subtree extents once in finish_line Replace SubtreeHistory::iter, which was filtered twice per line, with SubtreeHistory::current, which walks the history once in reverse and collects the latest entry of each root into a SmallVec. --- parley/src/layout/line_break.rs | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/parley/src/layout/line_break.rs b/parley/src/layout/line_break.rs index f76685105..8cf17dfc4 100644 --- a/parley/src/layout/line_break.rs +++ b/parley/src/layout/line_break.rs @@ -29,6 +29,7 @@ use crate::{ use core::ops::Range; use parley_engine::Atom; use parley_engine::shape::{Character, Whitespace}; +use smallvec::SmallVec; #[derive(Default)] struct LineLayout { @@ -240,16 +241,15 @@ impl SubtreeHistory { } /// The current extents of every subtree with content on the line. - fn iter(&self) -> impl Iterator { - let is_current = |(i, s): &(usize, &SubtreeExtents)| { - let newer = &self.entries[i + 1..]; - newer.iter().all(|n| n.root != s.root) - }; - self.entries - .iter() - .enumerate() - .filter(is_current) - .map(|(_, s)| *s) + fn current(&self) -> SmallVec<[SubtreeExtents; 4]> { + let mut current = SmallVec::<[SubtreeExtents; 4]>::new(); + for entry in self.entries.iter().rev() { + if current.iter().rev().all(|s| s.root != entry.root) { + current.push(*entry); + } + } + current.reverse(); + current } /// Grow the subtree rooted at `root` to include a box whose baseline is `baseline_offset` @@ -1537,9 +1537,10 @@ impl<'a, B: Brush> BreakLines<'a, B> { ) }; + let subtrees = self.state.subtrees.current(); let mut top_height = box_metrics.line_relative_top_height; let mut bottom_height = box_metrics.line_relative_bottom_height; - for subtree in self.state.subtrees.iter() { + for subtree in &subtrees { let height = subtree.line_box.height(); match self.layout.data.styles[usize::from(subtree.root)] .vertical_align @@ -1560,7 +1561,7 @@ impl<'a, B: Brush> BreakLines<'a, B> { let offsets = &mut self.lines.aligned_subtree_offsets; line.aligned_subtree_offsets.start = offsets.len() as u32; - for subtree in self.state.subtrees.iter() { + for subtree in &subtrees { let extents = subtree.line_box.or_zero(); let offset = match self.layout.data.styles[usize::from(subtree.root)] .vertical_align @@ -2033,7 +2034,7 @@ mod tests { add_box(&mut state, -4.); assert_eq!(state.line.box_metrics.line_height(), 16.); assert_eq!(subtree_height(&state), 16.); - assert_eq!(state.subtrees.iter().count(), 1); + assert_eq!(state.subtrees.current().len(), 1); let boundary = if emergency { state.emergency_boundary.take().unwrap() @@ -2044,7 +2045,7 @@ mod tests { let expected = if emergency { 12. } else { 8. }; assert_eq!(state.line.box_metrics.line_height(), expected); assert_eq!(subtree_height(&state), expected); - assert_eq!(state.subtrees.iter().count(), 1); + assert_eq!(state.subtrees.current().len(), 1); } } } From 0bb28392d0c22b769dcbd831d8f86248ce86934f Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Thu, 1 Oct 2026 23:56:54 +0000 Subject: [PATCH 03/11] line_break: don't reverse the collected subtree extents finish_line only takes maxima over the list, and the per-line offsets it pushes are looked up by root, so the order doesn't matter. --- parley/src/layout/line_break.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/parley/src/layout/line_break.rs b/parley/src/layout/line_break.rs index 8cf17dfc4..dde33e315 100644 --- a/parley/src/layout/line_break.rs +++ b/parley/src/layout/line_break.rs @@ -240,7 +240,7 @@ impl SubtreeHistory { self.entries.iter().rev().find(|s| s.root == root).copied() } - /// The current extents of every subtree with content on the line. + /// The current extents of every subtree with content on the line, in no particular order. fn current(&self) -> SmallVec<[SubtreeExtents; 4]> { let mut current = SmallVec::<[SubtreeExtents; 4]>::new(); for entry in self.entries.iter().rev() { @@ -248,7 +248,6 @@ impl SubtreeHistory { current.push(*entry); } } - current.reverse(); current } From 7429a3c44d11dea621bdb06bac0df9e9d3881ae4 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Fri, 2 Oct 2026 00:09:16 +0000 Subject: [PATCH 04/11] line_break: overwrite subtree history entries that no save needs Growing a subtree only has to push a new entry if its current one was there at the last saved line-breaking opportunity. Otherwise overwrite it, so that the history doesn't fill up with obsolete entries when there are few opportunities (e.g. without wrapping). The history now tracks which entries are frozen itself, behind save/restore instead of len/truncate. --- parley/src/layout/line_break.rs | 53 +++++++++++++++++++-------------- 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/parley/src/layout/line_break.rs b/parley/src/layout/line_break.rs index dde33e315..d53a26642 100644 --- a/parley/src/layout/line_break.rs +++ b/parley/src/layout/line_break.rs @@ -221,25 +221,23 @@ impl SubtreeExtents { /// box with `vertical-align: top | bottom`. Empty for lines without such content. /// /// Reverting to a saved line-breaking opportunity has to restore these extents to what they were -/// at that opportunity. So that saving an opportunity doesn't have to copy them, this is an -/// append-only history: growing a subtree pushes its new extents instead of overwriting the old -/// ones. Hence: +/// at that opportunity. So that saving an opportunity doesn't have to copy them, this is a +/// history: [saving](Self::save) freezes the entries pushed so far, and growing a subtree whose +/// entry is frozen pushes its new extents instead of overwriting the old ones. Hence: /// /// - the current extents of a subtree are the *last* entry with its root, and -/// - truncating to an earlier [length](Self::len) restores the extents as of that time. +/// - [restoring](Self::restore) a save drops every entry pushed since, leaving the frozen +/// entries as they were at that save. /// /// [aligned subtrees]: crate::layout::style_metrics#aligned-subtrees #[derive(Clone, Default)] struct SubtreeHistory { entries: Vec, + /// The first `frozen` entries may be needed by a save, so are never modified. + frozen: usize, } impl SubtreeHistory { - /// The current extents of the subtree rooted at `root`, if it has content on the line. - fn get(&self, root: u16) -> Option { - self.entries.iter().rev().find(|s| s.root == root).copied() - } - /// The current extents of every subtree with content on the line, in no particular order. fn current(&self) -> SmallVec<[SubtreeExtents; 4]> { let mut current = SmallVec::<[SubtreeExtents; 4]>::new(); @@ -254,25 +252,35 @@ impl SubtreeHistory { /// Grow the subtree rooted at `root` to include a box whose baseline is `baseline_offset` /// above the subtree's baseline. Returns the subtree's new extents. fn grow(&mut self, root: u16, baseline_offset: f32, metrics: BoxMetrics) -> SubtreeExtents { - let old = self.get(root); + let index = self.entries.iter().rposition(|s| s.root == root); + let old = index.map(|i| self.entries[i]); let mut new = old.unwrap_or(SubtreeExtents::new(root)); new.add(baseline_offset, metrics); if old != Some(new) { - self.entries.push(new); + match index { + Some(i) if i >= self.frozen => self.entries[i] = new, + _ => self.entries.push(new), + } } new } - fn len(&self) -> usize { - self.entries.len() + /// Save the current extents of every subtree, returning a token for [`Self::restore`]. + fn save(&mut self) -> usize { + self.frozen = self.entries.len(); + self.frozen } - fn truncate(&mut self, len: usize) { - self.entries.truncate(len); + /// Restore the extents of every subtree to what they were at `save`. Earlier saves can + /// still be restored afterwards; later ones can't. + fn restore(&mut self, save: usize) { + self.entries.truncate(save); + self.frozen = save; } fn clear(&mut self) { self.entries.clear(); + self.frozen = 0; } } @@ -494,8 +502,8 @@ struct PrevBoundaryState { state: LineState, /// Length of [`BreakerState::contributed`] at this opportunity. contributed_len: usize, - /// Length of [`BreakerState::subtrees`] at this opportunity. - subtrees_len: usize, + /// [Save](SubtreeHistory::save) of [`BreakerState::subtrees`] at this opportunity. + subtrees_save: usize, } /// Reason that the line breaker has yielded control flow @@ -725,7 +733,7 @@ impl BreakerState { cluster_idx: self.cluster_idx, state: self.line.clone(), contributed_len: self.contributed.len(), - subtrees_len: self.subtrees.len(), + subtrees_save: self.subtrees.save(), }); } @@ -738,7 +746,7 @@ impl BreakerState { cluster_idx: self.cluster_idx, state: self.line.clone(), contributed_len: self.contributed.len(), - subtrees_len: self.subtrees.len(), + subtrees_save: self.subtrees.save(), }); } @@ -749,7 +757,7 @@ impl BreakerState { self.cluster_idx = prev_state.cluster_idx; self.line = prev_state.state; self.contributed.truncate(prev_state.contributed_len); - self.subtrees.truncate(prev_state.subtrees_len); + self.subtrees.restore(prev_state.subtrees_save); } #[inline(always)] @@ -2024,9 +2032,10 @@ mod tests { .add_box(1, baseline_offset, metrics, &mut state.subtrees); }; let subtree_height = - |state: &BreakerState| state.subtrees.get(1).unwrap().line_box.height(); + |state: &BreakerState| state.subtrees.current()[0].line_box.height(); add_box(&mut state, 0.); + add_box(&mut state, 1.); state.mark_line_break_opportunity(); add_box(&mut state, 4.); state.mark_emergency_break_opportunity(); @@ -2041,7 +2050,7 @@ mod tests { state.prev_boundary.take().unwrap() }; state.reset_to(boundary); - let expected = if emergency { 12. } else { 8. }; + let expected = if emergency { 12. } else { 9. }; assert_eq!(state.line.box_metrics.line_height(), expected); assert_eq!(subtree_height(&state), expected); assert_eq!(state.subtrees.current().len(), 1); From 3d9e6fa75e12d60c43fb5c4e00680ef160071906 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Fri, 2 Oct 2026 00:38:42 +0000 Subject: [PATCH 05/11] line_break: rename subtrees_save to subtrees_len For consistency with contributed_len. --- parley/src/layout/line_break.rs | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/parley/src/layout/line_break.rs b/parley/src/layout/line_break.rs index d53a26642..06cd91964 100644 --- a/parley/src/layout/line_break.rs +++ b/parley/src/layout/line_break.rs @@ -265,17 +265,18 @@ impl SubtreeHistory { new } - /// Save the current extents of every subtree, returning a token for [`Self::restore`]. + /// Save the current extents of every subtree, returning the length of the history to pass + /// to [`Self::restore`]. fn save(&mut self) -> usize { self.frozen = self.entries.len(); self.frozen } - /// Restore the extents of every subtree to what they were at `save`. Earlier saves can - /// still be restored afterwards; later ones can't. - fn restore(&mut self, save: usize) { - self.entries.truncate(save); - self.frozen = save; + /// Restore the extents of every subtree to what they were at the save that returned `len`. + /// Earlier saves can still be restored afterwards; later ones can't. + fn restore(&mut self, len: usize) { + self.entries.truncate(len); + self.frozen = len; } fn clear(&mut self) { @@ -502,8 +503,8 @@ struct PrevBoundaryState { state: LineState, /// Length of [`BreakerState::contributed`] at this opportunity. contributed_len: usize, - /// [Save](SubtreeHistory::save) of [`BreakerState::subtrees`] at this opportunity. - subtrees_save: usize, + /// Length of [`BreakerState::subtrees`] at this opportunity, as [saved](SubtreeHistory::save). + subtrees_len: usize, } /// Reason that the line breaker has yielded control flow @@ -733,7 +734,7 @@ impl BreakerState { cluster_idx: self.cluster_idx, state: self.line.clone(), contributed_len: self.contributed.len(), - subtrees_save: self.subtrees.save(), + subtrees_len: self.subtrees.save(), }); } @@ -746,7 +747,7 @@ impl BreakerState { cluster_idx: self.cluster_idx, state: self.line.clone(), contributed_len: self.contributed.len(), - subtrees_save: self.subtrees.save(), + subtrees_len: self.subtrees.save(), }); } @@ -757,7 +758,7 @@ impl BreakerState { self.cluster_idx = prev_state.cluster_idx; self.line = prev_state.state; self.contributed.truncate(prev_state.contributed_len); - self.subtrees.restore(prev_state.subtrees_save); + self.subtrees.restore(prev_state.subtrees_len); } #[inline(always)] From eeead741ae82dc3f03cd8e86540a39b52764b6eb Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Fri, 2 Oct 2026 00:39:23 +0000 Subject: [PATCH 06/11] line_break: rename SubtreeHistory::frozen to frozen_up_to --- parley/src/layout/line_break.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/parley/src/layout/line_break.rs b/parley/src/layout/line_break.rs index 06cd91964..528ef50f7 100644 --- a/parley/src/layout/line_break.rs +++ b/parley/src/layout/line_break.rs @@ -233,8 +233,8 @@ impl SubtreeExtents { #[derive(Clone, Default)] struct SubtreeHistory { entries: Vec, - /// The first `frozen` entries may be needed by a save, so are never modified. - frozen: usize, + /// The entries before this index are frozen: a save may need them, so they are never modified. + frozen_up_to: usize, } impl SubtreeHistory { @@ -258,7 +258,7 @@ impl SubtreeHistory { new.add(baseline_offset, metrics); if old != Some(new) { match index { - Some(i) if i >= self.frozen => self.entries[i] = new, + Some(i) if i >= self.frozen_up_to => self.entries[i] = new, _ => self.entries.push(new), } } @@ -268,20 +268,20 @@ impl SubtreeHistory { /// Save the current extents of every subtree, returning the length of the history to pass /// to [`Self::restore`]. fn save(&mut self) -> usize { - self.frozen = self.entries.len(); - self.frozen + self.frozen_up_to = self.entries.len(); + self.frozen_up_to } /// Restore the extents of every subtree to what they were at the save that returned `len`. /// Earlier saves can still be restored afterwards; later ones can't. fn restore(&mut self, len: usize) { self.entries.truncate(len); - self.frozen = len; + self.frozen_up_to = len; } fn clear(&mut self) { self.entries.clear(); - self.frozen = 0; + self.frozen_up_to = 0; } } From 800af5accae875936e1bf85a2df4daa18d8bd6e3 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Fri, 2 Oct 2026 00:40:13 +0000 Subject: [PATCH 07/11] line_break: rename SubtreeHistory::frozen_up_to to frozen_count --- parley/src/layout/line_break.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/parley/src/layout/line_break.rs b/parley/src/layout/line_break.rs index 528ef50f7..40045c020 100644 --- a/parley/src/layout/line_break.rs +++ b/parley/src/layout/line_break.rs @@ -233,8 +233,8 @@ impl SubtreeExtents { #[derive(Clone, Default)] struct SubtreeHistory { entries: Vec, - /// The entries before this index are frozen: a save may need them, so they are never modified. - frozen_up_to: usize, + /// The first `frozen_count` entries may be needed by a save, so are never modified. + frozen_count: usize, } impl SubtreeHistory { @@ -258,7 +258,7 @@ impl SubtreeHistory { new.add(baseline_offset, metrics); if old != Some(new) { match index { - Some(i) if i >= self.frozen_up_to => self.entries[i] = new, + Some(i) if i >= self.frozen_count => self.entries[i] = new, _ => self.entries.push(new), } } @@ -268,20 +268,20 @@ impl SubtreeHistory { /// Save the current extents of every subtree, returning the length of the history to pass /// to [`Self::restore`]. fn save(&mut self) -> usize { - self.frozen_up_to = self.entries.len(); - self.frozen_up_to + self.frozen_count = self.entries.len(); + self.frozen_count } /// Restore the extents of every subtree to what they were at the save that returned `len`. /// Earlier saves can still be restored afterwards; later ones can't. fn restore(&mut self, len: usize) { self.entries.truncate(len); - self.frozen_up_to = len; + self.frozen_count = len; } fn clear(&mut self) { self.entries.clear(); - self.frozen_up_to = 0; + self.frozen_count = 0; } } From 04bb21c7a0dca4b46c38cf0a68bff4ed93d2c018 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Fri, 2 Oct 2026 07:41:25 +0000 Subject: [PATCH 08/11] style_metrics: define independent aligned subtrees CSS defines an aligned subtree for every inline box, recursively, but layout only ever deals with the ones that aren't part of a larger one: those rooted at the root span box or at a box with `vertical-align: top | bottom`. Name those "independent aligned subtrees" (the specifications have no term for them), say that this is what "aligned subtree" means elsewhere in the crate, and say explicitly which of them `SubtreeHistory` holds. Also fix the definition, which included every parent-relative descendant rather than stopping at `top`/`bottom` boxes. --- parley/src/layout/line_break.rs | 12 ++++++-- parley/src/layout/style_metrics.rs | 47 ++++++++++++++++++++++-------- 2 files changed, 44 insertions(+), 15 deletions(-) diff --git a/parley/src/layout/line_break.rs b/parley/src/layout/line_break.rs index 40045c020..f1a55cf95 100644 --- a/parley/src/layout/line_break.rs +++ b/parley/src/layout/line_break.rs @@ -217,8 +217,14 @@ impl SubtreeExtents { } } -/// The extents of the non-root [aligned subtrees] on the current line, i.e. those rooted at a -/// box with `vertical-align: top | bottom`. Empty for lines without such content. +/// The extents of the [independent aligned subtree] rooted at each span with +/// `vertical-align: top | bottom` that has content on the current line. Spans with any other +/// `vertical-align` never get extents of their own, as their boxes are part of the independent +/// aligned subtree they are nested in. Empty for lines without `top`/`bottom` spans. +/// +/// The extents of the root aligned subtree are in [`LineBoxMetrics::root`] instead. An +/// [`InlineBox`] with `vertical-align: top | bottom` has no descendants, so [`LineBoxMetrics`] +/// only keeps its height. /// /// Reverting to a saved line-breaking opportunity has to restore these extents to what they were /// at that opportunity. So that saving an opportunity doesn't have to copy them, this is a @@ -229,7 +235,7 @@ impl SubtreeExtents { /// - [restoring](Self::restore) a save drops every entry pushed since, leaving the frozen /// entries as they were at that save. /// -/// [aligned subtrees]: crate::layout::style_metrics#aligned-subtrees +/// [independent aligned subtree]: crate::layout::style_metrics#aligned-subtrees #[derive(Clone, Default)] struct SubtreeHistory { entries: Vec, diff --git a/parley/src/layout/style_metrics.rs b/parley/src/layout/style_metrics.rs index 049639c55..8b07264e8 100644 --- a/parley/src/layout/style_metrics.rs +++ b/parley/src/layout/style_metrics.rs @@ -13,15 +13,37 @@ //! //! # Aligned subtrees //! -//! CSS Inline Layout 3 §4.2.3 defines the *aligned subtree* of a span box as that box together -//! with every descendant span box (or [`InlineBox`]) whose `vertical-align` is parent-relative -//! (`baseline`, `sub`, `super`, `text-top`, `text-bottom`, `middle`, or a -//! ``/``), i.e. everything that is aligned relative to its parent's baseline -//! rather than to the line box. Boxes with `vertical-align: top | bottom` are aligned to the line -//! box instead, so each of them roots a new aligned subtree. Within a subtree all baselines are -//! fixed relative to each other, so its extents can be accumulated relative to the root's baseline -//! while the line is being built; the subtrees are only positioned against each other, and the -//! line box sized, once the line is complete. +//! CSS Inline Layout 3 §4.2.3 defines the *aligned subtree* of a span box recursively: it is that +//! box together with the aligned subtrees of each child span box (or [`InlineBox`]) whose +//! `vertical-align` is parent-relative (`baseline`, `sub`, `super`, `text-top`, `text-bottom`, +//! `middle`, or a ``/``). Children with `vertical-align: top | bottom` are +//! aligned to the line box instead, so they and their descendants are left out. +//! +//! By that definition every span box has an aligned subtree, most of them nested inside their +//! parent's. The ones that matter for layout are those that are not part of a larger one, which +//! Parley calls *independent aligned subtrees* (our term; the specifications don't name them). +//! Their roots are the root span box, whose subtree is the *root aligned subtree*, and every box +//! with `vertical-align: top | bottom`. They don't overlap, and each box on a line belongs to +//! exactly one of them: the one rooted at its nearest ancestor (or self) with +//! `vertical-align: top | bottom`, or the root aligned subtree if there is none (see +//! [`StyleMetrics::aligned_subtree`]). Where the rest of this crate says just "aligned subtree", +//! it means an independent one. For example, with these spans: +//! +//! ```text +//! root +//! ├─ a (super) +//! │ └─ b (baseline) +//! └─ c (top) +//! └─ d (sub) +//! ``` +//! +//! the CSS definition gives five aligned subtrees (`{root, a, b}`, `{a, b}`, `{b}`, `{c, d}` and +//! `{d}`), of which two are independent: `{root, a, b}` (the root aligned subtree) and `{c, d}`. +//! +//! Within an independent aligned subtree all baselines are fixed relative to each other, so its +//! extents can be accumulated relative to the root's baseline while the line is being built; the +//! independent aligned subtrees are only positioned against each other, and the line box sized, +//! once the line is complete. //! See and CSS 2 §10.8 //! . @@ -62,10 +84,11 @@ pub(crate) struct StyleMetrics { /// [`Self::baseline_offset`] before quantization. Children accumulate their shifts from this /// so that rounding never compounds along the ancestor chain. pub(crate) exact_baseline_offset: f32, - /// Style index of the root of the [aligned subtree] this box belongs to: `0` for the root - /// span box, or the nearest ancestor (or self) with `vertical-align: top | bottom`. + /// Style index of the root of the [independent aligned subtree] this box belongs to: the + /// nearest ancestor (or self) with `vertical-align: top | bottom`, or `0` (the root span box) + /// if there is none. /// - /// [aligned subtree]: self#aligned-subtrees + /// [independent aligned subtree]: self#aligned-subtrees pub(crate) aligned_subtree: u16, /// Style index of the parent span; always less than the span's own index, except for the /// root (index `0`), whose parent is `0`. From 16fa075637199718200e5d916792c2e52df9af32 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Fri, 2 Oct 2026 07:44:33 +0000 Subject: [PATCH 09/11] Use the independent/root aligned subtree terms consistently Follow-up to the module doc change: comments that said "non-root", "top-level" or just "aligned subtree" where the distinction matters now use the terms defined in `style_metrics`. Comments and one test name only; no behaviour change. --- parley/src/inline_box.rs | 8 +++---- parley/src/layout/data.rs | 13 ++++++----- parley/src/layout/line_break.rs | 32 +++++++++++++++----------- parley/src/layout/style_metrics.rs | 13 +++++++---- parley/src/style/mod.rs | 10 ++++---- parley/src/tests/test_style_metrics.rs | 2 +- parley_tests/tests/lines.rs | 5 ++-- 7 files changed, 48 insertions(+), 35 deletions(-) diff --git a/parley/src/inline_box.rs b/parley/src/inline_box.rs index 6622f2a0e..fdb826c02 100644 --- a/parley/src/inline_box.rs +++ b/parley/src/inline_box.rs @@ -33,10 +33,10 @@ pub(crate) struct LayoutInlineBox { /// Style index of the span containing the box, against which the box's `vertical_align` /// is resolved. The box itself has no style of its own. pub(crate) parent_style_index: u16, - /// Offset of the box's baseline above the baseline of its aligned subtree (that of the - /// parent style), resolved when the box is placed on a line (box sizes may change between - /// building and line breaking). Unused for out-of-flow and `vertical-align: top | bottom` - /// boxes. + /// Offset of the box's baseline above the baseline of the independent aligned subtree it + /// belongs to (that of the parent style), resolved when the box is placed on a line (box + /// sizes may change between building and line breaking). Unused for out-of-flow and + /// `vertical-align: top | bottom` boxes. pub(crate) baseline_offset: f32, } diff --git a/parley/src/layout/data.rs b/parley/src/layout/data.rs index 636790479..8d697fc12 100644 --- a/parley/src/layout/data.rs +++ b/parley/src/layout/data.rs @@ -98,15 +98,16 @@ pub(crate) struct LineData { pub(crate) indent: f32, /// This line's entries in [`LayoutData::aligned_subtree_offsets`]. /// - /// Empty for lines with only baseline-relative content because the top-level aligned - /// subtree for each line trivially has an offset of 0 + /// Empty for lines with only baseline-relative content because the root aligned subtree + /// for each line trivially has an offset of 0 pub(crate) aligned_subtree_offsets: Range, } -/// Position of an [aligned subtree] (rooted at a `vertical-align: top | bottom` style) on a line. -/// Computed for each non-top-level aligned subtree on the line in `BreakLines::finish_line`. +/// Position of an [independent aligned subtree] rooted at a span with +/// `vertical-align: top | bottom` on a line. Computed for each such subtree on the line in +/// `BreakLines::finish_line`. /// -/// [aligned subtree]: crate::layout::style_metrics#aligned-subtrees +/// [independent aligned subtree]: crate::layout::style_metrics#aligned-subtrees #[derive(Clone, Copy, Debug, PartialEq)] pub(crate) struct AlignedSubtreeOffset { /// Style index of the subtree root (a span with `vertical-align: top | bottom`) @@ -221,7 +222,7 @@ pub(crate) struct LayoutData { /// Items within each line pub(crate) line_items: Vec, /// Position of each aligned subtree rooted at a `vertical-align: top | bottom` style on each line. - /// The top-level aligned subtree of each line doesn't have an entry as its offset is trivially zero. + /// The root aligned subtree of each line doesn't have an entry as its offset is trivially zero. /// /// Each line owns a contiguous slice ([`LineData::aligned_subtree_offsets`]). pub(crate) aligned_subtree_offsets: Vec, diff --git a/parley/src/layout/line_break.rs b/parley/src/layout/line_break.rs index f1a55cf95..f2bfe3b04 100644 --- a/parley/src/layout/line_break.rs +++ b/parley/src/layout/line_break.rs @@ -102,19 +102,21 @@ impl LineState { /// Following CSS 2.2 § 10.8 (line height calculations in "Visual formatting model details"), line /// boxes are sized to fit the line's inline content. Span boxes and inline boxes with a /// parent-relative `vertical-align` are first aligned to each other relative to the baseline of -/// their parent span; each chain of such boxes forms an [aligned subtree], rooted at the root span -/// box or at a box with `vertical-align: top | bottom`. Each subtree's extents are tracked -/// relative to its own baseline: the root subtree's in [`Self::root`], the others' in -/// [`SubtreeHistory`]. The subtrees are only positioned against each other once the line is +/// their parent span; the boxes aligned to each other in this way form an +/// [independent aligned subtree], rooted at the root span box or at a box with +/// `vertical-align: top | bottom`. Each subtree's extents are tracked relative to its own +/// baseline: the root aligned subtree's in [`Self::root`], those rooted at a `top`/`bottom` span +/// in [`SubtreeHistory`]. The subtrees are only positioned against each other once the line is /// complete (see `BreakLines::finish_line`). /// See . /// -/// [aligned subtree]: crate::layout::style_metrics#aligned-subtrees +/// [independent aligned subtree]: crate::layout::style_metrics#aligned-subtrees #[derive(Clone, Copy, Debug)] struct LineBoxMetrics { /// Extents of the root aligned subtree. root: SubtreeExtents, - /// Height of the tallest non-root aligned subtree (see [`SubtreeHistory`]). + /// Height of the tallest aligned subtree rooted at a `top`/`bottom` span (see + /// [`SubtreeHistory`]). non_root_height: f32, /// Height of the tallest `vertical-align: top` inline box, which is positioned against the /// line box rather than a baseline. @@ -128,7 +130,10 @@ struct LineBoxMetrics { last_text: (usize, u16), } -/// Extents of one aligned subtree on the current line, measured from the subtree's own baseline. +/// Extents of one [independent aligned subtree] on the current line, measured from the subtree's +/// own baseline. +/// +/// [independent aligned subtree]: crate::layout::style_metrics#aligned-subtrees #[derive(Clone, Copy, Debug, PartialEq)] struct SubtreeExtents { /// Style index of the subtree root: `0` for the root span box, otherwise a style with @@ -620,8 +625,8 @@ pub struct BreakerState { /// [`LineBoxMetrics::add_style`]). Lives here rather than in [`LineState`] so that saving a /// line-breaking opportunity only records its length; reverting truncates it back. contributed: Vec, - /// Extents of the non-root aligned subtrees on the current line. Like [`Self::contributed`], - /// saving a line-breaking opportunity only records its length. + /// Extents of the aligned subtrees rooted at `top`/`bottom` spans on the current line. Like + /// [`Self::contributed`], saving a line-breaking opportunity only records its length. subtrees: SubtreeHistory, // Saved breaker states for reverting to a previously encountered line-breaking opportunity @@ -1536,11 +1541,12 @@ impl<'a, B: Brush> BreakLines<'a, B> { } } - // Position the aligned subtrees against each other (CSS 2.2 §10.8.1). + // Position the independent aligned subtrees against each other (CSS 2.2 §10.8.1). // - // The root subtree determines the line's baseline. `top`/`bottom` aligned subtrees sit - // flush with the line box's top/bottom edge. If taller than the root subtree then top-aligned - // subtrees grow the line box downwards and bottom-aligned subtrees grow it upwards. + // The root aligned subtree determines the line's baseline. Those rooted at a `top`/`bottom` + // span sit flush with the line box's top/bottom edge. If taller than the root aligned + // subtree then top-aligned subtrees grow the line box downwards and bottom-aligned subtrees + // grow it upwards. let box_metrics = &self.state.line.box_metrics; let (mut line_box_extents, mut content_box_extents) = if invisible { (Extents::default().or_zero(), Extents::default().or_zero()) diff --git a/parley/src/layout/style_metrics.rs b/parley/src/layout/style_metrics.rs index 8b07264e8..a5d3f14f2 100644 --- a/parley/src/layout/style_metrics.rs +++ b/parley/src/layout/style_metrics.rs @@ -77,9 +77,10 @@ pub(crate) struct StyleMetrics { pub(crate) under: f32, /// Resolved `line-height` (`over + under`). pub(crate) line_height: f32, - /// Offset of this box's baseline above the baseline of its [aligned subtree's] root. + /// Offset of this box's baseline above the baseline of the root of the + /// [independent aligned subtree] it belongs to. /// - /// [aligned subtree's]: Self::aligned_subtree + /// [independent aligned subtree]: Self::aligned_subtree pub(crate) baseline_offset: f32, /// [`Self::baseline_offset`] before quantization. Children accumulate their shifts from this /// so that rounding never compounds along the ancestor chain. @@ -297,7 +298,8 @@ pub(crate) fn shift_from_parent( alignment + shift } -/// Where an in-flow [`InlineBox`] sits relative to the baseline of its aligned subtree. +/// Where an in-flow [`InlineBox`] sits relative to the baseline of the independent aligned +/// subtree it belongs to. #[derive(Clone, Copy, Debug)] pub(crate) struct InlineBoxPlacement { /// Style index of the aligned subtree root (see [`StyleMetrics::aligned_subtree`]). @@ -314,8 +316,9 @@ pub(crate) struct InlineBoxPlacement { /// containing it (`parent_style`). /// /// A box without an explicit baseline sits on the baseline, i.e. it is all ascent. For -/// `vertical-align: top | bottom` the returned offset is relative to the parent's subtree, but -/// such boxes are positioned against the line box instead (see [`crate::Line::inline_box_top`]). +/// `vertical-align: top | bottom` the returned offset is relative to the subtree the parent +/// belongs to, but such boxes are positioned against the line box instead (see +/// [`crate::Line::inline_box_top`]). pub(crate) fn inline_box_placement( inline_box: &InlineBox, parent_style: u16, diff --git a/parley/src/style/mod.rs b/parley/src/style/mod.rs index 2da2ee126..d9977df4b 100644 --- a/parley/src/style/mod.rs +++ b/parley/src/style/mod.rs @@ -128,9 +128,10 @@ pub enum AlignmentBaseline { /// A shift applied to a span (or [`InlineBox`]) after [`AlignmentBaseline`] alignment. Mirrors /// the CSS `baseline-shift` property. /// -/// [`Top`](Self::Top) and [`Bottom`](Self::Bottom) are *line-relative*: the box and its -/// descendants form an aligned subtree that is placed against the line box, and the -/// [`AlignmentBaseline`] is ignored. All other values are relative to the parent span. +/// [`Top`](Self::Top) and [`Bottom`](Self::Bottom) are *line-relative*: the box, together with +/// the descendants that are aligned relative to it, is placed against the line box instead of +/// its parent span, and the [`AlignmentBaseline`] is ignored. All other values are relative to +/// the parent span. /// /// [`InlineBox`]: crate::InlineBox #[derive(Debug, Clone, Copy, PartialEq)] @@ -196,7 +197,8 @@ impl VerticalAlign { Self::new(AlignmentBaseline::Baseline, BaselineShift::Length(length)) } - /// Whether this value forms a line-relative aligned subtree (`top` or `bottom`). + /// Whether this value is line-relative (`top` or `bottom`), i.e. aligns the box to the line + /// box rather than to its parent span. pub fn is_line_relative(self) -> bool { matches!(self.shift, BaselineShift::Top | BaselineShift::Bottom) } diff --git a/parley/src/tests/test_style_metrics.rs b/parley/src/tests/test_style_metrics.rs index 72324774d..c75a68228 100644 --- a/parley/src/tests/test_style_metrics.rs +++ b/parley/src/tests/test_style_metrics.rs @@ -99,7 +99,7 @@ fn super_shifts_relative_to_parent() { } #[test] -fn top_starts_an_aligned_subtree() { +fn top_starts_an_independent_aligned_subtree() { let layout = build(); let c = metrics(&layout)[3]; let d = metrics(&layout)[4]; diff --git a/parley_tests/tests/lines.rs b/parley_tests/tests/lines.rs index c39cc260d..5b17ea981 100644 --- a/parley_tests/tests/lines.rs +++ b/parley_tests/tests/lines.rs @@ -765,7 +765,7 @@ fn lines_revert_restores_line_height() { } /// Like [`lines_revert_restores_line_height`], but the reverted content is in a -/// `vertical-align: top` span, i.e. in a non-root aligned subtree. +/// `vertical-align: top` span, i.e. not in the root aligned subtree. #[test] fn lines_revert_restores_aligned_subtree_line_height() { let mut env = TestEnv::new(test_name!(), None); @@ -806,7 +806,8 @@ fn lines_aligned_subtrees_grow_across_breaks() { .enumerate() { let size = 12.0 + 4.0 * ((i + j) % 4) as f32; - // `vertical-align` is inherited, so reset it to keep the word in the outer subtree. + // `vertical-align` is inherited, so reset it to keep the word in the subtree of the + // enclosing `top`/`bottom` span. builder.push_style_modification_span(&[ StyleProperty::FontSize(size), StyleProperty::VerticalAlign(VerticalAlign::BASELINE), From 6691862dcc9eaaf520b7c3d70d77e60126b126bf Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Fri, 2 Oct 2026 07:47:11 +0000 Subject: [PATCH 10/11] Rename `aligned_subtree` to `aligned_subtree_root` The field, parameters and locals of this name hold the style index of the root of an independent aligned subtree, not a subtree. No behaviour change. --- parley/src/layout/data.rs | 2 +- parley/src/layout/line.rs | 6 +++--- parley/src/layout/line_break.rs | 25 +++++++++++++------------ parley/src/layout/style_metrics.rs | 22 +++++++++++----------- parley/src/tests/test_style_metrics.rs | 10 +++++----- parley/src/tests/utils/asserts.rs | 8 ++++---- 6 files changed, 37 insertions(+), 36 deletions(-) diff --git a/parley/src/layout/data.rs b/parley/src/layout/data.rs index 8d697fc12..c591c65a0 100644 --- a/parley/src/layout/data.rs +++ b/parley/src/layout/data.rs @@ -143,7 +143,7 @@ impl LineData { metrics: &StyleMetrics, ) -> f32 { self.metrics.baseline - - self.aligned_subtree_offset(offsets, metrics.aligned_subtree) + - self.aligned_subtree_offset(offsets, metrics.aligned_subtree_root) - metrics.baseline_offset } diff --git a/parley/src/layout/line.rs b/parley/src/layout/line.rs index db5cd3a4b..3d59a199c 100644 --- a/parley/src/layout/line.rs +++ b/parley/src/layout/line.rs @@ -90,17 +90,17 @@ impl<'a, B: Brush> Line<'a, B> { BaselineShift::Top => self.data.metrics.block_min_coord, BaselineShift::Bottom => self.data.metrics.block_max_coord - inline_box.height, _ => { - let aligned_subtree = self + let aligned_subtree_root = self .layout .data .style_metrics .get(usize::from(layout_box.parent_style_index)) - .map_or(0, |m| m.aligned_subtree); + .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, + aligned_subtree_root, ) - layout_box.baseline_offset - ascent diff --git a/parley/src/layout/line_break.rs b/parley/src/layout/line_break.rs index f2bfe3b04..6822b5fdf 100644 --- a/parley/src/layout/line_break.rs +++ b/parley/src/layout/line_break.rs @@ -317,19 +317,19 @@ impl LineBoxMetrics { } /// Add a box whose baseline is `baseline_offset` above the baseline of the aligned subtree - /// rooted at `aligned_subtree`. + /// rooted at `aligned_subtree_root`. #[inline] fn add_box( &mut self, - aligned_subtree: u16, + aligned_subtree_root: u16, baseline_offset: f32, metrics: BoxMetrics, subtrees: &mut SubtreeHistory, ) { - if aligned_subtree == 0 { + if aligned_subtree_root == 0 { self.root.add(baseline_offset, metrics); } else { - let subtree = subtrees.grow(aligned_subtree, baseline_offset, metrics); + let subtree = subtrees.grow(aligned_subtree_root, baseline_offset, metrics); self.non_root_height = self.non_root_height.max(subtree.line_box.height()); } } @@ -367,7 +367,7 @@ impl LineBoxMetrics { under: metrics.under, }; self.add_box( - metrics.aligned_subtree, + metrics.aligned_subtree_root, metrics.baseline_offset, span_box, subtrees, @@ -458,18 +458,19 @@ impl LineBoxMetrics { else { return; }; - let (baseline_offset, aligned_subtree) = data + let (baseline_offset, aligned_subtree_root) = data .style_metrics .get(style) - .map_or((0., 0), |m| (m.baseline_offset, m.aligned_subtree)); - self.add_box(aligned_subtree, baseline_offset, run_box, subtrees); + .map_or((0., 0), |m| (m.baseline_offset, m.aligned_subtree_root)); + self.add_box(aligned_subtree_root, baseline_offset, run_box, subtrees); } /// Add an inline box extending `ascent` above and `descent` below a baseline that is - /// `baseline_offset` above the baseline of the `aligned_subtree` root. + /// `baseline_offset` above the baseline of the aligned subtree rooted at + /// `aligned_subtree_root`. fn add_inline_box( &mut self, - aligned_subtree: u16, + aligned_subtree_root: u16, baseline_offset: f32, ascent: f32, descent: f32, @@ -488,7 +489,7 @@ impl LineBoxMetrics { over: ascent, under: descent, }; - self.add_box(aligned_subtree, baseline_offset, inline_box, subtrees); + self.add_box(aligned_subtree_root, baseline_offset, inline_box, subtrees); } /// Add an inline box with `vertical-align: top | bottom`, which only constrains the line @@ -726,7 +727,7 @@ impl BreakerState { ); } else { self.line.box_metrics.add_inline_box( - placement.aligned_subtree, + placement.aligned_subtree_root, placement.baseline_offset, placement.ascent, placement.descent, diff --git a/parley/src/layout/style_metrics.rs b/parley/src/layout/style_metrics.rs index a5d3f14f2..536e13349 100644 --- a/parley/src/layout/style_metrics.rs +++ b/parley/src/layout/style_metrics.rs @@ -26,8 +26,8 @@ //! with `vertical-align: top | bottom`. They don't overlap, and each box on a line belongs to //! exactly one of them: the one rooted at its nearest ancestor (or self) with //! `vertical-align: top | bottom`, or the root aligned subtree if there is none (see -//! [`StyleMetrics::aligned_subtree`]). Where the rest of this crate says just "aligned subtree", -//! it means an independent one. For example, with these spans: +//! [`StyleMetrics::aligned_subtree_root`]). Where the rest of this crate says just "aligned +//! subtree", it means an independent one. For example, with these spans: //! //! ```text //! root @@ -80,7 +80,7 @@ pub(crate) struct StyleMetrics { /// Offset of this box's baseline above the baseline of the root of the /// [independent aligned subtree] it belongs to. /// - /// [independent aligned subtree]: Self::aligned_subtree + /// [independent aligned subtree]: Self::aligned_subtree_root pub(crate) baseline_offset: f32, /// [`Self::baseline_offset`] before quantization. Children accumulate their shifts from this /// so that rounding never compounds along the ancestor chain. @@ -90,7 +90,7 @@ pub(crate) struct StyleMetrics { /// if there is none. /// /// [independent aligned subtree]: self#aligned-subtrees - pub(crate) aligned_subtree: u16, + pub(crate) aligned_subtree_root: u16, /// Style index of the parent span; always less than the span's own index, except for the /// root (index `0`), whose parent is `0`. pub(crate) parent: u16, @@ -137,7 +137,7 @@ pub(crate) fn resolve_style_metrics( metrics.parent = 0; metrics.baseline_offset = 0.; metrics.exact_baseline_offset = 0.; - metrics.aligned_subtree = 0; + metrics.aligned_subtree_root = 0; } else { let parent_index = usize::from(style.parent); debug_assert!( @@ -156,7 +156,7 @@ pub(crate) fn resolve_style_metrics( if align.is_line_relative() { metrics.baseline_offset = 0.; metrics.exact_baseline_offset = 0.; - metrics.aligned_subtree = index as u16; + metrics.aligned_subtree_root = index as u16; } else { let shift = shift_from_parent(align, metrics.over, metrics.under, parent); metrics.exact_baseline_offset = parent.exact_baseline_offset + shift; @@ -167,7 +167,7 @@ pub(crate) fn resolve_style_metrics( } else { metrics.exact_baseline_offset }; - metrics.aligned_subtree = parent.aligned_subtree; + metrics.aligned_subtree_root = parent.aligned_subtree_root; } } @@ -265,7 +265,7 @@ impl StyleMetrics { line_height, baseline_offset: 0., exact_baseline_offset: 0., - aligned_subtree: 0, + aligned_subtree_root: 0, parent: 0, font_size: 0., } @@ -302,8 +302,8 @@ pub(crate) fn shift_from_parent( /// subtree it belongs to. #[derive(Clone, Copy, Debug)] pub(crate) struct InlineBoxPlacement { - /// Style index of the aligned subtree root (see [`StyleMetrics::aligned_subtree`]). - pub(crate) aligned_subtree: u16, + /// Style index of the aligned subtree root (see [`StyleMetrics::aligned_subtree_root`]). + pub(crate) aligned_subtree_root: u16, /// Offset of the box's baseline above the subtree root's baseline. pub(crate) baseline_offset: f32, /// Height of the box above its baseline. @@ -334,7 +334,7 @@ pub(crate) fn inline_box_placement( let shift = shift_from_parent(inline_box.vertical_align, ascent, descent, &parent); let baseline_offset = parent.exact_baseline_offset + shift; InlineBoxPlacement { - aligned_subtree: parent.aligned_subtree, + aligned_subtree_root: parent.aligned_subtree_root, baseline_offset: if quantize { baseline_offset.round() } else { diff --git a/parley/src/tests/test_style_metrics.rs b/parley/src/tests/test_style_metrics.rs index c75a68228..53efad39d 100644 --- a/parley/src/tests/test_style_metrics.rs +++ b/parley/src/tests/test_style_metrics.rs @@ -74,7 +74,7 @@ fn line_height_is_distributed_as_half_leading() { assert!(root.ascent > 15. && root.ascent < 20.); assert!(root.x_height > 9. && root.x_height < 12.); assert_eq!(root.baseline_offset, 0.); - assert_eq!(root.aligned_subtree, 0); + assert_eq!(root.aligned_subtree_root, 0); } #[test] @@ -85,7 +85,7 @@ fn ancestors_without_text_have_metrics() { assert_eq!(a.line_height, 50.); assert!(a.ascent > 30.); assert_eq!(a.baseline_offset, 0.); - assert_eq!(a.aligned_subtree, 0); + assert_eq!(a.aligned_subtree_root, 0); } #[test] @@ -95,7 +95,7 @@ fn super_shifts_relative_to_parent() { assert_eq!(layout.data.styles[2].parent, 1); // WebKit/Blink constant: a third of the *parent's* font size (40px). assert!((b.baseline_offset - 40. / 3.).abs() < 1e-4); - assert_eq!(b.aligned_subtree, 0); + assert_eq!(b.aligned_subtree_root, 0); } #[test] @@ -104,10 +104,10 @@ fn top_starts_an_independent_aligned_subtree() { let c = metrics(&layout)[3]; let d = metrics(&layout)[4]; assert_eq!(c.baseline_offset, 0.); - assert_eq!(c.aligned_subtree, 3); + assert_eq!(c.aligned_subtree_root, 3); // Children of a `top` box are relative to it, not to the root. assert_eq!(d.baseline_offset, 3.); - assert_eq!(d.aligned_subtree, 3); + assert_eq!(d.aligned_subtree_root, 3); } #[test] diff --git a/parley/src/tests/utils/asserts.rs b/parley/src/tests/utils/asserts.rs index 20f826182..f74f24baf 100644 --- a/parley/src/tests/utils/asserts.rs +++ b/parley/src/tests/utils/asserts.rs @@ -14,15 +14,15 @@ fn canonicalize_layout_data(layout_data: &LayoutData) -> LayoutData let mut remap = Vec::with_capacity(normalized.styles.len()); // The style tree (`parent`) and everything derived from it (the parent-relative - // `baseline_offset` and `aligned_subtree` of the style metrics) is intentionally not part of - // the comparison: the tree builder records span nesting that the flat builders cannot - // express, so only the visual style properties are compared. + // `baseline_offset` and `aligned_subtree_root` of the style metrics) is intentionally not + // part of the comparison: the tree builder records span nesting that the flat builders + // cannot express, so only the visual style properties are compared. for (style, metrics) in normalized.styles.iter().zip(&normalized.style_metrics) { let mut style = style.clone(); style.parent = 0; let mut metrics = *metrics; metrics.baseline_offset = 0.; - metrics.aligned_subtree = 0; + metrics.aligned_subtree_root = 0; metrics.parent = 0; if let Some(index) = canonical_styles .iter() From 2720d6888822065dffa99e74ed695b408d622cd0 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Fri, 2 Oct 2026 07:52:24 +0000 Subject: [PATCH 11/11] line_break: shorten the `SubtreeHistory` doc comment --- parley/src/layout/line_break.rs | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/parley/src/layout/line_break.rs b/parley/src/layout/line_break.rs index 6822b5fdf..2ff317842 100644 --- a/parley/src/layout/line_break.rs +++ b/parley/src/layout/line_break.rs @@ -222,14 +222,9 @@ impl SubtreeExtents { } } -/// The extents of the [independent aligned subtree] rooted at each span with -/// `vertical-align: top | bottom` that has content on the current line. Spans with any other -/// `vertical-align` never get extents of their own, as their boxes are part of the independent -/// aligned subtree they are nested in. Empty for lines without `top`/`bottom` spans. -/// -/// The extents of the root aligned subtree are in [`LineBoxMetrics::root`] instead. An -/// [`InlineBox`] with `vertical-align: top | bottom` has no descendants, so [`LineBoxMetrics`] -/// only keeps its height. +/// The extents of every [independent aligned subtree] on the current line other than the root +/// one (which is in [`LineBoxMetrics::root`]), i.e. those rooted at a span with +/// `vertical-align: top | bottom`. Empty for lines without such spans. /// /// Reverting to a saved line-breaking opportunity has to restore these extents to what they were /// at that opportunity. So that saving an opportunity doesn't have to copy them, this is a