Layout: a list label moves with its text when the paragraph is aligned (#1159) (#1163)

* Layout: a list label moves with its text when the paragraph is aligned (#1159)

Alignment shifted the text of a line but not the list label of the first line,
so a centred or end-aligned bullet or number stayed at the margin while its text
moved away. In Word the label stays attached to the start of the text. The
first line's label now moves by the same shift (not after a tab stop, where the
text before the tab stays too). Left-to-right and right-to-left paragraphs are
covered by one test.

* Docs: update the progress docs for this change

* Docs: put the new sentence in the WordCraft / evidence column

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

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
NetterSilver
2026-10-14 12:51:19 -04:00
committed by GitHub
co-authored by Claude Sonnet 5.5 Brandon Thomas
parent 7d3767ba99
commit 156029fe63
4 changed files with 56 additions and 2 deletions
+2
View File
@@ -151,6 +151,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm
| 2026-10-13 | Double-clicking the top or bottom margin of a page creates its header or footer when it has none and puts the caret in it, as in Word (#960): the one that page shows (different first page, odd and even pages), only in Print Layout; one left empty is taken away again when you leave it, so nothing empty is saved. `insert.editHeader`/`insert.editFooter` take `{"page"}` for agents |
| 2026-10-13 | Alt+numpad character codes, as in Word on Windows (#961): hold Alt, type a decimal code, release Alt — `0233` → é (Windows-1252), `233` → Θ (OEM code page 437; 850 in Western European locales and English outside the US), codes above 255 are Unicode (`8364` → €). Not on macOS; `text.typeCode` for agents |
| 2026-10-13 | Review › Language and the status bar's language menu offer Set Proofing Language… again (#860): the Language dialog marks text as French, German or any of 29 languages and Set As Default makes it the document's language; Language Preferences… opens Options › Language. Text in a language outside the short list shows by name in the status bar and checked in the menu. Spelling and grammar remain English, Polish and Portuguese; text in other languages gets no marks |
| 2026-10-12 | A list label moves with its text when the paragraph is centred or aligned to the end (it stayed at the margin), in left-to-right and right-to-left paragraphs (#1159) |
| 2026-10-12 | RTF import now uses the RTF spec's defaults where a file names none (#902): no space before or after paragraphs, single line spacing, and the `\deff` default font, instead of WordCraft's Normal style; RTF files that relied on the old defaults open tighter, like in Word |
| 2026-10-11 | Right-clicking a table cell offers the table's commands, and as in Word they follow the selection: with the caret in a cell Insert, Delete Cells, Split Cells, Text Direction and Table Properties…; with several cells Merge Cells instead of Split; with whole columns or rows Delete Columns or Rows, Merge Cells and Distribute Rows and Columns; with the whole table Delete Table, Distribute, AutoFit and Insert Caption…. A right-click beside selected cells moves the caret there instead of keeping them |
| 2026-10-11 | Drags, Shift+arrows and Shift+click over table cells select whole cells, as in Word: from one cell into another the rectangle between them (bold, alignment and Delete leave the other cells alone), into or out of a table whole rows; extending again goes on from where the selection started |
@@ -301,6 +302,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm
| 2026-10-13 | trivial | Progress log: interface audit back in CI (#1128) |
| 2026-10-13 | trivial | Progress log: double-click the margin to create and edit a header or footer (#960) |
| 2026-10-13 | trivial | Progress log: Set Proofing Language… in the Language menu (#860) |
| 2026-10-12 | trivial | Progress log: list label follows paragraph alignment (#1159) |
| 2026-10-12 | trivial | Progress log: Accept All and Reject All keep the right paragraph marks (#864) |
| 2026-10-13 | trivial | Progress log: equation pictures keep the theme colours on Windows (#1002) |
| 2026-10-11 | trivial | Progress log: Ctrl, Alt, left-edge and selected-cells table border drags (#711) |
+9
View File
@@ -1943,6 +1943,15 @@ fn break_lines(pl: &mut ParaLayout, env: &ParaEnv, mark_style: u16, level: Optio
for v in xs.iter_mut().skip(align_from - c0) {
*v += shift;
}
// The list label belongs to the start of the line: it moves with the text (Word keeps
// the bullet attached when a list item is centred or aligned to the end). After a tab
// stop in the line the label stays, as the text before the tab does.
if first
&& last_tab.is_none()
&& let Some(lab) = pl.label.as_mut()
{
lab.x += shift;
}
}
let justify = display.is_none() && ((rp.align == Align::Justify && end == LineEnd::Wrap) || rp.align == Align::Distribute);
// A line that only fits with shrunk spaces is shrunk, the last line of the paragraph too.
+42
View File
@@ -519,3 +519,45 @@ fn rtl_section_puts_its_first_column_on_the_right() {
page.items.iter().filter_map(|i| if let Placed::Rule { x0, x1, .. } = i { ((x0 - x1).abs() < 0.01).then_some(*x0) } else { None }).collect();
assert_eq!(sep, vec![72.0 + 234.0], "line between the columns");
}
#[test]
fn list_label_moves_with_its_text_when_the_paragraph_is_aligned() {
// Issue #1159: centring or end-aligning a list item moved the text and left the bullet at the
// margin. In Word the label stays attached to the start of the text, in both directions.
for (rtl, text) in [(false, "alpha beta"), (true, "مورد اول")] {
let mut d = doc_of(&[(text, rtl), (text, rtl), (text, rtl), (text, rtl)]);
let id = d.numbering.add_list(wordcraft_doc::ListKind::Bullet);
d.format_paragraphs(&Pos::body(0, 0), &Pos::body(3, 1), &|p: &mut ParaProps| {
p.numbering = Some(wordcraft_doc::props::NumRef { num: id, level: 0 })
})
.unwrap();
// Start edge (the default), centre, end edge, and a justified one.
for (i, a) in [(1, Align::Center), (2, Align::Right), (3, Align::Justify)] {
let at = Pos::body(i, 0);
d.format_paragraphs(&at, &at, &|p: &mut ParaProps| p.align = Some(a)).unwrap();
}
let l = lay(&d);
// How far the label's near edge is from where the text starts.
let gap = |i: u32| {
let (pl, x) = first_line(&l, i);
let lab = pl.label.as_ref().unwrap_or_else(|| panic!("no label: rtl={rtl} i={i} lines={}", pl.lines.len()));
let text_start = caret_x(&l, i as usize, 0);
if rtl { x + lab.x - text_start } else { text_start - (x + lab.x + lab.width) }
};
let g0 = gap(0);
assert!(g0 > 0.0 && g0 < 40.0, "rtl={rtl}: label then a tab, then text: {g0}");
for i in 1..4 {
assert!((gap(i) - g0).abs() < 0.5, "rtl={rtl}: paragraph {i}: label stays {g0} from the text, not {}", gap(i));
}
// The text really moved: further from the start edge when centred, furthest at the end.
let from_start = |i: u32| if rtl { 540.0 - caret_x(&l, i as usize, 0) } else { caret_x(&l, i as usize, 0) - 72.0 };
assert!(from_start(1) > from_start(0) + 5.0 && from_start(2) > from_start(1) + 5.0, "rtl={rtl}");
assert!((from_start(3) - from_start(0)).abs() < 0.5, "rtl={rtl}: justified text starts where the left-aligned one does");
// The label is inside the text area.
for i in 0..4 {
let (pl, x) = first_line(&l, i);
let lab = pl.label.as_ref().unwrap();
assert!(x + lab.x >= 71.5 && x + lab.x + lab.width <= 540.5, "rtl={rtl}: paragraph {i}");
}
}
}
+3 -2
View File
@@ -1,6 +1,6 @@
# Layout and pagination parity with Microsoft Word
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** 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-13 · **Change:** trivial (a list label moves with its first line when the paragraph is centred or end-aligned (#1159); 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,
@@ -35,7 +35,7 @@ font can't match Word's metrics.
| Drop caps | ✅ | 2026-10-06 | — |
| Columns, column breaks, separators, balancing | ✅ | Columns balance on a section's last page when the next section starts continuous (also at the document end, which Word only balances when the document ends with a continuous break); not after next-page/even/odd breaks or on a page with a column break; keep-together, keep-with-next, widow/orphan control and unsplittable rows can leave them slightly uneven (#350) | — |
| Section breaks (next page, continuous, even, odd), different first page, odd/even headers | ✅ | Section breaks don't show in Draft view (#42). A break inserted with text selected goes before the selection and keeps it, as in Word (#852) | 1–2 |
| List labels: number formats (decimal, letters, roman, ordinal text, Chinese/Japanese/Korean counting and legal, 一〇一 digits, 甲乙丙, ①…⑳, full-width), `%n` text such as `第%1章` / `(%1)`, legal numbering | ✅ | `doc::cjk_numbers` tests, `layout::tests::chinese_counting_list_labels_and_their_font_runs`; a label mixing faces (digit + CJK comma) is drawn run by run (#534). Hebrew, Arabic, Hindi, Thai, Vietnamese and Russian formats show decimal | — |
| List labels: number formats (decimal, letters, roman, ordinal text, Chinese/Japanese/Korean counting and legal, 一〇一 digits, 甲乙丙, ①…⑳, full-width), `%n` text such as `第%1章` / `(%1)`, legal numbering | ✅ | `doc::cjk_numbers` tests, `layout::tests::chinese_counting_list_labels_and_their_font_runs`; a label mixing faces (digit + CJK comma) is drawn run by run (#534). Hebrew, Arabic, Hindi, Thai, Vietnamese and Russian formats show decimal The label stays attached to the start of the text when the paragraph is centred or aligned to the end, as in Word (#1159). | — |
| Body top below a tall header, footer pushes body up | ✅ | #138; a footer taller than the bottom margin moves the body bottom up (#453), and a header or footer linked to the previous section takes its room too (#454) | — |
| Footnotes at the page bottom, endnotes at the end, note numbering | 🟡 | A long note continues onto the next page at a line boundary (its first line stays with the reference), first in that page's note area under a full-width continuation separator (#352); the document's own separator, continuation separator and continuation notice are drawn (the notice under a note that continues); on a page with footnotes a paragraph ending there keeps its space after above the separator, or its last lines go to the next page with the note (#1116); a long endnote continues on the next column or page at a line boundary (#456), and the caret finds note text on its continuation page (#457); per-section restart partial | 1–3 |
| Body top below a tall header, footer pushes body up | ✅ | #138 | — |
@@ -81,6 +81,7 @@ font can't match Word's metrics.
|---|---|---|
| 2026-10-13 | trivial | A shape or picture dragged into another column anchors there, so the text in that column wraps around it (#884) |
| 2026-10-13 | trivial | On a page with footnotes, a paragraph that ends there keeps its space after above the footnote separator; if it doesn't fit, its last line (and, with widow control, one more) goes to the next page with the note. Pages without notes are unchanged (#1116) |
| 2026-10-12 | trivial | a list label moves with its first line when the paragraph is centred or end-aligned (#1159) |
| 2026-10-12 | trivial | Spaces, tabs and ideographic spaces don't count toward a line's height, as in Word (a line of only them takes the paragraph mark's) |
| 2026-10-12 | trivial | A list label's font raises the first line's ascent but never its descent (Word keeps the text's descent below the baseline) |
| 2026-10-12 | trivial | Hidden end-of-cell mark (`w:hideMark`): a cell's empty last paragraph takes no room, spacing and all; a row of only such empty cells is just its borders |