diff --git a/crates/layout/src/display.rs b/crates/layout/src/display.rs index e271c7fab..c86b08b3e 100644 --- a/crates/layout/src/display.rs +++ b/crates/layout/src/display.rs @@ -668,7 +668,7 @@ fn lines( char_box(&ob, x0, x1, top, bottom, alpha, out); } // Label. - if li == 0 + if li == pl.label_line() && let Some(lab) = &pl.label { let gx = x + lab.x; diff --git a/crates/layout/src/para.rs b/crates/layout/src/para.rs index ffc1093ae..7d119b177 100644 --- a/crates/layout/src/para.rs +++ b/crates/layout/src/para.rs @@ -2484,6 +2484,16 @@ fn resolve_tab(pl: &mut ParaLayout, pending: &mut Option<(usize, TabStop, f32)>, } impl ParaLayout { + /// The line the list label is drawn on: the first, unless the paragraph opens with a page + /// break, when Word draws it with the text on the next page. + pub fn label_line(&self) -> usize { + let opens_with_break = self.lines.len() > 1 + && self.lines.first().is_some_and(|l| { + l.end == LineEnd::PageBreak + && self.clusters.get(l.c0..l.c1).is_some_and(|cs| cs.iter().all(|c| matches!(c.kind, ClKind::PageBreak | ClKind::Marker))) + }); + usize::from(opens_with_break) + } /// x of byte offset `off` within line `li` (clamped to the line). pub fn x_of(&self, li: usize, off: usize) -> Option { let l = self.lines.get(li)?; diff --git a/crates/layout/src/tests.rs b/crates/layout/src/tests.rs index fb8a78579..b64d8e83f 100644 --- a/crates/layout/src/tests.rs +++ b/crates/layout/src/tests.rs @@ -810,6 +810,26 @@ fn lists_get_labels_and_indent() { assert!((x - 108.0).abs() < 0.5, "{x}"); } +#[test] +fn a_numbered_heading_opening_with_a_page_break_takes_its_number_along() { + // "Before", then a numbered paragraph whose text opens with a page break: Word draws its + // number on the next page with the text, not alone at the bottom of the first. + let mut d = Document::from_text("Before\n\u{000C}Heading"); + let id = d.numbering.add_list(wordcraft_doc::ListKind::Numbered); + d.format_paragraphs(&Pos::body(1, 0), &Pos::body(1, 1), &|p| p.numbering = Some(wordcraft_doc::props::NumRef { num: id, level: 0 })).unwrap(); + let l = lay(&d); + let label_pages: Vec = l + .pages + .iter() + .enumerate() + .filter(|(_, pg)| { + flat(display::page_display(&d, pg, &Default::default())).iter().any(|x| matches!(x, display::Draw::Glyphs { text, .. } if text == "1.")) + }) + .map(|(i, _)| i) + .collect(); + assert_eq!(label_pages, [1]); +} + #[test] fn russian_letter_list_labels_draw_cyrillic() { // russianLower labelled items 1, 2, 3 instead of а), б), в). diff --git a/docs/layout-parity.md b/docs/layout-parity.md index 76a4e6478..ce416673d 100644 --- a/docs/layout-parity.md +++ b/docs/layout-parity.md @@ -1,6 +1,6 @@ # Layout and pagination parity with Microsoft Word -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-14 · **Change:** 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 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 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 | | 2026-10-14 | trivial | A paragraph of only a page break followed by a page break before, or carrying a next-page section break, starts one page, not two: the break's mark and its space after stay on the page before, as in Word | | 2026-10-13 | trivial | Justified vertical page alignment (#1320): the page's spare room goes equally between its blocks; a section's last page takes its own section's vertical alignment, not the next one's |