Skip to content
3 changes: 3 additions & 0 deletions apps/desktop/src/app/DesktopEnvironment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export interface MakeDesktopEnvironmentInput {
readonly dirname: string;
readonly homeDirectory: string;
readonly platform: NodeJS.Platform;
readonly systemVersion?: string | undefined;
readonly processArch: string;
readonly appVersion: string;
readonly appPath: string;
Expand All @@ -34,6 +35,7 @@ export class DesktopEnvironment extends Context.Service<
readonly path: Path.Path;
readonly dirname: string;
readonly platform: NodeJS.Platform;
readonly systemVersion: string | undefined;
readonly processArch: string;
readonly isPackaged: boolean;
readonly isDevelopment: boolean;
Expand Down Expand Up @@ -190,6 +192,7 @@ const make = Effect.fn("desktop.environment.make")(function* (
path,
dirname: input.dirname,
platform: input.platform,
systemVersion: input.systemVersion,
processArch: input.processArch,
isPackaged: input.isPackaged,
isDevelopment,
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/app/DesktopLifecycle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ function makeDesktopWindowLayer(
flushMainWindowBounds: input.flushMainWindowBounds ?? Effect.void,
dispatchMenuAction: () => Effect.void,
zoomMain: () => Effect.void,
isBackdropEnabled: () => false,
syncAppearance: Effect.void,
});
}
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/backend/DesktopBackendPool.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ function makePoolLayer(
flushMainWindowBounds: Effect.void,
dispatchMenuAction: () => Effect.die("unexpected menu action"),
zoomMain: () => Effect.die("unexpected zoom"),
isBackdropEnabled: () => false,
syncAppearance: Effect.void,
} satisfies DesktopWindow.DesktopWindow["Service"]),
),
Expand Down
2 changes: 2 additions & 0 deletions apps/desktop/src/ipc/DesktopIpcHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
getLocalEnvironmentBootstraps,
getLocalEnvironmentBearerToken,
getSystemLocale,
getWindowBackdropState,
getWindowFullscreenState,
openExternal,
probeRemoteEditors,
Expand All @@ -53,6 +54,7 @@ export const installDesktopIpcHandlers = Effect.fn("desktop.ipc.installHandlers"

yield* ipc.handleSync(getAppBranding);
yield* ipc.handleSync(getSystemLocale);
yield* ipc.handleSync(getWindowBackdropState);
yield* ipc.handleSync(getWindowFullscreenState);
yield* ipc.handleSync(getLocalEnvironmentBootstraps);
yield* ipc.handle(getLocalEnvironmentBearerToken);
Expand Down
2 changes: 2 additions & 0 deletions apps/desktop/src/ipc/channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ export const PROBE_REMOTE_EDITORS_CHANNEL = "desktop:probe-remote-editors";
export const MENU_ACTION_CHANNEL = "desktop:menu-action";
export const QUIT_SHORTCUT_CHANNEL = "desktop:quit-shortcut";
export const GET_WINDOW_FULLSCREEN_STATE_CHANNEL = "desktop:get-window-fullscreen-state";
export const GET_WINDOW_BACKDROP_STATE_CHANNEL = "desktop:get-window-backdrop-state";
export const WINDOW_BACKDROP_STATE_CHANNEL = "desktop:window-backdrop-state";
export const WINDOW_FULLSCREEN_STATE_CHANNEL = "desktop:window-fullscreen-state";
export const UPDATE_STATE_CHANNEL = "desktop:update-state";
export const UPDATE_GET_STATE_CHANNEL = "desktop:update-get-state";
Expand Down
3 changes: 3 additions & 0 deletions apps/desktop/src/ipc/methods/clientSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import * as Option from "effect/Option";
import * as Schema from "effect/Schema";

import * as DesktopClientSettings from "../../settings/DesktopClientSettings.ts";
import * as DesktopWindow from "../../window/DesktopWindow.ts";
import * as IpcChannels from "../channels.ts";
import * as DesktopIpc from "../DesktopIpc.ts";

Expand All @@ -24,5 +25,7 @@ export const setClientSettings = DesktopIpc.makeIpcMethod({
handler: Effect.fn("desktop.ipc.clientSettings.set")(function* (settings) {
const clientSettings = yield* DesktopClientSettings.DesktopClientSettings;
yield* clientSettings.set(settings);
const desktopWindow = yield* DesktopWindow.DesktopWindow;
yield* desktopWindow.syncAppearance;
}),
});
27 changes: 27 additions & 0 deletions apps/desktop/src/ipc/methods/window.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@ import type * as Electron from "electron";

import * as DesktopBackendManager from "../../backend/DesktopBackendManager.ts";
import * as DesktopBackendPool from "../../backend/DesktopBackendPool.ts";
import * as DesktopEnvironment from "../../app/DesktopEnvironment.ts";
import * as ElectronDialog from "../../electron/ElectronDialog.ts";
import * as ElectronWindow from "../../electron/ElectronWindow.ts";
import * as DesktopWindow from "../../window/DesktopWindow.ts";
import {
getLocalEnvironmentBootstraps,
getWindowBackdropState,
getWindowFullscreenState,
pickProjectFavicon,
} from "./window.ts";
Expand Down Expand Up @@ -153,6 +156,30 @@ describe("getWindowFullscreenState", () => {
});
});

