From c9ec6ef02b8b2b01e80aecfc1c34cd8aa2409b17 Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Fri, 12 Jun 2026 13:10:48 -0400 Subject: [PATCH] =?UTF-8?q?scroll-to-cursor=20only=20when=20the=20cursor?= =?UTF-8?q?=20moved=20=E2=80=94=20fixes=20minimap=20snap-back?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The daemon attaches a CursorByte to every frame it produces, including the frames the GPU's own Viewport sends trigger. The CursorByte handler followed the cursor unconditionally, so a minimap jump away from a stationary cursor snapped straight back: jump → Viewport → frame + re-announced CursorByte → scroll_to_cursor → snap, looping on every scrub move (the reported jitter). Wheel-scrolling past the cursor's screen had the same latent loop. The handler now compares the arriving position to own_cursor and follows only genuine movement — scrolling away from a cursor that isn't moving is the user's prerogative. Co-Authored-By: Claude Fable 5 --- pmacs-gpu/src/main.rs | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/pmacs-gpu/src/main.rs b/pmacs-gpu/src/main.rs index db77c83..4f63b9c 100644 --- a/pmacs-gpu/src/main.rs +++ b/pmacs-gpu/src/main.rs @@ -465,7 +465,7 @@ struct State { } /// pmacs-gpu's own cursor position, mirrored from `CursorByte`. -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] struct OwnCursor { buffer_id: BufferId, byte: u64, @@ -1895,17 +1895,29 @@ impl State { return None; } } - self.own_cursor = Some(OwnCursor { + let arrived = OwnCursor { buffer_id, byte: byte_pos, - }); + }; + let moved = self.own_cursor != Some(arrived); + self.own_cursor = Some(arrived); self.cursor_fresh = self.current_buffer_id == Some(buffer_id); // Session S1 — keep the caret on screen (Q#S2). When the // cursor leaves the visible slice (arrows past an edge, // PageUp/Down), scroll to follow it, re-shape the new // slice, and re-declare the scoped Viewport so the // producer ships spans for what's now visible. - if self.scroll_to_cursor() { + // + // Only when the cursor MOVED. The daemon attaches a + // CursorByte to every frame it produces — including + // the frames our own Viewport sends trigger — so an + // unconditional follow snapped the viewport back to + // a stationary cursor on every minimap jump / scrub + // (and on any wheel scroll past the cursor's screen): + // jump → Viewport → frame + re-announced CursorByte → + // snap, in a loop. Scrolling away from a cursor that + // isn't moving is the user's prerogative. + if moved && self.scroll_to_cursor() { // Pure scroll: retained lines keep their shape // caches; only newly exposed lines shape. self.rebuild_lines_reusing_scroll();