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:
Neil 2026-05-14 01:18:56 -07:00 committed by GitHub
parent 2908367186
commit 41d642bd29
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 110 additions and 5 deletions

View File

@ -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)
})
})

View File

@ -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.