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..c591c65a0 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`) @@ -142,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 } @@ -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.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 b9c1a3ad1..2ff317842 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, @@ -100,18 +102,22 @@ 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 in [`Self::subtrees`]; the subtrees are only positioned against -/// each other once the line is complete (see `BreakLines::finish_line`). +/// 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 -#[derive(Clone, Debug)] +/// [independent aligned subtree]: crate::layout::style_metrics#aligned-subtrees +#[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 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. line_relative_top_height: f32, @@ -124,8 +130,11 @@ struct LineBoxMetrics { last_text: (usize, u16), } -/// Extents of one aligned subtree on the current line, measured from the subtree's own baseline. -#[derive(Clone, Copy, Debug)] +/// 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 /// `vertical-align: top | bottom`. @@ -145,7 +154,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 +211,92 @@ 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 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 +/// 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 +/// - [restoring](Self::restore) a save drops every entry pushed since, leaving the frozen +/// entries as they were at that save. +/// +/// [independent aligned subtree]: crate::layout::style_metrics#aligned-subtrees +#[derive(Clone, Default)] +struct SubtreeHistory { + entries: Vec, + /// The first `frozen_count` entries may be needed by a save, so are never modified. + frozen_count: usize, +} + +impl SubtreeHistory { + /// 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() { + if current.iter().rev().all(|s| s.root != entry.root) { + current.push(*entry); + } + } + current + } + + /// 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 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) { + match index { + Some(i) if i >= self.frozen_count => self.entries[i] = new, + _ => self.entries.push(new), + } + } + new + } + + /// 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_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_count = len; + } + + fn clear(&mut self) { + self.entries.clear(); + self.frozen_count = 0; + } } 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 +308,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_root`. #[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_root: u16, + baseline_offset: f32, + metrics: BoxMetrics, + subtrees: &mut SubtreeHistory, + ) { + if aligned_subtree_root == 0 { + self.root.add(baseline_offset, metrics); + } else { + let subtree = subtrees.grow(aligned_subtree_root, 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 +347,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 +355,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_root, + metrics.baseline_offset, + span_box, + subtrees, + ); if index == 0 { return; } @@ -319,6 +394,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 +410,7 @@ impl LineBoxMetrics { characters, data, contributed, + subtrees, ); } @@ -349,16 +426,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, + ); } } } @@ -370,27 +453,23 @@ 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)); - 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); + .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, + 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 +478,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_root, baseline_offset, inline_box, subtrees); } /// Add an inline box with `vertical-align: top | bottom`, which only constrains the line @@ -427,6 +510,8 @@ struct PrevBoundaryState { state: LineState, /// Length of [`BreakerState::contributed`] at this opportunity. contributed_len: usize, + /// Length of [`BreakerState::subtrees`] at this opportunity, as [saved](SubtreeHistory::save). + subtrees_len: usize, } /// Reason that the line breaker has yielded control flow @@ -536,6 +621,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 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 /// Saved breaker state for the last non-emergency line-breaking opportunity @@ -559,6 +647,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 +682,7 @@ impl BreakerState { atom.characters(), data, &mut self.contributed, + &mut self.subtrees, ); self.update_max_height_exceeded(); } @@ -606,7 +696,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(); } @@ -630,10 +722,11 @@ impl BreakerState { ); } else { self.line.box_metrics.add_inline_box( - placement.aligned_subtree, + placement.aligned_subtree_root, placement.baseline_offset, placement.ascent, placement.descent, + &mut self.subtrees, ); } self.update_max_height_exceeded(); @@ -648,6 +741,7 @@ impl BreakerState { cluster_idx: self.cluster_idx, state: self.line.clone(), contributed_len: self.contributed.len(), + subtrees_len: self.subtrees.save(), }); } @@ -660,6 +754,7 @@ impl BreakerState { cluster_idx: self.cluster_idx, state: self.line.clone(), contributed_len: self.contributed.len(), + subtrees_len: self.subtrees.save(), }); } @@ -670,6 +765,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_len); } #[inline(always)] @@ -771,10 +867,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 +1523,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 { @@ -1438,24 +1537,26 @@ 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()) } 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 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 &box_metrics.subtrees[1..] { + for subtree in &subtrees { let height = subtree.line_box.height(); match self.layout.data.styles[usize::from(subtree.root)] .vertical_align @@ -1476,7 +1577,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 &subtrees { let extents = subtree.line_box.or_zero(); let offset = match self.layout.data.styles[usize::from(subtree.root)] .vertical_align @@ -1918,3 +2019,50 @@ 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.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(); + add_box(&mut state, -4.); + assert_eq!(state.line.box_metrics.line_height(), 16.); + assert_eq!(subtree_height(&state), 16.); + assert_eq!(state.subtrees.current().len(), 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 { 9. }; + assert_eq!(state.line.box_metrics.line_height(), expected); + assert_eq!(subtree_height(&state), expected); + assert_eq!(state.subtrees.current().len(), 1); + } + } +} diff --git a/parley/src/layout/style_metrics.rs b/parley/src/layout/style_metrics.rs index 53fde9e74..7b7b14786 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_root`]). 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 //! . @@ -53,18 +75,20 @@ pub(crate) struct StyleMetrics { pub(crate) over: f32, /// Distance from the baseline to the bottom of the line-height expanded span box. pub(crate) under: 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_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. 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 - pub(crate) aligned_subtree: u16, + /// [independent aligned subtree]: self#aligned-subtrees + 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, @@ -111,7 +135,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!( @@ -130,7 +154,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; @@ -141,7 +165,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; } } @@ -238,7 +262,7 @@ impl StyleMetrics { under: box_metrics.under, baseline_offset: 0., exact_baseline_offset: 0., - aligned_subtree: 0, + aligned_subtree_root: 0, parent: 0, font_size: 0., } @@ -271,11 +295,12 @@ 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`]). - 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. @@ -288,8 +313,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, @@ -305,7 +331,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/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 d81561475..d10ea7395 100644 --- a/parley/src/tests/test_style_metrics.rs +++ b/parley/src/tests/test_style_metrics.rs @@ -73,7 +73,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] @@ -84,7 +84,7 @@ fn ancestors_without_text_have_metrics() { assert!((a.over + a.under - 50.).abs() < 1e-4); assert!(a.ascent > 30.); assert_eq!(a.baseline_offset, 0.); - assert_eq!(a.aligned_subtree, 0); + assert_eq!(a.aligned_subtree_root, 0); } #[test] @@ -94,19 +94,19 @@ 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] -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]; 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() 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 000000000..086cf2a47 Binary files /dev/null and b/parley_tests/snapshots/lines_aligned_subtrees_grow_across_breaks-0.png differ diff --git a/parley_tests/tests/lines.rs b/parley_tests/tests/lines.rs index 0431236fc..5b17ea981 100644 --- a/parley_tests/tests/lines.rs +++ b/parley_tests/tests/lines.rs @@ -763,3 +763,77 @@ 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. not in the 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 subtree of the + // enclosing `top`/`bottom` span. + 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); +}