mirror of
https://github.com/storytold/wordcraft.git
synced 2026-10-24 10:27:44 -06:00
Layout: preserve equation baselines under exact line spacing (#2435)
* Layout: preserve equation baselines under exact line spacing (task 243) * layout-parity: keep the cell-margin inheritance note on the Tables row --------- Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
co-authored by
Brandon Thomas
parent
4cb4b2d231
commit
57dfacfc07
@@ -162,6 +162,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm
|
||||
| 2026-10-14 | DOCX row table border exceptions render and survive saving. Explicit cell borders win; a row exception does not change other rows. |
|
||||
| 2026-10-14 | Tab stops edited on the ruler: the corner selector picks Left/Center/Right/Decimal/Bar, a click sets a stop (Alt skips the snap), dragging moves it with a guide line and position tooltip, dragging off the ruler clears it; markers draw per kind and mirror for RTL; one `para.tabs` call and one undo step per gesture |
|
||||
| 2026-10-12 | Fields keep `w:dirty` through DOCX saves (#877): a begin marker flagged by a generator that couldn't compute a result (pandoc-style TOC placeholders) keeps the flag on open/save — as a buffered field or a range field — so Word still refreshes it on open; computing a result (Update Field, Update Table) clears it, like Word's F9 |
|
||||
| 2026-10-15 | Exact line spacing keeps the run font baseline around tall equations; automatic and minimum spacing still grow for math. |
|
||||
|---|---|
|
||||
| 2026-10-15 | The font family caches are bounded (4096 families for text, 1024 for math, names over 256 bytes aren't remembered), so a document naming thousands of missing fonts can't grow memory; equation fonts are looked up again after a font is added or removed instead of keeping the old choice (#1354) |
|
||||
| 2026-10-15 | AutoCorrect recognizes `-->` as `→` (#2223), respecting replacement settings and undo without changing paragraph-start arrow bullets. |
|
||||
@@ -506,6 +507,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm
|
||||
| 2026-10-15 | trivial | Progress log: plain-text paste keeps the paragraph formatting where it lands |
|
||||
| 2026-10-14 | trivial | Progress log: Add Font imports every selected font file (#1789) |
|
||||
| 2026-10-15 | trivial | Additive paragraph spacing retains the full before-space after a section page break; ordinary overlapping spacing is unchanged. |
|
||||
| 2026-10-15 | trivial | Exact line spacing keeps the run font baseline around tall equations; automatic and minimum spacing still grow for math. |
|
||||
| 2026-10-14 | trivial | Progress log: row table border exceptions |
|
||||
| 2026-10-12 | minor | Czech proofing review: short-word splits rank after deletion, NOSUGGEST excludes suggestions while accepting typed forms, affix comments apply only to headers, ratios keep spaces and repeated-word messages use Czech quotes; synthetic engine end-to-end and latency checks |
|
||||
| 2026-10-12 | minor | Czech spelling from the installed Hunspell dictionary, runtime-table suggestions and generic checks; Czech interface writes cs-CZ documents; no Czech hyphenation patterns bundled; estimates unchanged |
|
||||
|
||||
@@ -2227,8 +2227,9 @@ fn break_lines(pl: &mut ParaLayout, env: &ParaEnv, mark_style: u32, level: Optio
|
||||
// and descent, and grows above the baseline to fit a taller picture.
|
||||
let (a, d) = if matches!(c.kind, ClKind::Object(_)) && c.obj_h > 0.0 {
|
||||
if pl.maths.iter().any(|(k, _)| c.kind == ClKind::Object(*k)) {
|
||||
// An equation is text: its ascent and descent count like the text's.
|
||||
(c.obj_h, c.obj_d)
|
||||
// Exact spacing keeps the run's baseline; tall math may overflow the
|
||||
// fixed line box without moving adjacent text up by its denominator.
|
||||
if matches!(rp.line_spacing, LineSpacing::Exactly(_)) { (st.ascent, st.descent) } else { (c.obj_h, c.obj_d) }
|
||||
} else {
|
||||
obj_asc = obj_asc.max(c.obj_h);
|
||||
(st.ascent, st.descent)
|
||||
|
||||
@@ -8170,3 +8170,56 @@ fn partial_margins_inherit() {
|
||||
assert!((height(2.75, 2.75) - 16.5).abs() < 0.01);
|
||||
assert!((height(6.0, 8.0) - 25.0).abs() < 0.01);
|
||||
}
|
||||
|
||||
/// Exact line pitch keeps the text baseline even when an equation has a deep denominator.
|
||||
#[test]
|
||||
fn exact_math_keeps_run_baseline() {
|
||||
use wordcraft_doc::props::LineSpacing;
|
||||
for display in [false, true] {
|
||||
for spacing in [LineSpacing::Exactly(12.0), LineSpacing::AtLeast(12.0), LineSpacing::Multiple(1.0)] {
|
||||
let mut baseline = Vec::new();
|
||||
let mut following = Vec::new();
|
||||
for equation in [None, Some("a/(b/c)")] {
|
||||
let chr = wordcraft_doc::CharProps { font: Some(wordcraft_doc::styles::BODY_FONT.into()), size: Some(12.0), ..Default::default() };
|
||||
let mut p = wordcraft_doc::Paragraph::new();
|
||||
p.props.line_spacing = Some(spacing);
|
||||
match equation {
|
||||
None => {
|
||||
p.insert_text(0, "x", &chr).unwrap();
|
||||
}
|
||||
Some(text) => p
|
||||
.insert_object(
|
||||
0,
|
||||
InlineObject::Equation {
|
||||
linear: text.into(),
|
||||
display,
|
||||
math: wordcraft_doc::math::Math { nodes: wordcraft_doc::math::parse_linear(text), ..Default::default() },
|
||||
},
|
||||
&chr,
|
||||
)
|
||||
.unwrap(),
|
||||
}
|
||||
let mut d = Document::from_text("After");
|
||||
d.body.insert(0, wordcraft_doc::para_block(p));
|
||||
let l = lay(&d);
|
||||
let (y, para) = l.pages[0]
|
||||
.items
|
||||
.iter()
|
||||
.find_map(|it| match it {
|
||||
Placed::Lines { path, y, para, .. } if *path == Path::top(0) => Some((*y, para)),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap();
|
||||
baseline.push(y + para.lines[0].baseline);
|
||||
following.push(para_at(&l, &[1])[0].1);
|
||||
}
|
||||
match spacing {
|
||||
LineSpacing::Exactly(_) => {
|
||||
assert!((baseline[0] - baseline[1]).abs() < 0.01, "display={display}: {baseline:?}");
|
||||
assert!((following[0] - following[1]).abs() < 0.01, "fixed pitch: {following:?}");
|
||||
}
|
||||
_ => assert!(following[1] > following[0], "minimum pitch grows for math: {following:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,8 @@ Document renaming: desktop renaming requires a filesystem that supports hard lin
|
||||
|
||||
Additive paragraph spacing retains the full before-space after a section page break; ordinary overlapping spacing is unchanged. Other pagination gaps remain open.
|
||||
|
||||
Exact line spacing keeps the run font baseline around tall equations; automatic and minimum spacing still grow for math. Other pagination gaps remain open.
|
||||
|
||||
## Ranked
|
||||
|
||||
| # | Gap | B | Evidence | Impact | Hours | Doc |
|
||||
@@ -110,6 +112,7 @@ The Language dialog reads effective character formatting, including document def
|
||||
| 2026-10-14 | trivial | Gap 19: RTF export writes sections (ODT export landed too) |
|
||||
| 2026-10-14 | trivial | #19 narrowed: ODT export done (master page and page layout per section, columned `text:section`s) |
|
||||
| 2026-10-15 | trivial | Additive paragraph spacing retains the full before-space after a section page break; ordinary overlapping spacing is unchanged. |
|
||||
| 2026-10-15 | trivial | Exact line spacing keeps the run font baseline around tall equations; automatic and minimum spacing still grow for math. |
|
||||
| 2026-10-14 | trivial | Row table border exceptions fixed; other row table exceptions remain unsupported |
|
||||
| 2026-10-12 | minor | Czech proofing review: short-word splits rank after deletion, NOSUGGEST excludes suggestions while accepting typed forms, affix comments apply only to headers, ratios keep spaces and repeated-word messages use Czech quotes; synthetic engine end-to-end and latency checks |
|
||||
| 2026-10-12 | minor | Czech spelling from the installed Hunspell dictionary, runtime-table suggestions and generic checks; Czech interface writes cs-CZ documents; no Czech hyphenation patterns bundled; estimates unchanged |
|
||||
|
||||
@@ -14,6 +14,8 @@ font can't match Word's metrics.
|
||||
|
||||
Additive paragraph spacing retains the full before-space after a section page break; ordinary overlapping spacing is unchanged. Synthetic control: `layout::tests::section_start_summed_spacing`.
|
||||
|
||||
Exact line spacing keeps the run font baseline around tall equations; automatic and minimum spacing still grow for math. Synthetic control: `layout::tests::exact_math_keeps_run_baseline`.
|
||||
|
||||
## Checklist
|
||||
|
||||
✅ matches Word as far as our tests show · 🟡 implemented, differs or untested against Word · ❌ missing
|
||||
@@ -53,7 +55,7 @@ Additive paragraph spacing retains the full before-space after a section page br
|
||||
| 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) | — |
|
||||
| Tables: row border exceptions (`w:tblPrEx/w:tblBorders`) | ✅ | `row_borders_stay_on_row`, `row_nil_overrides_style`: explicit cell borders win; row exceptions precede table styles and table borders | — |
|
||||
| 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); each section's note options: footnotes below the text, endnotes at the end of each section, start at, numbering restarted each section or each page (counted from where the notes land, at most three passes) (#385) | 1–2 |
|
||||
| Tables: row heights (at least/exact), rows split across pages, header rows repeat, Can't Split | ✅ | 2026-10-06, #138; every cell in a row gets the row's largest top and bottom margin; an exact height covers the top border, top margin and text, the bottom margin comes on top, and an exact row never splits; a row split across pages keeps its cells' bottom margin, border and last space after on the first page, widow/orphan control applies in its cells, each cell continues at the top of the next page and an at-least row keeps its minimum; an at-least row with less room than its minimum on a page that already holds something starts the next page whole (Allow row to break on or off, vAlign centre too), as observed in Word for Mac 2026-10-15 (a row of about 2.6 cm), #1742; that repeated header rows go with it and that a row taller than a page splits from the top of the next page come from the issue and synthetic tests, not a Word check (`…_an_at_least_row_with_no_room_starts_the_next_page_whole_either_way_breaking_is_set`, `…_an_at_least_row_taller_than_a_page_splits_from_the_top_of_the_next_page`, `layout::tests::a_row_shares_its_largest_top_and_bottom_margins`, `…_a_split_row_keeps_its_bottom_margin_and_widow_control`, `…_split_row_cells_each_continue_at_the_top`) | — |
|
||||
| Tables: row heights (at least/exact), rows split across pages, header rows repeat, Can't Split | ✅ | 2026-10-06, #138; omitted cell-margin edges inherit current table defaults; every cell in a row gets the row's largest top and bottom margin; an exact height covers the top border, top margin and text, the bottom margin comes on top, and an exact row never splits; a row split across pages keeps its cells' bottom margin, border and last space after on the first page, widow/orphan control applies in its cells, each cell continues at the top of the next page and an at-least row keeps its minimum; an at-least row with less room than its minimum on a page that already holds something starts the next page whole (Allow row to break on or off, vAlign centre too), as observed in Word for Mac 2026-10-15 (a row of about 2.6 cm), #1742; that repeated header rows go with it and that a row taller than a page splits from the top of the next page come from the issue and synthetic tests, not a Word check (`…_an_at_least_row_with_no_room_starts_the_next_page_whole_either_way_breaking_is_set`, `…_an_at_least_row_taller_than_a_page_splits_from_the_top_of_the_next_page`, `layout::tests::a_row_shares_its_largest_top_and_bottom_margins`, `…_a_split_row_keeps_its_bottom_margin_and_widow_control`, `…_split_row_cells_each_continue_at_the_top`) | — |
|
||||
| Tables: autofit to contents/window, fixed widths, Word 2013 edge | 🟡 | #137; #44: AutoFit Contents measures each column's narrowest and widest text and shares the width like Word, once (not live as you type); consistent a percentage width past 100% is kept, as Word does; cell widths override stale grid proportions when their totals agree (`cell_widths_override_grid`); conflicting widths and spans still differ | 3–5 |
|
||||
| Floating tables (`w:tblpPr`) | 🟡 | #137; one placed on the page or margin that would cover visible text before it and does not fit below it moves to the next page (#534); empty line-break lines do not count as text; other overlap rules untested | 2–3 |
|
||||
| Nested tables | 🟡 | render; polish missing | 2–4 |
|
||||
@@ -214,6 +216,7 @@ Behind-text pictures in table cells participate in centre and bottom vertical al
|
||||
| 2026-10-15 | trivial | Additive paragraph spacing retains the full before-space after a section page break; ordinary overlapping spacing is unchanged. |
|
||||
| 2026-10-15 | trivial | Removed the 12 pt line-width floor in narrow cells and float spans; synthetic wrapping and oversized-glyph controls |
|
||||
| 2026-10-15 | trivial | Partial cell margins retain omitted edges through DOCX save and inherit current table defaults during layout. Original import/write and row-height regressions. |
|
||||
| 2026-10-15 | trivial | Exact line spacing keeps the run font baseline around tall equations; automatic and minimum spacing still grow for math. |
|
||||
| 2026-10-14 | trivial | Row table border exceptions draw without leaking into other rows |
|
||||
| 2026-10-14 | trivial | Consistent cell widths override a stale table grid |
|
||||
| 2026-10-14 | trivial | Paragraphs inside complex-field instructions take no flow height; empty result paragraphs keep their line. |
|
||||
|
||||
Reference in New Issue
Block a user