Ctrl and the wheel over a PDF
The same gesture Aether has, whose comment already claimed InkNote had it. Registered by hand rather than through onWheel, because React attaches wheel listeners passively and a passive listener may not call preventDefault — without which the browser zooms the whole application instead of the document. What stays still depends on how the zoom was asked for, and the two answers differ: the wheel holds the point under the pointer, the buttons hold the page being read. The buttons held nothing at all before, so three presses of + left you somewhere else in the file; that came out of building this and is fixed with it. The checks are the step, that the default really was prevented, and that a point measured on the page is still under the pointer afterwards. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8f8fecfb96
commit
580eed8992
4 files changed
+211
-7
No files matched your search
@@ -500,12 +500,93 @@ export function PdfViewer({ note, dark, onUpdate, onPagesReady, notesToggle, foc
|
||||
}
|
||||
};
|
||||
|
||||
/* ── zooming, and what stays still while it happens ──
|
||||
*
|
||||
* A zoom changes how tall the document is, and the scroll position means
|
||||
* something different afterwards. Left alone, every step drifts: press + three
|
||||
* times and you are somewhere else in the document.
|
||||
*
|
||||
* What should stay put depends on how the zoom was asked for. From the
|
||||
* buttons, it is the page you are reading — the request is "show me this page
|
||||
* larger". From the wheel, it is the point under the pointer: you are pointing
|
||||
* at a word and asking for *that* to grow, and having the page jump to its top
|
||||
* instead is exactly what makes a viewer feel unruly.
|
||||
*/
|
||||
const keepAnchor = useRef(null);
|
||||
const keepPage = useRef(null);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const el = scrollRef.current;
|
||||
if (!el) return;
|
||||
const anchor = keepAnchor.current;
|
||||
if (anchor) {
|
||||
keepAnchor.current = null;
|
||||
// The document grew by this much, so the point that was under the pointer
|
||||
// is now that much further from the top.
|
||||
const ratio = pageWidth / anchor.from;
|
||||
el.scrollTop = Math.max(0, anchor.py * ratio - anchor.cy);
|
||||
el.scrollLeft = Math.max(0, anchor.px * ratio - anchor.cx);
|
||||
return;
|
||||
}
|
||||
const page = keepPage.current;
|
||||
if (page === null) return;
|
||||
keepPage.current = null;
|
||||
const target = innerRef.current?.querySelector(`[data-pdf-page="${page}"]`);
|
||||
// Not `goToPage`: that one glides, which is right for "take me to page 7"
|
||||
// and wrong for holding still — the glide is a jump with a delay on it.
|
||||
if (target) el.scrollTop = Math.max(0, target.offsetTop - 8);
|
||||
}, [pageWidth]);
|
||||
|
||||
/** The next step up or down, or nothing at the end of the ladder. */
|
||||
const nextZoom = (from, dir) =>
|
||||
dir > 0 ? ZOOM_STEPS.find((s) => s > from + 0.001) : [...ZOOM_STEPS].reverse().find((s) => s < from - 0.001);
|
||||
|
||||
const stepZoom = (dir) => {
|
||||
const i = ZOOM_STEPS.indexOf(zoom);
|
||||
const next = ZOOM_STEPS[Math.min(ZOOM_STEPS.length - 1, Math.max(0, (i < 0 ? 2 : i) + dir))];
|
||||
if (next) setZoom(next);
|
||||
const next = nextZoom(zoom, dir);
|
||||
if (!next) return;
|
||||
keepPage.current = pageNo;
|
||||
setZoom(next);
|
||||
};
|
||||
|
||||
/**
|
||||
* Ctrl (or ⌘) and the wheel, as in every other viewer.
|
||||
*
|
||||
* Registered by hand rather than with `onWheel`: React attaches wheel
|
||||
* listeners passively, and a passive listener may not call `preventDefault`.
|
||||
* Without that call the *browser* zooms the whole page instead — toolbar,
|
||||
* sidebar and all — which is not what the gesture means over a document.
|
||||
*
|
||||
* A trackpad pinch arrives here as the same event, so it works too without
|
||||
* anything further.
|
||||
*/
|
||||
useEffect(() => {
|
||||
const el = scrollRef.current;
|
||||
if (!el || state.status !== "ready") return;
|
||||
const onWheel = (e) => {
|
||||
if (!e.ctrlKey && !e.metaKey) return;
|
||||
if (!e.deltaY) return; // a sideways wheel is not a zoom
|
||||
e.preventDefault();
|
||||
const next = nextZoom(zoom, e.deltaY < 0 ? 1 : -1);
|
||||
if (!next) return;
|
||||
const r = el.getBoundingClientRect();
|
||||
const cx = e.clientX - r.left;
|
||||
const cy = e.clientY - r.top;
|
||||
keepAnchor.current = {
|
||||
px: el.scrollLeft + cx,
|
||||
py: el.scrollTop + cy,
|
||||
cx,
|
||||
cy,
|
||||
// Measured in page width rather than in the zoom factor: the fitted
|
||||
// width is what the pages are actually drawn from, and it changes on
|
||||
// its own when the window is resized or the margin column appears.
|
||||
from: pageWidth,
|
||||
};
|
||||
setZoom(next);
|
||||
};
|
||||
el.addEventListener("wheel", onWheel, { passive: false });
|
||||
return () => el.removeEventListener("wheel", onWheel);
|
||||
}, [zoom, pageWidth, state.status]);
|
||||
|
||||
const barButton = {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
@@ -724,11 +805,16 @@ export function PdfViewer({ note, dark, onUpdate, onPagesReady, notesToggle, foc
|
||||
{notesToggle}
|
||||
|
||||
<div style={{ marginLeft: "auto", display: "flex", alignItems: "center", gap: 6 }}>
|
||||
<button onClick={() => stepZoom(-1)} title="Zoom out" style={barButton}>
|
||||
{/* The gesture is named in the tooltip, because a viewer that answers
|
||||
Ctrl + wheel and never says so is only helping the people who
|
||||
already tried it. */}
|
||||
<button onClick={() => stepZoom(-1)} title="Zoom out (Ctrl + wheel)" style={barButton}>
|
||||
−
|
||||
</button>
|
||||
<span style={{ minWidth: 42, textAlign: "center" }}>{Math.round(zoom * 100)}%</span>
|
||||
<button onClick={() => stepZoom(1)} title="Zoom in" style={barButton}>
|
||||
<span data-pdf-zoom={zoom} style={{ minWidth: 42, textAlign: "center" }}>
|
||||
{Math.round(zoom * 100)}%
|
||||
</span>
|
||||
<button onClick={() => stepZoom(1)} title="Zoom in (Ctrl + wheel)" style={barButton}>
|
||||
+
|
||||
</button>
|
||||
|
||||
|
||||
Reference in new issue
Block a user