From 5965c4a3bdef64c87eeae5d9b2ed218c8e6a6c44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E9=98=BF=E6=B1=9F=28Relakkes?= =?UTF-8?q?=29?= Date: Fri, 15 May 2026 20:29:45 +0800 Subject: [PATCH] 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 --- .../src/__tests__/generalSettings.test.tsx | 7 ++-- desktop/src/lib/appZoom.ts | 2 +- desktop/src/pages/Settings.tsx | 33 ++++++++++++++----- 3 files changed, 29 insertions(+), 13 deletions(-) diff --git a/desktop/src/__tests__/generalSettings.test.tsx b/desktop/src/__tests__/generalSettings.test.tsx index 3e36eafb..7a2d5e1d 100644 --- a/desktop/src/__tests__/generalSettings.test.tsx +++ b/desktop/src/__tests__/generalSettings.test.tsx @@ -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() 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') diff --git a/desktop/src/lib/appZoom.ts b/desktop/src/lib/appZoom.ts index 3127d42c..23fcb2e4 100644 --- a/desktop/src/lib/appZoom.ts +++ b/desktop/src/lib/appZoom.ts @@ -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' diff --git a/desktop/src/pages/Settings.tsx b/desktop/src/pages/Settings.tsx index 01d07690..ddbaaf44 100644 --- a/desktop/src/pages/Settings.tsx +++ b/desktop/src/pages/Settings.tsx @@ -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"