mirror of
https://github.com/lexmount/moli.git
synced 2026-10-16 16:01:29 +00:00
fix(layout): exclude collapsed line-end spaces from overflow
This commit is contained in:
@@ -576,10 +576,28 @@ pub(crate) fn build_inline_fragments(
|
||||
|
||||
for (line_index, line) in layout.lines().enumerate() {
|
||||
let metrics = line.metrics();
|
||||
let line_range = line.text_range();
|
||||
let trailing_start = overlapping_output_ranges(&context.text_units, &line_range)
|
||||
.iter()
|
||||
.rev()
|
||||
.take_while(|unit| unit.control || unit.collapsible_whitespace)
|
||||
.filter(|unit| unit.collapsible_whitespace)
|
||||
.map(|unit| unit.output_range.start)
|
||||
.last();
|
||||
let mut collapsed_trailing_advance = 0.0;
|
||||
if let Some(start) = trailing_start {
|
||||
for run in line.runs() {
|
||||
for cluster in run.visual_clusters() {
|
||||
if cluster.text_range().start >= start {
|
||||
collapsed_trailing_advance += cluster.advance().max(0.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let placement = line_placements
|
||||
.get(line_index)
|
||||
.filter(|placement| placement.line_index == line_index);
|
||||
let line_rect = placement.map_or_else(
|
||||
let mut line_rect = placement.map_or_else(
|
||||
|| {
|
||||
PaintRect::new(
|
||||
metrics.inline_min_coord + metrics.offset,
|
||||
@@ -590,6 +608,14 @@ pub(crate) fn build_inline_fragments(
|
||||
},
|
||||
|placement| placement.rect,
|
||||
);
|
||||
// Parley retains hanging spaces for shaping/line breaking. CSS
|
||||
// collapsed line-end spaces contribute neither range/inline-box
|
||||
// geometry nor scrollable overflow. Do not trim preserved or NBSP
|
||||
// advances merely because Parley classifies them as whitespace.
|
||||
line_rect.width = (line_rect.width - collapsed_trailing_advance).max(0.0);
|
||||
if layout.is_rtl() {
|
||||
line_rect.x += collapsed_trailing_advance;
|
||||
}
|
||||
fragments.lines.push(InlineLineFragment {
|
||||
line_index,
|
||||
rect: line_rect,
|
||||
@@ -614,6 +640,9 @@ pub(crate) fn build_inline_fragments(
|
||||
let run_metrics = run.font_metrics();
|
||||
for cluster in run.visual_clusters() {
|
||||
let range = cluster.text_range();
|
||||
if trailing_start.is_some_and(|start| range.start >= start) {
|
||||
continue;
|
||||
}
|
||||
let style_index = usize::from(cluster.style_index());
|
||||
let vertical_offset = placement.map_or(0.0, |placement| {
|
||||
placement.glyph_offset(run.index(), style_index)
|
||||
@@ -3415,6 +3444,40 @@ mod tests {
|
||||
normalizer.finish()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_collapsible_whitespaces_are_marked_for_line_end_removal() {
|
||||
let text = LayoutBoxId::from_index(1);
|
||||
for mode in [
|
||||
InlineWhiteSpaceCollapse::Collapse,
|
||||
InlineWhiteSpaceCollapse::PreserveBreaks,
|
||||
InlineWhiteSpaceCollapse::Preserve,
|
||||
InlineWhiteSpaceCollapse::BreakSpaces,
|
||||
] {
|
||||
let input = normalize(&[(text, "A \u{a0} B")], mode, InlineTextTransform::None);
|
||||
let collapsed = input
|
||||
.units
|
||||
.iter()
|
||||
.filter(|unit| unit.collapsible_whitespace)
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
collapsed.len(),
|
||||
if matches!(
|
||||
mode,
|
||||
InlineWhiteSpaceCollapse::Collapse | InlineWhiteSpaceCollapse::PreserveBreaks
|
||||
) {
|
||||
2
|
||||
} else {
|
||||
0
|
||||
}
|
||||
);
|
||||
assert!(
|
||||
collapsed
|
||||
.iter()
|
||||
.all(|unit| &input.text[unit.output_range.clone()] == " ")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preserve_merges_crlf_across_adjacent_text_nodes_with_both_origins() {
|
||||
let first = LayoutBoxId::from_index(1);
|
||||
@@ -3510,7 +3573,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn collapsed_spaces_remain_in_dom_order_across_inline_boundaries() {
|
||||
fn collapsible_whitespaces_remain_in_dom_order_across_inline_boundaries() {
|
||||
let root = LayoutBoxId::from_index(0);
|
||||
let first_inline = LayoutBoxId::from_index(1);
|
||||
let first_text = LayoutBoxId::from_index(2);
|
||||
|
||||
@@ -4349,3 +4349,68 @@ fn transformed_fixed_containing_block_still_clips_its_fixed_descendant() {
|
||||
"the fixed containing block's overflow clip must still apply"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inline_scroll_overflow_keeps_unbreakable_text_and_nbsp() {
|
||||
for text in ["abcdefghijklmno", "alpha\u{a0}beta\u{a0}gamma"] {
|
||||
let source = Source(vec![
|
||||
Node::element("root", vec![1]),
|
||||
Node::element("scroller", vec![2]),
|
||||
Node::element("fixed-font", vec![3]),
|
||||
Node::text("text", text),
|
||||
]);
|
||||
let (font_style, mut services) = fixed_inline_font();
|
||||
let mut styles = Styles::default();
|
||||
styles
|
||||
.0
|
||||
.insert(0, fixed_size(LayoutDisplay::Block, 320.0, 240.0));
|
||||
styles.0.insert(
|
||||
1,
|
||||
resolved(
|
||||
LayoutDisplay::Block,
|
||||
Style {
|
||||
size: Size {
|
||||
width: length(85.0),
|
||||
height: length(40.0),
|
||||
},
|
||||
overflow: Point {
|
||||
x: Overflow::Scroll,
|
||||
y: Overflow::Scroll,
|
||||
},
|
||||
..Style::default()
|
||||
},
|
||||
),
|
||||
);
|
||||
styles.0.insert(2, font_style);
|
||||
let output = build_layout_pass(
|
||||
&source,
|
||||
&mut styles,
|
||||
&mut services,
|
||||
LayoutPassRequest::new(LayoutViewport::new(320, 240, 1.0), LayoutFlushReason::Test),
|
||||
)
|
||||
.unwrap();
|
||||
let scroller = output.source_output(1).unwrap().principal_box.unwrap();
|
||||
assert!(
|
||||
output
|
||||
.scroll_extent(scroller)
|
||||
.unwrap()
|
||||
.horizontal_scrollbar
|
||||
.is_some(),
|
||||
"{text}"
|
||||
);
|
||||
assert!(
|
||||
output
|
||||
.element_metrics_for_source(1)
|
||||
.unwrap()
|
||||
.scroll_size
|
||||
.width
|
||||
> 85.0,
|
||||
"{text}"
|
||||
);
|
||||
assert!(
|
||||
!output
|
||||
.text_range_rects(3, 0..text.encode_utf16().count())
|
||||
.is_empty()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user