mirror of
https://github.com/stablyai/orca.git
synced 2026-10-16 16:03:26 +00:00
@@ -2,7 +2,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
TERMINAL_TUI_MOUSE_WHEEL_MULTIPLIER,
|
||||
attachTerminalMouseWheelMultiplier,
|
||||
createTerminalTuiMouseWheelAccelerationState,
|
||||
createTerminalTuiMouseWheelDistanceState,
|
||||
normalizeTerminalTuiMouseWheelMultiplier,
|
||||
resolveTerminalTuiMouseWheelReportCount,
|
||||
shouldMultiplyTerminalMouseWheel
|
||||
@@ -90,47 +90,175 @@ describe('terminal mouse wheel multiplier', () => {
|
||||
expect(normalizeTerminalTuiMouseWheelMultiplier(20)).toBe(10)
|
||||
})
|
||||
|
||||
it('keeps deliberate TUI wheel ticks precise even with a fast max', () => {
|
||||
const state = createTerminalTuiMouseWheelAccelerationState()
|
||||
it('keeps deliberate TUI wheel ticks precise at the one-report setting', () => {
|
||||
const state = createTerminalTuiMouseWheelDistanceState()
|
||||
|
||||
const reports = [0, 200, 400, 600].map((timeStamp) =>
|
||||
resolveTerminalTuiMouseWheelReportCount({ deltaY: 12, timeStamp }, 5, state)
|
||||
const reports = [0, 200, 400, 600].map(() =>
|
||||
resolveTerminalTuiMouseWheelReportCount(
|
||||
{ deltaY: 12, deltaMode: DOM_DELTA_PIXEL, wheelDeltaY: -120 },
|
||||
1,
|
||||
state,
|
||||
{ cellHeight: 16 }
|
||||
)
|
||||
)
|
||||
|
||||
expect(reports).toEqual([1, 1, 1, 1])
|
||||
})
|
||||
|
||||
it('keeps the one-report setting precise even during fast TUI wheel bursts', () => {
|
||||
const state = createTerminalTuiMouseWheelAccelerationState()
|
||||
it('scales notched TUI wheel ticks by the configured multiplier', () => {
|
||||
const state = createTerminalTuiMouseWheelDistanceState()
|
||||
|
||||
const reports = [0, 50, 100, 150, 200, 250].map((timeStamp) =>
|
||||
resolveTerminalTuiMouseWheelReportCount({ deltaY: 12, timeStamp }, 1, state)
|
||||
const reports = [0, 50, 100, 150].map(() =>
|
||||
resolveTerminalTuiMouseWheelReportCount(
|
||||
{ deltaY: 12, deltaMode: DOM_DELTA_PIXEL, wheelDeltaY: -120 },
|
||||
5,
|
||||
state,
|
||||
{ cellHeight: 16 }
|
||||
)
|
||||
)
|
||||
|
||||
expect(reports).toEqual([1, 1, 1, 1, 1, 1])
|
||||
expect(reports).toEqual([5, 5, 5, 5])
|
||||
})
|
||||
|
||||
it('accelerates repeated same-direction TUI wheel ticks with a front-loaded curve', () => {
|
||||
const state = createTerminalTuiMouseWheelAccelerationState()
|
||||
it('keeps paced 1x TUI wheel ticks precise', () => {
|
||||
const state = createTerminalTuiMouseWheelDistanceState()
|
||||
|
||||
const reports = [0, 50, 100, 150, 200, 250].map((timeStamp) =>
|
||||
resolveTerminalTuiMouseWheelReportCount({ deltaY: 12, timeStamp }, 5, state)
|
||||
const reports = [0, 80, 160, 240].map((timeStamp) =>
|
||||
resolveTerminalTuiMouseWheelReportCount(
|
||||
{ deltaY: 12, deltaMode: DOM_DELTA_PIXEL, wheelDeltaY: -120, timeStamp },
|
||||
1,
|
||||
state,
|
||||
{ cellHeight: 16 }
|
||||
)
|
||||
)
|
||||
|
||||
expect(reports).toEqual([1, 5, 5, 5, 5, 5])
|
||||
expect(reports).toEqual([1, 1, 1, 1])
|
||||
})
|
||||
|
||||
it('resets TUI wheel acceleration when the user changes direction', () => {
|
||||
const state = createTerminalTuiMouseWheelAccelerationState()
|
||||
it('adds a burst boost for very fast 1x TUI wheel scrolling', () => {
|
||||
const state = createTerminalTuiMouseWheelDistanceState()
|
||||
|
||||
const reports = [0, 16, 32, 48, 64].map((timeStamp) =>
|
||||
resolveTerminalTuiMouseWheelReportCount(
|
||||
{ deltaY: 12, deltaMode: DOM_DELTA_PIXEL, wheelDeltaY: -120, timeStamp },
|
||||
1,
|
||||
state,
|
||||
{ cellHeight: 16 }
|
||||
)
|
||||
)
|
||||
|
||||
expect(reports).toEqual([1, 1, 3, 3, 4])
|
||||
})
|
||||
|
||||
it('uses a hotter compressed wheel distance curve for larger TUI wheel movements', () => {
|
||||
const state = createTerminalTuiMouseWheelDistanceState()
|
||||
|
||||
const reports = [
|
||||
resolveTerminalTuiMouseWheelReportCount({ deltaY: 12, timeStamp: 0 }, 5, state),
|
||||
resolveTerminalTuiMouseWheelReportCount({ deltaY: 12, timeStamp: 50 }, 5, state),
|
||||
resolveTerminalTuiMouseWheelReportCount({ deltaY: -12, timeStamp: 100 }, 5, state),
|
||||
resolveTerminalTuiMouseWheelReportCount({ deltaY: -12, timeStamp: 150 }, 5, state)
|
||||
resolveTerminalTuiMouseWheelReportCount(
|
||||
{ deltaY: 16, deltaMode: DOM_DELTA_PIXEL, wheelDeltaY: -120 },
|
||||
1,
|
||||
state,
|
||||
{ cellHeight: 16 }
|
||||
),
|
||||
resolveTerminalTuiMouseWheelReportCount(
|
||||
{ deltaY: 16 * 12, deltaMode: DOM_DELTA_PIXEL, wheelDeltaY: -120 * 12 },
|
||||
1,
|
||||
state,
|
||||
{ cellHeight: 16 }
|
||||
)
|
||||
]
|
||||
|
||||
expect(reports).toEqual([1, 5, 1, 5])
|
||||
expect(reports).toEqual([1, 6])
|
||||
})
|
||||
|
||||
it('caps a single accelerated TUI wheel event before it becomes a huge jump', () => {
|
||||
const state = createTerminalTuiMouseWheelDistanceState()
|
||||
|
||||
const reports = resolveTerminalTuiMouseWheelReportCount(
|
||||
{ deltaY: 16 * 200, deltaMode: DOM_DELTA_PIXEL, wheelDeltaY: -120 * 200 },
|
||||
1,
|
||||
state,
|
||||
{ cellHeight: 16 }
|
||||
)
|
||||
|
||||
expect(reports).toBe(6)
|
||||
})
|
||||
|
||||
it('lets aggressive repeated TUI wheel events exceed the single-event cap', () => {
|
||||
const state = createTerminalTuiMouseWheelDistanceState()
|
||||
|
||||
const reports = [0, 16, 32, 48, 64].map((timeStamp) =>
|
||||
resolveTerminalTuiMouseWheelReportCount(
|
||||
{
|
||||
deltaY: 16 * 200,
|
||||
deltaMode: DOM_DELTA_PIXEL,
|
||||
timeStamp,
|
||||
wheelDeltaY: -120 * 200
|
||||
},
|
||||
1,
|
||||
state,
|
||||
{ cellHeight: 16 }
|
||||
)
|
||||
)
|
||||
|
||||
expect(reports).toEqual([6, 6, 8, 8, 9])
|
||||
})
|
||||
|
||||
it('does not carry burst boost into a decaying momentum tail', () => {
|
||||
const state = createTerminalTuiMouseWheelDistanceState()
|
||||
|
||||
const reports = [
|
||||
[200, 0],
|
||||
[200, 16],
|
||||
[200, 32],
|
||||
[80, 48],
|
||||
[20, 64],
|
||||
[5, 80]
|
||||
].map(([rows, timeStamp]) =>
|
||||
resolveTerminalTuiMouseWheelReportCount(
|
||||
{
|
||||
deltaY: 16 * rows,
|
||||
deltaMode: DOM_DELTA_PIXEL,
|
||||
timeStamp,
|
||||
wheelDeltaY: -120 * rows
|
||||
},
|
||||
1,
|
||||
state,
|
||||
{ cellHeight: 16 }
|
||||
)
|
||||
)
|
||||
|
||||
expect(reports).toEqual([6, 6, 8, 6, 6, 4])
|
||||
})
|
||||
|
||||
it('retains fractional trackpad distance until it reaches a full row', () => {
|
||||
const state = createTerminalTuiMouseWheelDistanceState()
|
||||
|
||||
const reports = [4, 4, 4, 4].map((deltaY) =>
|
||||
resolveTerminalTuiMouseWheelReportCount({ deltaY, deltaMode: DOM_DELTA_PIXEL }, 1, state, {
|
||||
cellHeight: 16
|
||||
})
|
||||
)
|
||||
|
||||
expect(reports).toEqual([0, 0, 0, 1])
|
||||
})
|
||||
|
||||
it('resets pending fractional distance when the user changes direction', () => {
|
||||
const state = createTerminalTuiMouseWheelDistanceState()
|
||||
|
||||
const reports = [
|
||||
resolveTerminalTuiMouseWheelReportCount({ deltaY: 4, deltaMode: DOM_DELTA_PIXEL }, 1, state, {
|
||||
cellHeight: 16
|
||||
}),
|
||||
resolveTerminalTuiMouseWheelReportCount(
|
||||
{ deltaY: -12, deltaMode: DOM_DELTA_PIXEL },
|
||||
1,
|
||||
state,
|
||||
{ cellHeight: 16 }
|
||||
)
|
||||
]
|
||||
|
||||
expect(reports).toEqual([0, 0])
|
||||
})
|
||||
|
||||
it('multiplies discrete wheel events when mouse reporting is active', () => {
|
||||
@@ -204,7 +332,8 @@ describe('terminal mouse wheel multiplier', () => {
|
||||
attachCustomWheelEventHandler: (handler) => {
|
||||
handlers.push(handler)
|
||||
},
|
||||
element: target
|
||||
element: target,
|
||||
rows: 24
|
||||
},
|
||||
{ getTuiMouseWheelMultiplier: () => 1 }
|
||||
)
|
||||
@@ -229,10 +358,9 @@ describe('terminal mouse wheel multiplier', () => {
|
||||
expect(shouldMultiplyTerminalMouseWheel(dispatched[0]!, target)).toBe(false)
|
||||
})
|
||||
|
||||
it('paces accelerated TUI wheel reports after the immediate first report', async () => {
|
||||
it('drains resolved TUI wheel reports without a frame-rate cap', async () => {
|
||||
vi.stubGlobal('WheelEvent', TestWheelEvent)
|
||||
const handlers: ((event: WheelEvent) => boolean)[] = []
|
||||
const scheduledFrames: (() => void)[] = []
|
||||
const target = Object.assign(new EventTarget(), {
|
||||
classList: {
|
||||
contains: (className: string) => className === 'enable-mouse-events'
|
||||
@@ -245,11 +373,11 @@ describe('terminal mouse wheel multiplier', () => {
|
||||
attachCustomWheelEventHandler: (handler) => {
|
||||
handlers.push(handler)
|
||||
},
|
||||
element: target
|
||||
element: target,
|
||||
rows: 24
|
||||
},
|
||||
{
|
||||
getTuiMouseWheelMultiplier: () => 3,
|
||||
scheduleWheelReplayFrame: (callback) => scheduledFrames.push(callback)
|
||||
getTuiMouseWheelMultiplier: () => 3
|
||||
}
|
||||
)
|
||||
const firstEvent = new TestWheelEvent('wheel', {
|
||||
@@ -283,14 +411,10 @@ describe('terminal mouse wheel multiplier', () => {
|
||||
|
||||
expect(handlers[0]?.(firstEvent)).toBe(false)
|
||||
await Promise.resolve()
|
||||
expect(dispatched).toHaveLength(1)
|
||||
expect(dispatched).toHaveLength(3)
|
||||
|
||||
expect(handlers[0]?.(secondEvent)).toBe(false)
|
||||
await Promise.resolve()
|
||||
expect(dispatched).toHaveLength(2)
|
||||
expect(scheduledFrames).toHaveLength(1)
|
||||
|
||||
scheduledFrames.shift()?.()
|
||||
expect(dispatched).toHaveLength(4)
|
||||
expect(dispatched).toHaveLength(6)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,42 +1,39 @@
|
||||
import type { Terminal } from '@xterm/xterm'
|
||||
import {
|
||||
createTerminalTuiMouseWheelDistanceState,
|
||||
isDiscreteTerminalTuiWheelEvent,
|
||||
normalizeTerminalTuiMouseWheelMultiplier,
|
||||
resolveTerminalTuiMouseWheelReportCount,
|
||||
resolveTerminalWheelDirection
|
||||
} from './pane-terminal-tui-wheel-reports'
|
||||
import type { TerminalTuiMouseWheelDistanceState } from './pane-terminal-tui-wheel-reports'
|
||||
|
||||
export {
|
||||
TERMINAL_TUI_MOUSE_WHEEL_MULTIPLIER,
|
||||
TERMINAL_TUI_MOUSE_WHEEL_MULTIPLIER_MAX,
|
||||
TERMINAL_TUI_MOUSE_WHEEL_MULTIPLIER_MIN,
|
||||
createTerminalTuiMouseWheelDistanceState,
|
||||
normalizeTerminalTuiMouseWheelMultiplier,
|
||||
resolveTerminalTuiMouseWheelReportCount
|
||||
} from './pane-terminal-tui-wheel-reports'
|
||||
export type { TerminalTuiMouseWheelDistanceState } from './pane-terminal-tui-wheel-reports'
|
||||
|
||||
const XTERM_MOUSE_REPORTING_CLASS = 'enable-mouse-events'
|
||||
const REPLAYED_WHEEL_EVENT_PROPERTY = '__orcaReplayedTerminalWheelEvent'
|
||||
const DOM_DELTA_PIXEL = 0
|
||||
const DOM_DELTA_LINE = 1
|
||||
const DISCRETE_PIXEL_WHEEL_DELTA_MIN = 50
|
||||
const LEGACY_MOUSE_WHEEL_DELTA_MIN = 100
|
||||
const TUI_WHEEL_ACCELERATION_WINDOW_MS = 140
|
||||
const TUI_WHEEL_REPORTS_PER_FRAME = 4
|
||||
|
||||
export const TERMINAL_TUI_MOUSE_WHEEL_MULTIPLIER = 1
|
||||
export const TERMINAL_TUI_MOUSE_WHEEL_MULTIPLIER_MIN = 1
|
||||
export const TERMINAL_TUI_MOUSE_WHEEL_MULTIPLIER_MAX = 10
|
||||
|
||||
type TerminalWheelTarget = Pick<Terminal, 'attachCustomWheelEventHandler' | 'element'>
|
||||
type TerminalWheelTarget = Pick<Terminal, 'attachCustomWheelEventHandler' | 'element' | 'rows'>
|
||||
|
||||
type TerminalMouseWheelMultiplierOptions = {
|
||||
getTuiMouseWheelMultiplier?: () => number | undefined
|
||||
scheduleWheelReplayFrame?: (callback: () => void) => void
|
||||
}
|
||||
|
||||
type ReplayedWheelEvent = WheelEvent & {
|
||||
[REPLAYED_WHEEL_EVENT_PROPERTY]?: boolean
|
||||
}
|
||||
|
||||
type WheelEventWithLegacyDelta = WheelEvent & {
|
||||
wheelDelta?: number
|
||||
wheelDeltaY?: number
|
||||
}
|
||||
|
||||
export type TerminalTuiMouseWheelAccelerationState = {
|
||||
lastDirection: -1 | 0 | 1
|
||||
lastInputAt: number | null
|
||||
streak: number
|
||||
}
|
||||
|
||||
type TerminalTuiMouseWheelReplayState = {
|
||||
acceleration: TerminalTuiMouseWheelAccelerationState
|
||||
distance: TerminalTuiMouseWheelDistanceState
|
||||
drainScheduled: boolean
|
||||
pendingDirection: -1 | 0 | 1
|
||||
pendingEvent: WheelEvent | null
|
||||
@@ -44,17 +41,9 @@ type TerminalTuiMouseWheelReplayState = {
|
||||
pendingTarget: EventTarget | null
|
||||
}
|
||||
|
||||
export function createTerminalTuiMouseWheelAccelerationState(): TerminalTuiMouseWheelAccelerationState {
|
||||
return {
|
||||
lastDirection: 0,
|
||||
lastInputAt: null,
|
||||
streak: 0
|
||||
}
|
||||
}
|
||||
|
||||
function createTerminalTuiMouseWheelReplayState(): TerminalTuiMouseWheelReplayState {
|
||||
return {
|
||||
acceleration: createTerminalTuiMouseWheelAccelerationState(),
|
||||
distance: createTerminalTuiMouseWheelDistanceState(),
|
||||
drainScheduled: false,
|
||||
pendingDirection: 0,
|
||||
pendingEvent: null,
|
||||
@@ -74,45 +63,6 @@ function markReplayedWheelEvent(event: WheelEvent): void {
|
||||
})
|
||||
}
|
||||
|
||||
function legacyVerticalWheelDelta(event: WheelEvent): number | null {
|
||||
const wheelEvent = event as WheelEventWithLegacyDelta
|
||||
if (typeof wheelEvent.wheelDeltaY === 'number' && Number.isFinite(wheelEvent.wheelDeltaY)) {
|
||||
return wheelEvent.wheelDeltaY
|
||||
}
|
||||
if (typeof wheelEvent.wheelDelta === 'number' && Number.isFinite(wheelEvent.wheelDelta)) {
|
||||
return wheelEvent.wheelDelta
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function isDiscreteWheelEvent(event: WheelEvent): boolean {
|
||||
if (event.deltaMode !== DOM_DELTA_PIXEL) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (Math.abs(event.deltaY) >= DISCRETE_PIXEL_WHEEL_DELTA_MIN) {
|
||||
return true
|
||||
}
|
||||
|
||||
const legacyDelta = legacyVerticalWheelDelta(event)
|
||||
return legacyDelta !== null && Math.abs(legacyDelta) >= LEGACY_MOUSE_WHEEL_DELTA_MIN
|
||||
}
|
||||
|
||||
function wheelDirection(event: Pick<WheelEvent, 'deltaY'>): -1 | 1 {
|
||||
return event.deltaY < 0 ? -1 : 1
|
||||
}
|
||||
|
||||
function wheelInputTime(event: Pick<WheelEvent, 'timeStamp'>): number {
|
||||
if (typeof event.timeStamp === 'number' && Number.isFinite(event.timeStamp)) {
|
||||
return event.timeStamp
|
||||
}
|
||||
return performance.now()
|
||||
}
|
||||
|
||||
function easeOutCubic(value: number): number {
|
||||
return 1 - Math.pow(1 - value, 3)
|
||||
}
|
||||
|
||||
function cloneWheelReportEvent(event: WheelEvent): WheelEvent {
|
||||
const clone = new WheelEvent(event.type, {
|
||||
bubbles: event.bubbles,
|
||||
@@ -140,32 +90,16 @@ function cloneWheelReportEvent(event: WheelEvent): WheelEvent {
|
||||
return clone
|
||||
}
|
||||
|
||||
export function resolveTerminalTuiMouseWheelReportCount(
|
||||
event: Pick<WheelEvent, 'deltaY' | 'timeStamp'>,
|
||||
maxReports: number,
|
||||
state: TerminalTuiMouseWheelAccelerationState
|
||||
): number {
|
||||
const normalizedMaxReports = normalizeTerminalTuiMouseWheelMultiplier(maxReports)
|
||||
const direction = wheelDirection(event)
|
||||
const currentInputAt = wheelInputTime(event)
|
||||
const elapsedMs = state.lastInputAt === null ? null : currentInputAt - state.lastInputAt
|
||||
const isAccelerating =
|
||||
state.lastDirection === direction &&
|
||||
elapsedMs !== null &&
|
||||
elapsedMs >= 0 &&
|
||||
elapsedMs <= TUI_WHEEL_ACCELERATION_WINDOW_MS
|
||||
|
||||
state.streak = isAccelerating ? state.streak + 1 : 0
|
||||
state.lastDirection = direction
|
||||
state.lastInputAt = currentInputAt
|
||||
|
||||
if (!isAccelerating || normalizedMaxReports <= 1 || elapsedMs === null) {
|
||||
return 1
|
||||
function resolveTerminalWheelCellHeight(terminal: TerminalWheelTarget): number | undefined {
|
||||
if (typeof terminal.element?.querySelector !== 'function') {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const cadence = 1 - elapsedMs / TUI_WHEEL_ACCELERATION_WINDOW_MS
|
||||
const acceleratedReports = 1 + Math.round((normalizedMaxReports - 1) * easeOutCubic(cadence))
|
||||
return Math.min(normalizedMaxReports, Math.max(1, acceleratedReports))
|
||||
const screen = terminal.element?.querySelector<HTMLElement>('.xterm-screen')
|
||||
const rect = screen?.getBoundingClientRect()
|
||||
if (!rect || rect.height <= 0 || terminal.rows <= 0) {
|
||||
return undefined
|
||||
}
|
||||
return rect.height / terminal.rows
|
||||
}
|
||||
|
||||
export function shouldMultiplyTerminalMouseWheel(
|
||||
@@ -177,7 +111,7 @@ export function shouldMultiplyTerminalMouseWheel(
|
||||
!terminalElement?.classList.contains(XTERM_MOUSE_REPORTING_CLASS) ||
|
||||
event.deltaY === 0 ||
|
||||
event.shiftKey ||
|
||||
!isDiscreteWheelEvent(event)
|
||||
!isDiscreteTerminalTuiWheelEvent(event)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
@@ -185,31 +119,7 @@ export function shouldMultiplyTerminalMouseWheel(
|
||||
return true
|
||||
}
|
||||
|
||||
export function normalizeTerminalTuiMouseWheelMultiplier(value: number | undefined): number {
|
||||
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
||||
return TERMINAL_TUI_MOUSE_WHEEL_MULTIPLIER
|
||||
}
|
||||
return Math.round(
|
||||
Math.min(
|
||||
TERMINAL_TUI_MOUSE_WHEEL_MULTIPLIER_MAX,
|
||||
Math.max(TERMINAL_TUI_MOUSE_WHEEL_MULTIPLIER_MIN, value)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
function defaultScheduleWheelReplayFrame(callback: () => void): void {
|
||||
if (typeof requestAnimationFrame === 'function') {
|
||||
requestAnimationFrame(() => callback())
|
||||
return
|
||||
}
|
||||
setTimeout(callback, 0)
|
||||
}
|
||||
|
||||
function drainTerminalTuiWheelReports(
|
||||
state: TerminalTuiMouseWheelReplayState,
|
||||
scheduleWheelReplayFrame: (callback: () => void) => void,
|
||||
reportsThisFrame: number
|
||||
): void {
|
||||
function drainTerminalTuiWheelReports(state: TerminalTuiMouseWheelReplayState): void {
|
||||
const target = state.pendingTarget
|
||||
const event = state.pendingEvent
|
||||
if (!target || !event || state.pendingReports <= 0) {
|
||||
@@ -217,33 +127,28 @@ function drainTerminalTuiWheelReports(
|
||||
return
|
||||
}
|
||||
|
||||
const reportsToDispatch = Math.min(reportsThisFrame, state.pendingReports)
|
||||
for (let i = 0; i < reportsToDispatch; i++) {
|
||||
const reportsToDispatch = state.pendingReports
|
||||
for (let i = 0; i < reportsToDispatch; i += 1) {
|
||||
target.dispatchEvent(cloneWheelReportEvent(event))
|
||||
}
|
||||
state.pendingReports -= reportsToDispatch
|
||||
|
||||
if (state.pendingReports <= 0) {
|
||||
state.drainScheduled = false
|
||||
state.pendingDirection = 0
|
||||
state.pendingEvent = null
|
||||
state.pendingTarget = null
|
||||
return
|
||||
}
|
||||
|
||||
scheduleWheelReplayFrame(() => {
|
||||
drainTerminalTuiWheelReports(state, scheduleWheelReplayFrame, TUI_WHEEL_REPORTS_PER_FRAME)
|
||||
})
|
||||
state.pendingReports = 0
|
||||
state.drainScheduled = false
|
||||
state.pendingDirection = 0
|
||||
state.pendingEvent = null
|
||||
state.pendingTarget = null
|
||||
}
|
||||
|
||||
function queueTerminalTuiWheelReports(
|
||||
state: TerminalTuiMouseWheelReplayState,
|
||||
target: EventTarget,
|
||||
event: WheelEvent,
|
||||
reportCount: number,
|
||||
scheduleWheelReplayFrame: (callback: () => void) => void
|
||||
reportCount: number
|
||||
): void {
|
||||
const direction = wheelDirection(event)
|
||||
if (reportCount <= 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const direction = resolveTerminalWheelDirection(event)
|
||||
if (state.pendingDirection !== 0 && state.pendingDirection !== direction) {
|
||||
state.pendingReports = 0
|
||||
}
|
||||
@@ -258,10 +163,10 @@ function queueTerminalTuiWheelReports(
|
||||
}
|
||||
|
||||
state.drainScheduled = true
|
||||
// Why: the first report should land immediately, while larger accelerated
|
||||
// batches are paced across frames so fast scrolling does not feel bursty.
|
||||
// Why: dispatch after xterm returns from the original wheel handler, but do
|
||||
// not frame-cap reports; fullscreen TUIs need the full wheel distance.
|
||||
queueMicrotask(() => {
|
||||
drainTerminalTuiWheelReports(state, scheduleWheelReplayFrame, 1)
|
||||
drainTerminalTuiWheelReports(state)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -282,19 +187,17 @@ export function attachTerminalMouseWheelMultiplier(
|
||||
}
|
||||
|
||||
// Why: xterm dampens small pixel deltas before emitting mouse reports;
|
||||
// line-mode replays make each notched wheel tick produce immediate reports.
|
||||
// line-mode replays let fullscreen TUIs receive one report per resolved row.
|
||||
const reportCount = resolveTerminalTuiMouseWheelReportCount(
|
||||
event,
|
||||
normalizeTerminalTuiMouseWheelMultiplier(options.getTuiMouseWheelMultiplier?.()),
|
||||
replayState.acceleration
|
||||
)
|
||||
queueTerminalTuiWheelReports(
|
||||
replayState,
|
||||
target,
|
||||
event,
|
||||
reportCount,
|
||||
options.scheduleWheelReplayFrame ?? defaultScheduleWheelReplayFrame
|
||||
replayState.distance,
|
||||
{
|
||||
cellHeight: resolveTerminalWheelCellHeight(terminal),
|
||||
rows: terminal.rows
|
||||
}
|
||||
)
|
||||
queueTerminalTuiWheelReports(replayState, target, event, reportCount)
|
||||
|
||||
return false
|
||||
})
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
const DOM_DELTA_PIXEL = 0
|
||||
const DOM_DELTA_LINE = 1
|
||||
const DOM_DELTA_PAGE = 2
|
||||
const DISCRETE_PIXEL_WHEEL_DELTA_MIN = 50
|
||||
const LEGACY_MOUSE_WHEEL_DELTA_MIN = 100
|
||||
const LEGACY_MOUSE_WHEEL_DELTA_UNIT = 120
|
||||
const DEFAULT_TERMINAL_CELL_HEIGHT = 16
|
||||
const TUI_WHEEL_ACCELERATED_DISTANCE_GAIN = 1.6
|
||||
const TUI_WHEEL_BURST_FULL_INTERVAL_MS = 16
|
||||
const TUI_WHEEL_BURST_MAX_INTERVAL_MS = 45
|
||||
const TUI_WHEEL_BURST_MAX_BONUS_ROWS = 3
|
||||
const TUI_WHEEL_BURST_RAMP_EVENTS = 4
|
||||
const TUI_WHEEL_MOMENTUM_TAIL_DECAY_RATIO = 0.85
|
||||
const TUI_WHEEL_COMPRESSED_MAX_DISTANCE_ROWS_PER_EVENT = 6
|
||||
const TUI_WHEEL_BURST_MAX_DISTANCE_ROWS_PER_EVENT = 9
|
||||
|
||||
export const TERMINAL_TUI_MOUSE_WHEEL_MULTIPLIER = 1
|
||||
export const TERMINAL_TUI_MOUSE_WHEEL_MULTIPLIER_MIN = 1
|
||||
export const TERMINAL_TUI_MOUSE_WHEEL_MULTIPLIER_MAX = 10
|
||||
|
||||
type WheelEventWithLegacyDelta = WheelEvent & {
|
||||
wheelDelta?: number
|
||||
wheelDeltaY?: number
|
||||
}
|
||||
|
||||
type TerminalTuiWheelEventInput = Pick<WheelEvent, 'deltaY'> &
|
||||
Partial<Pick<WheelEvent, 'deltaMode' | 'timeStamp'>> & {
|
||||
wheelDelta?: number
|
||||
wheelDeltaY?: number
|
||||
}
|
||||
|
||||
type TerminalTuiMouseWheelMetrics = {
|
||||
cellHeight?: number
|
||||
rows?: number
|
||||
}
|
||||
|
||||
export type TerminalTuiMouseWheelDistanceState = {
|
||||
fastStreak: number
|
||||
lastDistanceRows: number | null
|
||||
lastInputAt: number | null
|
||||
pendingDirection: -1 | 0 | 1
|
||||
pendingRows: number
|
||||
}
|
||||
|
||||
export function createTerminalTuiMouseWheelDistanceState(): TerminalTuiMouseWheelDistanceState {
|
||||
return {
|
||||
fastStreak: 0,
|
||||
lastDistanceRows: null,
|
||||
lastInputAt: null,
|
||||
pendingDirection: 0,
|
||||
pendingRows: 0
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveTerminalWheelDirection(event: Pick<WheelEvent, 'deltaY'>): -1 | 1 {
|
||||
return event.deltaY < 0 ? -1 : 1
|
||||
}
|
||||
|
||||
function legacyVerticalWheelDelta(event: TerminalTuiWheelEventInput): number | null {
|
||||
const wheelEvent = event as WheelEventWithLegacyDelta
|
||||
if (typeof wheelEvent.wheelDeltaY === 'number' && Number.isFinite(wheelEvent.wheelDeltaY)) {
|
||||
return wheelEvent.wheelDeltaY
|
||||
}
|
||||
if (typeof wheelEvent.wheelDelta === 'number' && Number.isFinite(wheelEvent.wheelDelta)) {
|
||||
return wheelEvent.wheelDelta
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export function isDiscreteTerminalTuiWheelEvent(event: TerminalTuiWheelEventInput): boolean {
|
||||
if ((event.deltaMode ?? DOM_DELTA_PIXEL) !== DOM_DELTA_PIXEL) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (Math.abs(event.deltaY) >= DISCRETE_PIXEL_WHEEL_DELTA_MIN) {
|
||||
return true
|
||||
}
|
||||
|
||||
const legacyDelta = legacyVerticalWheelDelta(event)
|
||||
return legacyDelta !== null && Math.abs(legacyDelta) >= LEGACY_MOUSE_WHEEL_DELTA_MIN
|
||||
}
|
||||
|
||||
function wheelInputTime(event: TerminalTuiWheelEventInput): number | null {
|
||||
if (typeof event.timeStamp === 'number' && Number.isFinite(event.timeStamp)) {
|
||||
return event.timeStamp
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function normalizeCellHeight(cellHeight: number | undefined): number {
|
||||
if (typeof cellHeight === 'number' && Number.isFinite(cellHeight) && cellHeight > 0) {
|
||||
return cellHeight
|
||||
}
|
||||
return DEFAULT_TERMINAL_CELL_HEIGHT
|
||||
}
|
||||
|
||||
function resolveWheelDistanceRows(
|
||||
event: TerminalTuiWheelEventInput,
|
||||
metrics: TerminalTuiMouseWheelMetrics
|
||||
): number {
|
||||
const deltaMode = event.deltaMode ?? DOM_DELTA_PIXEL
|
||||
const deltaY = Math.abs(event.deltaY)
|
||||
const rowsFromDelta =
|
||||
deltaMode === DOM_DELTA_LINE
|
||||
? deltaY
|
||||
: deltaMode === DOM_DELTA_PAGE
|
||||
? deltaY * Math.max(1, metrics.rows ?? 1)
|
||||
: deltaY / normalizeCellHeight(metrics.cellHeight)
|
||||
const legacyDelta = legacyVerticalWheelDelta(event)
|
||||
const rowsFromLegacy =
|
||||
legacyDelta === null ? 0 : Math.abs(legacyDelta) / LEGACY_MOUSE_WHEEL_DELTA_UNIT
|
||||
const rows = Math.max(rowsFromDelta, rowsFromLegacy)
|
||||
|
||||
return isDiscreteTerminalTuiWheelEvent(event) ? Math.max(1, rows) : rows
|
||||
}
|
||||
|
||||
function compressWheelDistanceRows(rows: number): number {
|
||||
if (rows <= 1) {
|
||||
return rows
|
||||
}
|
||||
|
||||
return Math.min(
|
||||
TUI_WHEEL_COMPRESSED_MAX_DISTANCE_ROWS_PER_EVENT,
|
||||
1 + Math.log2(rows) * TUI_WHEEL_ACCELERATED_DISTANCE_GAIN
|
||||
)
|
||||
}
|
||||
|
||||
function resolveBurstWheelDistanceRows(
|
||||
event: TerminalTuiWheelEventInput,
|
||||
state: TerminalTuiMouseWheelDistanceState,
|
||||
distanceRows: number
|
||||
): number {
|
||||
const currentInputAt = wheelInputTime(event)
|
||||
if (currentInputAt === null) {
|
||||
state.fastStreak = 0
|
||||
state.lastDistanceRows = null
|
||||
state.lastInputAt = null
|
||||
return 0
|
||||
}
|
||||
|
||||
const elapsedMs = state.lastInputAt === null ? null : currentInputAt - state.lastInputAt
|
||||
const isMomentumTail =
|
||||
state.lastDistanceRows !== null &&
|
||||
distanceRows < state.lastDistanceRows * TUI_WHEEL_MOMENTUM_TAIL_DECAY_RATIO
|
||||
state.lastDistanceRows = distanceRows
|
||||
state.lastInputAt = currentInputAt
|
||||
|
||||
if (
|
||||
isMomentumTail ||
|
||||
elapsedMs === null ||
|
||||
elapsedMs < 0 ||
|
||||
elapsedMs > TUI_WHEEL_BURST_MAX_INTERVAL_MS
|
||||
) {
|
||||
state.fastStreak = 0
|
||||
return 0
|
||||
}
|
||||
|
||||
const cadence =
|
||||
elapsedMs <= TUI_WHEEL_BURST_FULL_INTERVAL_MS
|
||||
? 1
|
||||
: (TUI_WHEEL_BURST_MAX_INTERVAL_MS - elapsedMs) /
|
||||
(TUI_WHEEL_BURST_MAX_INTERVAL_MS - TUI_WHEEL_BURST_FULL_INTERVAL_MS)
|
||||
state.fastStreak = Math.min(TUI_WHEEL_BURST_RAMP_EVENTS, state.fastStreak + 1)
|
||||
|
||||
return TUI_WHEEL_BURST_MAX_BONUS_ROWS * cadence * (state.fastStreak / TUI_WHEEL_BURST_RAMP_EVENTS)
|
||||
}
|
||||
|
||||
export function normalizeTerminalTuiMouseWheelMultiplier(value: number | undefined): number {
|
||||
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
||||
return TERMINAL_TUI_MOUSE_WHEEL_MULTIPLIER
|
||||
}
|
||||
return Math.round(
|
||||
Math.min(
|
||||
TERMINAL_TUI_MOUSE_WHEEL_MULTIPLIER_MAX,
|
||||
Math.max(TERMINAL_TUI_MOUSE_WHEEL_MULTIPLIER_MIN, value)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
export function resolveTerminalTuiMouseWheelReportCount(
|
||||
event: TerminalTuiWheelEventInput,
|
||||
multiplier: number,
|
||||
state: TerminalTuiMouseWheelDistanceState,
|
||||
metrics: TerminalTuiMouseWheelMetrics = {}
|
||||
): number {
|
||||
const direction = resolveTerminalWheelDirection(event)
|
||||
if (state.pendingDirection !== 0 && state.pendingDirection !== direction) {
|
||||
state.fastStreak = 0
|
||||
state.lastDistanceRows = null
|
||||
state.lastInputAt = null
|
||||
state.pendingRows = 0
|
||||
}
|
||||
state.pendingDirection = direction
|
||||
|
||||
const distanceRows = resolveWheelDistanceRows(event, metrics)
|
||||
const rows =
|
||||
Math.min(
|
||||
TUI_WHEEL_BURST_MAX_DISTANCE_ROWS_PER_EVENT,
|
||||
compressWheelDistanceRows(distanceRows) +
|
||||
resolveBurstWheelDistanceRows(event, state, distanceRows)
|
||||
) * normalizeTerminalTuiMouseWheelMultiplier(multiplier)
|
||||
const totalRows = state.pendingRows + rows
|
||||
const reports = Math.trunc(totalRows)
|
||||
state.pendingRows = totalRows - reports
|
||||
return reports
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
const ESC = '\x1b'
|
||||
const MOUSE_REPORT_PATTERN = new RegExp(`${ESC}\\[<(64|65);\\d+;\\d+M`, 'g')
|
||||
|
||||
let offset = 0
|
||||
let pending = ''
|
||||
|
||||
function write(data) {
|
||||
process.stdout.write(data)
|
||||
}
|
||||
|
||||
function visibleRows() {
|
||||
return Math.max(3, process.stdout.rows || 24)
|
||||
}
|
||||
|
||||
function render() {
|
||||
write(`${ESC}[H`)
|
||||
write(`TUI_SCROLL_READY offset=${offset}${ESC}[K`)
|
||||
for (let row = 1; row < visibleRows(); row += 1) {
|
||||
write(`\r\nTUI_SCROLL_ROW_${String(offset + row - 1).padStart(4, '0')}${ESC}[K`)
|
||||
}
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
write(`${ESC}[?1003l${ESC}[?1006l${ESC}[?25h${ESC}[?1049l`)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
process.stdin.setEncoding('utf8')
|
||||
if (process.stdin.isTTY) {
|
||||
process.stdin.setRawMode(true)
|
||||
}
|
||||
process.stdin.resume()
|
||||
|
||||
write(`${ESC}[?1049h${ESC}[?1003h${ESC}[?1006h${ESC}[?25l${ESC}[2J`)
|
||||
render()
|
||||
|
||||
process.stdin.on('data', (chunk) => {
|
||||
if (chunk.includes('\x03') || chunk.includes('q')) {
|
||||
cleanup()
|
||||
}
|
||||
|
||||
pending += chunk
|
||||
let match
|
||||
let lastIndex = 0
|
||||
|
||||
MOUSE_REPORT_PATTERN.lastIndex = 0
|
||||
while ((match = MOUSE_REPORT_PATTERN.exec(pending)) !== null) {
|
||||
offset = Math.max(0, offset + (match[1] === '65' ? 1 : -1))
|
||||
lastIndex = MOUSE_REPORT_PATTERN.lastIndex
|
||||
}
|
||||
|
||||
if (lastIndex > 0) {
|
||||
pending = pending.slice(lastIndex)
|
||||
render()
|
||||
return
|
||||
}
|
||||
|
||||
pending = pending.slice(-32)
|
||||
})
|
||||
|
||||
process.on('SIGINT', cleanup)
|
||||
@@ -1,7 +1,12 @@
|
||||
import type { Page } from '@stablyai/playwright-test'
|
||||
import path from 'node:path'
|
||||
import { test, expect } from './helpers/orca-app'
|
||||
import { ensureTerminalVisible, waitForActiveWorktree, waitForSessionReady } from './helpers/store'
|
||||
import { waitForActiveTerminalManager } from './helpers/terminal'
|
||||
import {
|
||||
execInTerminal,
|
||||
waitForActivePanePtyId,
|
||||
waitForActiveTerminalManager
|
||||
} from './helpers/terminal'
|
||||
|
||||
type WheelReportSample = {
|
||||
reportCount: number
|
||||
@@ -14,6 +19,10 @@ type TimedWheelReportSample = WheelReportSample & {
|
||||
}
|
||||
|
||||
const PHYSICAL_MOUSE_WHEEL_DELTA = -120
|
||||
const VISIBLE_TUI_FIXTURE_PATH = path.join(
|
||||
process.cwd(),
|
||||
'tests/e2e/fixtures/visible-tui-scroll-fixture.cjs'
|
||||
)
|
||||
|
||||
async function probeSmallMouseWheelReports(
|
||||
page: Page,
|
||||
@@ -194,6 +203,82 @@ async function probeTimedSmallMouseWheelReports(
|
||||
)
|
||||
}
|
||||
|
||||
async function readVisibleTuiOffset(page: Page): Promise<number | null> {
|
||||
return page.evaluate(() => {
|
||||
const state = window.__store?.getState()
|
||||
const worktreeId = state?.activeWorktreeId
|
||||
const tabId =
|
||||
state?.activeTabType === 'terminal'
|
||||
? state.activeTabId
|
||||
: worktreeId
|
||||
? (state?.activeTabIdByWorktree?.[worktreeId] ?? null)
|
||||
: null
|
||||
const manager = tabId ? window.__paneManagers?.get(tabId) : null
|
||||
const pane = manager?.getActivePane?.() ?? manager?.getPanes?.()[0] ?? null
|
||||
if (!pane?.terminal) {
|
||||
return null
|
||||
}
|
||||
|
||||
for (let row = 0; row < pane.terminal.rows; row += 1) {
|
||||
const text = pane.terminal.buffer.active.getLine(row)?.translateToString(true) ?? ''
|
||||
const match = /TUI_SCROLL_ROW_(\d+)/.exec(text)
|
||||
if (match) {
|
||||
return Number(match[1])
|
||||
}
|
||||
}
|
||||
return null
|
||||
})
|
||||
}
|
||||
|
||||
async function dispatchTuiWheel(
|
||||
page: Page,
|
||||
options: {
|
||||
deltaY: number
|
||||
wheelDeltaY?: number
|
||||
}
|
||||
): Promise<void> {
|
||||
await page.evaluate(({ deltaY, wheelDeltaY }) => {
|
||||
const state = window.__store?.getState()
|
||||
const worktreeId = state?.activeWorktreeId
|
||||
const tabId =
|
||||
state?.activeTabType === 'terminal'
|
||||
? state.activeTabId
|
||||
: worktreeId
|
||||
? (state?.activeTabIdByWorktree?.[worktreeId] ?? null)
|
||||
: null
|
||||
const manager = tabId ? window.__paneManagers?.get(tabId) : null
|
||||
const pane = manager?.getActivePane?.() ?? manager?.getPanes?.()[0] ?? null
|
||||
if (!pane?.terminal.element) {
|
||||
throw new Error('Active terminal pane unavailable')
|
||||
}
|
||||
|
||||
const screen = pane.terminal.element.querySelector<HTMLElement>('.xterm-screen')
|
||||
if (!screen) {
|
||||
throw new Error('Active terminal screen unavailable')
|
||||
}
|
||||
const rect = screen.getBoundingClientRect()
|
||||
const event = new WheelEvent('wheel', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
clientX: rect.left + rect.width / 2,
|
||||
clientY: rect.top + Math.min(rect.height - 1, 40),
|
||||
deltaMode: WheelEvent.DOM_DELTA_PIXEL,
|
||||
deltaY
|
||||
})
|
||||
if (wheelDeltaY !== undefined) {
|
||||
Object.defineProperty(event, 'wheelDeltaY', {
|
||||
configurable: true,
|
||||
value: wheelDeltaY
|
||||
})
|
||||
Object.defineProperty(event, 'wheelDelta', {
|
||||
configurable: true,
|
||||
value: wheelDeltaY
|
||||
})
|
||||
}
|
||||
pane.terminal.element.dispatchEvent(event)
|
||||
}, options)
|
||||
}
|
||||
|
||||
test.describe('terminal TUI wheel reports', () => {
|
||||
test('notched mouse wheel ticks produce immediate mouse-reporting TUI scroll reports', async ({
|
||||
orcaPage
|
||||
@@ -215,9 +300,92 @@ test.describe('terminal TUI wheel reports', () => {
|
||||
expect(samples.at(-1)?.reports.join('')).toContain('\x1b[<65;')
|
||||
})
|
||||
|
||||
test('fast notched mouse wheel ticks accelerate while slow ticks stay precise', async ({
|
||||
test('fullscreen mouse-reporting TUI scroll distance follows wheel magnitude @headful', async ({
|
||||
electronApp,
|
||||
orcaPage
|
||||
}) => {
|
||||
await electronApp.evaluate(({ BrowserWindow }) => {
|
||||
const win = BrowserWindow.getAllWindows()[0]
|
||||
if (!win) {
|
||||
throw new Error('No BrowserWindow available')
|
||||
}
|
||||
if (win.isMinimized()) {
|
||||
win.restore()
|
||||
}
|
||||
win.show()
|
||||
win.focus()
|
||||
win.setFullScreen(true)
|
||||
})
|
||||
await expect
|
||||
.poll(() =>
|
||||
electronApp.evaluate(
|
||||
({ BrowserWindow }) => BrowserWindow.getAllWindows()[0]?.isFullScreen() ?? false
|
||||
)
|
||||
)
|
||||
.toBe(true)
|
||||
await orcaPage.waitForTimeout(1200)
|
||||
|
||||
await waitForSessionReady(orcaPage)
|
||||
await waitForActiveWorktree(orcaPage)
|
||||
await ensureTerminalVisible(orcaPage)
|
||||
await waitForActiveTerminalManager(orcaPage, 30_000)
|
||||
await orcaPage.evaluate(() =>
|
||||
window.__store?.getState().updateSettings({ terminalTuiScrollSensitivity: 1 })
|
||||
)
|
||||
|
||||
const ptyId = await waitForActivePanePtyId(orcaPage)
|
||||
await execInTerminal(orcaPage, ptyId, `node ${JSON.stringify(VISIBLE_TUI_FIXTURE_PATH)}`)
|
||||
|
||||
await expect
|
||||
.poll(() => readVisibleTuiOffset(orcaPage), {
|
||||
timeout: 10_000,
|
||||
message: 'visible fullscreen TUI did not render numbered rows'
|
||||
})
|
||||
.toBe(0)
|
||||
|
||||
await dispatchTuiWheel(orcaPage, {
|
||||
deltaY: 10,
|
||||
wheelDeltaY: PHYSICAL_MOUSE_WHEEL_DELTA
|
||||
})
|
||||
await expect
|
||||
.poll(() => readVisibleTuiOffset(orcaPage), {
|
||||
timeout: 5_000,
|
||||
message: 'single notched wheel tick did not visibly scroll the TUI'
|
||||
})
|
||||
.toBe(1)
|
||||
|
||||
const cellHeight = await orcaPage.evaluate(() => {
|
||||
const state = window.__store?.getState()
|
||||
const worktreeId = state?.activeWorktreeId
|
||||
const tabId =
|
||||
state?.activeTabType === 'terminal'
|
||||
? state.activeTabId
|
||||
: worktreeId
|
||||
? (state?.activeTabIdByWorktree?.[worktreeId] ?? null)
|
||||
: null
|
||||
const manager = tabId ? window.__paneManagers?.get(tabId) : null
|
||||
const pane = manager?.getActivePane?.() ?? manager?.getPanes?.()[0] ?? null
|
||||
const screen = pane?.terminal.element?.querySelector<HTMLElement>('.xterm-screen')
|
||||
if (!pane?.terminal || !screen) {
|
||||
throw new Error('Active terminal screen unavailable')
|
||||
}
|
||||
return screen.getBoundingClientRect().height / pane.terminal.rows
|
||||
})
|
||||
|
||||
await dispatchTuiWheel(orcaPage, {
|
||||
deltaY: cellHeight * 12,
|
||||
wheelDeltaY: PHYSICAL_MOUSE_WHEEL_DELTA * 12
|
||||
})
|
||||
|
||||
await expect
|
||||
.poll(() => readVisibleTuiOffset(orcaPage), {
|
||||
timeout: 5_000,
|
||||
message: 'larger wheel movement did not visibly move the TUI farther'
|
||||
})
|
||||
.toBe(7)
|
||||
})
|
||||
|
||||
test('TUI scroll setting scales notched mouse wheel reports', async ({ orcaPage }) => {
|
||||
await waitForSessionReady(orcaPage)
|
||||
await waitForActiveWorktree(orcaPage)
|
||||
await ensureTerminalVisible(orcaPage)
|
||||
@@ -232,17 +400,22 @@ test.describe('terminal TUI wheel reports', () => {
|
||||
ticks: 5
|
||||
})
|
||||
await orcaPage.waitForTimeout(220)
|
||||
const fast = await probeTimedSmallMouseWheelReports(orcaPage, {
|
||||
const paced = await probeTimedSmallMouseWheelReports(orcaPage, {
|
||||
drainWaitMs: 220,
|
||||
intervalMs: 40,
|
||||
intervalMs: 80,
|
||||
ticks: 5
|
||||
})
|
||||
|
||||
expect(slow.reports.length, `slow SGR mouse reports: ${JSON.stringify(slow.samples)}`).toBe(5)
|
||||
expect(
|
||||
fast.samples.map((sample) => sample.reportDelta),
|
||||
`fast per-tick SGR mouse reports: ${JSON.stringify(fast.samples)}`
|
||||
).toEqual([1, 5, 5, 5, 5])
|
||||
expect(fast.reports.length, `fast SGR mouse reports: ${JSON.stringify(fast.samples)}`).toBe(21)
|
||||
slow.samples.map((sample) => sample.reportDelta),
|
||||
`slow per-tick SGR mouse reports: ${JSON.stringify(slow.samples)}`
|
||||
).toEqual([5, 5, 5, 5, 5])
|
||||
expect(
|
||||
paced.samples.map((sample) => sample.reportDelta),
|
||||
`paced per-tick SGR mouse reports: ${JSON.stringify(paced.samples)}`
|
||||
).toEqual([5, 5, 5, 5, 5])
|
||||
expect(paced.reports.length, `paced SGR mouse reports: ${JSON.stringify(paced.samples)}`).toBe(
|
||||
25
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user