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 3a15574255f..5fa784b9f6b 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 @@ -243,6 +243,21 @@ describe('terminal mouse wheel multiplier', () => { expect(reports).toEqual([0, 0, 0, 1]) }) + it('does not burst-boost rapid trackpad-like pixel deltas', () => { + const state = createTerminalTuiMouseWheelDistanceState() + + const reports = [0, 16, 32, 48].map((timeStamp) => + resolveTerminalTuiMouseWheelReportCount( + { deltaY: 4, deltaMode: DOM_DELTA_PIXEL, timeStamp }, + 1, + state, + { cellHeight: 16 } + ) + ) + + expect(reports).toEqual([0, 0, 0, 1]) + }) + it('resets pending fractional distance when the user changes direction', () => { const state = createTerminalTuiMouseWheelDistanceState() @@ -269,7 +284,7 @@ describe('terminal mouse wheel multiplier', () => { expect(shouldMultiplyTerminalMouseWheel(wheelEvent(), terminalElement(false))).toBe(false) }) - it('leaves trackpad-like pixel scrolling one-to-one', () => { + it('handles trackpad-like TUI pixel scrolling while mouse reporting is active', () => { expect( shouldMultiplyTerminalMouseWheel( wheelEvent({ @@ -278,7 +293,7 @@ describe('terminal mouse wheel multiplier', () => { }), terminalElement() ) - ).toBe(false) + ).toBe(true) }) it('multiplies notched mouse wheel ticks even when Chromium exposes a small pixel delta', () => { @@ -358,6 +373,58 @@ describe('terminal mouse wheel multiplier', () => { expect(shouldMultiplyTerminalMouseWheel(dispatched[0]!, target)).toBe(false) }) + it('replays trackpad-like TUI pixel scrolling with responsive direction reversal', 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 events = [ + [4, 0], + [4, 16], + [4, 32], + [4, 48], + [-4, 64], + [-4, 80], + [-4, 96], + [-4, 112] + ].map(([deltaY, timeStamp]) => { + const event = new TestWheelEvent('wheel', { + bubbles: true, + cancelable: true, + deltaMode: DOM_DELTA_PIXEL, + deltaY + }) as WheelEvent + Object.defineProperty(event, 'timeStamp', { + configurable: true, + value: timeStamp + }) + return event + }) + + for (const event of events) { + expect(handlers[0]?.(event)).toBe(false) + await Promise.resolve() + } + + expect(dispatched.map((entry) => entry.deltaY)).toEqual([1, -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-mouse-wheel.ts b/src/renderer/src/lib/pane-manager/pane-terminal-mouse-wheel.ts index ab192e393f7..f35c31efba7 100644 --- a/src/renderer/src/lib/pane-manager/pane-terminal-mouse-wheel.ts +++ b/src/renderer/src/lib/pane-manager/pane-terminal-mouse-wheel.ts @@ -1,7 +1,6 @@ import type { Terminal } from '@xterm/xterm' import { createTerminalTuiMouseWheelDistanceState, - isDiscreteTerminalTuiWheelEvent, normalizeTerminalTuiMouseWheelMultiplier, resolveTerminalTuiMouseWheelReportCount, resolveTerminalWheelDirection @@ -110,8 +109,7 @@ export function shouldMultiplyTerminalMouseWheel( isReplayedWheelEvent(event) || !terminalElement?.classList.contains(XTERM_MOUSE_REPORTING_CLASS) || event.deltaY === 0 || - event.shiftKey || - !isDiscreteTerminalTuiWheelEvent(event) + event.shiftKey ) { return false } 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 1b92edb390f..68304474e78 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 @@ -67,6 +67,11 @@ function legacyVerticalWheelDelta(event: TerminalTuiWheelEventInput): number | n return null } +function hasDiscreteLegacyWheelDelta(event: TerminalTuiWheelEventInput): boolean { + const legacyDelta = legacyVerticalWheelDelta(event) + return legacyDelta !== null && Math.abs(legacyDelta) >= LEGACY_MOUSE_WHEEL_DELTA_MIN +} + export function isDiscreteTerminalTuiWheelEvent(event: TerminalTuiWheelEventInput): boolean { if ((event.deltaMode ?? DOM_DELTA_PIXEL) !== DOM_DELTA_PIXEL) { return true @@ -76,8 +81,15 @@ export function isDiscreteTerminalTuiWheelEvent(event: TerminalTuiWheelEventInpu return true } - const legacyDelta = legacyVerticalWheelDelta(event) - return legacyDelta !== null && Math.abs(legacyDelta) >= LEGACY_MOUSE_WHEEL_DELTA_MIN + return hasDiscreteLegacyWheelDelta(event) +} + +function canBurstBoostWheelEvent(event: TerminalTuiWheelEventInput): boolean { + if ((event.deltaMode ?? DOM_DELTA_PIXEL) !== DOM_DELTA_PIXEL) { + return true + } + + return hasDiscreteLegacyWheelDelta(event) } function wheelInputTime(event: TerminalTuiWheelEventInput): number | null { @@ -130,6 +142,13 @@ function resolveBurstWheelDistanceRows( state: TerminalTuiMouseWheelDistanceState, distanceRows: number ): number { + if (!canBurstBoostWheelEvent(event)) { + state.fastStreak = 0 + state.lastDistanceRows = null + state.lastInputAt = null + return 0 + } + const currentInputAt = wheelInputTime(event) if (currentInputAt === null) { state.fastStreak = 0