From 553be49d9cae302cd4d048d5a5f3803af3de47d4 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Tue, 3 Nov 2026 18:47:22 +0000 Subject: [PATCH] text: caret and line lookups by bisection --- crates/text/src/layout.rs | 23 +++++++++++++++-------- crates/text/src/layout/tests.rs | 11 +++++++++++ 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/crates/text/src/layout.rs b/crates/text/src/layout.rs index 4c0b5a436..25cdb27f5 100644 --- a/crates/text/src/layout.rs +++ b/crates/text/src/layout.rs @@ -278,19 +278,26 @@ pub struct Layout { impl Layout { /// Line index for a caret at byte `pos`. pub fn line_of(&self, pos: usize) -> usize { - let mut li = 0; - for (i, l) in self.lines.iter().enumerate() { - if l.range.start <= pos { - li = i; - } - } - li + // Lines are in text order: the last one starting at or before `pos`. + self.lines.partition_point(|l| l.range.start <= pos).saturating_sub(1) } /// Caret position `(x, baseline, line)` for byte offset `pos`. pub fn caret(&self, pos: usize) -> (f32, f32, usize) { let li = self.line_of(pos); let Some(l) = self.lines.get(li) else { return (0.0, 0.0, 0) }; - let x = l.carets.iter().min_by_key(|(b, _)| b.abs_diff(pos)).map_or(l.x, |c| c.1); + // Carets are sorted by byte offset and unique: the nearest is next to the insertion point (the lower on a tie). + let i = l.carets.partition_point(|c| c.0 < pos); + let x = match (i.checked_sub(1).and_then(|k| l.carets.get(k)), l.carets.get(i)) { + (Some(p), Some(n)) => { + if pos - p.0 <= n.0 - pos { + p.1 + } else { + n.1 + } + } + (Some(c), None) | (None, Some(c)) => c.1, + (None, None) => l.x, + }; (x, l.baseline, li) } /// Nearest caret byte offset to point `(x, y)`. diff --git a/crates/text/src/layout/tests.rs b/crates/text/src/layout/tests.rs index 5792c1c16..099680148 100644 --- a/crates/text/src/layout/tests.rs +++ b/crates/text/src/layout/tests.rs @@ -171,6 +171,17 @@ fn caps_and_small_caps() { assert_eq!(l.underlines.len(), 1); } +#[test] +fn indexed_caret_lookup_matches_a_linear_scan() { + let l = layout("fi abc\n\nde fg hi", &st(40.0), &ParagraphStyle::default()); + for pos in 0..=l.lines.iter().map(|x| x.range.end).max().unwrap_or(0) + 2 { + let li = l.lines.iter().enumerate().filter(|(_, x)| x.range.start <= pos).map(|(i, _)| i).next_back().unwrap_or(0); + let line = &l.lines[li]; + let want = line.carets.iter().min_by_key(|(b, _)| b.abs_diff(pos)).map_or(line.x, |c| c.1); + assert_eq!(l.caret(pos), (want, line.baseline, li), "pos {pos}"); + } +} + #[test] fn ligature_caret_interpolates() { // Inter has an "fi"-like ligature only via calt in some versions; use "ffi" which most