From 4dd2d7e88934833ca4e8ed24bf99e3059660b219 Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Fri, 29 May 2026 10:40:27 -0400 Subject: [PATCH] =?UTF-8?q?9.3=20fix=20=E2=80=94=20rebase=20glyph=20offset?= =?UTF-8?q?s=20to=20buffer-absolute=20(selections=20blind)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The debug aid confirmed peer presence arrives with real selections (sel=Some { anchor: 1837, active: 1844 }), yet no wash drew — because the rect geometry was computed against the wrong coordinate space. cosmic-text's `LayoutGlyph::{start,end}` are byte offsets within the *original line* (`LayoutRun::line_i`), not the whole buffer. `push_glyph_extent_rects` was comparing those line-relative offsets against whole-buffer byte ranges from presence/`source_line_range`. They only coincide on line 0, so any Selection or CurrentLine past the first line never matched a glyph and produced no rect — "blind." This was latent since 9.1 (Selection was never visually validated) and was masked in 9.2 whenever the cursor happened to sit on line 0. Fix: build `line_byte_offsets(current_text)` — the buffer-absolute start of each `\n`-delimited line — and rebase each run's glyphs by `line_offsets[run.line_i]` before comparing. Computed once per `peer_background_rects` call and threaded into `push_glyph_extent_rects`. New test `line_byte_offsets_indexes_each_logical_line`. Gates: fmt clean; clippy -p pmacs-gpu -D warnings clean; pmacs-gpu unit 16 (+1). Co-Authored-By: Claude Opus 4.8 (1M context) --- pmacs-gpu/src/main.rs | 51 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 43 insertions(+), 8 deletions(-) diff --git a/pmacs-gpu/src/main.rs b/pmacs-gpu/src/main.rs index 47f55ef..b4dd44b 100644 --- a/pmacs-gpu/src/main.rs +++ b/pmacs-gpu/src/main.rs @@ -1204,6 +1204,12 @@ impl State { return Vec::new(); }; let text_len = self.current_text.len() as u64; + // Buffer-absolute byte offset of each `\n`-delimited line, + // indexed by `LayoutRun::line_i`. `LayoutGlyph::{start,end}` are + // offsets within the *original line*, not the whole buffer, so + // every byte range below must be rebased per line before it can + // be matched against glyph offsets. + let line_offsets = line_byte_offsets(&self.current_text); let mut rects = Vec::new(); for presence in self.peer_presences.values() { if presence.buffer_id != buffer_id { @@ -1212,7 +1218,7 @@ impl State { // CurrentLine: the source line containing the peer cursor. if let Some(color) = decoration_kind_to_bg_color(DecorationKind::CurrentLine) { let (lo, hi) = source_line_range(&self.current_text, presence.cursor); - self.push_glyph_extent_rects(&mut rects, lo, hi, color); + self.push_glyph_extent_rects(&mut rects, &line_offsets, lo, hi, color); } // Selection: the peer's selected byte range, normalized. if let Some(sel) = presence.selection @@ -1221,20 +1227,23 @@ impl State { let lo = sel.anchor.min(sel.active).min(text_len); let hi = sel.anchor.max(sel.active).min(text_len); if hi > lo { - self.push_glyph_extent_rects(&mut rects, lo, hi, color); + self.push_glyph_extent_rects(&mut rects, &line_offsets, lo, hi, color); } } } rects } - /// Push one rect per visual line whose glyphs overlap the byte - /// range `[lo, hi)`, spanning the matching glyphs' horizontal - /// extent. A range crossing visual-line boundaries (wrapped or - /// multi-line) fans out into one rect per run. + /// Push one rect per visual line whose glyphs overlap the + /// buffer-absolute byte range `[lo, hi)`, spanning the matching + /// glyphs' horizontal extent. A range crossing visual-line + /// boundaries (wrapped or multi-line) fans out into one rect per + /// run. `line_offsets[run.line_i]` rebases the run's line-relative + /// glyph offsets into buffer-absolute space for the comparison. fn push_glyph_extent_rects( &self, rects: &mut Vec, + line_offsets: &[u64], lo: u64, hi: u64, color: [f32; 4], @@ -1243,11 +1252,12 @@ impl State { return; } for run in self.buffer.layout_runs() { + let line_base = line_offsets.get(run.line_i).copied().unwrap_or(0); let mut min_x: Option = None; let mut max_x: Option = None; for glyph in run.glyphs { - let g_start = glyph.start as u64; - let g_end = glyph.end as u64; + let g_start = line_base + glyph.start as u64; + let g_end = line_base + glyph.end as u64; if g_end <= lo || g_start >= hi { continue; } @@ -1565,6 +1575,19 @@ fn debug_frame() -> bool { *FLAG.get_or_init(|| std::env::var_os("PMACS_GPU_DEBUG_FRAME").is_some()) } +/// Buffer-absolute byte offset of the start of each `\n`-delimited +/// line (index 0 = byte 0). Indexed by cosmic-text's +/// `LayoutRun::line_i` to rebase line-relative glyph offsets. +fn line_byte_offsets(text: &str) -> Vec { + let mut starts = vec![0u64]; + for (i, b) in text.bytes().enumerate() { + if b == b'\n' { + starts.push(i as u64 + 1); + } + } + starts +} + /// Byte range `[start, end)` of the source line containing `cursor`: /// `start` is just after the previous `\n` (or 0), `end` is just after /// the next `\n` (or text length). Mirrors the producer's @@ -1936,6 +1959,18 @@ mod tests { assert_eq!(source_line_range(text, 99), (7, 10)); } + #[test] + fn line_byte_offsets_indexes_each_logical_line() { + // "abc\nde\nfgh": lines start at bytes 0, 4, 7. Indexed by + // LayoutRun::line_i to rebase line-relative glyph offsets. + assert_eq!(line_byte_offsets("abc\nde\nfgh"), vec![0, 4, 7]); + // Trailing newline yields a final empty line at byte len. + assert_eq!(line_byte_offsets("a\nb\n"), vec![0, 2, 4]); + // No newline: one line at 0. + assert_eq!(line_byte_offsets("abc"), vec![0]); + assert_eq!(line_byte_offsets(""), vec![0]); + } + #[test] fn source_line_range_handles_empty_and_leading_newline() { assert_eq!(source_line_range("", 0), (0, 0));