Skip to content
Open
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
82 changes: 64 additions & 18 deletions apps/code/src/main/zoom.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,6 @@ 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;
}
Expand Down Expand Up @@ -89,50 +85,100 @@ describe("window zoom", () => {
expect(window.webContents.zoomLevel).toBe(0.5);
});

it("persists wheel zoom after Chromium updates its level", () => {
it.each([
["in", 1],
["out", 0],
] as const)(
"applies wheel zoom %s from the persisted level",
(direction, expected) => {
const window = createWindow();
setupWindowZoom(window);
const event = { preventDefault: vi.fn() };

window.webContents.emit("zoom-changed", event, direction);
vi.runAllTimers();

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

it("keeps wheel zoom after resizing", () => {
const window = createWindow();
setupWindowZoom(window);

window.webContents.emit("zoom-changed");
window.webContents.zoomLevel = 1.5;
window.webContents.emit("zoom-changed", { preventDefault: vi.fn() }, "in");
window.emit("resized");
vi.runAllTimers();

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

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

window.webContents.emit("zoom-changed");
window.webContents.zoomLevel = 1.5;
window.webContents.emit("zoom-changed", { preventDefault: vi.fn() }, "in");
window.webContents.emit("did-finish-load");
vi.runAllTimers();

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

it("serializes wheel and menu zoom changes", () => {
const window = createWindow();
setupWindowZoom(window);

window.webContents.emit("zoom-changed", { preventDefault: vi.fn() }, "in");
adjustWindowZoom(window, 0.5);
vi.runAllTimers();

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

it("waits for native zoom before restoring after a reload", () => {
it("uses the in-memory zoom level when persistence fails", () => {
const window = createWindow();
setupWindowZoom(window);
store.save.mockImplementation(() => {});

window.webContents.emit("zoom-changed");
window.webContents.zoomLevel = 1.5;
window.webContents.emit("did-finish-load");
window.webContents.emit("zoom-changed", { preventDefault: vi.fn() }, "in");
vi.runAllTimers();
window.webContents.emit("zoom-changed", { preventDefault: vi.fn() }, "in");
vi.runAllTimers();

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

Expand Down
50 changes: 35 additions & 15 deletions apps/code/src/main/zoom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,14 @@ const ZOOM_MIN = -3;
const ZOOM_MAX = 3;

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

Expand All @@ -25,8 +31,10 @@ interface ZoomWindow {
}

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

const zoomStates = new WeakMap<ZoomWindow, ZoomState>();
Expand All @@ -39,9 +47,13 @@ function getSavedZoomLevel(): number {
return clampZoomLevel(windowStateStore.get("zoomLevel", 0));
}

function runAfterNativeZoom(window: ZoomWindow, action: () => void): void {
function getCurrentZoomLevel(window: ZoomWindow): number {
return zoomStates.get(window)?.currentZoomLevel ?? getSavedZoomLevel();
}

function runAfterWheelZoom(window: ZoomWindow, action: () => void): void {
const state = zoomStates.get(window);
if (!state?.nativeZoomTimeout) {
if (!state?.wheelZoomTimeout) {
action();
return;
}
Expand All @@ -51,6 +63,8 @@ function runAfterNativeZoom(window: ZoomWindow, action: () => void): void {

export function setWindowZoom(window: ZoomWindow, level: number): void {
const nextLevel = clampZoomLevel(level);
const state = zoomStates.get(window);
if (state) state.currentZoomLevel = nextLevel;
window.webContents.setZoomLevel(nextLevel);
saveZoomLevel(nextLevel);
}
Expand All @@ -59,22 +73,25 @@ export function adjustWindowZoom(
window: ZoomWindow,
delta: number | "reset",
): void {
runAfterNativeZoom(window, () => {
const nextLevel = delta === "reset" ? 0 : getSavedZoomLevel() + delta;
runAfterWheelZoom(window, () => {
const nextLevel =
delta === "reset" ? 0 : getCurrentZoomLevel(window) + delta;
setWindowZoom(window, nextLevel);
});
}

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

export function setupWindowZoom(window: ZoomWindow): void {
const state: ZoomState = {
currentZoomLevel: getSavedZoomLevel(),
deferredActions: [],
nativeZoomTimeout: null,
wheelZoomDelta: 0,
wheelZoomTimeout: null,
};
let restoreTimeout: ReturnType<typeof setTimeout> | null = null;
zoomStates.set(window, state);
Expand All @@ -88,11 +105,14 @@ export function setupWindowZoom(window: ZoomWindow): void {
};

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()));
window.webContents.on("zoom-changed", (event, zoomDirection) => {
event.preventDefault();
state.wheelZoomDelta += zoomDirection === "in" ? ZOOM_STEP : -ZOOM_STEP;
state.wheelZoomTimeout ??= setTimeout(() => {
const nextLevel = state.currentZoomLevel + state.wheelZoomDelta;
state.wheelZoomDelta = 0;
state.wheelZoomTimeout = null;
setWindowZoom(window, nextLevel);
const deferredActions = state.deferredActions.splice(0);
for (const action of deferredActions) action();
}, 0);
Expand Down
Loading