Tables: a row with less room than its minimum height moves on whole (#1618)

* Tables: a row with less room than its minimum height moves on whole

Measured in Word with about 49 pt left on a page: a table row of eight
lines splits there, but the same row with an at-least height of 63.6 pt
goes to the next page whole, and so does one holding a nested table.
Without the height both split, as before. WordCraft split them all, so a
report's title row (a logo beside a small nested table) started at the
foot of its cover page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Tables: preserve splitting for oversized minimum-height rows

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
Fin Warman
2026-10-14 20:19:27 -04:00
committed by GitHub
co-authored by Claude Opus 5.5 Brandon Thomas
parent fee9b984eb
commit 1b60773196
3 changed files with 39 additions and 4 deletions
+9 -1
View File
@@ -3630,7 +3630,15 @@ fn place_table(ctx: &mut Ctx, pb: &mut PageBuilder, t: &wordcraft_doc::Table, bl
if pb.y + cur.height <= pb.bottom + 0.01 {
break;
}
let split = if splittable { table::split_row(cur, pb.bottom - pb.y) } else { None };
// Measured in Word: a row with an at-least height doesn't split where less than that
// height is left; it starts the next page whole.
let room = pb.bottom - pb.y;
let short_of_min = rest.is_none()
&& t.rows.get(ri).is_some_and(|r| {
r.props.height_rule == wordcraft_doc::props::HeightRule::AtLeast
&& r.props.height.is_some_and(|h| h > room + 0.01 && h <= pb.bottom - body_top + 0.01)
});
let split = if splittable && !short_of_min { table::split_row(cur, room) } else { None };
match split {
Some((a, b)) => {
place(pb, &a);
+28 -2
View File
@@ -2160,7 +2160,8 @@ fn split_row_cells_each_continue_at_the_top() {
}
/// Word never splits a row of an exact height: one the page bottom cuts through, its text
/// included, moves to the next page whole (an "at least" row of that height splits).
/// included, moves to the next page whole. An at-least row also moves when the remaining
/// room is less than its minimum height.
#[test]
fn exact_rows_move_on_whole() {
let text = "Dotted leader lines to fill in by hand later on. ".repeat(14);
@@ -2193,7 +2194,8 @@ fn exact_rows_move_on_whole() {
assert_eq!(exact.len(), 2);
assert_eq!(exact[0], 0, "the exact row moves on whole: {exact:?}");
let at_least = lay_row(wordcraft_doc::props::HeightRule::AtLeast);
assert!(at_least[0] > 0 && at_least.get(1).is_some_and(|n| *n > 0), "an at-least row splits: {at_least:?}");
assert_eq!(at_least[0], 0, "less than the minimum height remains: {at_least:?}");
assert!(at_least.get(1).is_some_and(|n| *n > 0), "the at-least row moves: {at_least:?}");
}
/// A nested table's cell the page bottom cuts through goes on at the top of the next page with
@@ -4145,6 +4147,30 @@ fn autofit_handles_spanning_cells_long_words_and_empty_tables() {
assert!(x > 72.0 + 200.0, "the second 50% column starts at {x}");
}
#[test]
fn a_row_with_less_room_than_its_minimum_height_moves_on_whole() {
use wordcraft_doc::props::HeightRule;
// Measured in Word: with 49 pt left on the page, a row of eight lines splits, but the same row
// with an at-least height of 63.6 pt goes to the next page whole.
let first_row_line_page = |min: Option<f32>| {
let lines: Vec<String> = (1..=200).map(|i| format!("Filler {i}")).collect();
let one_page = lay(&Document::from_text(&lines.join("\n"))).pages[0].items.iter().filter(|i| matches!(i, Placed::Lines { .. })).count();
let mut t = Table::new(1, 1, 400.0);
t.rows[0].props.height = min;
t.rows[0].props.height_rule = HeightRule::AtLeast;
t.rows[0].cells[0].blocks =
(1..=8).map(|k| wordcraft_doc::para_block(wordcraft_doc::Paragraph::with_text(&format!("Row line {k}"), Default::default()))).collect();
// Fill the page, leaving two filler paragraphs' room (less than 63.6 pt) for the row.
let mut d = Document::from_text(&lines[..one_page - 2].join("\n"));
let end = d.body.len();
d.insert_block(StoryRef::Body, &Path::top(end), wordcraft_doc::Block::Table(t)).unwrap();
let l = lay(&d);
l.pages.iter().position(|p| p.items.iter().any(|i| matches!(i, Placed::Lines { path, .. } if path.0.first() == Some(&(end as u32)))))
};
assert_eq!(first_row_line_page(None), Some(0), "a row without a minimum height splits");
assert_eq!(first_row_line_page(Some(63.6)), Some(1), "a row with a minimum height that doesn't fit moves on");
}
#[test]
fn text_overflowing_an_exact_row_is_clipped_to_it() {
use wordcraft_doc::props::HeightRule;
+2 -1
View File
@@ -1,6 +1,6 @@
# Layout and pagination parity with Microsoft Word
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-14 · **Change:** trivial (text past an exact-height row is cut off; previously trivial: a list number stays with text after a leading page break; previously trivial: a bookmark after a page break takes no line; previously trivial: a page break followed by a page break before or a next-page section starts one page; previously trivial: All Caps and Small Caps draw ß as SS, #1581; previously trivial: justified vertical page alignment, #1320; previously trivial: page numbers restart at 0; previously trivial: a list label moves with its first line when the paragraph is centred or end-aligned (#1159); previously trivial: PAGE fields in a paragraph split across pages show the page they are on, #1352; previously trivial: a dragged floating object anchors in the column it lands in, #884; previously trivial: a paragraph ending on a page with footnotes keeps its space after above the separator, #1116; previously trivial: symbol-font text drawn as Unicode symbols, #776; previously trivial: large-table scrolling measured and fixed, #811; previously trivial: text boxes use their own margins; Resize shape to fit text never cuts text, #743; previously trivial: complex-script shaping and Myanmar line breaking, #604; previously trivial: body page totals, table-cell footnotes and bookmark link pages fixed, #450 #451 #455; previously major: first version; line breaking, pagination and object placement checked against Word's behaviour from the layout source on origin/main) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-14 · **Change:** trivial (a fitting row with less room than its minimum height moves on whole; oversized rows still split; previously trivial: text past an exact-height row is cut off; previously trivial: a list number stays with text after a leading page break; previously trivial: a bookmark after a page break takes no line; previously trivial: a page break followed by a page break before or a next-page section starts one page; previously trivial: All Caps and Small Caps draw ß as SS, #1581; previously trivial: justified vertical page alignment, #1320; previously trivial: page numbers restart at 0; previously trivial: a list label moves with its first line when the paragraph is centred or end-aligned (#1159); previously trivial: PAGE fields in a paragraph split across pages show the page they are on, #1352; previously trivial: a dragged floating object anchors in the column it lands in, #884; previously trivial: a paragraph ending on a page with footnotes keeps its space after above the separator, #1116; previously trivial: symbol-font text drawn as Unicode symbols, #776; previously trivial: large-table scrolling measured and fixed, #811; previously trivial: text boxes use their own margins; Resize shape to fit text never cuts text, #743; previously trivial: complex-script shaping and Myanmar line breaking, #604; previously trivial: body page totals, table-cell footnotes and bookmark link pages fixed, #450 #451 #455; previously major: first version; line breaking, pagination and object placement checked against Word's behaviour from the layout source on origin/main) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4
A document only "opens correctly" if its pages break where Word breaks them: the same lines, the
same page count, footnotes and pictures on the same pages. This checklist covers line breaking,
@@ -159,6 +159,7 @@ No overall parity estimate changes from these probes.
| Date | Change | Summary |
|---|---|---|
| 2026-10-14 | trivial | A table row with an at-least height doesn't split where less than that height is left on the page; it starts the next page whole, as measured in Word |
| 2026-10-14 | trivial | Text that doesn't fit an exact-height table row is cut off at the cell's edge, as in Word, instead of drawn over the rows below |
| 2026-10-14 | trivial | A numbered paragraph whose text opens with a page break draws its number on the next page with the text, as Word does |
| 2026-10-14 | trivial | A bookmark (or other zero-width marker) after a paragraph's closing page break stays with the break instead of taking a line at the top of the next page |