mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-24 18:46:57 -06:00
Text: exact line spacing ends at the last line's descent
With exact line spacing (a:lnSpc/a:spcPts) PowerPoint puts the baseline at 80 % of the line pitch and ends the text at the last line's descent, not a full pitch below it. We drew every line a full pitch tall, so table rows whose cells use exact spacing grew taller than PowerPoint's (35.6 pt instead of 33 pt for 12 pt type at 26.59 pt), pushing tables and text boxes down and tripping shrink-on-overflow early. Measured on PowerPoint's own PDF of a real deck: rows now come out at 33.5 pt against PowerPoint's 32.9-33.8 pt.
This commit is contained in:
@@ -879,6 +879,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;
|
||||
@@ -947,7 +949,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);
|
||||
@@ -1084,7 +1090,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;
|
||||
// Columns: distribute lines by height.
|
||||
let col_h = if ncols > 1 { inner.height() } else { f64::INFINITY };
|
||||
|
||||
@@ -54,6 +54,33 @@ fn shrink_on_overflow() {
|
||||
assert!(!l.overflow || l.font_scale <= 0.11);
|
||||
}
|
||||
|
||||
#[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);
|
||||
|
||||
Reference in New Issue
Block a user