authorgravatar for ishansachu1@gmail.comIshan <ishansachu1@gmail.com> 2026-07-16 13:04:33+05:30
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-07-16 09:34:33+02:00
log0418301de79bb7b6f82305236325cb9811615e9e
tree909f568bf7ccd6fb6b69db0526255c29d0283c3b
parent5e8b2c800ecd98e23adbdbfc8970f4687ac8254b
signature Signed by PGP key B5690EEEBB952194

Fix macOS fullscreen titlebar spacing (#4019)


9 files changed, 124 insertions(+), 6 deletions(-)

apps/desktop/src/ipc/DesktopIpcHandlers.ts+2
......@@ -35,6 +35,7 @@ import {
3535 getAppBranding,
3636 getLocalEnvironmentBootstraps,
3737 getLocalEnvironmentBearerToken,
38 getWindowFullscreenState,
3839 openExternal,
3940 pickFolder,
4041 setTheme,
......@@ -48,6 +49,7 @@ export const installDesktopIpcHandlers = Effect.fn("desktop.ipc.installHandlers"
4849 yield* PreviewIpc.installPreviewEventForwarding();
4950
5051 yield* ipc.handleSync(getAppBranding);
52 yield* ipc.handleSync(getWindowFullscreenState);
5153 yield* ipc.handleSync(getLocalEnvironmentBootstraps);
5254 yield* ipc.handle(getLocalEnvironmentBearerToken);
5355
apps/desktop/src/ipc/channels.ts+2
......@@ -4,6 +4,8 @@ export const SET_THEME_CHANNEL = "desktop:set-theme";
44export const CONTEXT_MENU_CHANNEL = "desktop:context-menu";
55export const OPEN_EXTERNAL_CHANNEL = "desktop:open-external";
66export const MENU_ACTION_CHANNEL = "desktop:menu-action";
7export const GET_WINDOW_FULLSCREEN_STATE_CHANNEL = "desktop:get-window-fullscreen-state";
8export const WINDOW_FULLSCREEN_STATE_CHANNEL = "desktop:window-fullscreen-state";
79export const UPDATE_STATE_CHANNEL = "desktop:update-state";
810export const UPDATE_GET_STATE_CHANNEL = "desktop:update-get-state";
911export const UPDATE_SET_CHANNEL_CHANNEL = "desktop:update-set-channel";
apps/desktop/src/ipc/methods/window.test.ts+21-1
......@@ -1,10 +1,14 @@
11import { assert, describe, it } from "@effect/vitest";
22import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";
34import * as Option from "effect/Option";
45
6import type * as Electron from "electron";
7
58import * as DesktopBackendManager from "../../backend/DesktopBackendManager.ts";
69import * as DesktopBackendPool from "../../backend/DesktopBackendPool.ts";
7import { getLocalEnvironmentBootstraps } from "./window.ts";
10import * as ElectronWindow from "../../electron/ElectronWindow.ts";
11import { getLocalEnvironmentBootstraps, getWindowFullscreenState } from "./window.ts";
812
913const readyWslConfig: DesktopBackendManager.DesktopBackendStartConfig = {
1014 executablePath: "wsl.exe",
......@@ -126,3 +130,19 @@ describe("getLocalEnvironmentBootstraps", () => {
126130 }).pipe(Effect.provide(DesktopBackendPool.layerTest([stoppedInstance])));
127131 });
128132});
133
134describe("getWindowFullscreenState", () => {
135 it.effect("reads the current native window state", () => {
136 const window = { isFullScreen: () => true } as Electron.BrowserWindow;
137
138 return Effect.gen(function* () {
139 assert.isTrue(yield* getWindowFullscreenState.handler());
140 }).pipe(
141 Effect.provide(
142 Layer.mock(ElectronWindow.ElectronWindow)({
143 currentMainOrFirst: Effect.succeed(Option.some(window)),
144 }),
145 ),
146 );
147 });
148});
apps/desktop/src/ipc/methods/window.ts+10
......@@ -55,6 +55,16 @@ export const getAppBranding = DesktopIpc.makeSyncIpcMethod({
5555 }),
5656});
5757
58export const getWindowFullscreenState = DesktopIpc.makeSyncIpcMethod({
59 channel: IpcChannels.GET_WINDOW_FULLSCREEN_STATE_CHANNEL,
60 result: Schema.Boolean,
61 handler: Effect.fn("desktop.ipc.window.getWindowFullscreenState")(function* () {
62 const electronWindow = yield* ElectronWindow.ElectronWindow;
63 const window = yield* electronWindow.currentMainOrFirst;
64 return Option.isSome(window) && window.value.isFullScreen();
65 }),
66});
67
5868export const getLocalEnvironmentBootstraps = DesktopIpc.makeSyncIpcMethod({
5969 channel: IpcChannels.GET_LOCAL_ENVIRONMENT_BOOTSTRAPS_CHANNEL,
6070 result: Schema.Array(DesktopEnvironmentBootstrapSchema),
apps/desktop/src/preload.ts+13
......@@ -116,6 +116,19 @@ contextBridge.exposeInMainWorld("desktopBridge", {
116116 ipcRenderer.removeListener(IpcChannels.MENU_ACTION_CHANNEL, wrappedListener);
117117 };
118118 },
119 getWindowFullscreenState: () =>
120 ipcRenderer.sendSync(IpcChannels.GET_WINDOW_FULLSCREEN_STATE_CHANNEL) === true,
121 onWindowFullscreenStateChange: (listener) => {
122 const wrappedListener = (_event: Electron.IpcRendererEvent, fullscreen: unknown) => {
123 if (typeof fullscreen !== "boolean") return;
124 listener(fullscreen);
125 };
126
127 ipcRenderer.on(IpcChannels.WINDOW_FULLSCREEN_STATE_CHANNEL, wrappedListener);
128 return () => {
129 ipcRenderer.removeListener(IpcChannels.WINDOW_FULLSCREEN_STATE_CHANNEL, wrappedListener);
130 };
131 },
119132 getUpdateState: () => ipcRenderer.invoke(IpcChannels.UPDATE_GET_STATE_CHANNEL),
120133 setUpdateChannel: (channel) =>
121134 ipcRenderer.invoke(IpcChannels.UPDATE_SET_CHANNEL_CHANNEL, channel),
apps/desktop/src/window/DesktopWindow.test.ts+38-2
......@@ -28,7 +28,7 @@ import * as ElectronMenu from "../electron/ElectronMenu.ts";
2828import * as ElectronShell from "../electron/ElectronShell.ts";
2929import * as ElectronTheme from "../electron/ElectronTheme.ts";
3030import * as ElectronWindow from "../electron/ElectronWindow.ts";
31import { MENU_ACTION_CHANNEL } from "../ipc/channels.ts";
31import { MENU_ACTION_CHANNEL, WINDOW_FULLSCREEN_STATE_CHANNEL } from "../ipc/channels.ts";
3232import * as DesktopServerExposure from "../backend/DesktopServerExposure.ts";
3333import * as DesktopWindow from "./DesktopWindow.ts";
3434import * as PreviewManager from "../preview/Manager.ts";
......@@ -46,6 +46,7 @@ const environmentInput = {
4646} satisfies DesktopEnvironment.MakeDesktopEnvironmentInput;
4747
4848function makeFakeBrowserWindow() {
49 const windowListeners = new Map<string, (...args: readonly unknown[]) => void>();
4950 const webContentsListeners = new Map<string, (...args: readonly unknown[]) => void>();
5051 const webContents = {
5152 copyImageAt: vi.fn(),
......@@ -66,10 +67,13 @@ function makeFakeBrowserWindow() {
6667 close: vi.fn(),
6768 focus: vi.fn(),
6869 isDestroyed: vi.fn(() => false),
70 isFullScreen: vi.fn(() => false),
6971 isMinimized: vi.fn(() => false),
7072 isVisible: vi.fn(() => true),
7173 loadURL: vi.fn(() => Promise.resolve()),
72 on: vi.fn(),
74 on: vi.fn((eventName: string, listener: (...args: readonly unknown[]) => void) => {
75 windowListeners.set(eventName, listener);
76 }),
7377 once: vi.fn(),
7478 restore: vi.fn(),
7579 setBackgroundColor: vi.fn(),
......@@ -88,6 +92,7 @@ function makeFakeBrowserWindow() {
8892 send: webContents.send,
8993 setAutoHideCursor: window.setAutoHideCursor,
9094 webContentsListeners,
95 windowListeners,
9196 };
9297}
9398
......@@ -338,6 +343,37 @@ describe("DesktopWindow", () => {
338343 }),
339344 );
340345
346 it.effect("publishes native macOS fullscreen changes to the renderer", () =>
347 Effect.gen(function* () {
348 const fakeWindow = makeFakeBrowserWindow();
349 const createCount = yield* Ref.make(0);
350 const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
351 const layer = makeTestLayer({
352 window: fakeWindow.window,
353 createCount,
354 mainWindow,
355 });
356
357 yield* Effect.gen(function* () {
358 const desktopWindow = yield* DesktopWindow.DesktopWindow;
359 yield* desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773"));
360
361 const enterFullscreen = fakeWindow.windowListeners.get("enter-full-screen");
362 const leaveFullscreen = fakeWindow.windowListeners.get("leave-full-screen");
363 if (!enterFullscreen || !leaveFullscreen) {
364 return yield* Effect.die("fullscreen listeners were not registered");
365 }
366
367 enterFullscreen();
368 leaveFullscreen();
369 assert.deepEqual(fakeWindow.send.mock.calls, [
370 [WINDOW_FULLSCREEN_STATE_CHANNEL, true],
371 [WINDOW_FULLSCREEN_STATE_CHANNEL, false],
372 ]);
373 }).pipe(Effect.provide(layer));
374 }),
375 );
376
341377 it.effect("recovers when the development renderer is temporarily unreachable", () =>
342378 Effect.gen(function* () {
343379 const fakeWindow = makeFakeBrowserWindow();
apps/desktop/src/window/DesktopWindow.ts+10-1
......@@ -15,7 +15,7 @@ import { getDesktopUrl } from "../electron/ElectronProtocol.ts";
1515import * as ElectronShell from "../electron/ElectronShell.ts";
1616import * as ElectronTheme from "../electron/ElectronTheme.ts";
1717import * as ElectronWindow from "../electron/ElectronWindow.ts";
18import { MENU_ACTION_CHANNEL } from "../ipc/channels.ts";
18import { MENU_ACTION_CHANNEL, WINDOW_FULLSCREEN_STATE_CHANNEL } from "../ipc/channels.ts";
1919import * as PreviewManager from "../preview/Manager.ts";
2020
2121const TITLEBAR_HEIGHT = 40;
......@@ -365,6 +365,15 @@ export const make = Effect.gen(function* () {
365365 window.setTitle(environment.displayName);
366366 });
367367
368 if (environment.platform === "darwin") {
369 window.on("enter-full-screen", () => {
370 window.webContents.send(WINDOW_FULLSCREEN_STATE_CHANNEL, true);
371 });
372 window.on("leave-full-screen", () => {
373 window.webContents.send(WINDOW_FULLSCREEN_STATE_CHANNEL, false);
374 });
375 }
376
368377 let developmentLoadRetryIndex = 0;
369378 let developmentLoadRetryFiber: Fiber.Fiber<void, never> | undefined;
370379 const clearDevelopmentLoadRetry = () => {
apps/web/src/components/AppSidebarLayout.tsx+26-2
......@@ -1,5 +1,5 @@
11import { useAtomValue } from "@effect/atom-react";
2import { useEffect, type CSSProperties, type ReactNode } from "react";
2import { useEffect, useState, type CSSProperties, type ReactNode } from "react";
33import { useNavigate } from "@tanstack/react-router";
44
55import { isElectron } from "../env";
......@@ -55,11 +55,35 @@ function SidebarControl() {
5555
5656export function AppSidebarLayout({ children }: { children: ReactNode }) {
5757 const navigate = useNavigate();
58 const isMacosDesktop = isElectron && isMacPlatform(navigator.platform);
59 const [isWindowFullscreen, setIsWindowFullscreen] = useState(() => {
60 const getWindowFullscreenState = window.desktopBridge?.getWindowFullscreenState;
61 return isMacosDesktop && typeof getWindowFullscreenState === "function"
62 ? getWindowFullscreenState()
63 : false;
64 });
5865 const macosWindowControlsStyle =
59 isElectron && isMacPlatform(navigator.platform)
66 isMacosDesktop && !isWindowFullscreen
6067 ? ({ "--workspace-controls-left": MACOS_TRAFFIC_LIGHTS_LEFT_INSET } as CSSProperties)
6168 : undefined;
6269
70 useEffect(() => {
71 if (!isMacosDesktop) return;
72 const bridge = window.desktopBridge;
73 if (!bridge) return;
74 const { getWindowFullscreenState, onWindowFullscreenStateChange } = bridge;
75 if (
76 typeof getWindowFullscreenState !== "function" ||
77 typeof onWindowFullscreenStateChange !== "function"
78 ) {
79 return;
80 }
81
82 const unsubscribe = onWindowFullscreenStateChange(setIsWindowFullscreen);
83 setIsWindowFullscreen(getWindowFullscreenState());
84 return unsubscribe;
85 }, [isMacosDesktop]);
86
6387 useEffect(() => {
6488 const onMenuAction = window.desktopBridge?.onMenuAction;
6589 if (typeof onMenuAction !== "function") {
packages/contracts/src/ipc.ts+2
......@@ -994,6 +994,8 @@ export interface DesktopBridge {
994994 ) => Promise<T | null>;
995995 openExternal: (url: string) => Promise<boolean>;
996996 onMenuAction: (listener: (action: string) => void) => () => void;
997 getWindowFullscreenState: () => boolean;
998 onWindowFullscreenStateChange: (listener: (fullscreen: boolean) => void) => () => void;
997999 getUpdateState: () => Promise<DesktopUpdateState>;
9981000 setUpdateChannel: (channel: DesktopUpdateChannel) => Promise<DesktopUpdateState>;
9991001 checkForUpdate: () => Promise<DesktopUpdateCheckResult>;