Coalesce pane divider drag updates (#1816)
Batch terminal pane splitter flex writes behind requestAnimationFrame so high-rate drag pointermove bursts do at most one layout write per frame, while flushing the latest size before final refit.
This commit is contained in:
parent
2908367186
commit
41d642bd29
|
|
@ -0,0 +1,43 @@
|
|||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { createDividerFlexFrameScheduler } from './pane-divider'
|
||||
|
||||
describe('createDividerFlexFrameScheduler', () => {
|
||||
it('coalesces repeated drag updates into one flex write per animation frame', () => {
|
||||
const apply = vi.fn()
|
||||
const queuedFrames: FrameRequestCallback[] = []
|
||||
const requestFrame = vi.fn((callback: FrameRequestCallback) => {
|
||||
queuedFrames.push(callback)
|
||||
return queuedFrames.length
|
||||
})
|
||||
const cancelFrame = vi.fn()
|
||||
const scheduler = createDividerFlexFrameScheduler({ apply, requestFrame, cancelFrame })
|
||||
|
||||
scheduler.schedule(120, 280)
|
||||
scheduler.schedule(140, 260)
|
||||
scheduler.schedule(160, 240)
|
||||
|
||||
expect(requestFrame).toHaveBeenCalledTimes(1)
|
||||
expect(apply).not.toHaveBeenCalled()
|
||||
|
||||
queuedFrames[0]?.(16)
|
||||
|
||||
expect(apply).toHaveBeenCalledTimes(1)
|
||||
expect(apply).toHaveBeenLastCalledWith(160, 240)
|
||||
expect(cancelFrame).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('flushes the latest drag update before final pane refit', () => {
|
||||
const apply = vi.fn()
|
||||
const requestFrame = vi.fn(() => 7)
|
||||
const cancelFrame = vi.fn()
|
||||
const scheduler = createDividerFlexFrameScheduler({ apply, requestFrame, cancelFrame })
|
||||
|
||||
scheduler.schedule(120, 280)
|
||||
scheduler.schedule(180, 220)
|
||||
scheduler.flush()
|
||||
|
||||
expect(cancelFrame).toHaveBeenCalledWith(7)
|
||||
expect(apply).toHaveBeenCalledTimes(1)
|
||||
expect(apply).toHaveBeenCalledWith(180, 220)
|
||||
})
|
||||
})
|
||||
|
|
@ -16,6 +16,59 @@ type DividerCallbacks = {
|
|||
onLayoutChanged?: () => void
|
||||
}
|
||||
|
||||
type DividerFlexFrameScheduler = {
|
||||
schedule: (prevFlex: number, nextFlex: number) => void
|
||||
flush: () => void
|
||||
cancel: () => void
|
||||
}
|
||||
|
||||
export function createDividerFlexFrameScheduler({
|
||||
apply,
|
||||
requestFrame = requestAnimationFrame,
|
||||
cancelFrame = cancelAnimationFrame
|
||||
}: {
|
||||
apply: (prevFlex: number, nextFlex: number) => void
|
||||
requestFrame?: (callback: FrameRequestCallback) => number
|
||||
cancelFrame?: (handle: number) => void
|
||||
}): DividerFlexFrameScheduler {
|
||||
let frameId: number | null = null
|
||||
let pending: { prevFlex: number; nextFlex: number } | null = null
|
||||
|
||||
const applyPending = (): void => {
|
||||
frameId = null
|
||||
const next = pending
|
||||
pending = null
|
||||
if (!next) {
|
||||
return
|
||||
}
|
||||
apply(next.prevFlex, next.nextFlex)
|
||||
}
|
||||
|
||||
return {
|
||||
schedule(prevFlex, nextFlex) {
|
||||
pending = { prevFlex, nextFlex }
|
||||
if (frameId !== null) {
|
||||
return
|
||||
}
|
||||
frameId = requestFrame(applyPending)
|
||||
},
|
||||
flush() {
|
||||
if (frameId !== null) {
|
||||
cancelFrame(frameId)
|
||||
frameId = null
|
||||
}
|
||||
applyPending()
|
||||
},
|
||||
cancel() {
|
||||
if (frameId !== null) {
|
||||
cancelFrame(frameId)
|
||||
frameId = null
|
||||
}
|
||||
pending = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function createDivider(
|
||||
isVertical: boolean,
|
||||
styleOptions: PaneStyleOptions,
|
||||
|
|
@ -57,9 +110,19 @@ function attachDividerDrag(
|
|||
let totalSize = 0
|
||||
let prevEl: HTMLElement | null = null
|
||||
let nextEl: HTMLElement | null = null
|
||||
const flexScheduler = createDividerFlexFrameScheduler({
|
||||
apply: (newPrev, newNext) => {
|
||||
if (!prevEl || !nextEl) {
|
||||
return
|
||||
}
|
||||
prevEl.style.flex = `${newPrev} 1 0%`
|
||||
nextEl.style.flex = `${newNext} 1 0%`
|
||||
}
|
||||
})
|
||||
|
||||
const onPointerDown = (e: PointerEvent): void => {
|
||||
e.preventDefault()
|
||||
flexScheduler.cancel()
|
||||
divider.setPointerCapture(e.pointerId)
|
||||
divider.classList.add('is-dragging')
|
||||
dragging = true
|
||||
|
|
@ -108,11 +171,9 @@ function attachDividerDrag(
|
|||
newPrev = totalSize - MIN_PANE_SIZE
|
||||
}
|
||||
|
||||
// Why: keep drag-time work to flex layout only; pane-local ResizeObservers
|
||||
// schedule the terminal fit on the next frame so the divider stays smooth.
|
||||
// Use flex-grow proportionally
|
||||
prevEl.style.flex = `${newPrev} 1 0%`
|
||||
nextEl.style.flex = `${newNext} 1 0%`
|
||||
// Why: pointermove can outpace paint during split resizing. Coalescing the
|
||||
// flex writes keeps drag reflow to one update per frame.
|
||||
flexScheduler.schedule(newPrev, newNext)
|
||||
}
|
||||
|
||||
const onPointerUp = (e: PointerEvent): void => {
|
||||
|
|
@ -120,6 +181,7 @@ function attachDividerDrag(
|
|||
return
|
||||
}
|
||||
dragging = false
|
||||
flexScheduler.flush()
|
||||
divider.releasePointerCapture(e.pointerId)
|
||||
divider.classList.remove('is-dragging')
|
||||
// Final refit at the exact drop position.
|
||||
|
|
|
|||
Loading…
Reference in New Issue