From 9f93ac03faa3f623d485f8310b53b0b0eeb8c5e3 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Thu, 12 Nov 2026 06:29:20 -0500 Subject: [PATCH] Text layout: a wrapped line finds its glyphs and end caret by bisection * Text layout: a wrapped line finds its glyphs and end caret by bisection * Fix merge of layout tests --- crates/text/src/layout.rs | 37 +++++++++++++++++++++++++++++---- crates/text/src/layout/tests.rs | 19 +++++++++++++++++ 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/crates/text/src/layout.rs b/crates/text/src/layout.rs index acf292889..9eb19d919 100644 --- a/crates/text/src/layout.rs +++ b/crates/text/src/layout.rs @@ -378,6 +378,34 @@ struct Item { range: Range, rtl: bool, glyphs: Vec, + /// Whether the glyphs' clusters never decrease (`Some(true)`) or never increase + /// (`Some(false)`) along `glyphs`: the glyphs of a cluster range are then contiguous. + cluster_order: Option, +} + +impl Item { + fn new(range: Range, rtl: bool, glyphs: Vec) -> Self { + let cluster_order = if glyphs.windows(2).all(|w| w[0].cluster <= w[1].cluster) { + Some(true) + } else if glyphs.windows(2).all(|w| w[0].cluster >= w[1].cluster) { + Some(false) + } else { + None + }; + Self { range, rtl, glyphs, cluster_order } + } + + /// The glyphs whose cluster lies in `run`, in glyph order, found by bisection when the clusters + /// are ordered (a line of a long paragraph otherwise scanned the whole item). + fn glyphs_in(&self, run: &Range) -> impl Iterator { + let g = &self.glyphs; + let (lo, hi) = match self.cluster_order { + Some(true) => (g.partition_point(|x| x.cluster < run.start), g.partition_point(|x| x.cluster < run.end)), + Some(false) => (g.partition_point(|x| x.cluster >= run.end), g.partition_point(|x| x.cluster >= run.start)), + None => (0, g.len()), + }; + g[lo..hi].iter().filter(|x| run.contains(&x.cluster)) + } } fn upper_single(c: char) -> char { @@ -929,7 +957,7 @@ impl<'a> Para<'a> { idx.reverse(); } for k in idx { - for g in self.items[k].glyphs.iter().filter(|g| run.contains(&g.cluster)) { + for g in self.items[k].glyphs_in(&run) { let st = &self.runs[g.run].1; let x0 = pen; pen += g.pre; @@ -990,9 +1018,10 @@ impl<'a> Para<'a> { } } // end of line caret - let end_x = match self.chars.iter().rev().find(|c| c.0 < r.end && c.0 >= r.start) { + let last_char = self.chars.partition_point(|c| c.0 < r.end).checked_sub(1).and_then(|i| self.chars.get(i)).filter(|c| c.0 >= r.start); + let end_x = match last_char { Some(c) => { - let cs = cluster_starts.iter().rev().find(|s| **s <= c.0).copied(); + let cs = cluster_starts.partition_point(|s| *s <= c.0).checked_sub(1).and_then(|i| cluster_starts.get(i)).copied(); match cs.and_then(|s| extents.get(&s)) { Some(&(x0, x1, rtl)) => { if rtl { @@ -1095,7 +1124,7 @@ fn build_items(text: &str, chars: &[CharInfo], runs: &[(Range, TextStyle) let mut glyphs = shape_item(&sub, rtl, face, size, style, si, form); space_item(&mut glyphs, style, text, true); let end = if j < chars.len() { chars[j].0 } else { text.len() }; - items.push(Item { range: chars[i].0..end, rtl, glyphs }); + items.push(Item::new(chars[i].0..end, rtl, glyphs)); i = j; } items diff --git a/crates/text/src/layout/tests.rs b/crates/text/src/layout/tests.rs index c467cb5f9..bf275cad8 100644 --- a/crates/text/src/layout/tests.rs +++ b/crates/text/src/layout/tests.rs @@ -506,6 +506,25 @@ fn a_later_vertical_run_keeps_the_first_line_baseline() { assert!((mixed.lines[0].baseline - plain.lines[0].baseline).abs() < 1e-3, "{} vs {}", mixed.lines[0].baseline, plain.lines[0].baseline); } +/// `Item::glyphs_in` finds a cluster range by bisection when the clusters are ordered, and agrees +/// with a full scan for ascending, descending and unordered clusters. +#[test] +fn item_glyphs_in_matches_a_full_scan() { + let glyph = |cluster| ShapedGlyph { face: 0, id: cluster as u32, cluster, adv: 1.0, pre: 0.0, dx: 0.0, dy: 0.0, size: 10.0, run: 0, rise: 0.0 }; + let asc: Vec = vec![0, 0, 1, 3, 3, 3, 5, 8]; + let desc: Vec = asc.iter().rev().copied().collect(); + let mixed: Vec = vec![3, 0, 5, 1, 8, 3, 0, 3]; + for (clusters, order) in [(&asc, Some(true)), (&desc, Some(false)), (&mixed, None)] { + let item = Item::new(0..9, false, clusters.iter().map(|&c| glyph(c)).collect()); + assert_eq!(item.cluster_order, order); + for (a, b) in [(0, 9), (0, 1), (1, 4), (3, 4), (4, 9), (6, 8), (9, 9), (2, 3)] { + let got: Vec = item.glyphs_in(&(a..b)).map(|g| g.cluster).collect(); + let want: Vec = clusters.iter().copied().filter(|c| (a..b).contains(c)).collect(); + assert_eq!(got, want, "{clusters:?} {a}..{b}"); + } + } +} + #[test] fn run_lookup_by_bisection_matches_the_scan() { let scan = |runs: &[(Range, TextStyle)], b: usize| {