From 8537fd01062e53ca51e67ccfea5395ee606d5200 Mon Sep 17 00:00:00 2001 From: Steffen Becker Date: Thu, 20 Aug 2026 12:26:03 +0200 Subject: [PATCH] fix: zooming the page grid keeps the view anchored The grid snapped the scroll to the top of the first row on every zoom, losing the reader's place whenever it sat mid-row and ignoring the pointer anchor of a trackpad zoom. It now uses the same anchor logic as a single pane, measured against the grid canvas: the pointer for a wheel zoom, else the viewport top; firstRow is re-read after the write so the page cells re-aim immediately. Measured drift < 1px across a 92% -> 194% wheel burst. --- src/lib/components/Editor.svelte | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/src/lib/components/Editor.svelte b/src/lib/components/Editor.svelte index e90891c..521a67b 100644 --- a/src/lib/components/Editor.svelte +++ b/src/lib/components/Editor.svelte @@ -788,9 +788,22 @@ import { EMPTY_PAGE_DECOR, type PageDecor } from '../storage/pageDecor'; } const s = prevZoom / 100; prevZoom = z; - // The grid re-lays out around its rows, so the row at the top is the anchor — - // held in the post-effect, where the new geometry is on the DOM. - if (multiPage) return; + // The grid scrolls as one canvas, so the anchor lives in canvas space: the + // pointer for a wheel zoom, else the top of the viewport, like a single pane. + if (multiPage) { + const container = scrollers[0]; + if (!container || !canvasEl) { pendingAnchorDoc = []; return; } + const editorRect = container.getBoundingClientRect(); + const canvasRect = canvasEl.getBoundingClientRect(); + const screenX = wheel ? wheel.x : canvasRect.left; + const screenY = wheel ? wheel.y : editorRect.top; + pendingAnchorDoc = [{ + x: (screenX - canvasRect.left) / s, + y: (screenY - canvasRect.top) / s, + screenX, screenY, + }]; + return; + } pendingAnchorDoc = Array.from({ length: paneCount }, (_, i) => i).map((i) => { const container = scrollers[i]; const paper = papers[i]; @@ -814,7 +827,16 @@ import { EMPTY_PAGE_DECOR, type PageDecor } from '../storage/pageDecor'; // Untracked: the scroll this writes comes back as a firstRow change, and an // effect that reads what its own scroll writes never stops. if (multiPage) { - untrack(() => scrollGridToPage(firstRow * gridCols + 1)); + const anchor = pendingAnchorDoc[0]; + pendingAnchorDoc = []; + const container = scrollers[0]; + if (anchor && container && canvasEl) { + const z = appliedZoom / 100; + const rect = canvasEl.getBoundingClientRect(); + container.scrollLeft += rect.left + anchor.x * z - anchor.screenX; + container.scrollTop += rect.top + anchor.y * z - anchor.screenY; + untrack(readFirstRow); + } return; } const anchors = pendingAnchorDoc;