R18: a list label adds ascent, not descent, to its line (#928)

Word sizes a list item's first line with the label's ascent when it is
taller, but keeps the text's descent below the baseline: a Symbol bullet's
deeper descent (and a Courier New label's) doesn't count. Measured in Word
for Windows: 6 synthetic cases. NIH's bulleted items were a fifth of a point
taller each, enough to push lines over a page bottom.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Brandon ThomasandClaude Opus 5.5 authored and GitHub committed 2026-10-12 09:41:37 -04:00
1 parent 99561097d5
commit 2e66cbb261
4 files changed
+36 -1

No files matched your search

+1
View File
@@ -146,6 +146,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm
| 2026-10-12 | Line heights like Word's: spaces and tabs never make a line taller (13 cases measured in Word for Windows); tables of contents with big tab runs were a third taller |
| 2026-10-12 | `.doc` import: tab stops (centred titles, right-aligned dates, leaders) and sections ending at their section marks (continuous breaks, footers); EPA .doc templates now paginate like Word |
| 2026-10-12 | DOCX headings numbered like Word when a heading style gives only a list level (6.4.5, not 6.15) |
| 2026-10-12 | A list label makes its line taller above the baseline only, as in Word (6 cases measured in Word for Windows): Symbol bullets no longer add their deeper descent, so bulleted lists keep Word's height and page breaks |
| 2026-10-11 | Hide White Space (#645): `view.hideWhiteSpace` (View › Show checkbox, or double-click the gap between pages) collapses margins, headers and footers between pages to a thin line in Print Layout; screen only, page count unchanged |
| 2026-10-11 | Fonts: Palatino Linotype and Book Antiqua fall back to Palatino (macOS) or TeX Gyre Pagella / P052 / URW Palladio (Linux) before generic serif (#670) |
|---|---|
+3 -1
View File
@@ -1813,10 +1813,12 @@ fn break_lines(pl: &mut ParaLayout, env: &ParaEnv, mark_style: u16, level: Optio
asc = st.ascent;
desc = st.descent;
}
// A list label can make the line taller above the baseline, never below it: Word keeps
// the text's descent (measured in Word for Windows: a Symbol bullet's taller ascent
// counts, a Courier New label's deeper descent doesn't).
if first && let Some(lab) = &pl.label {
for st in lab.runs.iter().filter_map(|r| pl.styles.get(r.style as usize)) {
asc = asc.max(st.ascent);
desc = desc.max(st.descent);
}
}
// Line spacing scales the text's height, never a picture's: a line is at least as tall
+31
View File
@@ -4177,3 +4177,34 @@ fn spaces_and_tabs_do_not_make_a_line_taller() {
assert!(pitch("x", &big) > plain + 5.0, "a 20 pt letter does make it taller");
assert!(pitch("\u{A0}", &big) > plain + 5.0, "so does a no-break space");
}
/// A list label taller than its text makes the first line taller above the baseline only: Word
/// keeps the text's descent below it (measured in Word for Windows, `plan/word` R18: a Symbol
/// bullet's taller ascent counts, a Courier New label's deeper descent doesn't). NIH's bulleted
/// items each came out a fifth of a point taller, enough to push a paragraph's last lines over
/// the page bottom.
#[test]
fn a_list_label_adds_ascent_but_not_descent() {
let first_line = |label_size: Option<f32>| {
let mut d = Document::new();
let num = d.numbering.add_list(wordcraft_doc::ListKind::Numbered);
for a in &mut d.numbering.abstracts {
for l in &mut a.levels {
l.chr.size = label_size;
}
}
let ten = wordcraft_doc::props::CharProps { size: Some(10.0), ..Default::default() };
let mut p = wordcraft_doc::Paragraph::with_text("Item", ten.clone());
p.props.numbering = Some(wordcraft_doc::props::NumRef { num, level: 0 });
p.mark = ten;
p.props.line_spacing = Some(wordcraft_doc::props::LineSpacing::Multiple(1.0));
d.body = vec![wordcraft_doc::para_block(p)];
let l = lay(&d);
l.pages[0].items.iter().find_map(|it| if let Placed::Lines { para, .. } = it { para.lines.first().cloned() } else { None }).unwrap()
};
let plain = first_line(None);
let big = first_line(Some(30.0));
let below = |l: &crate::para::Line| l.top + l.height - l.baseline;
assert!(big.baseline - big.top > plain.baseline - plain.top + 5.0, "the big label raises the baseline");
assert!((below(&big) - below(&plain)).abs() < 0.01, "the text's descent: {} vs {}", below(&big), below(&plain));
}
+1
View File
@@ -76,6 +76,7 @@ font can't match Word's metrics.
| Date | Change | Summary |
|---|---|---|
| 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-11 | trivial | Chinese documents (#534): indents in characters, a page-anchored floating table after a full page moves to the next page, no blank page from a page break at the bottom of a full page |
| 2026-10-11 | trivial | East Asian list number formats (一、二、三, 第一章, ①); list labels whose characters come from two fonts draw each part with its own font (the digits of "1、" no longer show as other letters); a list level without indents leaves the label at the paragraph edge (#534) |
| 2026-10-11 | trivial | Vertical sections: `w:textDirection` in `w:sectPr`, upright East Asian glyphs (#491) |