diff --git a/apps/code/src/main/menu.ts b/apps/code/src/main/menu.ts index 29276b4ecd..ca79b8dfa8 100644 --- a/apps/code/src/main/menu.ts +++ b/apps/code/src/main/menu.ts @@ -20,6 +20,26 @@ import { container } from "./di/container"; import { AUTH_SERVICE, UPDATES_SERVICE } from "./di/tokens"; import { isDevBuild } from "./utils/env"; import { getLogFilePath } from "./utils/logger"; +import { saveZoomLevel } from "./utils/store"; + +// Zoom is measured in Electron "levels" (factor = 1.2 ** level; 0 = 100%). +// ZOOM_STEP is one Zoom In/Out notch; the bounds clamp the level so a runaway +// accelerator can't persist an unusable zoom across restarts. +const ZOOM_STEP = 0.5; +const ZOOM_MIN = -3; +const ZOOM_MAX = 3; + +// Apply a zoom change to the focused window and persist the new level so it +// survives restarts. `delta` adjusts relative to the current level; "reset" +// returns to 100%. +function applyZoom(delta: number | "reset"): void { + const webContents = BrowserWindow.getFocusedWindow()?.webContents; + if (!webContents) return; + const next = delta === "reset" ? 0 : webContents.getZoomLevel() + delta; + const level = Math.max(ZOOM_MIN, Math.min(ZOOM_MAX, next)); + webContents.setZoomLevel(level); + saveZoomLevel(level); +} function findLatestCrashDump(): string | null { const pendingDir = path.join(app.getPath("crashDumps"), "pending"); @@ -308,9 +328,29 @@ function buildViewMenu(): MenuItemConstructorOptions { }, { role: "toggleDevTools" }, { type: "separator" }, - { role: "resetZoom" }, - { role: "zoomIn" }, - { role: "zoomOut" }, + { + label: "Actual Size", + accelerator: "CmdOrCtrl+0", + click: () => applyZoom("reset"), + }, + { + label: "Zoom In", + accelerator: "CmdOrCtrl+Plus", + click: () => applyZoom(ZOOM_STEP), + }, + // Hidden duplicate so Cmd+= (i.e. Cmd++ without Shift) also zooms in, + // matching the built-in zoomIn role's dual accelerator. + { + label: "Zoom In", + accelerator: "CmdOrCtrl+=", + visible: false, + click: () => applyZoom(ZOOM_STEP), + }, + { + label: "Zoom Out", + accelerator: "CmdOrCtrl+-", + click: () => applyZoom(-ZOOM_STEP), + }, { type: "separator" }, { role: "togglefullscreen" }, { type: "separator" }, diff --git a/apps/code/src/main/utils/store.ts b/apps/code/src/main/utils/store.ts index 4f511563e5..26a1b51ebc 100644 --- a/apps/code/src/main/utils/store.ts +++ b/apps/code/src/main/utils/store.ts @@ -24,6 +24,7 @@ export interface WindowStateSchema { width: number; height: number; isMaximized: boolean; + zoomLevel: number; } const userDataDir = getUserDataDir(); @@ -50,5 +51,10 @@ export const windowStateStore = new Store({ width: 1200, height: 600, isMaximized: true, + zoomLevel: 0, }, }); + +export function saveZoomLevel(level: number): void { + windowStateStore.set("zoomLevel", level); +} diff --git a/apps/code/src/main/window.ts b/apps/code/src/main/window.ts index 653945c449..64ffbf1a27 100644 --- a/apps/code/src/main/window.ts +++ b/apps/code/src/main/window.ts @@ -19,7 +19,11 @@ import { trpcRouter } from "./trpc/router"; import { collectMemorySnapshot } from "./utils/crash-diagnostics"; import { isDevBuild } from "./utils/env"; import { logger, readChromiumLogTail } from "./utils/logger"; -import { type WindowStateSchema, windowStateStore } from "./utils/store"; +import { + saveZoomLevel, + type WindowStateSchema, + windowStateStore, +} from "./utils/store"; const log = logger.scope("window"); @@ -44,6 +48,7 @@ function getSavedWindowState(): WindowStateSchema { width: windowStateStore.get("width", 1200), height: windowStateStore.get("height", 600), isMaximized: windowStateStore.get("isMaximized", true), + zoomLevel: windowStateStore.get("zoomLevel", 0), }; // Validate position is still on a connected display @@ -233,6 +238,22 @@ export function createWindow(): void { mainWindow.once("ready-to-show", showWindow); const showFallback = setTimeout(showWindow, 3000); + // Restore the zoom level once the renderer has loaded. Read the latest + // persisted value from the store (not the create-time snapshot) so zooming + // done during the session survives in-app reloads, which otherwise reset + // Chromium's per-webContents zoom. + mainWindow.webContents.on("did-finish-load", () => { + mainWindow?.webContents.setZoomLevel(windowStateStore.get("zoomLevel", 0)); + }); + + // Persist mouse-wheel/pinch zoom. Menu-driven zoom is persisted by the + // menu items themselves (see buildViewMenu in menu.ts). + mainWindow.webContents.on("zoom-changed", () => { + if (mainWindow) { + saveZoomLevel(mainWindow.webContents.getZoomLevel()); + } + }); + // Persist window state on changes mainWindow.on( "resize",