fix(desktop): align native preview with app zoom (#1042)

This commit is contained in:
程序员阿江(Relakkes)
2026-07-17 18:33:29 +08:00
parent 0554c88c8e
commit e2c57cde2c
4 changed files with 61 additions and 12 deletions
@@ -28,6 +28,7 @@ import { getDefaultBaseUrl, setBaseUrl } from '../../api/client'
import { useBrowserPanelStore } from '../../stores/browserPanelStore'
import { useWorkspacePanelStore } from '../../stores/workspacePanelStore'
import { useOverlayStore } from '../../stores/overlayStore'
import { useSettingsStore } from '../../stores/settingsStore'
afterEach(() => {
cleanup()
@@ -37,6 +38,7 @@ afterEach(() => {
// browserPanelStore.open() now also opens the unified workbench; keep it isolated.
useWorkspacePanelStore.setState(useWorkspacePanelStore.getInitialState(), true)
useOverlayStore.setState(useOverlayStore.getInitialState(), true)
useSettingsStore.setState({ uiZoom: 1 })
setBaseUrl(getDefaultBaseUrl())
})
@@ -49,6 +51,40 @@ describe('BrowserSurface', () => {
})
})
it('rescales native preview bounds when app zoom changes', async () => {
vi.spyOn(HTMLElement.prototype, 'getBoundingClientRect').mockReturnValue({
left: 180,
top: 150,
width: 420,
height: 300,
} as DOMRect)
useSettingsStore.setState({ uiZoom: 1.25 })
useBrowserPanelStore.getState().open('s1', 'http://localhost:5173/')
render(<BrowserSurface sessionId="s1" />)
await waitFor(() => {
expect(bridge.open).toHaveBeenCalledWith('http://localhost:5173/', {
x: 225,
y: 187.5,
width: 525,
height: 375,
})
})
act(() => {
useSettingsStore.setState({ uiZoom: 1.5 })
})
await waitFor(() => {
expect(bridge.setBounds).toHaveBeenLastCalledWith({
x: 270,
y: 225,
width: 630,
height: 450,
})
})
})
it('waits for local preview URLs before opening the native preview', async () => {
const url = 'http://127.0.0.1:59028/preview-fs/s1/66estmutl_files/index.html'
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response(null, { status: 200 }))
@@ -1,4 +1,4 @@
import { useEffect, useLayoutEffect, useRef } from 'react'
import { useCallback, useEffect, useLayoutEffect, useRef } from 'react'
import { Camera, Loader2, Minus, MousePointer2, Plus, RotateCcw } from 'lucide-react'
import { BrowserAddressBar } from './BrowserAddressBar'
import { computeWebviewBounds } from './computeWebviewBounds'
@@ -16,6 +16,7 @@ import {
useBrowserPanelStore,
} from '../../stores/browserPanelStore'
import { useOverlayStore } from '../../stores/overlayStore'
import { useSettingsStore } from '../../stores/settingsStore'
const LOCAL_PREVIEW_PATH_PREFIXES = ['/preview-fs/', '/local-file/']
const LOCAL_PREVIEW_READY_TIMEOUT_MS = 2500
@@ -68,6 +69,7 @@ export function BrowserSurface({ sessionId }: { sessionId: string }) {
const requestedUrlRef = useRef<string | null>(null)
const hasNativePreviewRef = useRef(false)
const session = useBrowserPanelStore((s) => s.bySession[sessionId])
const appZoom = useSettingsStore((s) => s.uiZoom)
const store = useBrowserPanelStore.getState()
const overlayCount = useOverlayStore((s) => s.count)
const previewZoom = session?.zoom ?? DEFAULT_BROWSER_ZOOM
@@ -75,11 +77,11 @@ export function BrowserSurface({ sessionId }: { sessionId: string }) {
const canZoomOut = previewZoom > MIN_BROWSER_ZOOM
const canZoomIn = previewZoom < MAX_BROWSER_ZOOM
const reportBounds = () => {
const reportBounds = useCallback(() => {
const el = hostRef.current
if (!el) return
previewBridge.setBounds(computeWebviewBounds(el.getBoundingClientRect()))
}
previewBridge.setBounds(computeWebviewBounds(el.getBoundingClientRect(), appZoom))
}, [appZoom])
const loadNativePreview = (
url: string,
@@ -118,7 +120,7 @@ export function BrowserSurface({ sessionId }: { sessionId: string }) {
const el = hostRef.current
hasNativePreviewRef.current = true
if (el) {
await previewBridge.open(url, computeWebviewBounds(el.getBoundingClientRect()))
await previewBridge.open(url, computeWebviewBounds(el.getBoundingClientRect(), appZoom))
} else {
await previewBridge.navigate(url)
}
@@ -168,8 +170,11 @@ export function BrowserSurface({ sessionId }: { sessionId: string }) {
ro.observe(el)
window.addEventListener('resize', reportBounds)
return () => { ro.disconnect(); window.removeEventListener('resize', reportBounds) }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [sessionId])
}, [reportBounds, sessionId])
useLayoutEffect(() => {
reportBounds()
}, [reportBounds, sessionId])
useEffect(() => {
let unsub: (() => void) | undefined
@@ -7,6 +7,11 @@ describe('computeWebviewBounds', () => {
expect(computeWebviewBounds(rect)).toEqual({ x: 100.4, y: 50.6, width: 800.2, height: 600.9 })
})
it('maps zoomed CSS coordinates back to the native parent coordinate space', () => {
const rect = { left: 180, top: 150, width: 420, height: 300 } as DOMRect
expect(computeWebviewBounds(rect, 1.25)).toEqual({ x: 225, y: 187.5, width: 525, height: 375 })
})
it('clamps negative/zero sizes to 0', () => {
const rect = { left: -5, top: -5, width: -10, height: 0 } as DOMRect
expect(computeWebviewBounds(rect)).toEqual({ x: -5, y: -5, width: 0, height: 0 })
@@ -1,10 +1,13 @@
export type WebviewBounds = { x: number; y: number; width: number; height: number }
export function computeWebviewBounds(rect: Pick<DOMRect, 'left' | 'top' | 'width' | 'height'>): WebviewBounds {
export function computeWebviewBounds(
rect: Pick<DOMRect, 'left' | 'top' | 'width' | 'height'>,
appZoom = 1,
): WebviewBounds {
return {
x: rect.left,
y: rect.top,
width: Math.max(0, rect.width),
height: Math.max(0, rect.height),
x: rect.left * appZoom,
y: rect.top * appZoom,
width: Math.max(0, rect.width * appZoom),
height: Math.max(0, rect.height * appZoom),
}
}