mirror of
https://github.com/storytold/wordcraft.git
synced 2026-10-27 03:47:09 -06:00
A table with repeated header rows that started near the bottom of a page left the header rows there alone when the first row after them didn't fit, then repeated them on the next page above that row. Word starts such a table on the next page. place_table now checks, before the first row, whether the row after the header rows can start under them (whole, or split when it may split). If it can't and the page already holds something, the table starts on the next page. A table at the top of a page keeps its header there as before. Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
dfgodvan
Claude Opus 5.5
parent
babb331528
commit
daf8caada3
@@ -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::<f32>();
|
||||
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)
|
||||
|
||||
@@ -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<Vec<usize>> {
|
||||
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() {
|
||||
|
||||
Reference in New Issue
Block a user