#3907: review — a kept paragraph low in a continuous section moves to the next page; a page break before a stacked mark keeps the new page's bottom

CodeRabbit (local): keep-lines compared its height with the room below the
mid-page section start, so a paragraph that fits on a new page split instead.
It now compares with the next column, or the next page's body when this is the
last column. A stacked mark whose page break before starts a page no longer
gets the previous page's bottom back.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CX1hMKX28764Lrx5moxs1o
This commit is contained in:
Martin Ball
2026-10-27 09:56:44 +00:00
co-authored by Claude Opus 5.5
parent 7757828295
commit f2fe7654e9
2 changed files with 35 additions and 2 deletions
+7 -2
View File
@@ -42,7 +42,10 @@ pub(crate) fn place_para(ctx: &mut Ctx, pb: &mut PageBuilder, p: &Paragraph, blo
let (y, prev, bottom, content_bottom, at) = (pb.y, pb.prev.clone(), pb.bottom, pb.content_bottom, (pb.pages.len(), pb.col));
pb.bottom = f32::MAX / 4.0;
place_para_in_flow(ctx, pb, p, block, body_top);
pb.bottom = bottom;
// A break before it starts a page with its own bottom.
if pb.pages.len() == at.0 {
pb.bottom = bottom;
}
if (pb.pages.len(), pb.col) == at && pb.y > bottom + 0.01 {
// At the bottom: nothing more fits on the page, even a line smaller than the mark's.
(pb.y, pb.prev, pb.content_bottom) = (bottom.max(y), prev, content_bottom);
@@ -176,8 +179,10 @@ fn place_para_in_flow(ctx: &mut Ctx, pb: &mut PageBuilder, p: &Paragraph, block:
if pl.rp.keep_lines || (pl.rp.keep_next && pb.y + height <= pb.bottom + 0.01) {
let need = height + if pl.rp.keep_next { next_first_line(ctx, block, width, 0, &mut ctx.counters.clone()) } else { 0.0 };
// (Against the whole column: balanced columns grow to hold it.)
// Room in the next column, or on the next page when this is the last.
let next_top = if pb.col + 1 < pb.cols.len() { pb.top } else { pb.page_top };
// With the 0.01 pt of slack the other fit checks take.
if pb.y + need > pb.bottom + 0.01 && !pb.fresh() && need <= pb.uncapped_bottom() - pb.top {
if pb.y + need > pb.bottom + 0.01 && !pb.fresh() && need <= pb.uncapped_bottom() - next_top {
pb.advance(block, body_top);
pb.y += pl.rp.space_before.min(0.0);
applied_before = pl.rp.space_before.min(0.0);
+28
View File
@@ -11681,6 +11681,34 @@ fn a_row_leaves_room_for_the_border_below_it() {
}
}
/// A kept-together paragraph starting a continuous section low on a page, with room there for
/// one of its lines, moves whole to the next page, where it fits (#3907 review).
#[test]
fn kept_lines_at_a_low_continuous_section_move_to_the_next_page() {
use wordcraft_doc::props::LineSpacing;
use wordcraft_doc::section::{SectionProps, SectionStart};
let fill = 53; // 53 lines of 12 pt leave one line of a 648 pt body
let text = format!("{}{}", "x\n".repeat(fill), "word ".repeat(40));
let mut d = Document::from_text(&text);
for i in 0..=fill {
let p = d.para_mut(StoryRef::Body, &Path::top(i)).unwrap();
p.props.line_spacing = Some(LineSpacing::Exactly(12.0));
p.props.space_before = Some(0.0);
p.props.space_after = Some(0.0);
}
d.para_mut(StoryRef::Body, &Path::top(fill - 1)).unwrap().section =
Some(Box::new(SectionProps { start: SectionStart::Continuous, ..Default::default() }));
d.last_section.start = SectionStart::Continuous;
let kept = d.para_mut(StoryRef::Body, &Path::top(fill)).unwrap();
kept.props.keep_lines = Some(true);
kept.props.widow_control = Some(false);
let l = lay(&d);
let last_x = l.caret(&Pos::body(fill - 1, 0)).unwrap();
let first = l.caret(&Pos::body(fill, 0)).unwrap();
assert_eq!(last_x.page, 0, "the filler fits on page 1: {last_x:?}");
assert_eq!(first.page, 1, "kept paragraph moves whole: {first:?}");
}
/// Sections starting continuously one after another still go onto new pages: a paragraph that
/// doesn't fit below the text above it moves on rather than running off the page (#3907). A
/// hundred one-line sections lay out like a hundred plain paragraphs.