From a5466741c437f71d73d58db74a2cc2f25e3fdfdb Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Mon, 12 Oct 2026 13:57:30 -0400 Subject: [PATCH] Text: superscript and subscript advance at their drawn size Superscript and subscript glyphs were drawn at two thirds of the run's size but advanced by the full size, so the letters spread apart ("s u p e r") and the line ran wider than in PowerPoint. The cluster advance now uses the same scale as the glyphs. Co-Authored-By: Claude Opus 5.5 (1M context) --- crates/text/src/lib.rs | 14 ++++++++++---- crates/text/src/tests.rs | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/crates/text/src/lib.rs b/crates/text/src/lib.rs index 225f4af..287c442 100644 --- a/crates/text/src/lib.rs +++ b/crates/text/src/lib.rs @@ -566,6 +566,9 @@ fn shape_cells(ctx: &Ctx, text: &str, char_style: &[usize], styles: &[Style], fa cells } +/// Size of superscript and subscript text relative to the run's size. +const SUB_SUPER: f64 = 0.66; + /// Shape cells `range` (one item) and hang each cluster's glyphs on its first cell, sharing the /// cluster's advance among its graphemes (by the ligature's GDEF carets when it has them). #[allow(clippy::too_many_arguments)] @@ -593,7 +596,11 @@ fn shape_item( let st = styles.get(cell.style); let size = st.map(|s| s.size).unwrap_or(18.0); let small = st.is_some_and(|s| s.caps == Caps::Small) && cell.ch.is_lowercase(); - (if small { size * 0.8 } else { size }) / upem + let size = if small { size * 0.8 } else { size }; + // Superscript and subscript glyphs are drawn at two thirds of the size, so they advance by + // two thirds too. + let size = if st.is_some_and(|s| s.baseline != 0.0) { size * SUB_SUPER } else { size }; + size / upem }; // Cluster heads in this item, ascending; each cluster spans to the next head. let mut heads: Vec = glyphs.iter().filter_map(|g| char_at_byte.get(&g.cluster).copied()).filter(|h| range.contains(h)).collect(); @@ -1155,7 +1162,7 @@ fn layout_scaled(ctx: &Ctx, shape: &Shape, body: &TextBody, bp: &BodyProps, opts let Some(st) = p.styles.get(cell.style) else { continue }; let Some(&(cx, cx1)) = line.edges.get(k) else { continue }; let base_y = line.baseline - st.baseline * st.size; - let size = if st.baseline != 0.0 { st.size * 0.66 } else { st.size }; + let size = if st.baseline != 0.0 { st.size * SUB_SUPER } else { st.size }; let size = if st.caps == Caps::Small && cell.ch.is_lowercase() { size * 0.8 } else { size }; let key = (cell.style, cell.face); if key != cur_key { @@ -1204,9 +1211,8 @@ fn layout_scaled(ctx: &Ctx, shape: &Shape, body: &TextBody, bp: &BodyProps, opts } } } - let k66 = if st.baseline != 0.0 { 0.66 } else { 1.0 }; for (gid, gx, gy) in &cell.glyphs { - r.glyphs.push((*gid, cl + kash + gx * k66, base_y + gy)); + r.glyphs.push((*gid, cl + kash + gx, base_y + gy)); r.cells.push((ci, cell.ch)); } } diff --git a/crates/text/src/tests.rs b/crates/text/src/tests.rs index f3d5db5..0f6c647 100644 --- a/crates/text/src/tests.rs +++ b/crates/text/src/tests.rs @@ -395,3 +395,41 @@ fn hostile_bidi_input_never_panics() { } } } + +/// Width of `word` laid out in a run with `baseline` (0 = normal, 0.3 = superscript …). +fn run_width(word: &str, baseline: Option) -> f64 { + let mut run = Run::new(word); + run.props.size = Some(24.0); + run.props.baseline = baseline; + let (p, sh) = bidi_setup(vec![run], 2000.0, false, Align::Left); + let l = lay(&p, &sh, 2000.0, 300.0); + let line = &l.lines[0]; + line.caret_x.last().unwrap() - line.caret_x.first().unwrap() +} + +#[test] +fn superscript_and_subscript_advance_at_their_drawn_size() { + // The glyphs are drawn at two thirds of the size; advancing by the full size spread the + // letters apart ("s u p e r"). + let normal = run_width("superscript", None); + for b in [Some(0.3), Some(-0.25)] { + let w = run_width("superscript", b); + assert!((w / normal - SUB_SUPER).abs() < 0.02, "{b:?}: {w} vs {normal}"); + } +} + +#[test] +fn superscript_glyphs_sit_inside_their_cells() { + let mut run = Run::new("super"); + run.props.size = Some(24.0); + run.props.baseline = Some(0.3); + let (p, sh) = bidi_setup(vec![run], 2000.0, false, Align::Left); + let l = lay(&p, &sh, 2000.0, 300.0); + let line = &l.lines[0]; + let r = l.runs.iter().find(|r| !r.cells.is_empty()).unwrap(); + assert!((r.size - 24.0 * SUB_SUPER).abs() < 1e-6); + for (g, (ci, _)) in r.glyphs.iter().zip(&r.cells) { + let (x0, x1) = line.edges[*ci - line.start]; + assert!(g.1 >= x0 - 0.01 && g.1 < x1, "glyph at {} outside its cell {x0}..{x1}", g.1); + } +}