Merge PR #249 from xmaks82: Text: exact line spacing ends at the last line's descent

# Conflicts:
#	crates/text/src/tests.rs
This commit is contained in:
Brandon Thomas committed 2026-10-14 12:58:01 -04:00
commit 97e7164026
2 files changed
+36 -2

No files matched your search

+9 -2
View File
@@ -1619,6 +1619,8 @@ fn layout_scaled(ctx: &Ctx, shape: &Shape, body: &TextBody, bp: &BodyProps, opts
let mut numbering: Vec<u32> = vec![0; 9];
let mut all_lines: Vec<(LineInfo, LinePlace, f64, Option<(String, f64, f64)>)> = vec![];
let mut prev_after = 0.0;
// How far the last line's full pitch reaches past the text (exact spacing; see below).
let mut tail = 0.0;
let mut shaped_paras: Vec<ParaShaped> = Vec::with_capacity(body.paragraphs.len());
for (pi, para) in body.paragraphs.iter().enumerate() {
let lvl = para.level.min(8) as usize;
@@ -1695,7 +1697,11 @@ fn layout_scaled(ctx: &Ctx, shape: &Shape, body: &TextBody, bp: &BodyProps, opts
};
let ratio = if natural > 0.0 { lh / natural } else { 1.0 };
let top = y;
let baseline = top + asc * ratio;
// Exact spacing (`spcPts`), as PowerPoint sets it: the baseline sits at 80 % of the
// line pitch, and the text ends at the last line's descent rather than a full pitch.
let exact = matches!(props.line_spacing, Some(Spacing::Pts(_)));
let baseline = if exact { top + lh * 0.8 } else { top + asc * ratio };
tail = if exact { (lh * 0.2 - desc).max(-lh) } else { 0.0 };
y += lh;
let x0 = if li == 0 { first_x } else { rest_x };
let avail = (col_w - x0 - mr).max(0.0);
@@ -1840,7 +1846,8 @@ fn layout_scaled(ctx: &Ctx, shape: &Shape, body: &TextBody, bp: &BodyProps, opts
out.para_lines.push((first_line_idx, raw.len()));
prev_after = spacing_pts(props.space_after, p.lead_size, reduce).max(0.0);
}
let total = y;
// The text ends at the last line's descent under exact spacing.
let total = (y - tail).max(0.0);
out.content_height = total;
// Where column `c` starts: from the left, or from the right with `rtlCol` (PowerPoint puts the
// first column on the right).
+27
View File
@@ -206,6 +206,33 @@ fn saved_shrink_scale_is_drawn() {
assert_eq!((l.font_scale, l.line_reduction), (1.0, 0.2));
}
#[test]
fn exact_line_spacing_ends_at_the_last_descent() {
// 12 pt text at exactly 26.59 pt per line (a table cell PowerPoint sizes to 24.3 pt of text):
// the baseline at 80 % of the pitch, the text ending at the last line's descent.
let (p, mut sh) = setup("One\nTwo", 600.0, 300.0);
for para in &mut sh.text.as_mut().unwrap().paragraphs {
para.props.line_spacing = Some(Spacing::Pts(26.59));
para.props.bullet = Some(Bullet::None);
for r in &mut para.runs {
r.props.size = Some(12.0);
}
}
let l = lay(&p, &sh, 600.0, 300.0);
let first = &l.lines[0];
assert!((first.baseline - first.top - 26.59 * 0.8).abs() < 0.01, "{:?}", first.baseline - first.top);
// The text ends at the last line's descent: above its full pitch by 20 % of it less the
// descent (~2.3 pt for 12 pt type).
let short = (l.lines[1].bottom - l.lines[0].top) - l.content_height;
assert!(short > 1.0 && short < 26.59 * 0.2, "{short}");
// Proportional spacing keeps full pitches.
for para in &mut sh.text.as_mut().unwrap().paragraphs {
para.props.line_spacing = Some(Spacing::Pct(1.0));
}
let l = lay(&p, &sh, 600.0, 300.0);
assert!((l.content_height - (l.lines[1].bottom - l.lines[0].top)).abs() < 0.01);
}
#[test]
fn caret_and_hit_roundtrip() {
let (p, sh) = setup("Hello world", 600.0, 100.0);