diff --git a/desktop/src/components/browser/BrowserSurface.test.tsx b/desktop/src/components/browser/BrowserSurface.test.tsx index f20294e9..ab25e286 100644 --- a/desktop/src/components/browser/BrowserSurface.test.tsx +++ b/desktop/src/components/browser/BrowserSurface.test.tsx @@ -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() + + 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 })) diff --git a/desktop/src/components/browser/BrowserSurface.tsx b/desktop/src/components/browser/BrowserSurface.tsx index 1dbffaf7..dca5fc34 100644 --- a/desktop/src/components/browser/BrowserSurface.tsx +++ b/desktop/src/components/browser/BrowserSurface.tsx @@ -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(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 diff --git a/desktop/src/components/browser/computeWebviewBounds.test.ts b/desktop/src/components/browser/computeWebviewBounds.test.ts index 31e324eb..71762095 100644 --- a/desktop/src/components/browser/computeWebviewBounds.test.ts +++ b/desktop/src/components/browser/computeWebviewBounds.test.ts @@ -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 }) diff --git a/desktop/src/components/browser/computeWebviewBounds.ts b/desktop/src/components/browser/computeWebviewBounds.ts index 2c6564b4..bb0f10a4 100644 --- a/desktop/src/components/browser/computeWebviewBounds.ts +++ b/desktop/src/components/browser/computeWebviewBounds.ts @@ -1,10 +1,13 @@ export type WebviewBounds = { x: number; y: number; width: number; height: number } -export function computeWebviewBounds(rect: Pick): WebviewBounds { +export function computeWebviewBounds( + rect: Pick, + 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), } }