Limit table row deferral to genuine widow splits (#2925)

This commit is contained in:
Fin Warman authored and GitHub committed 2026-10-18 17:11:52 -04:00
1 parent 61e565cd1c
commit d588e0b873
2 files changed
+71 -2

No files matched your search

+6 -2
View File
@@ -841,11 +841,15 @@ pub fn split_row(row: &RowLayout, cut: f32) -> Option<(RowLayout, RowLayout)> {
.collect();
// A cell that widow/orphan control leaves with nothing on this page, though its first line
// would fit there, takes the whole row on with it: Word doesn't leave the other cells behind
// beside an empty one (observed with synthetic documents).
// beside an empty one (observed with synthetic documents). Trailing space alone can move
// the last paragraph without deferring the whole row.
for (items, splits) in cells.iter().zip(&splits) {
let first = items.iter().zip(splits).find_map(|(it, &split)| text_lines(it).map(|t| (t, split)));
if let Some(((para, l0, _, y), split)) = first
if let Some(((para, l0, l1, y), split)) = first
&& split == l0
&& para.rp.widow_control
&& l1.saturating_sub(l0) > 1
&& crate::item_bottom(y, para, l0, l1).is_some_and(|bottom| !fits(bottom))
&& para.lines.get(l0).is_some_and(|l| fits(y + l.top - para.lines.get(l0).map_or(0.0, |f| f.top) + l.height))
{
return None;
+65
View File
@@ -2567,6 +2567,71 @@ fn a_cell_emptied_by_widow_control_takes_its_row_to_the_next_page() {
assert_eq!(pages_of(45, 4, true)[..2], [(0, 0), (1, 4)], "an orphan: the whole row moves");
}
// Use real paragraph and table layout to distinguish content from trailing spacing.
fn row_content_pieces(text: &str, props: ParaProps, room: f32) -> Vec<(usize, usize)> {
use wordcraft_doc::props::LineSpacing;
let exact = ParaProps { line_spacing: Some(LineSpacing::Exactly(14.0)), space_before: Some(0.0), space_after: Some(0.0), ..Default::default() };
let mut d = Document::from_text("Filler");
*para_props(&mut d, 0) = ParaProps { line_spacing: Some(LineSpacing::Exactly(d.last_section.text_height() - room)), ..exact.clone() };
let mut table = Table::new(1, 2, 400.0);
table.props.style = None;
table.props.cell_margins = Some([0.0; 4]);
let neighbor = ["First", "Second", "Third", "Fourth"].join(&wordcraft_doc::para::LINE_BREAK.to_string());
for (cell, text) in [text, neighbor.as_str()].into_iter().enumerate() {
let mut p = Paragraph::with_text(text, Default::default());
p.props = exact.clone();
if cell == 0 {
p.props.space_after = props.space_after;
p.props.widow_control = props.widow_control;
} else {
p.props.widow_control = Some(false);
}
table.rows[0].cells[cell].blocks = vec![wordcraft_doc::para_block(p)];
}
d.body.push(std::sync::Arc::new(Block::Table(table)));
lay(&d)
.pages
.iter()
.map(|page| {
let count = |cell| {
page.items
.iter()
.filter_map(|item| match item {
Placed::Lines { path, l0, l1, .. } if path.0 == [1, 0, cell, 0] => Some(l1 - l0),
_ => None,
})
.sum::<usize>()
};
(count(0), count(1))
})
.collect()
}
#[test]
fn row_split_ignores_afterspace() {
let two = ["Top", "Bottom"].join(&wordcraft_doc::para::LINE_BREAK.to_string());
for (text, room, expected) in [("Label", 18.0, vec![(0, 1), (1, 3)]), (two.as_str(), 32.0, vec![(0, 2), (2, 2)])] {
let props = ParaProps { space_after: Some(8.0), widow_control: Some(true), ..Default::default() };
assert_eq!(row_content_pieces(text, props, room), expected, "text={text:?}, room={room}");
}
}
#[test]
fn row_split_respects_widows() {
let two = ["Top", "Bottom"].join(&wordcraft_doc::para::LINE_BREAK.to_string());
let three = ["Top", "Middle", "Bottom"].join(&wordcraft_doc::para::LINE_BREAK.to_string());
for (text, widow, room, expected) in [
(two.as_str(), true, 18.0, vec![(0, 0), (2, 4)]),
(two.as_str(), false, 18.0, vec![(1, 1), (1, 3)]),
(three.as_str(), true, 32.0, vec![(0, 0), (3, 4)]),
(three.as_str(), false, 32.0, vec![(2, 2), (1, 2)]),
] {
let props = ParaProps { space_after: Some(0.0), widow_control: Some(widow), ..Default::default() };
assert_eq!(row_content_pieces(text, props, room), expected, "text={text:?}, widow={widow}, room={room}");
}
}
/// The cell's last paragraph needs its space after on the page too: a row ending in an empty
/// paragraph with 8pt after, whose text fits but whose space after doesn't, splits before it
/// (Word keeps the "Remarks:" line on the page) instead of moving whole.