From 670968f60a51a8626521f32bb2fe852f77100e66 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Tue, 3 Nov 2026 12:59:45 +0000 Subject: [PATCH] perf(text): look only at a paragraph's own runs --- crates/text/src/layout.rs | 9 +++++++-- crates/text/src/shape.rs | 8 +++++++- crates/text/src/tests.rs | 13 +++++++++++++ 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/crates/text/src/layout.rs b/crates/text/src/layout.rs index 669777687..9d7ed666e 100644 --- a/crates/text/src/layout.rs +++ b/crates/text/src/layout.rs @@ -140,7 +140,9 @@ impl Ctx<'_> { /// stretch. fn char_directions(&self, r: &Range) -> Vec<(Range, ParaDirection)> { let mut out: Vec<(Range, ParaDirection)> = Vec::new(); - for (rr, st) in &self.runs { + // Only the paragraph's own runs (they are in order), not every run of the text. + let first = self.runs.partition_point(|(rr, _)| rr.end <= r.start); + for (rr, st) in self.runs.iter().skip(first).take_while(|(rr, _)| rr.start < r.end) { let (a, b) = (rr.start.max(r.start), rr.end.min(r.end)); let Some(d) = st.char_direction.filter(|_| a < b) else { continue }; let (a, b) = (a - r.start, b - r.start); @@ -212,7 +214,10 @@ impl Ctx<'_> { fn prepend_marker(&self, pr: &Range, para: &ParaStyle, text: Option<&str>, rtl: bool, sg: &mut Vec) -> usize { let Some(text) = text.filter(|_| para.list.is_some() && !pr.is_empty()) else { return 0 }; let st = vectorcraft_doc::text::ParaList::marker_style(self.style_at(pr.start)); - let Some(run) = self.runs.iter().position(|(r, _)| r.start <= pr.start && pr.start < r.end) else { return 0 }; + let run = self.runs.partition_point(|(r, _)| r.end <= pr.start); + if !self.runs.get(run).is_some_and(|(r, _)| r.start <= pr.start && pr.start < r.end) { + return 0; + } // The paragraph's direction: the marker starts a right-to-left paragraph at its right. let level = if rtl { Level::rtl() } else { Level::ltr() }; let mut marker = Vec::with_capacity(text.len() + 1); diff --git a/crates/text/src/shape.rs b/crates/text/src/shape.rs index c5f82071d..a257a3ae8 100644 --- a/crates/text/src/shape.rs +++ b/crates/text/src/shape.rs @@ -375,7 +375,13 @@ pub(crate) fn shape_range( art: Option<&'a InlineArt>, } let mut spans: Vec> = Vec::new(); - for (ri, (rr, st)) in runs.iter().enumerate() { + // The runs are in order: start at the first that ends inside the range and stop past it, so + // that each paragraph looks only at its own runs. + let first = runs.partition_point(|(rr, _)| rr.end <= range.start); + for (ri, (rr, st)) in runs.iter().enumerate().skip(first) { + if rr.start >= range.end { + break; + } let a = rr.start.max(range.start); let b = rr.end.min(range.end); if a >= b || text.get(a..b).is_none() { diff --git a/crates/text/src/tests.rs b/crates/text/src/tests.rs index 3f86a85bd..002e4b921 100644 --- a/crates/text/src/tests.rs +++ b/crates/text/src/tests.rs @@ -555,6 +555,19 @@ fn multiple_runs_and_styles() { assert_ne!(l.glyphs[0].font_id, l.glyphs[3].font_id); } +#[test] +fn many_paragraphs_of_their_own_runs_lay_out_in_linear_time() { + // 20,000 one-letter paragraphs, each a run of its own: every paragraph looking through every + // run would be 400 million steps. + let n = 20_000; + let mut t = point("", style(10.0)); + t.runs = (0..n).map(|i| TextRun { text: "a\n".into(), style: style(if i % 2 == 0 { 10.0 } else { 11.0 }), inline: None }).collect(); + let start = std::time::Instant::now(); + let l = layout(db(), &t); + assert!(start.elapsed() < std::time::Duration::from_secs(60), "{:?}", start.elapsed()); + assert!(l.glyphs.iter().any(|g| g.run == n - 1)); +} + #[test] fn layout_is_fast() { let text: String = LOREM.chars().cycle().take(1000).collect();