Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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/ipc/DesktopIpcHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import {
pickProjectFavicon,
pickThemeFiles,
setTheme,
syncHtmlFullscreen,
showContextMenu,
} from "./methods/window.ts";
import {
Expand Down Expand Up @@ -149,6 +150,7 @@ export const installDesktopIpcHandlers = Effect.fn("desktop.ipc.installHandlers"
yield* ipc.handle(checkSystemPermission);
yield* ipc.handle(pasteAsText);
yield* ipc.handle(revealWindow);
yield* ipc.handle(syncHtmlFullscreen);
yield* ipc.handle(probeRemoteEditors);
yield* ipc.handle(getUpdateState);
yield* ipc.handle(setUpdateChannel);
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/ipc/channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export const TRACKPAD_SCROLL_BEGIN_CHANNEL = "desktop:trackpad-scroll-begin";
export const TRACKPAD_SCROLL_END_CHANNEL = "desktop:trackpad-scroll-end";
export const GET_WINDOW_FULLSCREEN_STATE_CHANNEL = "desktop:get-window-fullscreen-state";
export const WINDOW_FULLSCREEN_STATE_CHANNEL = "desktop:window-fullscreen-state";
export const SYNC_HTML_FULLSCREEN_CHANNEL = "desktop:sync-html-fullscreen";
export const DESKTOP_APP_ACTIVATION_READY_CHANNEL = "desktop:app-activation-ready";
export const DESKTOP_APP_ACTIVATION_COMPLETE_CHANNEL = "desktop:app-activation-complete";
export const DESKTOP_APP_ACTIVATION_REQUEST_CHANNEL = "desktop:app-activation-request";
Expand Down
59 changes: 59 additions & 0 deletions apps/desktop/src/ipc/methods/window.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
pickProjectFavicon,
probeRemoteEditors,
readMedia,
syncHtmlFullscreen,
} from "./window.ts";

const readyWslConfig: DesktopBackendManager.DesktopBackendStartConfig = {
Expand Down Expand Up @@ -356,6 +357,64 @@ describe("pickProjectFavicon", () => {
);
});

describe("syncHtmlFullscreen", () => {
const fullscreenWindow = (fullscreen: boolean) => {
let current = fullscreen;
const leaveListeners: Array<() => void> = [];
const setFullScreen = vi.fn((next: boolean) => {
const left = current && !next;
current = next;
if (left) for (const listener of leaveListeners.splice(0)) listener();
});
const window = {
webContents: { id: 1 },
isDestroyed: () => false,
isFullScreen: () => current,
setFullScreen,
once: (event: string, listener: () => void) => {
if (event === "leave-full-screen") leaveListeners.push(listener);
},
} as unknown as Electron.BrowserWindow;
const layer = Layer.mock(ElectronWindow.ElectronWindow)({ main: Effect.succeedSome(window) });
return { window, setFullScreen, layer };
};
const fromMainRenderer = { sender: { id: 1 } };

it.effect("takes the screen for the app's own fullscreen element and gives it back", () => {
const { window, layer } = fullscreenWindow(false);
return Effect.gen(function* () {
yield* syncHtmlFullscreen.handler(true, fromMainRenderer);
assert.isTrue(window.isFullScreen());

yield* syncHtmlFullscreen.handler(false, fromMainRenderer);
assert.isFalse(window.isFullScreen());
}).pipe(Effect.provide(layer));
});

it.effect("keeps a window the user had already made fullscreen", () => {
const { window, setFullScreen, layer } = fullscreenWindow(true);
return Effect.gen(function* () {
yield* syncHtmlFullscreen.handler(true, fromMainRenderer);
yield* syncHtmlFullscreen.handler(false, fromMainRenderer);

assert.isTrue(window.isFullScreen());
assert.strictEqual(setFullScreen.mock.calls.length, 0);
}).pipe(Effect.provide(layer));
});

it.effect("hands the window back once the user leaves OS fullscreen during playback", () => {
const { window, layer } = fullscreenWindow(false);
return Effect.gen(function* () {
yield* syncHtmlFullscreen.handler(true, fromMainRenderer);
window.setFullScreen(false);
window.setFullScreen(true);
yield* syncHtmlFullscreen.handler(false, fromMainRenderer);

assert.isTrue(window.isFullScreen());
}).pipe(Effect.provide(layer));
});
});

it.effect.skipIf(HostProcess.Platform.defaultValue() === "win32")(
"finds remote editors installed without PATH launchers",
() =>
Expand Down
32 changes: 32 additions & 0 deletions apps/desktop/src/ipc/methods/window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,38 @@ export const revealWindow = DesktopIpc.makeIpcMethod({
}),
});

const htmlFullscreenWindows = new WeakSet<Electron.BrowserWindow>();

// The main window keeps HTML fullscreen inside itself so preview guests stay in their webview,
// so the app's own fullscreen elements, such as chat videos, take the screen here.
export const syncHtmlFullscreen = DesktopIpc.makeIpcMethod({
channel: IpcChannels.SYNC_HTML_FULLSCREEN_CHANNEL,
payload: Schema.Boolean,
result: Schema.Void,
handler: Effect.fn("desktop.ipc.window.syncHtmlFullscreen")(function* (fullscreen, event) {
const electronWindow = yield* ElectronWindow.ElectronWindow;
const main = yield* electronWindow.main;
if (
event === undefined ||
Option.isNone(main) ||
main.value.isDestroyed() ||
main.value.webContents.id !== event.sender.id
) {
return;
}
const window = main.value;
if (fullscreen) {
if (window.isFullScreen()) return;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
htmlFullscreenWindows.add(window);
// Leaving OS fullscreen any other way, such as F11, hands the window back to the user.
window.once("leave-full-screen", () => htmlFullscreenWindows.delete(window));
window.setFullScreen(true);
} else if (htmlFullscreenWindows.delete(window)) {
window.setFullScreen(false);
}
}),
});

const PICKED_THEME_FILE_MAX_BYTES = 256 * 1024;

export const pickThemeFiles = DesktopIpc.makeIpcMethod({
Expand Down
12 changes: 12 additions & 0 deletions apps/desktop/src/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,18 @@ if (clientPlatform === "darwin") {
window.addEventListener("resize", syncWindowControlInset);
}

const syncHtmlFullscreen = () => {
const element = document.fullscreenElement;
// A preview guest's fullscreen surfaces here as its <webview>, which stays inside the window.
void ipcRenderer.invoke(
IpcChannels.SYNC_HTML_FULLSCREEN_CHANNEL,
element !== null && element.tagName !== "WEBVIEW",
);
};
// A reload or renderer crash never reports the old document's exit, so each document reports its start.
syncHtmlFullscreen();
document.addEventListener("fullscreenchange", syncHtmlFullscreen);

function unwrapEnsureSshEnvironmentResult(result: unknown) {
if (
typeof result === "object" &&
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/window/DesktopWindow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,7 @@ export const make = Effect.gen(function* () {
sandbox: true,
webviewTag: true,
// Guest fullscreen propagates to the embedder; both must opt out to keep it inside the webview.
// The app's own fullscreen elements reach OS fullscreen through syncHtmlFullscreen.
disableHtmlFullscreenWindowResize: true,
},
});
Expand Down
Loading