From 25c1a4f5b85ade1f5bec30352c8ebf062cb9233b Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Fri, 3 Jul 2026 01:04:16 -0400 Subject: [PATCH] Cap TUI trackpad report batches Co-authored-by: Orca --- .../pane-terminal-mouse-wheel.test.ts | 53 ++++++++++++++++++- .../pane-terminal-tui-wheel-reports.ts | 31 +++++++++-- 2 files changed, 78 insertions(+), 6 deletions(-) diff --git a/src/renderer/src/lib/pane-manager/pane-terminal-mouse-wheel.test.ts b/src/renderer/src/lib/pane-manager/pane-terminal-mouse-wheel.test.ts index 0e96ca19847..f9031290aa6 100644 --- a/src/renderer/src/lib/pane-manager/pane-terminal-mouse-wheel.test.ts +++ b/src/renderer/src/lib/pane-manager/pane-terminal-mouse-wheel.test.ts @@ -270,7 +270,7 @@ describe('terminal mouse wheel multiplier', () => { ) ) - expect(reports).toEqual([1, 1, 2, 2, 0, 0, 0, 0]) + expect(reports).toEqual([1, 1, 1, 1, 0, 0, 0, 0]) }) it('suppresses alternating trackpad-like momentum after direction changes', () => { @@ -285,7 +285,22 @@ describe('terminal mouse wheel multiplier', () => { ) ) - expect(reports).toEqual([1, 1, 2, 1, 1, 2, 0, 0, 0, 0]) + expect(reports).toEqual([1, 1, 1, 1, 1, 1, 0, 0, 0, 0]) + }) + + it('does not batch multiple reports for one trackpad-like pixel event', () => { + const state = createTerminalTuiMouseWheelDistanceState() + + const reports = [16 * 12, 16 * 12, 16 * 12].map((deltaY, index) => + resolveTerminalTuiMouseWheelReportCount( + { deltaY, deltaMode: DOM_DELTA_PIXEL, timeStamp: index * 16 }, + 1, + state, + { cellHeight: 16 } + ) + ) + + expect(reports).toEqual([1, 1, 1]) }) it('resets pending fractional distance when the user changes direction', () => { @@ -455,6 +470,40 @@ describe('terminal mouse wheel multiplier', () => { expect(dispatched.map((entry) => entry.deltaY)).toEqual([1, -1]) }) + it('does not enqueue a synthetic report batch for a large trackpad-like pixel event', async () => { + vi.stubGlobal('WheelEvent', TestWheelEvent) + const handlers: ((event: WheelEvent) => boolean)[] = [] + const target = Object.assign(new EventTarget(), { + classList: { + contains: (className: string) => className === 'enable-mouse-events' + } + }) as unknown as EventTarget & HTMLElement + const dispatched: WheelEvent[] = [] + target.addEventListener('wheel', (event) => dispatched.push(event as WheelEvent)) + attachTerminalMouseWheelMultiplier( + { + attachCustomWheelEventHandler: (handler) => { + handlers.push(handler) + }, + element: target, + rows: 24 + }, + { getTuiMouseWheelMultiplier: () => 1 } + ) + + const event = new TestWheelEvent('wheel', { + bubbles: true, + cancelable: true, + deltaMode: DOM_DELTA_PIXEL, + deltaY: 16 * 12 + }) as WheelEvent + + expect(handlers[0]?.(event)).toBe(false) + await Promise.resolve() + + expect(dispatched.map((entry) => entry.deltaY)).toEqual([1]) + }) + it('drains resolved TUI wheel reports without a frame-rate cap', async () => { vi.stubGlobal('WheelEvent', TestWheelEvent) const handlers: ((event: WheelEvent) => boolean)[] = [] diff --git a/src/renderer/src/lib/pane-manager/pane-terminal-tui-wheel-reports.ts b/src/renderer/src/lib/pane-manager/pane-terminal-tui-wheel-reports.ts index 905bb6b81ec..e10f7a80c64 100644 --- a/src/renderer/src/lib/pane-manager/pane-terminal-tui-wheel-reports.ts +++ b/src/renderer/src/lib/pane-manager/pane-terminal-tui-wheel-reports.ts @@ -106,8 +106,7 @@ function canBurstBoostWheelEvent(event: TerminalTuiWheelEventInput): boolean { function isTrackpadLikePixelWheelEvent(event: TerminalTuiWheelEventInput): boolean { return ( - (event.deltaMode ?? DOM_DELTA_PIXEL) === DOM_DELTA_PIXEL && - !isDiscreteTerminalTuiWheelEvent(event) + (event.deltaMode ?? DOM_DELTA_PIXEL) === DOM_DELTA_PIXEL && !hasDiscreteLegacyWheelDelta(event) ) } @@ -264,6 +263,29 @@ function shouldSuppressTrackpadMomentumTail( return false } +function resolveTrackpadPixelWheelReportCount( + event: TerminalTuiWheelEventInput, + state: TerminalTuiMouseWheelDistanceState, + distanceRows: number +): number | null { + if (!isTrackpadLikePixelWheelEvent(event)) { + return null + } + + if (shouldSuppressTrackpadMomentumTail(event, state, distanceRows)) { + return 0 + } + + const totalRows = state.pendingRows + distanceRows + if (totalRows < 1) { + state.pendingRows = totalRows + return 0 + } + + state.pendingRows = 0 + return 1 +} + export function normalizeTerminalTuiMouseWheelMultiplier(value: number | undefined): number { if (typeof value !== 'number' || !Number.isFinite(value)) { return TERMINAL_TUI_MOUSE_WHEEL_MULTIPLIER @@ -292,8 +314,9 @@ export function resolveTerminalTuiMouseWheelReportCount( state.pendingDirection = direction const distanceRows = resolveWheelDistanceRows(event, metrics) - if (shouldSuppressTrackpadMomentumTail(event, state, distanceRows)) { - return 0 + const trackpadReportCount = resolveTrackpadPixelWheelReportCount(event, state, distanceRows) + if (trackpadReportCount !== null) { + return trackpadReportCount } const rows =