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); + } +}