diff --git a/parley/src/layout/data.rs b/parley/src/layout/data.rs index 0f4c0be81..87eccff8c 100644 --- a/parley/src/layout/data.rs +++ b/parley/src/layout/data.rs @@ -371,7 +371,7 @@ impl LayoutData { #[expect(clippy::cast_possible_truncation, reason = "deferred")] pub(crate) fn calculate_content_widths(&self) -> ContentWidths { fn whitespace_advance(atom: Option<(Whitespace, f32)>) -> f32 { - atom.filter(|(whitespace, _)| whitespace.is_space_or_nbsp()) + atom.filter(|(whitespace, _)| *whitespace == Whitespace::Space) .map_or(0.0, |(_, advance)| advance) } diff --git a/parley/src/layout/line_break.rs b/parley/src/layout/line_break.rs index 61b68aea8..48741c9d0 100644 --- a/parley/src/layout/line_break.rs +++ b/parley/src/layout/line_break.rs @@ -301,12 +301,12 @@ impl LineBoxMetrics { descent: f32, quantize: bool, ) { - let (ascent, descent) = if quantize { - (ascent.round(), descent.round()) - } else { - (ascent, descent) - }; - if ascent + descent > 0. { + // 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. + let _ = quantize; + // Negative margins can make the reserved space negative; the box is still content. + // Out-of-flow boxes have infinitely negative extents and are not. + if (ascent != 0. || descent != 0.) && ascent.is_finite() && descent.is_finite() { self.has_content = true; } let subtree = self.subtree_mut(aligned_subtree); diff --git a/parley/src/tests/test_style_metrics.rs b/parley/src/tests/test_style_metrics.rs index e29122106..38cefa904 100644 --- a/parley/src/tests/test_style_metrics.rs +++ b/parley/src/tests/test_style_metrics.rs @@ -317,3 +317,82 @@ fn line_with_only_empty_inline_boxes_is_invisible() { // A non-empty in-flow box does. assert_eq!(line_height(10., InlineBoxKind::InFlow), 30.); } + +/// With quantization, an inline box's fractional height must not be rounded up twice (once for +/// the ascent and once for the descent): the line reserves exactly the box's height. +#[test] +fn quantized_inline_box_extents_are_not_rounded_separately() { + let mut fcx = create_font_context(); + let mut lcx: LayoutContext = LayoutContext::new(); + let root = root_style(); + let mut builder = lcx.tree_builder(&mut fcx, 1., true, &root); + // Baseline splits the box into 40.5 above and 40.5 below the baseline; rounding each + // separately would reserve 82px for an 81px box. + builder.push_inline_box(InlineBox { + id: 0, + kind: InlineBoxKind::InFlow, + index: 0, + width: 10., + height: 81., + baseline: Some(40.5), + vertical_align: VerticalAlign::BASELINE, + }); + let (mut layout, _) = builder.build(); + layout.break_all_lines(None); + let line = layout.lines().next().unwrap(); + assert_eq!(line.metrics().line_height, 81.); +} + +/// A box whose reserved space is negative (e.g. an atomic inline with a large negative +/// `margin-top`) is still content: the strut applies and the line keeps its height. +#[test] +fn line_with_negative_height_inline_box_is_not_invisible() { + let mut fcx = create_font_context(); + let mut lcx: LayoutContext = LayoutContext::new(); + let root = root_style(); + let mut builder = lcx.tree_builder(&mut fcx, 1., false, &root); + builder.push_inline_box(InlineBox { + id: 0, + kind: InlineBoxKind::InFlow, + index: 0, + width: 10., + height: -300., + baseline: Some(-305.), + vertical_align: VerticalAlign::BASELINE, + }); + let (mut layout, _) = builder.build(); + layout.break_all_lines(None); + let line = layout.lines().next().unwrap(); + assert_eq!(line.metrics().line_height, 30.); + let inline_box = line + .items() + .find_map(|item| match item { + crate::PositionedLayoutItem::InlineBox(inline_box) => Some(inline_box), + _ => None, + }) + .unwrap(); + // The box's own baseline coincides with the line's baseline. + assert_eq!( + inline_box.y + inline_box.baseline.unwrap(), + line.metrics().baseline + ); +} + +/// No-break spaces are neither collapsible nor hangable (css-text-3 §4.1.1, §4.1.3), so a +/// line of only NBSPs keeps its width and never wraps at its own max-content width. +#[test] +fn no_break_spaces_are_not_hanging_whitespace() { + let mut fcx = create_font_context(); + let mut lcx: LayoutContext = LayoutContext::new(); + let root = root_style(); + let mut builder = lcx.tree_builder(&mut fcx, 1., false, &root); + builder.push_text("\u{a0}\u{a0}\u{a0}"); + let (mut layout, _) = builder.build(); + let content_widths = layout.calculate_content_widths(); + assert!(content_widths.max > 0.); + assert_eq!(content_widths.min, content_widths.max); + layout.break_all_lines(Some(content_widths.max)); + assert_eq!(layout.len(), 1); + let line = layout.lines().next().unwrap(); + assert_eq!(line.metrics().advance, content_widths.max); +}