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
38 changes: 35 additions & 3 deletions apps/code/src/main/menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { container } from "./di/container";
import { MAIN_TOKENS } from "./di/tokens";
import { isDevBuild } from "./utils/env";
import { getLogFilePath } from "./utils/logger";
import { adjustZoom, setZoom } from "./utils/zoom";

function findLatestCrashDump(): string | null {
const pendingDir = path.join(app.getPath("crashDumps"), "pending");
Expand Down Expand Up @@ -308,9 +309,40 @@ function buildViewMenu(): MenuItemConstructorOptions {
},
{ role: "toggleDevTools" },
{ type: "separator" },
{ role: "resetZoom" },
{ role: "zoomIn" },
{ role: "zoomOut" },
{
label: "Reset Zoom",
accelerator: "CmdOrCtrl+0",
click: () => {
const win = BrowserWindow.getFocusedWindow();
if (win) setZoom(win, 0);
},
},
{
label: "Zoom In",
accelerator: "CmdOrCtrl+Plus",
click: () => {
const win = BrowserWindow.getFocusedWindow();
if (win) adjustZoom(win, 1);
},
},
{
// Alias so Cmd+= (no Shift) also zooms in; accelerator fires while hidden.
label: "Zoom In",
accelerator: "CmdOrCtrl+=",
visible: false,
click: () => {
const win = BrowserWindow.getFocusedWindow();
if (win) adjustZoom(win, 1);
},
},
{
label: "Zoom Out",
accelerator: "CmdOrCtrl+-",
click: () => {
const win = BrowserWindow.getFocusedWindow();
if (win) adjustZoom(win, -1);
},
},
{ type: "separator" },
{ role: "togglefullscreen" },
{ type: "separator" },
Expand Down
3 changes: 3 additions & 0 deletions apps/code/src/main/utils/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ export interface WindowStateSchema {
width: number;
height: number;
isMaximized: boolean;
// Native zoom level (0 = 100%), persisted across reloads and restarts.
zoomLevel: number;
}

const userDataDir = getUserDataDir();
Expand All @@ -50,5 +52,6 @@ export const windowStateStore = new Store<WindowStateSchema>({
width: 1200,
height: 600,
isMaximized: true,
zoomLevel: 0,
},
});
55 changes: 55 additions & 0 deletions apps/code/src/main/utils/zoom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { logger } from "./logger";
import { windowStateStore } from "./store";

const log = logger.scope("zoom");

// Structural subset of BrowserWindow, kept local so this util avoids an
// `electron` import. A real BrowserWindow satisfies it.
interface ZoomableWindow {
isDestroyed(): boolean;
webContents: {
getZoomLevel(): number;
setZoomLevel(level: number): void;
};
}

// Half a zoom level ≈ 9.5% per press; clamp to ~58%–173%.
const ZOOM_STEP = 0.5;
const ZOOM_MIN = -3;
const ZOOM_MAX = 3;

function clampZoom(level: number): number {
return Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, level));
}

export function getSavedZoomLevel(): number {
return clampZoom(windowStateStore.get("zoomLevel", 0));
}

// Native zoom resets to 100% on every reload, so call this on each
// `did-finish-load`, not just at startup.
export function applySavedZoom(window: ZoomableWindow): void {
if (window.isDestroyed()) return;
const level = getSavedZoomLevel();
window.webContents.setZoomLevel(level);
}

function persistZoom(level: number): void {
windowStateStore.set("zoomLevel", level);
}

// Set absolute zoom level (0 = 100%) and persist.
export function setZoom(window: ZoomableWindow, level: number): void {
if (window.isDestroyed()) return;
const next = clampZoom(level);
window.webContents.setZoomLevel(next);
persistZoom(next);
log.info("zoom set", { level: next });
}

// Adjust zoom by N steps and persist. Baseline comes from the persisted store,
// not the live webContents, which is reset to 0 during reloads.
export function adjustZoom(window: ZoomableWindow, steps: number): void {
if (window.isDestroyed()) return;
setZoom(window, getSavedZoomLevel() + steps * ZOOM_STEP);
}
9 changes: 8 additions & 1 deletion apps/code/src/main/window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { trpcRouter } from "./trpc/router";
import { isDevBuild } from "./utils/env";
import { logger, readChromiumLogTail } from "./utils/logger";
import { type WindowStateSchema, windowStateStore } from "./utils/store";
import { applySavedZoom } from "./utils/zoom";

const log = logger.scope("window");

Expand All @@ -34,7 +35,7 @@ function isPositionOnScreen(x: number, y: number): boolean {
});
}

function getSavedWindowState(): WindowStateSchema {
function getSavedWindowState(): Omit<WindowStateSchema, "zoomLevel"> {
const state = {
x: windowStateStore.get("x"),
y: windowStateStore.get("y"),
Expand Down Expand Up @@ -100,6 +101,11 @@ function setupExternalLinkHandlers(window: BrowserWindow): void {
});
}

function setupZoomPersistence(window: BrowserWindow): void {
// Native zoom resets on every load (incl. crash-recovery reload); re-apply it.
window.webContents.on("did-finish-load", () => applySavedZoom(window));
}

function setupCrashLogging(window: BrowserWindow): void {
window.webContents.on("render-process-gone", (_event, details) => {
log.error("Renderer process gone", {
Expand Down Expand Up @@ -248,6 +254,7 @@ export function createWindow(): void {
setupExternalLinkHandlers(mainWindow);
setupEditableContextMenu(mainWindow);
setupCrashLogging(mainWindow);
setupZoomPersistence(mainWindow);
buildApplicationMenu();

if (MAIN_WINDOW_VITE_DEV_SERVER_URL) {
Expand Down
Loading