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
28 changes: 28 additions & 0 deletions apps/desktop/src/electron/ElectronWindow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,34 @@ describe("ElectronWindow", () => {
}).pipe(Effect.provide(layerTestFor("win32"))),
);

it.effect("reports which windows were created with a title bar overlay", () =>
Effect.gen(function* () {
const framed = { isDestroyed: () => false };
const overlay = { isDestroyed: () => false };
browserWindowMock
.mockImplementationOnce(function FramedWindowMock() {
return framed;
})
.mockImplementationOnce(function OverlayWindowMock() {
return overlay;
});
const electronWindow = yield* ElectronWindow.ElectronWindow;
yield* electronWindow.create({});
yield* electronWindow.create({
titleBarStyle: "hidden",
titleBarOverlay: { color: "#01000000", height: 40 },
});
getAllWindowsMock.mockReturnValue([framed, overlay, { isDestroyed: () => false }]);

const seen: Array<boolean> = [];
yield* electronWindow.syncAllAppearance((_window, { titleBarOverlay }) =>
Effect.sync(() => seen.push(titleBarOverlay)),
);

assert.deepEqual(seen, [false, true, false]);
}).pipe(Effect.provide(layerTestFor("win32"))),
);

it.effect("preserves destroy failures and continues with later windows", () =>
Effect.gen(function* () {
const cause = new Error("window destroy failed");
Expand Down
16 changes: 13 additions & 3 deletions apps/desktop/src/electron/ElectronWindow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,10 @@ export class ElectronWindow extends Context.Service<
readonly sendAll: (channel: string, ...args: readonly unknown[]) => Effect.Effect<void>;
readonly destroyAll: Effect.Effect<void>;
readonly syncAllAppearance: <E, R>(
sync: (window: Electron.BrowserWindow) => Effect.Effect<void, E, R>,
sync: (
window: Electron.BrowserWindow,
appearance: { readonly titleBarOverlay: boolean },
) => Effect.Effect<void, E, R>,
) => Effect.Effect<void, E, R>;
}
>()("@t3tools/desktop/electron/ElectronWindow") {}
Expand All @@ -144,6 +147,7 @@ export const make = Effect.gen(function* () {
yield* Effect.addFinalizer(() => Effect.sync(() => windowsForegroundFocus?.close()));
const mainWindowRef = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
const auxiliaryWindows = new WeakSet<Electron.BrowserWindow>();
const titleBarOverlayWindows = new WeakSet<Electron.BrowserWindow>();

const listWindows = Effect.try({
try: () => Electron.BrowserWindow.getAllWindows(),
Expand Down Expand Up @@ -240,6 +244,9 @@ export const make = Effect.gen(function* () {
try: () => {
const window = new Electron.BrowserWindow(options);
if (behavior?.auxiliary === true) auxiliaryWindows.add(window);
if (options.titleBarOverlay !== undefined && options.titleBarOverlay !== false) {
titleBarOverlayWindows.add(window);
}
return window;
},
catch: (cause) => new ElectronWindowCreateError({ options: diagnosticOptions, cause }),
Expand Down Expand Up @@ -382,14 +389,17 @@ export const make = Effect.gen(function* () {
}
}),
syncAllAppearance: Effect.fn("desktop.electron.window.syncAllAppearance")(function* <E, R>(
sync: (window: Electron.BrowserWindow) => Effect.Effect<void, E, R>,
sync: (
window: Electron.BrowserWindow,
appearance: { readonly titleBarOverlay: boolean },
) => Effect.Effect<void, E, R>,
) {
const windows = yield* listWindows;
for (const window of windows) {
if (yield* isWindowDestroyed(window)) {
continue;
}
yield* sync(window);
yield* sync(window, { titleBarOverlay: titleBarOverlayWindows.has(window) });
}
}),
});
Expand Down
76 changes: 63 additions & 13 deletions apps/desktop/src/window/DesktopWindow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,17 +200,20 @@ const layerElectronTheme = Layer.succeed(ElectronTheme.ElectronTheme, {
onUpdated: () => Effect.void,
} satisfies ElectronTheme.ElectronTheme["Service"]);

const layerDesktopEnvironment = 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 layerDesktopEnvironmentFor = (platform: NodeJS.Platform) =>
DesktopEnvironment.layer({ ...environmentInput, platform }).pipe(
Layer.provide(
Layer.mergeAll(
NodeServices.layer,
DesktopConfig.layerTest({
T3CODE_PORT: "3773",
VITE_DEV_SERVER_URL: "http://127.0.0.1:5733",
}),
),
),
),
);
);

const layerDesktopEnvironment = layerDesktopEnvironmentFor(environmentInput.platform);

const desktopWindowBoundsEquivalence = Schema.toEquivalence(
DesktopAppSettings.DesktopWindowBoundsSchema,
Expand All @@ -236,6 +239,11 @@ function layerTest(input: {
readonly previewZoomReapplies?: number[];
readonly onReveal?: (window: Electron.BrowserWindow) => void;
readonly focusedWindow?: Electron.BrowserWindow;
readonly platform?: NodeJS.Platform;
readonly appearanceWindows?: ReadonlyArray<{
readonly window: Electron.BrowserWindow;
readonly titleBarOverlay: boolean;
}>;
}) {
let desktopSettings = input.desktopSettings ?? DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS;
const layerDesktopAppSettings = Layer.succeed(DesktopAppSettings.DesktopAppSettings, {
Expand Down Expand Up @@ -293,7 +301,14 @@ function layerTest(input: {
reveal: (window) => Effect.sync(() => input.onReveal?.(window)),
sendAll: () => Effect.void,
destroyAll: input.destroyAll ?? Effect.void,
syncAllAppearance: (sync) => sync(input.window),
syncAllAppearance: (sync) =>
input.appearanceWindows === undefined
? sync(input.window, { titleBarOverlay: true })
: Effect.forEach(
input.appearanceWindows,
({ window, titleBarOverlay }) => sync(window, { titleBarOverlay }),
{ discard: true },
),
} satisfies ElectronWindow.ElectronWindow["Service"]);

return DesktopWindow.layer.pipe(
Expand All @@ -305,7 +320,9 @@ function layerTest(input: {
recordMetrics: () => Effect.void,
shutdown: Effect.void,
}),
layerDesktopEnvironment,
input.platform === undefined
? layerDesktopEnvironment
: layerDesktopEnvironmentFor(input.platform),
layerDesktopAppSettings,
layerDesktopClientSettings,
layerDesktopServerExposure,
Expand Down Expand Up @@ -414,7 +431,8 @@ const makeSplashScenario = (createOutcomes: readonly (Electron.BrowserWindow | n
reveal: (window) => Ref.update(revealedWindows, (windows) => [...windows, window]),
sendAll: () => Effect.void,
destroyAll: Effect.void,
syncAllAppearance: (sync) => (fallbackWindow ? sync(fallbackWindow) : Effect.void),
syncAllAppearance: (sync) =>
fallbackWindow ? sync(fallbackWindow, { titleBarOverlay: true }) : Effect.void,
} satisfies ElectronWindow.ElectronWindow["Service"];

const layer = DesktopWindow.layer.pipe(
Expand Down Expand Up @@ -941,6 +959,38 @@ describe("DesktopWindow", () => {
}),
);

it.effect.each(["linux", "win32"] satisfies ReadonlyArray<NodeJS.Platform>)(
"updates title bar overlays only on overlay windows on %s",
(platform) =>
Effect.gen(function* () {
const framed = makeFakeBrowserWindow();
vi.mocked(framed.window.setTitleBarOverlay).mockImplementation(() => {
throw new TypeError("Titlebar overlay is not enabled");
});
const overlay = makeFakeBrowserWindow();
const layer = layerTest({
window: overlay.window,
createCount: yield* Ref.make(0),
mainWindow: yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none()),
platform,
appearanceWindows: [
{ window: framed.window, titleBarOverlay: false },
{ window: overlay.window, titleBarOverlay: true },
],
});

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

assert.deepEqual(vi.mocked(framed.window.setBackgroundColor).mock.calls, [["#ffffff"]]);
assert.lengthOf(vi.mocked(framed.window.setTitleBarOverlay).mock.calls, 0);
assert.deepEqual(vi.mocked(overlay.window.setBackgroundColor).mock.calls, [["#ffffff"]]);
assert.lengthOf(vi.mocked(overlay.window.setTitleBarOverlay).mock.calls, 1);
}),
);

it.effect("maps macOS horizontal swipes to available history navigation", () =>
Effect.gen(function* () {
const fakeWindow = makeFakeBrowserWindow();
Expand Down
7 changes: 4 additions & 3 deletions apps/desktop/src/window/DesktopWindow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,7 @@ function getWindowTitleBarOptions(

function syncWindowAppearance(
window: Electron.BrowserWindow,
hasTitleBarOverlay: boolean,
shouldUseDarkColors: boolean,
platform: NodeJS.Platform,
): Effect.Effect<void> {
Expand All @@ -267,7 +268,7 @@ function syncWindowAppearance(

window.setBackgroundColor(getInitialWindowBackgroundColor(shouldUseDarkColors));
const { titleBarOverlay } = getWindowTitleBarOptions(shouldUseDarkColors, platform);
if (typeof titleBarOverlay === "object") {
if (hasTitleBarOverlay && typeof titleBarOverlay === "object") {
window.setTitleBarOverlay(titleBarOverlay);
}
});
Expand Down Expand Up @@ -1038,8 +1039,8 @@ export const make = Effect.gen(function* () {
}),
syncAppearance: Effect.gen(function* () {
const shouldUseDarkColors = yield* electronTheme.shouldUseDarkColors;
yield* electronWindow.syncAllAppearance((window) =>
syncWindowAppearance(window, shouldUseDarkColors, environment.platform),
yield* electronWindow.syncAllAppearance((window, { titleBarOverlay }) =>
syncWindowAppearance(window, titleBarOverlay, shouldUseDarkColors, environment.platform),
);
}).pipe(Effect.withSpan("desktop.window.syncAppearance")),
});
Expand Down
Loading