#2617: centre equation-array rows without & (#2690)

* #2617: centre equation-array rows without &

A row with no alignment mark sat in the first column and was right-aligned
to it, widening that column and pushing the aligned rows right (Word's
"First Property of Radicals" case equation). Word centres such a row in the
array; it no longer widens the aligned columns.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* #2617: review — test a lone eq-array row wider than the aligned columns

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012WEhZXSxzS8orX7f9J3qpr

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
This commit is contained in:
Martin Ball
2026-10-21 20:04:57 -04:00
committed by GitHub
co-authored by Claude Opus 5.5 aiongg
parent 4913b20259
commit dbebb2eca7
3 changed files with 47 additions and 16 deletions
+1
View File
@@ -147,6 +147,7 @@ Imported Word 97–2003 `.doc`/`.dot` files now ask for a supported DOCX target
| 2026-10-18 | Long navigation headings wrap onto further lines inside the resizable pane instead of ending in an ellipsis: the row grows with the text and the pane keeps its width (#759, #3015). |
| 2026-10-18 | Clearing a font query with no matches keeps Recently Used Fonts visible at the top of a scrollable list. The initial scroll position no longer skips its heading; measured keyboard scrolling is unchanged (#2068). |
| 2026-10-17 | Edit Recipient List finds records (#1576): a Find in list box hides rows with no cell containing the text (any case) and counts the matches, and "Show only records that will be merged" hides the rows the filter leaves out; both only change the view. Rows are striped, and edits and removes go to the right record however the grid is narrowed |
| 2026-10-17 | Equation arrays centre a row that has no `&` among rows that do, as Word does, so case equations like Word's "First Property of Radicals" no longer push the aligned row right (#2617). |
| 2026-10-16 | English dialects (#1281, #1045): English (UK), (Australia), (Canada) and the other Englishes are proofed in their own spelling — *colour* is flagged in a US document and *color* in a British one, with the right form suggested first — from our own list of American/British spellings, so nothing to install. Review › Language lists them, the .docx `w:lang` keeps them, and an English interface on a British (or Canadian, Australian…) system starts new documents in that English |
| 2026-10-15 | Triple borders (tables, paragraphs, the `###` AutoFormat line) draw as three lines on screen and in PDF, across the line for vertical borders too; they used to draw as double lines (#1740) |
| 2026-10-14 | DOCX row table border exceptions render and survive saving. Explicit cell borders win; a row exception does not change other rows. |
+43 -14
View File
@@ -1509,38 +1509,37 @@ impl Ctx {
segs_rows.push(segs.into_iter().map(hcat).collect());
}
let ncols = segs_rows.iter().map(|r| r.len()).max().unwrap_or(1);
// Rows without `&` are centred below, so they don't widen the aligned columns.
let mut col_w = vec![0.0f32; ncols];
let mut lone_w = 0.0f32;
for r in &segs_rows {
if let [b] = r.as_slice() {
lone_w = lone_w.max(b.w);
continue;
}
for (i, b) in r.iter().enumerate() {
if let Some(c) = col_w.get_mut(i) {
*c = c.max(b.w);
}
}
}
let total: f32 = col_w.iter().sum();
let cols: f32 = col_w.iter().sum();
let total = cols.max(lone_w);
let mut placed = Vec::new();
for (r, path) in segs_rows.into_iter().zip(row_paths) {
let mut row = Bx::default();
let mut x = 0.0f32;
let single = r.len() == 1 && ncols > 1;
// A row without `&` is centred in the array, as Word does (#2617).
let single = r.len() == 1;
for (i, b) in r.into_iter().enumerate() {
let cw = col_w.get(i).copied().unwrap_or(0.0);
let bw = b.w;
let dx = if ncols == 1 {
0.0
} else if single {
// A row without `&` aligns with the first column's right edge.
cw - bw
} else if i % 2 == 0 {
cw - bw
} else {
0.0
};
let dx = if single || i % 2 == 1 { 0.0 } else { cw - bw };
row.put(b, x + dx, 0.0);
x += cw;
x += if single { bw } else { cw };
row.w = row.w.max(x);
}
let rx = if ncols == 1 { (total - row.w) / 2.0 } else { 0.0 };
let rx = (total - if single { row.w } else { cols }) / 2.0;
finalize(&mut row, &path, em);
placed.push((row, rx.max(0.0)));
}
@@ -2006,6 +2005,36 @@ mod tests {
layout_equation(math, lin, &rc(), display, avail, mp, &mut 0)
}
#[test]
fn eq_array_rows_without_ampersand_are_centred() {
// Word centres a row with no `&` in the array (#2617: a case row "a, n odd" above
// "|a|, &n even"), whether or not the other rows have one.
use wordcraft_doc::math::*;
let run = |s: &str| vec![MNode::Run(MRun::new(s))];
let w_a = lay("a", true).width;
for other in ["bbbbbb&c", "bbbbbbc"] {
let math = Math { nodes: vec![MNode::EqArr { rows: vec![run("a"), run(other)] }], ..Default::default() };
let m = lay_in(&math, "", true, 400.0, &MathProps::default());
let ls = lines(&m);
let xa = ls.first().and_then(|l| l.1.first()).map(|g| g.0).unwrap_or(f32::NAN);
let x0 = ls.get(1).and_then(|l| l.1.first()).map(|g| g.0).unwrap_or(f32::NAN);
let want = x0 + (m.width - x0 - w_a) / 2.0;
assert!((xa - want).abs() < 1.0, "{other}: a at {xa}, want {want} ({ls:?}, width {})", m.width);
// The lone row doesn't widen the aligned columns.
assert!(x0.abs() < 0.5 && m.width < lay("bbbbbbc", true).width + 1.0, "{other}: {ls:?}, width {}", m.width);
}
// A lone row wider than the aligned columns sets the array's width; the aligned
// rows are centred under it as one block.
let math = Math { nodes: vec![MNode::EqArr { rows: vec![run("bbbbbbbbbb"), run("a&c")] }], ..Default::default() };
let m = lay_in(&math, "", true, 400.0, &MathProps::default());
let ls = lines(&m);
let xw = ls.first().and_then(|l| l.1.first()).map(|g| g.0).unwrap_or(f32::NAN);
let x0 = ls.get(1).and_then(|l| l.1.first()).map(|g| g.0).unwrap_or(f32::NAN);
assert!(xw.abs() < 0.5 && (m.width - lay("bbbbbbbbbb", true).width).abs() < 1.0, "wide lone row: {ls:?}, width {}", m.width);
let want = (m.width - w_a - lay("c", true).width) / 2.0;
assert!(want > 1.0 && (x0 - want).abs() < 1.0, "aligned row at {x0}, want {want} ({ls:?})");
}
#[test]
fn long_display_equations_wrap_before_operators_and_indent() {
let sum = "y=a+b+c+d+e+f+g+h+i+j+k+l+m+n+o+p+q+r+s+t+u+v+w";
+3 -2
View File
@@ -1,6 +1,6 @@
# Layout and pagination parity with Microsoft Word
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-18 · **Change:** trivial (shapes drawn with their gradients and transparency, #1500; previously trivial: paragraphs above a float's anchor wrap around it, also when that pushes the anchor onto the next page, #2048; previously trivial: triple borders draw three lines, #1740; previously trivial: the caret, clicks and selection edges fall between the letters of a ligature, #1974; previously trivial: a word after a slash wraps whole, like Word, #2963; previously trivial: VML header-cell floats always on the page, user-checked in Word for Mac, #1775; previously trivial: an at-least row with no room for its minimum starts the next page whole, #1742; previously trivial: AutoFit donors lose equal widths down to their content minima; previously trivial: automatic space between Asian text and Latin text or digits, #2358; previously trivial: hint-reliant CJK outlines; previously trivial: positional tabs go to the margins or indents, #2367; previously trivial: field results with right-to-left text read right to left, #1670; previously trivial: a list bullet no installed font can draw shows the level's default bullet, #1683; previously trivial: a missing Thai font is drawn in one Thai face for the whole run, #1549; previously trivial: the caret after spaces hanging past the margin stays at the margin, #2146; previously trivial: covered black-circle fallback before UI sans; previously trivial: resolved split-cell continuation top insets; previously trivial: Symbol PCL6 alias uses Symbol encoding; previously trivial: exact full font names select their installed face; previously trivial: Arial Unicode MS uses proportional fallbacks; previously trivial: DrawingML outline dashes, including zero-width paths; previously trivial: TOCs include table-cell headings and retain full targets; previously trivial: kept table rows follow the next row; previously trivial: consistent cell widths override a stale table grid; previously trivial: multi-paragraph field instructions no longer add blank lines; previously trivial: page-break-only list paragraphs hide their labels; previously trivial: floating-table overlap ignores empty line-break lines; previously trivial: paragraph spacing adds up when the document turns HTML auto spacing off; previously trivial: an inherited footer sizes its percentage tables by its own section; previously trivial: an empty section mark stays on a full page; previously trivial: a fitting row with less room than its minimum height moves on whole; oversized rows still split; previously trivial: text past an exact-height row is cut off; previously 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
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-18 · **Change:** trivial (an equation-array row without `&` is centred, #2617; previously trivial: shapes drawn with their gradients and transparency, #1500; previously trivial: paragraphs above a float's anchor wrap around it, also when that pushes the anchor onto the next page, #2048; previously trivial: triple borders draw three lines, #1740; previously trivial: the caret, clicks and selection edges fall between the letters of a ligature, #1974; previously trivial: a word after a slash wraps whole, like Word, #2963; previously trivial: VML header-cell floats always on the page, user-checked in Word for Mac, #1775; previously trivial: an at-least row with no room for its minimum starts the next page whole, #1742; previously trivial: AutoFit donors lose equal widths down to their content minima; previously trivial: automatic space between Asian text and Latin text or digits, #2358; previously trivial: hint-reliant CJK outlines; previously trivial: positional tabs go to the margins or indents, #2367; previously trivial: field results with right-to-left text read right to left, #1670; previously trivial: a list bullet no installed font can draw shows the level's default bullet, #1683; previously trivial: a missing Thai font is drawn in one Thai face for the whole run, #1549; previously trivial: the caret after spaces hanging past the margin stays at the margin, #2146; previously trivial: covered black-circle fallback before UI sans; previously trivial: resolved split-cell continuation top insets; previously trivial: Symbol PCL6 alias uses Symbol encoding; previously trivial: exact full font names select their installed face; previously trivial: Arial Unicode MS uses proportional fallbacks; previously trivial: DrawingML outline dashes, including zero-width paths; previously trivial: TOCs include table-cell headings and retain full targets; previously trivial: kept table rows follow the next row; previously trivial: consistent cell widths override a stale table grid; previously trivial: multi-paragraph field instructions no longer add blank lines; previously trivial: page-break-only list paragraphs hide their labels; previously trivial: floating-table overlap ignores empty line-break lines; previously trivial: paragraph spacing adds up when the document turns HTML auto spacing off; previously trivial: an inherited footer sizes its percentage tables by its own section; previously trivial: an empty section mark stays on a full page; previously trivial: a fitting row with less room than its minimum height moves on whole; oversized rows still split; previously trivial: text past an exact-height row is cut off; previously 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,
@@ -78,7 +78,7 @@ Exact line spacing keeps the run font baseline around tall equations; automatic
| Ruby / phonetic guide, enclose characters, combined characters | 🟡 | ruby (Phonetic Guide) landed (#288): ruby text over its base by alignment, line grows to fit, in PDF; enclose and combined characters missing; ruby not yet compared with Word page by page | 2–4 |
| Automatic space between Asian text and Latin text or digits (`w:autoSpaceDE`, `w:autoSpaceDN`) | 🟡 | #2358: a quarter of the Asian character's font size after the first of two touching characters, across runs (measured on Word's PDF of a Chinese document at 16 pt, as JIS X 4051 asks); not beside East Asian punctuation, Hangul, Latin punctuation or an existing space; a field is classed by the text it shows; none at a line's end, none on a character grid the paragraph snaps to (`layout::tests_autospace`). Not yet: justification widening the space as Word does, `autoSpaceLikeWord95` | 1–2 |
| Bidirectional paragraphs, mixed-direction lines; RTL tables and sections | 🟡 | #207; RTL tables (#362): grid columns from the right edge, start alignment and indent from the right margin, cell margins and left/right borders mirrored, merged and turned cells and nested tables included; RTL sections (#362): newspaper columns right to left with the line between mirrored. Right-hand page gutter (`w:rtlGutter`) missing | 1 |
| Equations: display layout, numbering, wrapping long display equations | ✅ | #191; long display equations break at top-level operators (`m:brkBin`, `m:brkBinSub`), manual breaks and `m:alnAt`, continuation lines indented by `m:wrapIndent` or set right (`m:wrapRight`) (#326); an n-ary operator, delimiter, accent or group character the math font lacks is drawn from a fallback font instead of a missing-glyph box (#1273) | — |
| Equations: display layout, numbering, wrapping long display equations | ✅ | #191; long display equations break at top-level operators (`m:brkBin`, `m:brkBinSub`), manual breaks and `m:alnAt`, continuation lines indented by `m:wrapIndent` or set right (`m:wrapRight`) (#326); an n-ary operator, delimiter, accent or group character the math font lacks is drawn from a fallback font instead of a missing-glyph box (#1273); an equation-array row without `&` is centred among aligned rows, as in Word (#2617) | — |
| Hidden text excluded from breaking and hyphenation | ✅ | #173 | — |
| Fields: PAGE, NUMPAGES, SECTIONPAGES, TOC page numbers | ✅ | #52; totals in the body need a second pass once the page count is known (#451); SECTIONPAGES includes shared continuous-section start pages | — |
| Compatibility modes (Word 2003/2007/2010 layout for old files: `w:compat` options) | 🟡 | `allowSpaceOfSameStyleInTable` supported in modes 11/12/14; mode 15 retains contextual suppression. Most other legacy compat options remain ignored | 10–20 |
@@ -256,6 +256,7 @@ Page-relative footer tables keep their physical wrap bounds without adding to fo
| 2026-10-17 | trivial | Footer page tables preserve text height and wrap intersecting body lines |
| 2026-10-17 | trivial | Non-overlapping body floating tables clear header tables; overlapping tables keep their requested position |
| 2026-10-16 | trivial | Synthetic regression confirms a fitting exact-height row containing overflowing pictures moves to the next page whole. Existing runtime behavior is unchanged. |
| 2026-10-17 | trivial | An equation-array row without `&` is centred among aligned rows (#2617) |
| 2026-10-15 | trivial | The caret, clicks and selection edges fall between the letters of a ligature (#1974) |
| 2026-10-15 | trivial | Triple borders draw three lines (screen and PDF), not two (#1740) |
| 2026-10-14 | trivial | Row table border exceptions draw without leaking into other rows |