Skip to content
This repository was archived by the owner on Aug 6, 2026. It is now read-only.
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
33 changes: 11 additions & 22 deletions apps/code/src/main/menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type { UIService } from "@posthog/core/ui/ui";
import type { UpdatesService } from "@posthog/core/updates/updates";
import {
app,
type BaseWindow,
BrowserWindow,
clipboard,
dialog,
Expand All @@ -20,25 +21,13 @@ 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";
import { adjustWindowZoom, ZOOM_STEP } from "./zoom";

// 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.
export 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%.
export 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 applyZoom(
window: BaseWindow | undefined,
delta: number | "reset",
): void {
if (window instanceof BrowserWindow) adjustWindowZoom(window, delta);
}

function findLatestCrashDump(): string | null {
Expand Down Expand Up @@ -331,25 +320,25 @@ function buildViewMenu(): MenuItemConstructorOptions {
{
label: "Actual Size",
accelerator: "CmdOrCtrl+0",
click: () => applyZoom("reset"),
click: (_menuItem, window) => applyZoom(window, "reset"),
},
{
label: "Zoom In",
accelerator: "CmdOrCtrl+Plus",
click: () => applyZoom(ZOOM_STEP),
click: (_menuItem, window) => applyZoom(window, 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),
click: (_menuItem, window) => applyZoom(window, ZOOM_STEP),
},
{
label: "Zoom Out",
accelerator: "CmdOrCtrl+-",
click: () => applyZoom(-ZOOM_STEP),
click: (_menuItem, window) => applyZoom(window, -ZOOM_STEP),
},
{ type: "separator" },
{ role: "togglefullscreen" },
Expand Down
11 changes: 7 additions & 4 deletions apps/code/src/main/platform-adapters/electron-main-window.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { IMainWindow } from "@posthog/platform/main-window";
import { app, type BrowserWindow } from "electron";
import { injectable } from "inversify";
import { applyZoom, ZOOM_STEP } from "../menu";
import { adjustWindowZoom, ZOOM_STEP } from "../zoom";

@injectable()
export class ElectronMainWindow implements IMainWindow {
Expand Down Expand Up @@ -38,14 +38,17 @@ export class ElectronMainWindow implements IMainWindow {
}

public zoomIn(): void {
applyZoom(ZOOM_STEP);
const window = this.getBrowserWindow();
if (window) adjustWindowZoom(window, ZOOM_STEP);
}

public zoomOut(): void {
applyZoom(-ZOOM_STEP);
const window = this.getBrowserWindow();
if (window) adjustWindowZoom(window, -ZOOM_STEP);
}

public resetZoom(): void {
applyZoom("reset");
const window = this.getBrowserWindow();
if (window) adjustWindowZoom(window, "reset");
}
}
18 changes: 2 additions & 16 deletions apps/code/src/main/window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@ import { isDevBuild } from "./utils/env";
import { logger, readChromiumLogTail } from "./utils/logger";
import {
saveFullScreenState,
saveZoomLevel,
setRestoreFullScreenOnNextLaunch,
type WindowStateSchema,
windowStateStore,
} from "./utils/store";
import { setupWindowZoom } from "./zoom";

const log = logger.scope("window");
const trpcLog = logger.scope("host-trpc");
Expand Down Expand Up @@ -270,21 +270,7 @@ 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());
}
});
setupWindowZoom(mainWindow);

