From 85a7f0ce3e4fa190fa6128461cf15f79adea6870 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Tue, 13 Oct 2026 06:07:56 -0400 Subject: [PATCH] Text: a fallback face (emoji, symbols, a missing script) no longer moves the line's baseline Line height and baseline were the largest over every face on the line, fallback faces included, so a line with an emoji, a math symbol or Thai in an Aptos run sat 1-5 px lower than PowerPoint's and pushed the lines below it down. PowerPoint keeps the run's own font metrics (measured: the second line of six boxes starts at the same height whatever the first line holds). Faces that are the run's Latin, East Asian, complex-script or symbol font still count. w14-xml s8 (baselines) 5.54 -> 3.21, w09-scripts s1 10.27 -> 8.39, s2 9.37 -> 7.46, w15-mixed s8 13.65 -> 8.77; w15-mixed s2 (Arabic) 6.57 -> 7.05. Co-Authored-By: Claude Opus 5.5 (1M context) --- crates/text/src/lib.rs | 6 +++++- crates/text/src/tests.rs | 16 ++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/crates/text/src/lib.rs b/crates/text/src/lib.rs index cd43630..6354d17 100644 --- a/crates/text/src/lib.rs +++ b/crates/text/src/lib.rs @@ -1684,7 +1684,11 @@ fn layout_scaled(ctx: &Ctx, shape: &Shape, body: &TextBody, bp: &BodyProps, opts for c in p.cells.get(rl.start..rl.end.max(rl.start)).unwrap_or(&[]) { if let (Some(st), Some(face)) = (p.styles.get(c.style), p.faces.get(c.face)) { let sz = st.size; - let (a, d) = line_metrics(face, sz); + // The run's own fonts set the line; a fallback face (emoji, a symbol or a + // script the run's fonts lack) does not move the baseline in PowerPoint. + let own = Arc::ptr_eq(face, &st.face) + || [st.ea.as_deref(), st.cs.as_deref(), st.sym.as_deref()].into_iter().flatten().any(|f| face.family.eq_ignore_ascii_case(f)); + let (a, d) = line_metrics(if own { face } else { &st.face }, sz); asc = asc.max(a); desc = desc.max(d); size = size.max(sz); diff --git a/crates/text/src/tests.rs b/crates/text/src/tests.rs index 65f38ae..0b2c6cc 100644 --- a/crates/text/src/tests.rs +++ b/crates/text/src/tests.rs @@ -1134,3 +1134,19 @@ fn symbols_after_an_emoji_keep_a_text_face() { } } } + +#[test] +fn fallback_faces_do_not_move_the_baseline() { + // PowerPoint keeps a line's baseline and pitch from the run's own font when an emoji, a symbol + // or a script the font lacks is drawn from another face. + let base = |text: &str| { + let (p, sh) = bidi_setup(vec![Run::new(text)], 2000.0, false, Align::Left); + let l = lay(&p, &sh, 2000.0, 300.0); + (l.lines[0].baseline, l.lines[0].bottom) + }; + let plain = base("Hxg Hxg"); + for text in ["Hxg \u{1F600} Hxg", "Hxg \u{0E2A}\u{0E27}\u{0E31}\u{0E2A}", "Hxg \u{222B}\u{2211}"] { + let b = base(text); + assert!((b.0 - plain.0).abs() < 0.01 && (b.1 - plain.1).abs() < 0.01, "{text:?}: {b:?} vs {plain:?}"); + } +}