diff --git a/apps/web/src/browser/HostedBrowserWebview.tsx b/apps/web/src/browser/HostedBrowserWebview.tsx index 28edc42d406a8..e95b557eb6c3a 100644 --- a/apps/web/src/browser/HostedBrowserWebview.tsx +++ b/apps/web/src/browser/HostedBrowserWebview.tsx @@ -13,6 +13,7 @@ import { } from "~/components/preview/thumbButtons"; import { usePreviewBridge } from "~/components/preview/usePreviewBridge"; import { useClientSettingsHydrated } from "~/hooks/useSettings"; +import { useViewportSize } from "~/hooks/useViewportSize"; import { cn, isMacPlatform } from "~/lib/utils"; import { resolveBrowserSurfacePanelRect, useBrowserSurfaceStore } from "./browserSurfaceStore"; @@ -79,6 +80,7 @@ export function HostedBrowserWebview(props: { serverRendering, } = props; const clientSettingsHydrated = useClientSettingsHydrated(); + const windowSize = useViewportSize(); const config = usePreviewWebviewConfig(threadRef.environmentId, profileId); const [initialSrc] = useState(() => initialUrl ?? "about:blank"); const tabLeaseRef = useRef(null); @@ -331,6 +333,7 @@ export function HostedBrowserWebview(props: { zIndex: presentation.zIndex, rect: lastRect, hiddenSize: { width: hiddenWidth, height: hiddenHeight }, + windowSize, }); return ( diff --git a/apps/web/src/browser/hostedBrowserWebviewStyle.test.ts b/apps/web/src/browser/hostedBrowserWebviewStyle.test.ts new file mode 100644 index 0000000000000..4f676dc6e6cf2 --- /dev/null +++ b/apps/web/src/browser/hostedBrowserWebviewStyle.test.ts @@ -0,0 +1,32 @@ +import { expect, it } from "vite-plus/test"; + +import { resolveHostedBrowserWebviewWrapperStyle } from "./hostedBrowserWebviewStyle"; + +it.each([ + { hiddenSize: { width: 1440, height: 900 }, windowSize: { width: 1840, height: 1016 }, scale: 1 }, + { + hiddenSize: { width: 3440, height: 1440 }, + windowSize: { width: 1720, height: 1016 }, + scale: 0.5, + }, + { + hiddenSize: { width: 1000, height: 2000 }, + windowSize: { width: 1840, height: 500 }, + scale: 0.25, + }, + { hiddenSize: { width: 3440, height: 1440 }, windowSize: { width: 0, height: 0 }, scale: 1 }, +])( + "fits a $hiddenSize.width x $hiddenSize.height capture guest in a $windowSize.width x $windowSize.height window", + ({ hiddenSize, windowSize, scale }) => { + const style = resolveHostedBrowserWebviewWrapperStyle({ + active: false, + renderingActive: true, + rect: null, + hiddenSize, + windowSize, + }); + expect(style).toMatchObject({ left: 0, top: 0, ...hiddenSize, visibility: "visible" }); + expect(style.transform).toBe(scale < 1 ? `scale(${scale})` : undefined); + expect(style.transformOrigin).toBe(scale < 1 ? "top left" : undefined); + }, +); diff --git a/apps/web/src/browser/hostedBrowserWebviewStyle.ts b/apps/web/src/browser/hostedBrowserWebviewStyle.ts index f37c222b08e9c..c4a31c6c56900 100644 --- a/apps/web/src/browser/hostedBrowserWebviewStyle.ts +++ b/apps/web/src/browser/hostedBrowserWebviewStyle.ts @@ -14,6 +14,8 @@ export interface HostedBrowserWebviewWrapperStyle { readonly pointerEvents: "auto" | "none"; readonly borderRadius?: number; readonly visibility?: "hidden" | "visible"; + readonly transform?: string; + readonly transformOrigin?: "top left"; } const HIDDEN_BROWSER_WEBVIEW_OFFSET = -100_000; @@ -26,6 +28,7 @@ export function resolveHostedBrowserWebviewWrapperStyle(input: { readonly zIndex?: number; readonly rect: BrowserSurfaceRect | null; readonly hiddenSize: HostedBrowserWebviewSize; + readonly windowSize: HostedBrowserWebviewSize; }): HostedBrowserWebviewWrapperStyle { const { active, @@ -35,6 +38,7 @@ export function resolveHostedBrowserWebviewWrapperStyle(input: { rect, renderingActive, zIndex = 30, + windowSize, } = input; if (active && rect) { return { @@ -49,7 +53,12 @@ export function resolveHostedBrowserWebviewWrapperStyle(input: { } if (renderingActive) { - // Electron stops compositing offscreen guests; keep capture-active guests inside the viewport behind the app. + // Electron clips oversized native surfaces; fit the whole guest without changing its DOM viewport. + // A minimized window can report a zero viewport; scale(0) would stop the guest compositing. + const scale = + windowSize.width > 0 && windowSize.height > 0 + ? Math.min(1, windowSize.width / hiddenSize.width, windowSize.height / hiddenSize.height) + : 1; return { left: 0, top: 0, @@ -58,6 +67,7 @@ export function resolveHostedBrowserWebviewWrapperStyle(input: { zIndex: -1, pointerEvents: "none", visibility: "visible", + ...(scale < 1 ? { transform: `scale(${scale})`, transformOrigin: "top left" as const } : {}), }; }