Stop zoom slider flicker during drag

Dragging the Settings zoom slider was applying app-wide zoom on every range change. That resized the Settings panel and the range control under the pointer, creating visible flicker and a poor drag feel.

The slider now keeps drag movement local to the Settings preview and commits the shared app zoom only when the drag is released or when keyboard/non-pointer changes occur. The control step is also reduced to one percent so adjustments feel less coarse.

Constraint: App-wide zoom changes resize the control currently being dragged
Rejected: Throttle live app zoom during drag | still resizes the target under the pointer and can flicker on slower WebViews
Confidence: high
Scope-risk: narrow
Directive: Do not apply app-wide zoom continuously during pointer drag; keep live resizing to keyboard shortcuts and final slider commit
Tested: bun run check:desktop
Tested: agent-browser Settings smoke confirms drag preview keeps cssZoom at 1 until release, then commits 1.23
This commit is contained in:
程序员阿江(Relakkes) 2026-05-15 20:29:45 +08:00
parent c279643cce
commit 5965c4a3bd
3 changed files with 29 additions and 13 deletions

View File

@ -268,7 +268,7 @@ describe('Settings > General tab', () => {
expect((uiZoomHeading.compareDocumentPosition(webFetchHeading) & Node.DOCUMENT_POSITION_FOLLOWING) !== 0).toBe(true)
})
it('applies UI zoom while dragging so the slider and app zoom stay synchronized', async () => {
it('previews UI zoom while dragging and applies it once on release', async () => {
render(<Settings />)
fireEvent.click(screen.getByText('General'))
@ -278,6 +278,7 @@ describe('Settings > General tab', () => {
expect(screen.getByText('0 resets zoom to 100%.')).toBeInTheDocument()
const slider = screen.getByLabelText('UI Zoom')
expect(slider).toHaveAttribute('step', '0.01')
fireEvent.pointerDown(slider, { pointerId: 1 })
await act(async () => {
@ -287,8 +288,8 @@ describe('Settings > General tab', () => {
})
expect(screen.getAllByText('125%')).toHaveLength(2)
expect(useSettingsStore.getState().setUiZoom).toHaveBeenCalledWith(1.25)
expect(useSettingsStore.getState().uiZoom).toBe(1.25)
expect(useSettingsStore.getState().setUiZoom).not.toHaveBeenCalledWith(1.25)
expect(useSettingsStore.getState().uiZoom).toBe(1)
expect(slider).toHaveValue('1.25')
expect(slider).toHaveClass('settings-zoom-range')
expect(slider.closest('.settings-zoom-control')).toHaveClass('is-dragging')

View File

@ -4,7 +4,7 @@ export const DEFAULT_APP_ZOOM = 1
export const MIN_APP_ZOOM = 0.5
export const MAX_APP_ZOOM = 2
export const APP_ZOOM_STEP = 0.1
export const APP_ZOOM_CONTROL_STEP = 0.05
export const APP_ZOOM_CONTROL_STEP = 0.01
export type AppZoomAction = 'in' | 'out' | 'reset'

View File

@ -1394,6 +1394,7 @@ function GeneralSettings() {
const [notificationActionRunning, setNotificationActionRunning] = useState(false)
const [uiZoomDraft, setUiZoomDraft] = useState(uiZoom)
const [isUiZoomDragging, setIsUiZoomDragging] = useState(false)
const isUiZoomDraggingRef = useRef(false)
const webSearchDirty = JSON.stringify(webSearchDraft) !== JSON.stringify(webSearch)
const uiZoomPercent = Math.round(uiZoomDraft * 100)
const uiZoomRangeProgress = `${Math.round(((uiZoomDraft - UI_ZOOM_MIN) / (UI_ZOOM_MAX - UI_ZOOM_MIN)) * 1000) / 10}%`
@ -1523,11 +1524,14 @@ function GeneralSettings() {
}
}
const commitUiZoom = (value: number, options: { keepDragging?: boolean } = {}) => {
const setUiZoomDraggingState = (dragging: boolean) => {
isUiZoomDraggingRef.current = dragging
setIsUiZoomDragging(dragging)
}
const commitUiZoom = (value: number) => {
const nextZoom = Number.isFinite(value) ? value : UI_ZOOM_DEFAULT
if (!options.keepDragging) {
setIsUiZoomDragging(false)
}
setUiZoomDraggingState(false)
setUiZoomDraft(nextZoom)
setUiZoom(nextZoom)
}
@ -1602,16 +1606,27 @@ function GeneralSettings() {
step={UI_ZOOM_STEP}
value={uiZoomDraft}
onPointerDown={() => {
setIsUiZoomDragging(true)
setUiZoomDraggingState(true)
}}
onPointerUp={(e) => commitUiZoom(e.currentTarget.valueAsNumber)}
onPointerCancel={() => {
setUiZoomDraggingState(false)
setUiZoomDraft(uiZoom)
}}
onChange={(e) => {
const nextZoom = Number.isFinite(e.currentTarget.valueAsNumber)
? e.currentTarget.valueAsNumber
: UI_ZOOM_DEFAULT
setUiZoomDraft(nextZoom)
if (!isUiZoomDraggingRef.current) {
setUiZoom(nextZoom)
}
}}
onPointerUp={() => setIsUiZoomDragging(false)}
onPointerCancel={() => setIsUiZoomDragging(false)}
onChange={(e) => commitUiZoom(e.currentTarget.valueAsNumber, { keepDragging: isUiZoomDragging })}
onBlur={(e) => {
if (uiZoomDraft !== uiZoom) {
commitUiZoom(e.currentTarget.valueAsNumber)
} else {
setIsUiZoomDragging(false)
setUiZoomDraggingState(false)
}
}}
className="settings-zoom-range w-full"