diff --git a/ROADMAP.md b/ROADMAP.md index 2b9fc164e..70f36e5f9 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -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) | diff --git a/crates/layout/src/para.rs b/crates/layout/src/para.rs index 41f9fadf0..5dac2efca 100644 --- a/crates/layout/src/para.rs +++ b/crates/layout/src/para.rs @@ -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. diff --git a/crates/layout/src/tests_bidi.rs b/crates/layout/src/tests_bidi.rs index 9fa922974..5d20141f6 100644 --- a/crates/layout/src/tests_bidi.rs +++ b/crates/layout/src/tests_bidi.rs @@ -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}"); + } + } +} diff --git a/docs/layout-parity.md b/docs/layout-parity.md index 2a8bc2bf2..116a56d86 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-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 |