Skip to content
Merged
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
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 @@ -78,6 +78,7 @@ describe("DesktopLifecycle", () => {
handleBackendNotReady: Effect.void,
flushMainWindowBounds: Effect.void,
dispatchMenuAction: () => Effect.void,
zoomMain: () => Effect.void,
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 @@ -91,6 +91,7 @@ function makePoolLayer(
handleBackendNotReady: Effect.void,
flushMainWindowBounds: Effect.void,
dispatchMenuAction: () => Effect.die("unexpected menu action"),
zoomMain: () => Effect.die("unexpected zoom"),
syncAppearance: Effect.void,
} satisfies DesktopWindow.DesktopWindow["Service"]),
),
Expand Down
80 changes: 61 additions & 19 deletions apps/desktop/src/window/DesktopApplicationMenu.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ const makeDesktopWindowLayer = (selectedAction: Deferred.Deferred<string>) =>
handleBackendNotReady: Effect.void,
flushMainWindowBounds: Effect.void,
dispatchMenuAction: (action) => Deferred.succeed(selectedAction, action).pipe(Effect.asVoid),
zoomMain: (direction) =>
Deferred.succeed(selectedAction, `zoom-${direction}`).pipe(Effect.asVoid),
syncAppearance: Effect.void,
} satisfies DesktopWindow.DesktopWindow["Service"]);

Expand All @@ -94,32 +96,38 @@ const makeElectronMenuLayer = (
showContextMenu: () => Effect.succeed(Option.none()),
} satisfies ElectronMenu.ElectronMenu["Service"]);

const configureMenu = (
selectedAction: Deferred.Deferred<string>,
applicationMenuTemplate: Deferred.Deferred<readonly Electron.MenuItemConstructorOptions[]>,
) =>
Effect.gen(function* () {
const menu = yield* DesktopApplicationMenu.DesktopApplicationMenu;
yield* menu.configure;
}).pipe(
Effect.provide(
DesktopApplicationMenu.layer.pipe(
Layer.provideMerge(makeElectronMenuLayer(applicationMenuTemplate)),
Layer.provideMerge(makeDesktopWindowLayer(selectedAction)),
Layer.provideMerge(desktopUpdatesLayer),
Layer.provideMerge(electronDialogLayer),
Layer.provideMerge(electronAppLayer),
Layer.provideMerge(
DesktopEnvironment.layer(environmentInput).pipe(
Layer.provide(Layer.mergeAll(NodeServices.layer, DesktopConfig.layerTest({}))),
),
),
),
),
);

