diff --git a/apps/code/snapshots.yml b/apps/code/snapshots.yml index cfbdd290b1..26cb8c4587 100644 --- a/apps/code/snapshots.yml +++ b/apps/code/snapshots.yml @@ -27,7 +27,7 @@ snapshots: archive-archivedtasksview--many-tasks--dark: hash: v1.k4693efd2.40d24f07a24c6400c32ae68b39fd908a056beb5dc6df8bcb237ec2ab2b494f4a.l2vjfSQDalCPRtDNV7pbfTKGlMsQoI81jI-UdJfLHWE archive-archivedtasksview--many-tasks--light: - hash: v1.k4693efd2.52ec9963bfe6f248ffcdaa4c26945d2b7e303115825b65d978aa5aefd4af1007.ZLtOfTZUrznhYeB-N2SIHzQWRzF6TDSaipNkfd8uzjM + hash: v1.k4693efd2.74c25b303262f2d4c0f22890d351e76f482827548548f7c023bc309327c927b8.DP9VVrvBz1naIJMwHORkfqm69BlO785ulOt4o6zFs94 archive-archivedtasksview--mixed-modes--dark: hash: v1.k4693efd2.d94039b8cc17a4ad1b720364f41fee58a4843aa9a901443997ba62602f698794.441LWZhT-2WQZJHni4LoOgQsOSr2O103NZOk1pF8ME4 archive-archivedtasksview--mixed-modes--light: diff --git a/apps/code/src/main/zoom.test.ts b/apps/code/src/main/zoom.test.ts index a7b5792377..3798c6766d 100644 --- a/apps/code/src/main/zoom.test.ts +++ b/apps/code/src/main/zoom.test.ts @@ -20,9 +20,20 @@ vi.mock("./utils/store", () => ({ import { adjustWindowZoom, restoreWindowZoom, setupWindowZoom } from "./zoom"; class FakeWebContents extends EventEmitter { + public destroyed = false; + public readonly setZoomLevelCalls: number[] = []; public zoomLevel = 0; + public isDestroyed(): boolean { + return this.destroyed; + } + + public getZoomLevel(): number { + return this.zoomLevel; + } + public setZoomLevel(level: number): void { + this.setZoomLevelCalls.push(level); this.zoomLevel = level; } } @@ -85,6 +96,30 @@ describe("window zoom", () => { expect(window.webContents.zoomLevel).toBe(0.5); }); + it("restores the current level after an external window resize", () => { + const window = createWindow(); + setupWindowZoom(window); + + window.webContents.emit("zoom-changed", { preventDefault: vi.fn() }, "in"); + vi.runAllTimers(); + window.webContents.zoomLevel = 0; + + window.emit("resize"); + vi.runAllTimers(); + const restoredZoomLevel = window.webContents.zoomLevel; + adjustWindowZoom(window, 0.5); + + expect({ + restoredZoomLevel, + zoomLevel: window.webContents.zoomLevel, + saved: store.save.mock.calls, + }).toEqual({ + restoredZoomLevel: 1, + zoomLevel: 1.5, + saved: [[1], [1.5]], + }); + }); + it.each([ ["in", 1], ["out", 0], @@ -110,20 +145,70 @@ describe("window zoom", () => { }, ); - it("keeps wheel zoom after resizing", () => { + it.each(["resize", "resized"] as const)( + "keeps wheel zoom after %s", + (resizeEvent) => { + const window = createWindow(); + setupWindowZoom(window); + + window.webContents.emit( + "zoom-changed", + { preventDefault: vi.fn() }, + "in", + ); + window.emit(resizeEvent); + vi.runAllTimers(); + + expect({ + zoomLevel: window.webContents.zoomLevel, + saved: store.save.mock.calls, + }).toEqual({ + zoomLevel: 1, + saved: [[1]], + }); + }, + ); + + it("skips redundant restoration during a resize storm", () => { + const window = createWindow(); + setupWindowZoom(window); + window.webContents.zoomLevel = 0.5; + + window.emit("resize"); + vi.runAllTimers(); + vi.advanceTimersByTime(16); + window.emit("resize"); + vi.runAllTimers(); + const callsBeforeReset = [...window.webContents.setZoomLevelCalls]; + + window.webContents.zoomLevel = 0; + window.emit("resize"); + vi.runAllTimers(); + + expect({ + callsBeforeReset, + callsAfterReset: window.webContents.setZoomLevelCalls, + }).toEqual({ + callsBeforeReset: [], + callsAfterReset: [0.5], + }); + }); + + it("ignores queued zoom work after the window is destroyed", () => { const window = createWindow(); setupWindowZoom(window); window.webContents.emit("zoom-changed", { preventDefault: vi.fn() }, "in"); - window.emit("resized"); + window.emit("resize"); + window.webContents.destroyed = true; vi.runAllTimers(); expect({ - zoomLevel: window.webContents.zoomLevel, + zoomLevelCalls: window.webContents.setZoomLevelCalls, saved: store.save.mock.calls, }).toEqual({ - zoomLevel: 1, - saved: [[1]], + zoomLevelCalls: [], + saved: [], }); }); diff --git a/apps/code/src/main/zoom.ts b/apps/code/src/main/zoom.ts index af4e49ddba..db845cd78f 100644 --- a/apps/code/src/main/zoom.ts +++ b/apps/code/src/main/zoom.ts @@ -6,6 +6,8 @@ const ZOOM_MIN = -3; const ZOOM_MAX = 3; interface ZoomWebContents { + getZoomLevel(): number; + isDestroyed(): boolean; on(event: "did-finish-load", listener: () => void): void; on( event: "zoom-changed", @@ -23,6 +25,7 @@ interface ZoomWindow { | "enter-full-screen" | "leave-full-screen" | "maximize" + | "resize" | "resized" | "unmaximize", listener: () => void, @@ -62,6 +65,7 @@ function runAfterWheelZoom(window: ZoomWindow, action: () => void): void { } export function setWindowZoom(window: ZoomWindow, level: number): void { + if (window.webContents.isDestroyed()) return; const nextLevel = clampZoomLevel(level); const state = zoomStates.get(window); if (state) state.currentZoomLevel = nextLevel; @@ -82,7 +86,11 @@ export function adjustWindowZoom( export function restoreWindowZoom(window: ZoomWindow): void { runAfterWheelZoom(window, () => { - window.webContents.setZoomLevel(getCurrentZoomLevel(window)); + if (window.webContents.isDestroyed()) return; + const zoomLevel = getCurrentZoomLevel(window); + if (window.webContents.getZoomLevel() !== zoomLevel) { + window.webContents.setZoomLevel(zoomLevel); + } }); } @@ -120,6 +128,7 @@ export function setupWindowZoom(window: ZoomWindow): void { window.on("maximize", scheduleRestore); window.on("unmaximize", scheduleRestore); + window.on("resize", scheduleRestore); window.on("resized", scheduleRestore); window.on("enter-full-screen", scheduleRestore); window.on("leave-full-screen", scheduleRestore);