9.3 fix — rebase glyph offsets to buffer-absolute (selections blind)

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) <noreply@anthropic.com>
This commit is contained in:
Levi Neuwirth 2026-05-29 10:40:27 -04:00
parent 965cdd9560
commit 4dd2d7e889
1 changed files with 43 additions and 8 deletions

View File

@ -1204,6 +1204,12 @@ impl State {
return Vec::new(); return Vec::new();
}; };
let text_len = self.current_text.len() as u64; 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(); let mut rects = Vec::new();
for presence in self.peer_presences.values() { for presence in self.peer_presences.values() {
if presence.buffer_id != buffer_id { if presence.buffer_id != buffer_id {
@ -1212,7 +1218,7 @@ impl State {
// CurrentLine: the source line containing the peer cursor. // CurrentLine: the source line containing the peer cursor.
if let Some(color) = decoration_kind_to_bg_color(DecorationKind::CurrentLine) { if let Some(color) = decoration_kind_to_bg_color(DecorationKind::CurrentLine) {
let (lo, hi) = source_line_range(&self.current_text, presence.cursor); 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. // Selection: the peer's selected byte range, normalized.
if let Some(sel) = presence.selection if let Some(sel) = presence.selection
@ -1221,20 +1227,23 @@ impl State {
let lo = sel.anchor.min(sel.active).min(text_len); let lo = sel.anchor.min(sel.active).min(text_len);
let hi = sel.anchor.max(sel.active).min(text_len); let hi = sel.anchor.max(sel.active).min(text_len);
if hi > lo { 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 rects
} }
/// Push one rect per visual line whose glyphs overlap the byte /// Push one rect per visual line whose glyphs overlap the
/// range `[lo, hi)`, spanning the matching glyphs' horizontal /// buffer-absolute byte range `[lo, hi)`, spanning the matching
/// extent. A range crossing visual-line boundaries (wrapped or /// glyphs' horizontal extent. A range crossing visual-line
/// multi-line) fans out into one rect per run. /// 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( fn push_glyph_extent_rects(
&self, &self,
rects: &mut Vec<MinimapRect>, rects: &mut Vec<MinimapRect>,
line_offsets: &[u64],
lo: u64, lo: u64,
hi: u64, hi: u64,
color: [f32; 4], color: [f32; 4],
@ -1243,11 +1252,12 @@ impl State {
return; return;
} }
for run in self.buffer.layout_runs() { 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<f32> = None; let mut min_x: Option<f32> = None;
let mut max_x: Option<f32> = None; let mut max_x: Option<f32> = None;
for glyph in run.glyphs { for glyph in run.glyphs {
let g_start = glyph.start as u64; let g_start = line_base + glyph.start as u64;
let g_end = glyph.end as u64; let g_end = line_base + glyph.end as u64;
if g_end <= lo || g_start >= hi { if g_end <= lo || g_start >= hi {
continue; continue;
} }
@ -1565,6 +1575,19 @@ fn debug_frame() -> bool {
*FLAG.get_or_init(|| std::env::var_os("PMACS_GPU_DEBUG_FRAME").is_some()) *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<u64> {
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`: /// Byte range `[start, end)` of the source line containing `cursor`:
/// `start` is just after the previous `\n` (or 0), `end` is just after /// `start` is just after the previous `\n` (or 0), `end` is just after
/// the next `\n` (or text length). Mirrors the producer's /// 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)); 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] #[test]
fn source_line_range_handles_empty_and_leading_newline() { fn source_line_range_handles_empty_and_leading_newline() {
assert_eq!(source_line_range("", 0), (0, 0)); assert_eq!(source_line_range("", 0), (0, 0));