describe("DesktopApplicationMenu", () => {
it.effect("installs the native menu and routes Settings through DesktopWindow", () =>
Effect.gen(function* () {
const selectedAction = yield* Deferred.make<string>();
const applicationMenuTemplate =
yield* Deferred.make<readonly Electron.MenuItemConstructorOptions[]>();

yield* Effect.gen(function* () {
const menu = yield* DesktopApplicationMenu.DesktopApplicationMenu;
yield* menu.configure;
}).pipe(
Effect.provide(
DesktopApplicationMenu.layer.pipe(
Layer.provideMerge(makeElectronMenuLayer(applicationMenuTemplate)),
Layer.provideMerge(makeDesktopWindowLayer(selectedAction)),
Layer.provideMerge(desktopUpdatesLayer),
Layer.provideMerge(electronDialogLayer),
Layer.provideMerge(electronAppLayer),
Layer.provideMerge(
DesktopEnvironment.layer(environmentInput).pipe(
Layer.provide(Layer.mergeAll(NodeServices.layer, DesktopConfig.layerTest({}))),
),
),
),
),
);
yield* configureMenu(selectedAction, applicationMenuTemplate);

const template = yield* Deferred.await(applicationMenuTemplate);
const fileMenu = template.find((item) => item.label === "File");
Expand All @@ -138,4 +146,38 @@ describe("DesktopApplicationMenu", () => {
assert.equal(yield* Deferred.await(selectedAction), "open-settings");
}),
);

// Zoom must route through DesktopWindow.zoomMain instead of the Electron
// zoom roles: the roles zoom whichever webContents has focus, which breaks
// app zoom while an embedded preview WebContentsView holds focus.
it.effect("routes View menu zoom to the main window instead of zoom roles", () =>
Effect.gen(function* () {
const selectedAction = yield* Deferred.make<string>();
const applicationMenuTemplate =
yield* Deferred.make<readonly Electron.MenuItemConstructorOptions[]>();

yield* configureMenu(selectedAction, applicationMenuTemplate);

const template = yield* Deferred.await(applicationMenuTemplate);
const viewMenu = template.find((item) => item.label === "View");
assert.isDefined(viewMenu);
if (!Array.isArray(viewMenu.submenu)) {
throw new Error("Expected View menu submenu to be an array.");
}

assert.isUndefined(
viewMenu.submenu.find((item) => item.role?.toLowerCase().includes("zoom")),
);

const zoomIn = viewMenu.submenu.find((item) => item.label === "Zoom In");
assert.isDefined(zoomIn);
assert.equal(zoomIn.accelerator, "CmdOrCtrl+=");
if (typeof zoomIn.click !== "function") {
throw new Error("Expected Zoom In menu item to have a click handler.");
}

zoomIn.click({} as Electron.MenuItem, {} as Electron.BrowserWindow, {} as KeyboardEvent);
assert.equal(yield* Deferred.await(selectedAction), "zoom-in");
}),
);
});
29 changes: 25 additions & 4 deletions apps/desktop/src/window/DesktopApplicationMenu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,13 @@ const dispatchMenuAction = Effect.fn("desktop.menu.dispatchMenuAction")(function
yield* desktopWindow.dispatchMenuAction(action);
});

const zoomMainWindow = Effect.fn("desktop.menu.zoomMainWindow")(function* (
direction: DesktopWindow.MainWindowZoomDirection,
): Effect.fn.Return<void, never, DesktopWindow.DesktopWindow> {
const desktopWindow = yield* DesktopWindow.DesktopWindow;
yield* desktopWindow.zoomMain(direction);
});

const checkForUpdatesFromMenu = Effect.gen(function* () {
const updates = yield* DesktopUpdates.DesktopUpdates;
const electronDialog = yield* ElectronDialog.ElectronDialog;
Expand Down Expand Up @@ -127,6 +134,9 @@ export const make = Effect.gen(function* () {
const settingsClick = () => {
runMenuEffect("open-settings", dispatchMenuAction("open-settings"));
};
const zoomClick = (direction: DesktopWindow.MainWindowZoomDirection) => () => {
runMenuEffect(`zoom-${direction}`, zoomMainWindow(direction));
};
const template: Electron.MenuItemConstructorOptions[] = [];

if (environment.platform === "darwin") {
Expand Down Expand Up @@ -181,10 +191,21 @@ export const make = Effect.gen(function* () {
{ role: "forceReload" },
{ role: "toggleDevTools" },
{ type: "separator" },
{ role: "resetZoom" },
{ role: "zoomIn", accelerator: "CmdOrCtrl+=" },
{ role: "zoomIn", accelerator: "CmdOrCtrl+Plus", visible: false },
{ role: "zoomOut" },
/*
Not the zoom roles: those act on the focused webContents, so with
an embedded preview WebContentsView focused they zoom the guest
page and the app UI appears stuck. These always zoom the main
window (see DesktopWindow.zoomMain).
*/
{ label: "Actual Size", accelerator: "CmdOrCtrl+0", click: zoomClick("reset") },
{ label: "Zoom In", accelerator: "CmdOrCtrl+=", click: zoomClick("in") },
{
label: "Zoom In",
accelerator: "CmdOrCtrl+Plus",
visible: false,
click: zoomClick("in"),
},
{ label: "Zoom Out", accelerator: "CmdOrCtrl+-", click: zoomClick("out") },
{ type: "separator" },
{ role: "togglefullscreen" },
],
Expand Down
20 changes: 20 additions & 0 deletions apps/desktop/src/window/DesktopWindow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ export type DesktopWindowError =
| ElectronWindow.ElectronWindowCreateError
| PreviewManager.PreviewManagerError;

export type MainWindowZoomDirection = "in" | "out" | "reset";

export class DesktopWindow extends Context.Service<
DesktopWindow,
{
Expand All @@ -87,6 +89,12 @@ export class DesktopWindow extends Context.Service<
readonly handleBackendNotReady: Effect.Effect<void>;
readonly flushMainWindowBounds: Effect.Effect<void>;
readonly dispatchMenuAction: (action: string) => Effect.Effect<void, DesktopWindowError>;
// Zooms the main window's own webContents. The Electron `zoomIn`/`zoomOut`
// menu roles act on whichever webContents has keyboard focus, so with an
// embedded preview WebContentsView (or DevTools) focused they zoom the
// guest page instead of the app UI. The menu routes here to always target
// the main window.
readonly zoomMain: (direction: MainWindowZoomDirection) => Effect.Effect<void>;
readonly syncAppearance: Effect.Effect<void>;
}
>()("@t3tools/desktop/window/DesktopWindow") {}
Expand Down Expand Up @@ -836,6 +844,18 @@ export const make = Effect.gen(function* () {

send();
}),
zoomMain: Effect.fn("desktop.window.zoomMain")(function* (direction) {
yield* Effect.annotateCurrentSpan({ direction });
const window = yield* focusedMainWindow;
if (Option.isNone(window) || window.value.isDestroyed()) {
return;
}
const webContents = window.value.webContents;
// Same step size as the Electron zoomIn/zoomOut menu roles.
webContents.setZoomLevel(
direction === "reset" ? 0 : webContents.getZoomLevel() + (direction === "in" ? 0.5 : -0.5),
);
}),
syncAppearance: Effect.gen(function* () {
const shouldUseDarkColors = yield* electronTheme.shouldUseDarkColors;
yield* electronWindow.syncAllAppearance((window) =>
Expand Down
Loading