Skip to content
Closed
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
34 changes: 34 additions & 0 deletions apps/desktop/src/window/DesktopApplicationMenu.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,4 +246,38 @@ describe("DesktopApplicationMenu", () => {
assert.equal(yield* Deferred.await(selectedAction), "zoom-in");
}),
);

// The keypad sends its own key codes, so without these the number-row
// shortcuts are the only way to zoom. They stay hidden so the View menu
// lists each command once.
it.effect("zooms from numeric keypad accelerators without listing them twice", () =>
Effect.gen(function* () {
for (const [accelerator, expected] of [
["CmdOrCtrl+numadd", "zoom-in"],
["CmdOrCtrl+numsub", "zoom-out"],
["CmdOrCtrl+num0", "zoom-reset"],
] as const) {
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");
if (!viewMenu || !Array.isArray(viewMenu.submenu)) {
throw new Error("Expected View menu submenu to be an array.");
}
const item = viewMenu.submenu.find((entry) => entry.accelerator === accelerator);
assert.isDefined(item);
assert.equal(item.visible, false);
if (typeof item.click !== "function") {
throw new Error(`Expected ${accelerator} menu item to have a click handler.`);
}

item.click({} as Electron.MenuItem, {} as Electron.BrowserWindow, {} as KeyboardEvent);
assert.equal(yield* Deferred.await(selectedAction), expected);
}
}),
);
});
20 changes: 20 additions & 0 deletions apps/desktop/src/window/DesktopApplicationMenu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,26 @@ export const make = Effect.gen(function* () {
click: zoomClick("in"),
},
{ label: "Zoom Out", accelerator: "CmdOrCtrl+-", click: zoomClick("out") },
// The numeric keypad sends its own key codes, so it needs its own
// accelerators; hidden so the menu lists each command once.
{
label: "Zoom In",
accelerator: "CmdOrCtrl+numadd",
visible: false,
click: zoomClick("in"),
},
{
label: "Zoom Out",
accelerator: "CmdOrCtrl+numsub",
visible: false,
click: zoomClick("out"),
},
{
label: "Actual Size",
accelerator: "CmdOrCtrl+num0",
visible: false,
click: zoomClick("reset"),
},
{ type: "separator" },
{ role: "togglefullscreen" },
],
Expand Down
37 changes: 37 additions & 0 deletions apps/desktop/src/window/DesktopWindow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -803,6 +803,43 @@ describe("DesktopWindow", () => {
}),
);

// Ctrl+wheel and pinch never reach the menu accelerators: Chromium reports
// them as zoom-changed on the window's webContents and leaves the zoom to
// the app, so the same path (and preview restore) has to run from there.
it.effect("zooms the app from Ctrl+wheel like the menu does", () =>
Effect.gen(function* () {
const fakeWindow = makeFakeBrowserWindow();
const createCount = yield* Ref.make(0);
const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
const previewZoomReapplies: number[] = [];
const layer = makeTestLayer({
window: fakeWindow.window,
createCount,
mainWindow,
previewZoomReapplies,
});

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

const zoomChanged = fakeWindow.webContentsListeners.get("zoom-changed");
if (!zoomChanged) {
return yield* Effect.die("zoom-changed listener was not registered");
}
zoomChanged({}, "in");
zoomChanged({}, "out");
zoomChanged({}, "out");

assert.deepEqual(
fakeWindow.setZoomLevel.mock.calls.map(([level]) => level),
[0.5, 0, -0.5],
);
assert.deepEqual(previewZoomReapplies, [0.5, 0, -0.5]);
}).pipe(Effect.provide(layer));
}),
);

it.effect("uses the persisted main window bounds when opening the window", () =>
Effect.gen(function* () {
const fakeWindow = makeFakeBrowserWindow();
Expand Down
35 changes: 25 additions & 10 deletions apps/desktop/src/window/DesktopWindow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,22 @@ export const make = Effect.gen(function* () {
const currentMainWindow = electronWindow.currentMainOrFirst.pipe(Effect.flatMap(withoutSplash));
const focusedMainWindow = electronWindow.focusedMainOrFirst.pipe(Effect.flatMap(withoutSplash));

// Same step size as the Electron zoomIn/zoomOut menu roles. Chromium pushes
// the new level down to embedded guests, which would zoom the previewed page
// along with the app UI. The preview browser keeps its own zoom, so put each
// guest back where the preview left it.
const zoomWindow = Effect.fn("desktop.window.zoomWindow")(function* (
window: Electron.BrowserWindow,
direction: MainWindowZoomDirection,
) {
const webContents = window.webContents;
webContents.setZoomLevel(
direction === "reset" ? 0 : webContents.getZoomLevel() + (direction === "in" ? 0.5 : -0.5),
);
if (environment.platform === "darwin") syncMacosWindowButtons(window);
yield* previewManager.reapplyZoom();
});

const createWindow = Effect.fn("desktop.window.createWindow")(function* (): Effect.fn.Return<
Electron.BrowserWindow,
DesktopWindowError
Expand Down Expand Up @@ -665,6 +681,14 @@ export const make = Effect.gen(function* () {
window.webContents.on("input-event", (_event, input) => {
if (input.type === "gestureScrollEnd") window.webContents.send(TRACKPAD_SCROLL_END_CHANNEL);
});
// Ctrl+wheel asks for zoom through this event rather than an accelerator,
// and Chromium leaves applying it to the app. Only the main window's own
// webContents emits it; a preview guest under the pointer gets its own
// event and keeps its own zoom.
window.webContents.on("zoom-changed", (_event, direction) => {
if (window.isDestroyed()) return;
runFork(zoomWindow(window, direction));
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});

window.on("page-title-updated", (event) => {
event.preventDefault();
Expand Down Expand Up @@ -1010,16 +1034,7 @@ export const make = Effect.gen(function* () {
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),
);
if (environment.platform === "darwin") syncMacosWindowButtons(window.value);
// Chromium pushes the new level down to embedded guests, which would zoom
// the previewed page along with the app UI. The preview browser keeps its
// own zoom, so put each guest back where the preview left it.
yield* previewManager.reapplyZoom();
yield* zoomWindow(window.value, direction);
}),
syncAppearance: Effect.gen(function* () {
const shouldUseDarkColors = yield* electronTheme.shouldUseDarkColors;
Expand Down
Loading