[eric] canvas: accelerated mouse wheels (fractional deltaY) classify as mouse, so the Zoom/Scroll toggle finally bites on real hardware (3rd report)

This commit is contained in:
ciregenz
2026-08-16 14:36:53 -07:00
parent 267f692410
commit fd081de06e
4 changed files with 71 additions and 10 deletions
+1
View File
@@ -95,6 +95,7 @@ P_RELEASES: List[ReleaseNote] = [
"Finished apps opened from the dock no longer sit on \"Starting preview\" forever. Apps served straight from their built files have no server process by design, and the preview was waiting for one that would never exist.",
"A chat that was cut off mid-answer now says so right on the board with an amber \"Stopped mid-task, click to resume\" chip, instead of looking idle until you open it and hunt for the resume button.",
"An app or browser card whose page process dies now reloads itself instead of sitting as a solid black rectangle. The crash fired no load event at all, so nothing ever repainted it.",
"The mouse-wheel Zoom/Scroll setting works on real mice now. Accelerated wheels (Magic Mouse, Logitech smooth scrolling) report fractional scroll amounts that were being mistaken for a trackpad, so the wheel always panned no matter what the setting said.",
"Heavy sessions no longer vanish without a trace. When memory climbs past the safe line the app now sheds weight itself: preview thumbnails pause and refetchable caches drop, instead of growing until the operating system kills it mid-task.",
],
),
@@ -0,0 +1,38 @@
// The zoom/scroll toggle's 3rd field report: physical wheels did nothing different when the
// setting flipped, while every synthetic harness verification passed. Gap = this classifier:
// macOS wheel acceleration emits NON-INTEGER deltaY for real notches, and the old rule sent any
// fractional delta to the trackpad branch (always pan) regardless of size. Synthetic wheels have
// integer deltas, so harnesses could never see it. These pin the traces for both device families.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { classifyWheelDevice } from './classifyWheelDevice';
const TRACKPAD = true;
const MOUSE = false;
test('synthetic harness wheel (integer, legacy 120-multiple): mouse', () => {
assert.equal(classifyWheelDevice({ deltaMode: 0, wheelDeltaY: -120 }, 0, 40), MOUSE);
});
test('accelerated physical wheel notch (large FRACTIONAL deltaY): mouse — the 3rd-report fix', () => {
// Magic Mouse / Logitech smooth scrolling: wheelDeltaY not a 120-multiple, deltaY fractional.
assert.equal(classifyWheelDevice({ deltaMode: 0, wheelDeltaY: -173 }, 0, 57.999755859375), MOUSE);
assert.equal(classifyWheelDevice({ deltaMode: 0 }, 0, 88.5), MOUSE);
});
test('two-finger trackpad drift (small fractional, dx jitter): trackpad', () => {
assert.equal(classifyWheelDevice({ deltaMode: 0, wheelDeltaY: -9 }, 0.5, 3.2), TRACKPAD);
assert.equal(classifyWheelDevice({ deltaMode: 0 }, 0, 2.75), TRACKPAD);
});
test('slow trackpad vertical-only small integers stay trackpad (old conservative verdict)', () => {
assert.equal(classifyWheelDevice({ deltaMode: 0, wheelDeltaY: -9 }, 0, 3), TRACKPAD);
});
test('line-mode deltas (Windows wheel config) are mouse', () => {
assert.equal(classifyWheelDevice({ deltaMode: 1 }, 0, 3), MOUSE);
});
test('any sideways component is fingers on glass', () => {
assert.equal(classifyWheelDevice({ deltaMode: 0 }, 12, 80), TRACKPAD);
});
@@ -0,0 +1,29 @@
// Is this wheel event a trackpad or a mouse wheel? The zoom/scroll toggle hangs off this verdict,
// and it was wrong for accelerated mouse wheels (3rd field report): macOS acceleration and
// smooth-scroll drivers (Magic Mouse, Logitech Options) emit NON-INTEGER deltaY for real wheel
// notches, and the old rule sent every non-integer delta to the trackpad branch regardless of
// size, so the wheel always panned and the setting looked dead. Synthetic harness wheels have
// integer deltas, which is exactly why two harness verifications passed while every physical
// wheel failed. Rule now: a chunky vertical-only delta is a wheel notch whatever its fraction;
// only SMALL deltas (accelerated-to-nothing notches vs slow two-finger drift) fall back to the
// integer heuristic.
export interface WheelDeviceEvent {
deltaMode: number;
wheelDeltaY?: number;
}
export function classifyWheelDevice(e: WheelDeviceEvent, dx: number, dy: number): boolean {
if (e.deltaMode !== 0) return false;
const legacy = e.wheelDeltaY ?? 0;
// Chromium stamps discrete notches with legacy wheelDeltaY = ticks*120; definitive when present.
if (legacy !== 0 && legacy % 120 === 0 && dx === 0) return false;
// Any sideways component means fingers on glass; wheels (tilt aside, handled upstream by the
// dominant-axis branch) are vertical-only.
if (dx !== 0) return true;
// Chunky vertical-only = a wheel notch, fractional or not. Trackpad two-finger streams open
// with small deltas, and stream continuity upstream keeps a gesture's first verdict. THIS line
// is the fix: the old rule sent large fractional deltas to the trackpad branch.
if (Math.abs(dy) >= 40) return false;
// Small vertical-only deltas keep the old conservative verdict: glass, whichever their fraction.
return true;
}
@@ -8,6 +8,7 @@ import { getWebview } from '@/shared/browserRegistry';
import { applyBrowserZoom } from '@/shared/browserZoom';
import { syncTiledGeometry } from '../../canvas/tiledGeometry';
import { revealZoom, REVEAL_MIN_ZOOM } from '../../canvas/revealZoom';
import { classifyWheelDevice } from './classifyWheelDevice';
// Surfaces that are WINDOWS, not canvas cards: they behave like an OS window, so a wheel inside one
// belongs to it whether or not you clicked in first. Canvas cards (agent, browser, view) keep the
@@ -39,7 +40,6 @@ const WHEEL_ZOOM_DELTA_CAP = 24;
// big integer no-X delta = mouse); events inside a burst inherit the previous verdict because a
// physical device can't change mid-gesture, which keeps momentum-glide events panning.
const WHEEL_STREAM_GAP_MS = 150;
const MOUSE_NOTCH_MIN_DELTA = 40;
// Maps the 1 to 100 user setting to an internal multiplier. Recentered twice (Eric): 2026-07-24
// made the old max the new 50, and 2026-08-08 turned it up again so 50 feels like the old 75; the
@@ -301,15 +301,8 @@ export function useCanvasControls(
const inStream = e.timeStamp - lastWheelDeviceAt < WHEEL_STREAM_GAP_MS;
lastWheelDeviceAt = e.timeStamp;
if (inStream) return lastWheelWasTrackpad;
// Chromium stamps discrete wheel notches with legacy wheelDeltaY = ticks*120; trackpads report 3x the pixel delta. This catches slow mouse notches that macOS acceleration shrinks below any pixel threshold.
const legacy = (e as WheelEvent & { wheelDeltaY?: number }).wheelDeltaY ?? 0;
let trackpad: boolean;
if (e.deltaMode !== 0) trackpad = false;
else if (legacy !== 0 && legacy % 120 === 0 && dx === 0) trackpad = false;
else if (dx !== 0 || !Number.isInteger(dy)) trackpad = true;
else trackpad = Math.abs(dy) < MOUSE_NOTCH_MIN_DELTA;
lastWheelWasTrackpad = trackpad;
return trackpad;
lastWheelWasTrackpad = classifyWheelDevice(e as WheelEvent & { wheelDeltaY?: number }, dx, dy);
return lastWheelWasTrackpad;
};
const flushWheel = () => {