From 4d027de4022aecb9bc79abd357b9fbcb71ac3595 Mon Sep 17 00:00:00 2001 From: Martin Ball Date: Thu, 22 Oct 2026 15:27:00 +0100 Subject: [PATCH] #3692: math text runs find character boundaries in one walk Finding 23: Ctx::glyphs found each character's first glyph by scanning the shaped glyphs from the start (quadratic in a long run). The first glyph at or past a boundary never moves back as the boundary grows, so one walk gives the same cuts. Test: math::tests::a_long_text_run_lays_out_in_linear_time. Co-Authored-By: Claude Opus 5.5 (1M context) --- crates/layout/src/math.rs | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/crates/layout/src/math.rs b/crates/layout/src/math.rs index 7291e89dd..a581635bc 100644 --- a/crates/layout/src/math.rs +++ b/crates/layout/src/math.rs @@ -631,19 +631,18 @@ impl Ctx { last_gid = Some(g.gid); } b.w = x; - // Character boundaries: where the first glyph of each character starts. + // Character boundaries: where the first glyph of each character starts. The glyph for a + // boundary is never before the previous boundary's, so one walk of the glyphs finds all. let mut cuts = Vec::with_capacity(text.len() + 1); + let (mut j, mut gx) = (0usize, 0.0f32); for (cb, _) in text.char_indices() { - let mut gx = 0.0f32; - let mut found = None; - for g in &shaped { - if g.cluster >= cb { - found = Some(gx); - break; - } + while let Some(g) = shaped.get(j) + && g.cluster < cb + { gx += g.x_advance as f32 * k; + j += 1; } - cuts.push(found.unwrap_or(x)); + cuts.push(if j < shaped.len() { gx } else { x }); } cuts.push(x); b.cuts = cuts; @@ -2374,4 +2373,19 @@ mod tests { } } } + + /// Finding 23: each character's boundary in a run of text was found by walking the run's + /// glyphs from the first. + #[test] + fn a_long_text_run_lays_out_in_linear_time() { + use crate::tests::{SCALE, cost_ratio}; + use wordcraft_doc::math::*; + let eq = |n: usize| Math { nodes: vec![MNode::Run(MRun { nor: true, ..MRun::new("a".repeat(n)) })], ..Default::default() }; + let (small, big) = (eq(400), eq(SCALE * 400)); + // Each timed 16 times over, so the timings are long against the clock's tick. + let lay_16 = |m: &Math| (0..16).for_each(|_| drop(std::hint::black_box(lay_in(m, "", false, 1e6, &MathProps::default())))); + let (t1, tk, ratio) = cost_ratio(SCALE, || lay_16(&small), || lay_16(&big)); + eprintln!("t(n) {t1:.4} s, t(8n) {tk:.4} s, ratio {ratio:.1}"); + assert!(ratio < 24.0, "8x the text took {ratio:.1}x the time ({t1:.4} s → {tk:.4} s): not linear"); + } }