describe("getWindowBackdropState", () => {
it.effect("returns false when the current platform is not Windows", () => {
const window = {} as Electron.BrowserWindow;

return Effect.gen(function* () {
assert.isFalse(yield* getWindowBackdropState.handler());
}).pipe(
Effect.provide(
Layer.mergeAll(
Layer.succeed(DesktopEnvironment.DesktopEnvironment, {
platform: "darwin",
} as DesktopEnvironment.DesktopEnvironment["Service"]),
Layer.mock(DesktopWindow.DesktopWindow)({
isBackdropEnabled: () => false,
}),
Layer.mock(ElectronWindow.ElectronWindow)({
currentMainOrFirst: Effect.succeed(Option.some(window)),
}),
),
),
);
});
});

describe("pickProjectFavicon", () => {
it.effect("opens a single-image picker from the project directory", () =>
Effect.gen(function* () {
Expand Down
15 changes: 15 additions & 0 deletions apps/desktop/src/ipc/methods/window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import * as ElectronMenu from "../../electron/ElectronMenu.ts";
import * as ElectronShell from "../../electron/ElectronShell.ts";
import * as ElectronTheme from "../../electron/ElectronTheme.ts";
import * as ElectronWindow from "../../electron/ElectronWindow.ts";
import * as DesktopWindow from "../../window/DesktopWindow.ts";
import * as IpcChannels from "../channels.ts";
import * as DesktopIpc from "../DesktopIpc.ts";
import {
Expand Down Expand Up @@ -85,6 +86,20 @@ export const getWindowFullscreenState = DesktopIpc.makeSyncIpcMethod({
}),
});

export const getWindowBackdropState = DesktopIpc.makeSyncIpcMethod({
channel: IpcChannels.GET_WINDOW_BACKDROP_STATE_CHANNEL,
result: Schema.Boolean,
handler: Effect.fn("desktop.ipc.window.getWindowBackdropState")(function* () {
const environment = yield* DesktopEnvironment.DesktopEnvironment;
if (environment.platform !== "win32") return false;

const desktopWindow = yield* DesktopWindow.DesktopWindow;
const electronWindow = yield* ElectronWindow.ElectronWindow;
const window = yield* electronWindow.currentMainOrFirst;
return Option.isSome(window) && desktopWindow.isBackdropEnabled(window.value);
}),
});

export const getLocalEnvironmentBootstraps = DesktopIpc.makeSyncIpcMethod({
channel: IpcChannels.GET_LOCAL_ENVIRONMENT_BOOTSTRAPS_CHANNEL,
result: Schema.Array(DesktopEnvironmentBootstrapSchema),
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ const desktopEnvironmentLayer = Layer.unwrap(
dirname: __dirname,
homeDirectory: NodeOS.homedir(),
platform,
systemVersion: process.getSystemVersion(),
processArch,
...metadata,
});
Expand Down
13 changes: 13 additions & 0 deletions apps/desktop/src/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,8 @@ contextBridge.exposeInMainWorld("desktopBridge", {
},
getWindowFullscreenState: () =>
ipcRenderer.sendSync(IpcChannels.GET_WINDOW_FULLSCREEN_STATE_CHANNEL) === true,
getWindowBackdropState: () =>
ipcRenderer.sendSync(IpcChannels.GET_WINDOW_BACKDROP_STATE_CHANNEL) === true,
onWindowFullscreenStateChange: (listener) => {
const wrappedListener = (_event: Electron.IpcRendererEvent, fullscreen: unknown) => {
if (typeof fullscreen !== "boolean") return;
Expand All @@ -151,6 +153,17 @@ contextBridge.exposeInMainWorld("desktopBridge", {
ipcRenderer.removeListener(IpcChannels.WINDOW_FULLSCREEN_STATE_CHANNEL, wrappedListener);
};
},
onWindowBackdropStateChange: (listener) => {
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
const wrappedListener = (_event: Electron.IpcRendererEvent, enabled: unknown) => {
if (typeof enabled !== "boolean") return;
listener(enabled);
};

ipcRenderer.on(IpcChannels.WINDOW_BACKDROP_STATE_CHANNEL, wrappedListener);
return () => {
ipcRenderer.removeListener(IpcChannels.WINDOW_BACKDROP_STATE_CHANNEL, wrappedListener);
};
},
getUpdateState: () => ipcRenderer.invoke(IpcChannels.UPDATE_GET_STATE_CHANNEL),
setUpdateChannel: (channel) =>
ipcRenderer.invoke(IpcChannels.UPDATE_SET_CHANNEL_CHANNEL, channel),
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/settings/DesktopClientSettings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const clientSettings: ClientSettings = {
dismissedProviderUpdateNotificationKeys: [],
diffIgnoreWhitespace: true,
environmentIdentificationMode: "artwork",
desktopBackdropEnabled: true,
favorites: [],
fontFamilyCode: "",
fontFamilyComposer: "",
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/window/DesktopApplicationMenu.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ const makeDesktopWindowLayer = (selectedAction: Deferred.Deferred<string>) =>
dispatchMenuAction: (action) => Deferred.succeed(selectedAction, action).pipe(Effect.asVoid),
zoomMain: (direction) =>
Deferred.succeed(selectedAction, `zoom-${direction}`).pipe(Effect.asVoid),
isBackdropEnabled: () => false,
syncAppearance: Effect.void,
} satisfies DesktopWindow.DesktopWindow["Service"]);

Expand Down
127 changes: 115 additions & 12 deletions apps/desktop/src/window/DesktopWindow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,11 @@ import * as ElectronMenu from "../electron/ElectronMenu.ts";
import * as ElectronShell from "../electron/ElectronShell.ts";
import * as ElectronTheme from "../electron/ElectronTheme.ts";
import * as ElectronWindow from "../electron/ElectronWindow.ts";
import { MENU_ACTION_CHANNEL, WINDOW_FULLSCREEN_STATE_CHANNEL } from "../ipc/channels.ts";
import {
MENU_ACTION_CHANNEL,
WINDOW_BACKDROP_STATE_CHANNEL,
WINDOW_FULLSCREEN_STATE_CHANNEL,
} from "../ipc/channels.ts";
import * as DesktopServerExposure from "../backend/DesktopServerExposure.ts";
import * as DesktopWindow from "./DesktopWindow.ts";
import * as PreviewManager from "../preview/Manager.ts";
Expand All @@ -52,6 +56,7 @@ const environmentInput = {
dirname: "/repo/apps/desktop/dist-electron",
homeDirectory: "/Users/alice",
platform: "darwin",
systemVersion: undefined,
processArch: "arm64",
appVersion: "1.2.3",
appPath: "/repo",
Expand Down Expand Up @@ -104,6 +109,7 @@ function makeFakeBrowserWindow() {
}),
restore: vi.fn(),
setBackgroundColor: vi.fn(),
setBackgroundMaterial: vi.fn(),
setAutoHideCursor: vi.fn(),
setTitle: vi.fn(),
setTitleBarOverlay: vi.fn(),
Expand All @@ -126,6 +132,8 @@ function makeFakeBrowserWindow() {
send: webContents.send,
setZoomLevel: webContents.setZoomLevel,
setBackgroundThrottling: webContents.setBackgroundThrottling,
setBackgroundColor: window.setBackgroundColor,
setBackgroundMaterial: window.setBackgroundMaterial,
setAutoHideCursor: window.setAutoHideCursor,
webContentsListeners,
windowListeners,
Expand Down Expand Up @@ -176,17 +184,23 @@ const electronThemeLayer = Layer.succeed(ElectronTheme.ElectronTheme, {
onUpdated: () => Effect.void,
} satisfies ElectronTheme.ElectronTheme["Service"]);

const desktopEnvironmentLayer = DesktopEnvironment.layer(environmentInput).pipe(
Layer.provide(
Layer.mergeAll(
NodeServices.layer,
DesktopConfig.layerTest({
T3CODE_PORT: "3773",
VITE_DEV_SERVER_URL: "http://127.0.0.1:5733",
}),
const makeDesktopEnvironmentLayer = (
platform: NodeJS.Platform = environmentInput.platform,
systemVersion: string | undefined = environmentInput.systemVersion,
) =>
DesktopEnvironment.layer({ ...environmentInput, platform, systemVersion }).pipe(
Layer.provide(
Layer.mergeAll(
NodeServices.layer,
DesktopConfig.layerTest({
T3CODE_PORT: "3773",
VITE_DEV_SERVER_URL: "http://127.0.0.1:5733",
}),
),
),
),
);
);

const desktopEnvironmentLayer = makeDesktopEnvironmentLayer();

const desktopWindowBoundsEquivalence = Schema.toEquivalence(
DesktopAppSettings.DesktopWindowBoundsSchema,
Expand All @@ -205,6 +219,8 @@ function makeTestLayer(input: {
) => Effect.Effect<void>;
readonly openedExternalUrls?: unknown[];
readonly previewZoomReapplies?: number[];
readonly platform?: NodeJS.Platform;
readonly systemVersion?: string;
}) {
let desktopSettings = input.desktopSettings ?? DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS;
const desktopAppSettingsLayer = Layer.succeed(DesktopAppSettings.DesktopAppSettings, {
Expand Down Expand Up @@ -263,7 +279,7 @@ function makeTestLayer(input: {
Layer.provide(
Layer.mergeAll(
desktopAssetsLayer,
desktopEnvironmentLayer,
makeDesktopEnvironmentLayer(input.platform, input.systemVersion),
desktopAppSettingsLayer,
desktopClientSettingsLayer,
desktopServerExposureLayer,
Expand Down Expand Up @@ -394,6 +410,93 @@ const makeSplashScenario = (createOutcomes: readonly (Electron.BrowserWindow | n
});

describe("DesktopWindow", () => {
it("uses transparent Acrylic-ready window options only on Windows", () => {
assert.isTrue(DesktopWindow.isWindowsAcrylicSupported("win32", "10.0.22621"));
assert.isTrue(DesktopWindow.isWindowsAcrylicSupported("win32", "10.0.26100"));
assert.isFalse(DesktopWindow.isWindowsAcrylicSupported("win32", "10.0.22000"));
assert.isFalse(DesktopWindow.isWindowsAcrylicSupported("win32", undefined));
assert.isFalse(DesktopWindow.isWindowsAcrylicSupported("darwin", "10.0.22621"));

assert.deepEqual(DesktopWindow.getWindowBackdropOptions("win32", true, true, true), {
backgroundColor: "#00000000",
backgroundMaterial: "acrylic",
frame: false,
roundedCorners: true,
thickFrame: true,
transparent: true,
});
assert.deepEqual(DesktopWindow.getWindowBackdropOptions("win32", true, false, true), {
backgroundColor: "#0a0a0a",
backgroundMaterial: "none",
frame: false,
roundedCorners: true,
thickFrame: true,
transparent: true,
});
assert.deepEqual(DesktopWindow.getWindowBackdropOptions("win32", true, true, false), {
backgroundColor: "#0a0a0a",
backgroundMaterial: "none",
frame: false,
roundedCorners: true,
thickFrame: true,
transparent: true,
});
assert.deepEqual(DesktopWindow.getWindowBackdropOptions("darwin", true), {
backgroundColor: "#0a0a0a",
});
});

it.effect("does not apply the Windows backdrop to unowned auxiliary windows", () =>
Effect.gen(function* () {
const fakeWindow = makeFakeBrowserWindow();
const createCount = yield* Ref.make(0);
const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
const layer = makeTestLayer({
window: fakeWindow.window,
createCount,
mainWindow,
platform: "win32",
});

yield* Effect.gen(function* () {
const desktopWindow = yield* DesktopWindow.DesktopWindow;
yield* desktopWindow.syncAppearance;
}).pipe(Effect.provide(layer));

assert.equal(fakeWindow.setBackgroundMaterial.mock.calls.length, 0);
assert.equal(fakeWindow.setBackgroundColor.mock.calls.length, 0);
}),
);

it.effect("keeps the Windows window opaque when Acrylic is unsupported", () =>
Effect.gen(function* () {
const fakeWindow = makeFakeBrowserWindow();
const createCount = yield* Ref.make(0);
const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
const createdWindowOptions: Electron.BrowserWindowConstructorOptions[] = [];
const layer = makeTestLayer({
window: fakeWindow.window,
createCount,
mainWindow,
createdWindowOptions,
platform: "win32",
systemVersion: "10.0.22000",
});

yield* Effect.gen(function* () {
const desktopWindow = yield* DesktopWindow.DesktopWindow;
yield* desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773"));

assert.equal(createdWindowOptions[0]?.backgroundMaterial, "none");
assert.equal(createdWindowOptions[0]?.backgroundColor, "#ffffff");
assert.deepEqual(fakeWindow.setBackgroundMaterial.mock.calls, [["none"]]);
assert.deepEqual(fakeWindow.setBackgroundColor.mock.calls, [["#ffffff"]]);
assert.isFalse(desktopWindow.isBackdropEnabled(fakeWindow.window));
assert.deepEqual(fakeWindow.send.mock.calls, [[WINDOW_BACKDROP_STATE_CHANNEL, false]]);
}).pipe(Effect.provide(layer));
}),
);

it("restores bounds only when the window fits within a connected display", () => {
const persistedBounds = { x: 2040, y: 80, width: 1320, height: 880 };
const displays = [
Expand Down
Loading
Loading