Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion parley/src/layout/data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,7 @@ impl<B: Brush> LayoutData<B> {
#[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)
}

Expand Down
12 changes: 6 additions & 6 deletions parley/src/layout/line_break.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
79 changes: 79 additions & 0 deletions parley/src/tests/test_style_metrics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ColorBrush> = 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<ColorBrush> = 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<ColorBrush> = 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);
}