diff --git a/crates/layout/src/lib.rs b/crates/layout/src/lib.rs index 596d5e7b3..a7decb564 100644 --- a/crates/layout/src/lib.rs +++ b/crates/layout/src/lib.rs @@ -3678,6 +3678,19 @@ fn place_table(ctx: &mut Ctx, pb: &mut PageBuilder, t: &wordcraft_doc::Table, bl pb.y += row.height; pb.content_bottom = pb.content_bottom.max(pb.y); }; + // Like Word, repeated header rows don't stay alone at the bottom of a page: when the first row + // after them can't start under them there, the table starts on the next page. + let first = header_rows.len(); + if !header_rows.is_empty() + && !pb.at_top() + && let Some(row) = tl.rows.get(first) + { + let room = pb.bottom - pb.y - tl.rows.iter().take(first).map(|r| r.height).sum::(); + let splittable = !t.rows.get(first).is_some_and(|r| r.props.cant_split); + if row.height > room + 0.01 && !(splittable && table::split_row(row, room).is_some()) { + pb.advance(block, body_top); + } + } for (ri, row) in tl.rows.iter().enumerate() { // Word never splits a row of an exact height: it moves on whole. let splittable = !header_rows.contains(&ri) diff --git a/crates/layout/src/tests.rs b/crates/layout/src/tests.rs index 2670952c8..fad17f1ed 100644 --- a/crates/layout/src/tests.rs +++ b/crates/layout/src/tests.rs @@ -2474,6 +2474,32 @@ fn repeated_header_is_followed_by_the_next_row_even_when_it_overflows() { assert!(pages.iter().all(|p| p.first() == Some(&0)), "{pages:?}"); } +#[test] +fn repeated_header_moves_with_the_first_row_that_does_not_fit() { + // A paragraph that leaves `room` points of the letter page's 648 pt body, then a table with a + // 20 pt header row and 40 pt one-line rows. Returns the table rows on each page. + let rows_per_page = |room: f32| -> Vec> { + let mut d = Document::from_text("Text above the table"); + d.para_mut(StoryRef::Body, &Path::top(0)).unwrap().props.space_after = Some(648.0 - room - 14.0); + let mut t = Table::new(3, 1, 468.0); + t.rows[0].props.header = true; + for (row, h) in t.rows.iter_mut().zip([20.0, 40.0, 40.0]) { + row.props.height = Some(h); + row.props.height_rule = wordcraft_doc::props::HeightRule::AtLeast; + } + d.insert_block(StoryRef::Body, &Path::top(1), wordcraft_doc::Block::Table(t)).unwrap(); + lay(&d) + .pages + .iter() + .map(|p| p.items.iter().filter_map(|i| if let Placed::Cell { row, cell: 0, .. } = i { Some(*row) } else { None }).collect()) + .collect() + }; + // Header and first row fit: the table starts on the first page. + assert_eq!(rows_per_page(80.0)[0], vec![0, 1]); + // Only the header fits: it goes to the next page with the first row instead of staying alone. + assert_eq!(rows_per_page(40.0), vec![vec![], vec![0, 1, 2]]); +} + /// A drop-cap paragraph in a font larger than 1000 pt used to panic in layout (`clamp` with min > max). #[test] fn drop_cap_with_a_huge_font_lays_out() {