diff --git a/crates/text/src/lib.rs b/crates/text/src/lib.rs index 225f4afb..13f9d4b4 100644 --- a/crates/text/src/lib.rs +++ b/crates/text/src/lib.rs @@ -320,10 +320,12 @@ fn face_chain(ctx: &Ctx, st: &Style, slot: FontSlot, script: Option) chain } -fn spacing_pts(s: Option, size: f64, reduce: f64) -> f64 { +/// Space before or after a paragraph, points. Shrink-on-overflow's line spacing reduction +/// doesn't apply to it (PowerPoint keeps it). +fn spacing_pts(s: Option, size: f64) -> f64 { match s { - Some(Spacing::Pct(p)) => p * size * 1.2 * (1.0 - reduce), - Some(Spacing::Pts(v)) => v * (1.0 - reduce), + Some(Spacing::Pct(p)) => p * size * 1.2, + Some(Spacing::Pts(v)) => v, None => 0.0, } } @@ -918,7 +920,7 @@ fn layout_scaled(ctx: &Ctx, shape: &Shape, body: &TextBody, bp: &BodyProps, opts let first_w = (col_w - first_x - mr).max(1.0); let rest_w = (col_w - rest_x - mr).max(1.0); let raw = break_lines(p, first_w, rest_w, wrap, &tabs, props.default_tab.unwrap_or(72.0), first_x, rest_x); - let before = if pi == 0 { 0.0 } else { spacing_pts(props.space_before, p.lead_size, reduce) }; + let before = if pi == 0 { 0.0 } else { spacing_pts(props.space_before, p.lead_size) }; y += before.max(0.0) + prev_after; let first_line_idx = all_lines.len(); for (li, rl) in raw.iter().enumerate() { @@ -941,7 +943,9 @@ fn layout_scaled(ctx: &Ctx, shape: &Shape, body: &TextBody, bp: &BodyProps, opts } let natural = (asc + desc).max(size * 1.0); let lh = match props.line_spacing { - Some(Spacing::Pct(f)) => natural * f.max(0.0) * (1.0 - reduce), + // The reduction comes off the percentage (90 % less 20 % is 70 %), as PowerPoint + // applies `lnSpcReduction`. + Some(Spacing::Pct(f)) => natural * (f - reduce).max(f * 0.1).max(0.0), Some(Spacing::Pts(v)) => v * (1.0 - reduce), None => natural * (1.0 - reduce), }; @@ -1082,7 +1086,7 @@ 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); + prev_after = spacing_pts(props.space_after, p.lead_size).max(0.0); } let total = y; out.content_height = total; diff --git a/crates/text/src/tests.rs b/crates/text/src/tests.rs index f3d5db5d..f130780c 100644 --- a/crates/text/src/tests.rs +++ b/crates/text/src/tests.rs @@ -54,6 +54,37 @@ fn shrink_on_overflow() { assert!(!l.overflow || l.font_scale <= 0.11); } +#[test] +fn line_spacing_reduction_comes_off_the_percentage() { + // 90 % line spacing and 10 pt before paragraphs, shrunk with a 20 % line spacing reduction: + // PowerPoint draws 70 % lines and keeps the 10 pt. + let (p, mut sh) = setup("One\nTwo\nThree", 600.0, 400.0); + for (i, para) in sh.text.as_mut().unwrap().paragraphs.iter_mut().enumerate() { + para.props.line_spacing = Some(Spacing::Pct(0.9)); + para.props.space_before = Some(Spacing::Pts(10.0)); + para.props.bullet = Some(Bullet::None); + if i == 0 { + para.runs[0].text.push_str(" wraps onto a second line because this paragraph is long enough to wrap in a narrow box"); + } + } + let ctx = Ctx::for_slide(&p, &p.slides[0]).unwrap(); + let body = sh.text.clone().unwrap(); + let bp = resolve::body(&ctx, &sh); + let opts = Opts { rect: Rect::new(0.0, 0.0, 200.0, 400.0), fields: &NoFields, prompt_color: None, no_shrink: true }; + let full = layout_scaled(&ctx, &sh, &body, &bp, &opts, 1.0, 0.0); + let reduced = layout_scaled(&ctx, &sh, &body, &bp, &opts, 1.0, 0.2); + let pitch = |l: &TextLayout| l.lines[1].top - l.lines[0].top; + // Lines within the first paragraph: 0.7 / 0.9 of the full pitch. + assert!(full.lines[1].para == 0 && reduced.lines[1].para == 0); + assert!((pitch(&reduced) / pitch(&full) - 0.7 / 0.9).abs() < 0.005, "{} {}", pitch(&reduced), pitch(&full)); + // The space before the next paragraph is the same 10 pt. + let gap = |l: &TextLayout| { + let k = l.lines.iter().position(|x| x.para == 1).unwrap(); + l.lines[k].top - l.lines[k - 1].bottom + }; + assert!((gap(&full) - 10.0).abs() < 1e-6 && (gap(&reduced) - 10.0).abs() < 1e-6, "{} {}", gap(&full), gap(&reduced)); +} + #[test] fn caret_and_hit_roundtrip() { let (p, sh) = setup("Hello world", 600.0, 100.0);