Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/desktop/src/preview/GuestProtocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,5 @@ export const PASSKEY_GET_CHANNEL = "preview:passkey-get";
export const PASSKEY_BRIDGE_ARGUMENT = "--t3code-preview-passkey-bridge";
export const ANNOTATION_VOICE_EVENT_CHANNEL = "preview:annotation-voice-event";
export const ANNOTATION_VOICE_STATE_CHANNEL = "preview:annotation-voice-state";
// HostedBrowserWebview listens for this on the webview's ipc-message event.
export const GUEST_PAINTED_CHANNEL = "preview:guest-painted";
8 changes: 8 additions & 0 deletions apps/desktop/src/preview/PickPreload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
ANNOTATION_VOICE_STATE_CHANNEL,
CANCEL_PICK_CHANNEL,
ELEMENT_PICKED_CHANNEL,
GUEST_PAINTED_CHANNEL,
HUMAN_INPUT_CHANNEL,
MOUSE_NAVIGATE_CHANNEL,
RECORDING_CURSOR_CHANNEL,
Expand Down Expand Up @@ -1809,3 +1810,10 @@ ipcRenderer.on(ANNOTATION_THEME_CHANNEL, (_event, theme: DesktopPreviewAnnotatio
activeSession?.applyTheme(theme);
});
ipcRenderer.on(CANCEL_PICK_CHANNEL, () => activeSession?.teardown(false));

// Chromium runs no frame until the body exists and render-blocking stylesheets load, so this is the first paint.
if (location.href !== "about:blank") {
requestAnimationFrame(() =>
requestAnimationFrame(() => ipcRenderer.sendToHost(GUEST_PAINTED_CHANNEL)),
);
}
23 changes: 15 additions & 8 deletions apps/web/src/browser/HostedBrowserWebview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ import {
type WebviewCrashRecoveryState,
} from "./webviewCrashRecovery";

// Sent by the desktop guest preload (apps/desktop/src/preview/GuestProtocol.ts).
const GUEST_PAINTED_CHANNEL = "preview:guest-painted";

interface ElectronWebview extends HTMLElement {
src: string;
partition: string;
Expand Down Expand Up @@ -135,6 +138,8 @@ export function HostedBrowserWebview(props: {

const [webviewGeneration, setWebviewGeneration] = useState(0);
const [recoverySrc, setRecoverySrc] = useState(initialSrc);
// The guest is transparent until a page paints, so the host shows the theme until the first frame.
const [paintedGeneration, setPaintedGeneration] = useState<number | null>(null);
const active = presentation.visible && presentation.rect !== null;
const shown = active || pictureInPicture;
const reportedShown = useEffectEvent(() => serverDriven && shown);
Expand Down Expand Up @@ -186,8 +191,13 @@ export function HostedBrowserWebview(props: {
new PointerEvent("pointerdown", { bubbles: true, pointerType: "mouse" }),
);
};
const onGuestMessage = (event: Event) => {
if ((event as Event & { channel?: string }).channel !== GUEST_PAINTED_CHANNEL) return;
setPaintedGeneration(webviewGeneration);
};
webview.addEventListener("did-attach", register);
webview.addEventListener("dom-ready", register);
webview.addEventListener("ipc-message", onGuestMessage);
webview.addEventListener("render-process-gone", recoverGuest);
webview.addEventListener("focus", dismissHostPopups);
register();
Expand All @@ -196,16 +206,11 @@ export function HostedBrowserWebview(props: {
if (recoveryTimeout !== null) clearTimeout(recoveryTimeout);
webview.removeEventListener("did-attach", register);
webview.removeEventListener("dom-ready", register);
webview.removeEventListener("ipc-message", onGuestMessage);
webview.removeEventListener("render-process-gone", recoverGuest);
webview.removeEventListener("focus", dismissHostPopups);
};
}, [
clientSettingsHydrated,
config,
runtimeTabId,
// oxlint-disable-next-line react/exhaustive-effect-dependencies -- A crash recovery remounts the webview under a new key, so the listeners attach to the new element.
webviewGeneration,
]);
}, [clientSettingsHydrated, config, runtimeTabId, webviewGeneration]);

const lastRect = presentation.rect;
const layoutZoomFactor = useBrowserLayoutZoomFactor(zoomFactor);
Expand Down Expand Up @@ -362,7 +367,9 @@ export function HostedBrowserWebview(props: {
}
aria-hidden={active ? undefined : true}
className={cn(
"absolute flex overflow-hidden bg-white",
"absolute flex overflow-hidden",
// White is the browser canvas behind transparent pages once a document exists.
paintedGeneration === webviewGeneration ? "bg-white" : "bg-background",
active && !layout.fillsPanel && "ring-1 ring-border/70 shadow-sm",
)}
style={{
Expand Down
Loading