import { getDesktopHost } from "@/desktop/host"; const RESIDENT_BROWSER_HOST_ID = "paseo-browser-resident-webviews"; const BROWSER_ID_ATTRIBUTE = "data-paseo-browser-id"; const RESIDENT_VIEWPORT_WIDTH = 1280; const RESIDENT_VIEWPORT_HEIGHT = 800; const residentWebviewsByBrowserId = new Map(); const activeCapturePreparations = new Map(); let captureBridgeInstallCount = 0; let captureBridgeDisposer: (() => void) | null = null; let nextCapturePreparationId = 0; interface BrowserWebviewElement extends HTMLElement { src: string; } function trimNonEmpty(value: string | null | undefined): string | null { if (typeof value !== "string") { return null; } const trimmed = value.trim(); return trimmed.length > 0 ? trimmed : null; } function readDocument(): Document | null { return typeof document === "undefined" ? null : document; } function applyResidentHostParkingStyle(host: HTMLElement): void { host.setAttribute("aria-hidden", "true"); host.style.position = "fixed"; host.style.left = "-20000px"; host.style.top = "0"; host.style.width = `${RESIDENT_VIEWPORT_WIDTH}px`; host.style.height = `${RESIDENT_VIEWPORT_HEIGHT}px`; host.style.overflow = "hidden"; host.style.opacity = "0"; host.style.pointerEvents = "none"; host.style.zIndex = ""; host.style.clipPath = ""; host.style.visibility = ""; host.style.transform = ""; } function applyResidentHostCaptureStyle(host: HTMLElement): void { host.setAttribute("aria-hidden", "true"); host.style.position = "fixed"; host.style.left = "0"; host.style.top = "0"; host.style.width = "1px"; host.style.height = "1px"; host.style.overflow = "hidden"; host.style.opacity = "1"; host.style.pointerEvents = "none"; host.style.zIndex = "1"; host.style.clipPath = ""; host.style.visibility = ""; host.style.transform = ""; } function getResidentBrowserHost(ownerDocument: Document): HTMLElement { const existing = ownerDocument.getElementById(RESIDENT_BROWSER_HOST_ID); if (existing) { return existing; } const host = ownerDocument.createElement("div"); host.id = RESIDENT_BROWSER_HOST_ID; applyResidentHostParkingStyle(host); ownerDocument.body.appendChild(host); return host; } function findBrowserWebview(browserId: string, ownerDocument: Document): HTMLElement | null { for (const element of ownerDocument.querySelectorAll(`[${BROWSER_ID_ATTRIBUTE}]`)) { if (!(element instanceof HTMLElement)) { continue; } if (element.getAttribute(BROWSER_ID_ATTRIBUTE) === browserId) { return element; } } return null; } function applyResidentWebviewStyle(webview: HTMLElement): void { webview.style.display = "inline-flex"; webview.style.flex = "0 0 auto"; webview.style.width = `${RESIDENT_VIEWPORT_WIDTH}px`; webview.style.height = `${RESIDENT_VIEWPORT_HEIGHT}px`; webview.style.border = "0"; webview.style.background = "transparent"; } function hasActiveResidentHostPreparation(): boolean { for (const preparation of activeCapturePreparations.values()) { if (preparation.preparesResidentHost) { return true; } } return false; } function nextAnimationFrame(): Promise { if (typeof requestAnimationFrame !== "function") { return Promise.resolve(); } return new Promise((resolve) => { requestAnimationFrame(() => { resolve(); }); }); } async function waitForCapturePaint(webview: HTMLElement): Promise { await nextAnimationFrame(); await nextAnimationFrame(); webview.getBoundingClientRect(); } export function prepareBrowserWebview( webview: HTMLElement, input: { browserId: string; initialUrl?: string | null }, ): void { webview.setAttribute(BROWSER_ID_ATTRIBUTE, input.browserId); webview.setAttribute("partition", `persist:paseo-browser-${input.browserId}`); webview.setAttribute("allowpopups", "true"); webview.setAttribute("spellcheck", "false"); webview.setAttribute("autosize", "on"); if (input.initialUrl) { (webview as BrowserWebviewElement).src = input.initialUrl; } } export function ensureResidentBrowserWebview(input: { browserId: string; url: string; }): HTMLElement | null { const browserId = trimNonEmpty(input.browserId); if (!browserId) { return null; } const ownerDocument = readDocument(); if (!ownerDocument) { return null; } const resident = residentWebviewsByBrowserId.get(browserId) ?? null; if (resident?.isConnected) { return resident; } const existing = findBrowserWebview(browserId, ownerDocument); if (existing) { return existing; } const webview = ownerDocument.createElement("webview") as BrowserWebviewElement; prepareBrowserWebview(webview, { browserId, initialUrl: input.url }); releaseResidentBrowserWebview(browserId, webview); return webview; } export function takeResidentBrowserWebview(browserId: string): HTMLElement | null { const normalizedBrowserId = trimNonEmpty(browserId); if (!normalizedBrowserId) { return null; } const webview = residentWebviewsByBrowserId.get(normalizedBrowserId) ?? null; if (!webview) { return null; } residentWebviewsByBrowserId.delete(normalizedBrowserId); return webview; } export function releaseResidentBrowserWebview(browserId: string, webview: HTMLElement): void { const normalizedBrowserId = trimNonEmpty(browserId); if (!normalizedBrowserId) { webview.remove(); return; } const ownerDocument = readDocument(); if (!ownerDocument) { return; } residentWebviewsByBrowserId.set(normalizedBrowserId, webview); applyResidentWebviewStyle(webview); getResidentBrowserHost(ownerDocument).appendChild(webview); } export async function prepareResidentBrowserWebviewForPixelCapture(input: { browserId: string; }): Promise<{ token: string }> { const browserId = trimNonEmpty(input.browserId); if (!browserId) { throw new Error("Browser id is required for pixel capture preparation."); } const ownerDocument = readDocument(); if (!ownerDocument) { throw new Error("Browser pixel capture preparation requires a document."); } const host = getResidentBrowserHost(ownerDocument); const webview = findBrowserWebview(browserId, ownerDocument); if (!webview) { throw new Error(`Browser webview ${browserId} is not mounted.`); } const token = `capture-${++nextCapturePreparationId}`; const preparesResidentHost = webview.parentElement === host; activeCapturePreparations.set(token, { preparesResidentHost }); try { if (preparesResidentHost) { applyResidentHostCaptureStyle(host); applyResidentWebviewStyle(webview); } await waitForCapturePaint(webview); return { token }; } catch (error) { activeCapturePreparations.delete(token); if (!hasActiveResidentHostPreparation()) { applyResidentHostParkingStyle(host); } throw error; } } export async function restoreResidentBrowserWebviewAfterPixelCapture(input: { token: string; }): Promise { const preparation = activeCapturePreparations.get(input.token); if (!preparation) { return; } activeCapturePreparations.delete(input.token); if (!preparation.preparesResidentHost || hasActiveResidentHostPreparation()) { return; } const ownerDocument = readDocument(); if (!ownerDocument) { return; } const host = ownerDocument.getElementById(RESIDENT_BROWSER_HOST_ID); if (host instanceof HTMLElement) { applyResidentHostParkingStyle(host); } } export function installResidentBrowserCaptureBridge(): () => void { captureBridgeInstallCount += 1; if (!captureBridgeDisposer) { const browserBridge = getDesktopHost()?.browser; const disposePrepare = browserBridge?.onPrepareForPixelCapture?.( prepareResidentBrowserWebviewForPixelCapture, ); const disposeRestore = browserBridge?.onRestorePixelCapture?.( restoreResidentBrowserWebviewAfterPixelCapture, ); captureBridgeDisposer = () => { disposePrepare?.(); disposeRestore?.(); }; } return () => { captureBridgeInstallCount = Math.max(0, captureBridgeInstallCount - 1); if (captureBridgeInstallCount > 0) { return; } captureBridgeDisposer?.(); captureBridgeDisposer = null; }; } export function removeResidentBrowserWebview(browserId: string): void { const normalizedBrowserId = trimNonEmpty(browserId); if (!normalizedBrowserId) { return; } const resident = residentWebviewsByBrowserId.get(normalizedBrowserId) ?? null; residentWebviewsByBrowserId.delete(normalizedBrowserId); resident?.remove(); } export function clearResidentBrowserWebviewsForTests(): void { for (const webview of residentWebviewsByBrowserId.values()) { webview.remove(); } residentWebviewsByBrowserId.clear(); activeCapturePreparations.clear(); nextCapturePreparationId = 0; readDocument()?.getElementById(RESIDENT_BROWSER_HOST_ID)?.remove(); }