diff --git a/apps/desktop/src/preload.ts b/apps/desktop/src/preload.ts index bf317349d5..b1092009b8 100644 --- a/apps/desktop/src/preload.ts +++ b/apps/desktop/src/preload.ts @@ -15,6 +15,7 @@ import { type BbDesktopBrowserSnapshotHandler, type BbDesktopBrowserStateHandler, type BbDesktopBrowserUnsubscribe, + type BbDesktopBrowserViewBounds, type BbDesktopCloseWindowRequestHandler, type BbDesktopInfo, type BbDesktopInfoChangeHandler, @@ -188,9 +189,32 @@ const bbSpellcheckApi: BbDesktopSpellcheckApi = { }, }; +function browserViewBoundsAtWindowScale( + bounds: BbDesktopBrowserViewBounds, +): BbDesktopBrowserViewBounds { + const zoomFactor = webFrame.getZoomFactor(); + if (zoomFactor === 1) { + return bounds; + } + const x = Math.round(bounds.x * zoomFactor); + const y = Math.round(bounds.y * zoomFactor); + return { + x, + y, + width: Math.max(0, Math.round((bounds.x + bounds.width) * zoomFactor) - x), + height: Math.max( + 0, + Math.round((bounds.y + bounds.height) * zoomFactor) - y, + ), + }; +} + const bbBrowserApi: BbDesktopBrowserApi = { attach(request): void { - ipcRenderer.send(BB_DESKTOP_BROWSER_ATTACH_CHANNEL, request); + ipcRenderer.send(BB_DESKTOP_BROWSER_ATTACH_CHANNEL, { + ...request, + bounds: browserViewBoundsAtWindowScale(request.bounds), + }); }, detach(tabId): void { ipcRenderer.send(BB_DESKTOP_BROWSER_DETACH_CHANNEL, { tabId }); @@ -211,7 +235,10 @@ const bbBrowserApi: BbDesktopBrowserApi = { ipcRenderer.send(BB_DESKTOP_BROWSER_STOP_CHANNEL, { tabId }); }, setBounds(request): void { - ipcRenderer.send(BB_DESKTOP_BROWSER_SET_BOUNDS_CHANNEL, request); + ipcRenderer.send(BB_DESKTOP_BROWSER_SET_BOUNDS_CHANNEL, { + ...request, + bounds: browserViewBoundsAtWindowScale(request.bounds), + }); }, setVisible(request): void { ipcRenderer.send(BB_DESKTOP_BROWSER_SET_VISIBLE_CHANNEL, request); diff --git a/apps/desktop/test/preload-browser-api.test.ts b/apps/desktop/test/preload-browser-api.test.ts index 7ef396e563..a99b1a4958 100644 --- a/apps/desktop/test/preload-browser-api.test.ts +++ b/apps/desktop/test/preload-browser-api.test.ts @@ -74,6 +74,7 @@ const electronMock = vi.hoisted(() => { let exposedSpellcheckApi: { getCorrectionContext(word: string): unknown; } | null = null; + let zoomFactor = 1; return { get exposedApi() { @@ -97,6 +98,10 @@ const electronMock = vi.hoisted(() => { invokeCalls.length = 0; listeners.clear(); sendCalls.length = 0; + zoomFactor = 1; + }, + setZoomFactor(nextZoomFactor: number): void { + zoomFactor = nextZoomFactor; }, contextBridge: { exposeInMainWorld(name: string, api: unknown): void { @@ -129,6 +134,9 @@ const electronMock = vi.hoisted(() => { }, }, webFrame: { + getZoomFactor(): number { + return zoomFactor; + }, getWordSuggestions(word: string): string[] { return word === "recieve" ? ["receive", "relieve"] : []; }, @@ -298,6 +306,41 @@ describe("desktop preload browser API", () => { ); }, 10_000); + it("converts zoomed renderer bounds to native window coordinates", async () => { + const api = await loadPreload(); + electronMock.setZoomFactor(1.25); + + api.browser.attach({ + tabId: "browser:zoomed", + url: "https://example.com/", + bounds: { x: 800, y: 40, width: 400, height: 600 }, + visible: false, + }); + api.browser.setBounds({ + tabId: "browser:zoomed", + bounds: { x: 801, y: 41, width: 399, height: 599 }, + }); + + expect(electronMock.sendCalls).toEqual([ + { + channel: BB_DESKTOP_BROWSER_ATTACH_CHANNEL, + payload: { + tabId: "browser:zoomed", + url: "https://example.com/", + bounds: { x: 1000, y: 50, width: 500, height: 750 }, + visible: false, + }, + }, + { + channel: BB_DESKTOP_BROWSER_SET_BOUNDS_CHANNEL, + payload: { + tabId: "browser:zoomed", + bounds: { x: 1001, y: 51, width: 499, height: 749 }, + }, + }, + ]); + }); + it("validates browser event payloads before notifying renderer listeners", async () => { const api = await loadPreload(); const states: BbDesktopBrowserState[] = []; diff --git a/packages/desktop-contract/src/browser.ts b/packages/desktop-contract/src/browser.ts index 9d034ffaea..8376306714 100644 --- a/packages/desktop-contract/src/browser.ts +++ b/packages/desktop-contract/src/browser.ts @@ -10,14 +10,15 @@ export const BB_DESKTOP_BROWSER_MAX_URL_LENGTH = 4096; export const BB_DESKTOP_BROWSER_MAX_TITLE_LENGTH = 1024; /** - * Pixel rect (CSS px, which equal device-independent points on macOS) of the - * panel region the native browser view must overlay, measured by the renderer - * against its own layout viewport. This rect is the single placement - * authority: the renderer re-measures and pushes it whenever its layout moves - * the panel, and the desktop main process only intersects it with the live - * window content bounds — it never extrapolates placement from native window - * resizes, whose size the renderer's (possibly lagging) chrome paint does not - * yet reflect. + * Pixel rect of the panel region the native browser view must overlay, + * measured by the renderer against its own layout viewport. The preload + * converts these CSS pixels to native window points at the current page zoom + * before it sends the rect to the desktop main process. This rect is the + * single placement authority: the renderer re-measures and pushes it whenever + * its layout moves the panel, and the desktop main process only intersects it + * with the live window content bounds — it never extrapolates placement from + * native window resizes, whose size the renderer's (possibly lagging) chrome + * paint does not yet reflect. */ export const bbDesktopBrowserViewBoundsSchema = z .object({