// Persist window state on changes
mainWindow.on(
Expand Down
147 changes: 147 additions & 0 deletions apps/code/src/main/zoom.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import { EventEmitter } from "node:events";
import { beforeEach, describe, expect, it, vi } from "vitest";

const store = vi.hoisted(() => {
const state = { zoomLevel: 0.5 };
return {
get: vi.fn(() => state.zoomLevel),
save: vi.fn((level: number) => {
state.zoomLevel = level;
}),
state,
};
});

vi.mock("./utils/store", () => ({
windowStateStore: { get: store.get },
saveZoomLevel: store.save,
}));

import { adjustWindowZoom, restoreWindowZoom, setupWindowZoom } from "./zoom";

class FakeWebContents extends EventEmitter {
public zoomLevel = 0;

public getZoomLevel(): number {
return this.zoomLevel;
}

public setZoomLevel(level: number): void {
this.zoomLevel = level;
}
}

class FakeWindow extends EventEmitter {
public readonly webContents = new FakeWebContents();
}

type ZoomWindow = Parameters<typeof adjustWindowZoom>[0];

function createWindow(): FakeWindow & ZoomWindow {
return new FakeWindow() as FakeWindow & ZoomWindow;
}

describe("window zoom", () => {
beforeEach(() => {
vi.useFakeTimers();
store.state.zoomLevel = 0.5;
store.get.mockReset();
store.get.mockImplementation(() => store.state.zoomLevel);
store.save.mockReset();
store.save.mockImplementation((level: number) => {
store.state.zoomLevel = level;
});
});

it("adjusts from the persisted level when Chromium has reset", () => {
const window = createWindow();
window.webContents.zoomLevel = 0;

adjustWindowZoom(window, 0.5);

expect({
zoomLevel: window.webContents.zoomLevel,
saved: store.save.mock.calls,
}).toEqual({
zoomLevel: 1,
saved: [[1]],
});
});

it("restores the persisted level after maximizing", () => {
const window = createWindow();
setupWindowZoom(window);
window.webContents.zoomLevel = 0;

window.emit("maximize");
vi.runAllTimers();

expect(window.webContents.zoomLevel).toBe(0.5);
});

it("restores the persisted level after renderer reloads", () => {
const window = createWindow();
setupWindowZoom(window);
window.webContents.zoomLevel = 0;

window.webContents.emit("did-finish-load");

expect(window.webContents.zoomLevel).toBe(0.5);
});

it("persists wheel zoom after Chromium updates its level", () => {
const window = createWindow();
setupWindowZoom(window);

window.webContents.emit("zoom-changed");
window.webContents.zoomLevel = 1.5;
vi.runAllTimers();

expect(store.save).toHaveBeenCalledWith(1.5);
});

it("waits for native zoom before applying a menu adjustment", () => {
const window = createWindow();
setupWindowZoom(window);

window.webContents.emit("zoom-changed");
window.webContents.zoomLevel = 1.5;
adjustWindowZoom(window, 0.5);
vi.runAllTimers();

expect({
zoomLevel: window.webContents.zoomLevel,
saved: store.save.mock.calls,
}).toEqual({
zoomLevel: 2,
saved: [[1.5], [2]],
});
});

it("waits for native zoom before restoring after a reload", () => {
const window = createWindow();
setupWindowZoom(window);

window.webContents.emit("zoom-changed");
window.webContents.zoomLevel = 1.5;
window.webContents.emit("did-finish-load");
vi.runAllTimers();

expect({
zoomLevel: window.webContents.zoomLevel,
saved: store.save.mock.calls,
}).toEqual({
zoomLevel: 1.5,
saved: [[1.5]],
});
});

it("clamps invalid persisted levels before restoring", () => {
store.get.mockReturnValue(10);
const window = createWindow();

restoreWindowZoom(window);

expect(window.webContents.zoomLevel).toBe(3);
});
});
106 changes: 106 additions & 0 deletions apps/code/src/main/zoom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { saveZoomLevel, windowStateStore } from "./utils/store";

export const ZOOM_STEP = 0.5;

const ZOOM_MIN = -3;
const ZOOM_MAX = 3;

interface ZoomWebContents {
getZoomLevel(): number;
on(event: "did-finish-load" | "zoom-changed", listener: () => void): void;
setZoomLevel(level: number): void;
}

interface ZoomWindow {
on(
event:
| "enter-full-screen"
| "leave-full-screen"
| "maximize"
| "resized"
| "unmaximize",
listener: () => void,
): void;
webContents: ZoomWebContents;
}

interface ZoomState {
deferredActions: Array<() => void>;
nativeZoomTimeout: ReturnType<typeof setTimeout> | null;
}

const zoomStates = new WeakMap<ZoomWindow, ZoomState>();

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

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

function runAfterNativeZoom(window: ZoomWindow, action: () => void): void {
const state = zoomStates.get(window);
if (!state?.nativeZoomTimeout) {
action();
return;
}

state.deferredActions.push(action);
}

export function setWindowZoom(window: ZoomWindow, level: number): void {
const nextLevel = clampZoomLevel(level);
window.webContents.setZoomLevel(nextLevel);
saveZoomLevel(nextLevel);
}

export function adjustWindowZoom(
window: ZoomWindow,
delta: number | "reset",
): void {
runAfterNativeZoom(window, () => {
const nextLevel = delta === "reset" ? 0 : getSavedZoomLevel() + delta;
setWindowZoom(window, nextLevel);
});
}

export function restoreWindowZoom(window: ZoomWindow): void {
runAfterNativeZoom(window, () => {
window.webContents.setZoomLevel(getSavedZoomLevel());
});
}

export function setupWindowZoom(window: ZoomWindow): void {
const state: ZoomState = {
deferredActions: [],
nativeZoomTimeout: null,
};
let restoreTimeout: ReturnType<typeof setTimeout> | null = null;
zoomStates.set(window, state);

const scheduleRestore = () => {
if (restoreTimeout) clearTimeout(restoreTimeout);
restoreTimeout = setTimeout(() => {
restoreTimeout = null;
restoreWindowZoom(window);
}, 0);
};

window.webContents.on("did-finish-load", () => restoreWindowZoom(window));
window.webContents.on("zoom-changed", () => {
if (state.nativeZoomTimeout) clearTimeout(state.nativeZoomTimeout);
state.nativeZoomTimeout = setTimeout(() => {
state.nativeZoomTimeout = null;
saveZoomLevel(clampZoomLevel(window.webContents.getZoomLevel()));
const deferredActions = state.deferredActions.splice(0);
for (const action of deferredActions) action();
}, 0);
});
Comment thread
richardsolomou marked this conversation as resolved.

window.on("maximize", scheduleRestore);
window.on("unmaximize", scheduleRestore);
window.on("resized", scheduleRestore);
window.on("enter-full-screen", scheduleRestore);
window.on("leave-full-screen", scheduleRestore);
}
Loading