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:
parent
965cdd9560
commit
4dd2d7e889
|
|
@ -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));
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue