From 3742be78b65150966d0d8ce1ed3abe688aa629ba Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 27 Oct 2026 05:28:48 +0000 Subject: [PATCH 1/5] test(desktop): reproduce #4841 maximized window restoring on the wrong display These tests reproduce #4841: a window maximized on display 2 whose normal bounds sit on display 1 persists bounds that restore on display 1. Co-Authored-By: Claude Opus 5.5 --- apps/desktop/src/window/DesktopWindow.test.ts | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/apps/desktop/src/window/DesktopWindow.test.ts b/apps/desktop/src/window/DesktopWindow.test.ts index 8b6ab7c7d8a5e..06665e4e540a6 100644 --- a/apps/desktop/src/window/DesktopWindow.test.ts +++ b/apps/desktop/src/window/DesktopWindow.test.ts @@ -819,6 +819,65 @@ describe("DesktopWindow", () => { }), ); + it.effect("persists bounds on the display a maximized window was moved to", () => + Effect.gen(function* () { + const displayOne = { x: 0, y: 0, width: 1920, height: 1080 }; + const displayTwo = { x: 1920, y: 0, width: 1920, height: 1080 }; + const getAllDisplays = vi.mocked(Electron.screen.getAllDisplays); + getAllDisplays.mockReturnValue([ + { bounds: displayOne, workArea: displayOne }, + { bounds: displayTwo, workArea: displayTwo }, + ] as unknown as Electron.Display[]); + const fakeWindow = makeFakeBrowserWindow(); + fakeWindow.isMaximized.mockReturnValue(true); + fakeWindow.getBounds.mockReturnValue(displayTwo); + fakeWindow.getNormalBounds.mockReturnValue({ x: 220, y: 140, width: 1380, height: 920 }); + const createCount = yield* Ref.make(0); + const mainWindow = yield* Ref.make>(Option.none()); + const mainWindowBoundsUpdates: DesktopAppSettings.DesktopWindowBounds[] = []; + const mainWindowMaximizedUpdates: boolean[] = []; + const layer = layerTest({ + window: fakeWindow.window, + createCount, + mainWindow, + mainWindowBoundsUpdates, + mainWindowMaximizedUpdates, + }); + + yield* Effect.gen(function* () { + const desktopWindow = yield* DesktopWindow.DesktopWindow; + yield* desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773")); + + const move = fakeWindow.windowListeners.get("move"); + if (!move) { + return yield* Effect.die("window move listener was not registered"); + } + move(); + yield* TestClock.adjust(500); + yield* Effect.promise(() => Promise.resolve()); + + const persisted = mainWindowBoundsUpdates.at(-1); + assert.isDefined(persisted, "maximized window bounds were not persisted"); + const persistedDisplay = [displayOne, displayTwo].findIndex( + (display) => + persisted.x >= display.x && + persisted.y >= display.y && + persisted.x + persisted.width <= display.x + display.width && + persisted.y + persisted.height <= display.y + display.height, + ); + assert.equal( + persistedDisplay, + 1, + `window maximized on display 2 persisted bounds ${JSON.stringify(persisted)}, which restore on display ${persistedDisplay + 1}`, + ); + assert.deepEqual(mainWindowMaximizedUpdates, [true]); + }).pipe( + Effect.provide(layer), + Effect.ensuring(Effect.sync(() => getAllDisplays.mockReset())), + ); + }), + ); + it.effect("preserves unrestorable bounds until the user changes the window", () => Effect.gen(function* () { const fakeWindow = makeFakeBrowserWindow(); From ca11aa43ffb9182d30ed862feb1a06e387c2bfea Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 27 Oct 2026 05:32:42 +0000 Subject: [PATCH 2/5] fix(desktop): maximized window reopens on the display it was maximized on A maximized or fullscreen window persisted its normal bounds even when those sat on another display, so it reopened there. When the normal bounds are not on the display the window occupies, persist them moved onto that display, keeping size and offset and clamping to fit. Fixes pingdotgg/t3code#4841 Co-Authored-By: Claude Opus 5.5 --- apps/desktop/src/window/DesktopWindow.test.ts | 1 - apps/desktop/src/window/DesktopWindow.ts | 64 ++++++++++++++++++- 2 files changed, 61 insertions(+), 4 deletions(-) diff --git a/apps/desktop/src/window/DesktopWindow.test.ts b/apps/desktop/src/window/DesktopWindow.test.ts index 06665e4e540a6..a0dac15cc2b4e 100644 --- a/apps/desktop/src/window/DesktopWindow.test.ts +++ b/apps/desktop/src/window/DesktopWindow.test.ts @@ -814,7 +814,6 @@ describe("DesktopWindow", () => { assert.deepEqual(mainWindowBoundsUpdates, [{ x: 220, y: 140, width: 1380, height: 920 }]); assert.deepEqual(mainWindowMaximizedUpdates, [true]); assert.equal(fakeWindow.getNormalBounds.mock.calls.length, 1); - assert.equal(fakeWindow.getBounds.mock.calls.length, 0); }).pipe(Effect.provide(layer)); }), ); diff --git a/apps/desktop/src/window/DesktopWindow.ts b/apps/desktop/src/window/DesktopWindow.ts index 6270f80a4a7e6..d6bceb97e712d 100644 --- a/apps/desktop/src/window/DesktopWindow.ts +++ b/apps/desktop/src/window/DesktopWindow.ts @@ -148,6 +148,58 @@ function windowFitsWithinDisplay( ); } +function overlapArea(left: DisplayBounds, right: DisplayBounds): number { + const width = Math.min(left.x + left.width, right.x + right.width) - Math.max(left.x, right.x); + const height = Math.min(left.y + left.height, right.y + right.height) - Math.max(left.y, right.y); + return width > 0 && height > 0 ? width * height : 0; +} + +function displayMatching( + bounds: DisplayBounds, + displays: readonly DisplayBounds[], +): DisplayBounds | undefined { + let match: DisplayBounds | undefined; + let matchArea = 0; + for (const display of displays) { + const area = overlapArea(bounds, display); + if (area > matchArea) { + match = display; + matchArea = area; + } + } + return match; +} + +// Normal bounds of a maximized window can sit on another display than the window itself. +function normalBoundsOnWindowDisplay( + normalBounds: DisplayBounds, + windowBounds: DisplayBounds, + displays: readonly DisplayBounds[], +): DisplayBounds { + const target = displayMatching(windowBounds, displays); + if (target === undefined || windowFitsWithinDisplay(normalBounds, target)) { + return normalBounds; + } + const source = displayMatching(normalBounds, displays) ?? target; + const width = Math.min(normalBounds.width, target.width); + const height = Math.min(normalBounds.height, target.height); + const clamp = (value: number, min: number, max: number) => Math.min(Math.max(value, min), max); + return { + x: clamp(target.x + normalBounds.x - source.x, target.x, target.x + target.width - width), + y: clamp(target.y + normalBounds.y - source.y, target.y, target.y + target.height - height), + width, + height, + }; +} + +function readDisplayBoundsOrEmpty(): readonly DisplayBounds[] { + try { + return Electron.screen.getAllDisplays().map((display) => display.bounds); + } catch { + return []; + } +} + function windowBoundsEqual( left: DesktopAppSettings.DesktopWindowBounds, right: DesktopAppSettings.DesktopWindowBounds, @@ -411,9 +463,15 @@ export const make = Effect.gen(function* () { return null; } const bounds = - window.isFullScreen() || window.isMaximized() || window.isMinimized() - ? window.getNormalBounds() - : window.getBounds(); + window.isFullScreen() || window.isMaximized() + ? normalBoundsOnWindowDisplay( + window.getNormalBounds(), + window.getBounds(), + readDisplayBoundsOrEmpty(), + ) + : window.isMinimized() + ? window.getNormalBounds() + : window.getBounds(); return DesktopAppSettings.normalizeMainWindowBounds({ x: Math.round(bounds.x), y: Math.round(bounds.y), From 94f161d1c8b22d3db1c771790f1b613690f36c00 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 27 Oct 2026 05:47:29 +0000 Subject: [PATCH 3/5] fix(desktop): minimized quit keeps a maximized window's display While the main window is minimized, bounds persistence writes nothing, so the last state saved before minimizing (bounds and maximized flag) stays. macOS reports a minimized window as unmaximized with its pre-move normal frame and Windows reports -32000, so saving then lost the display. Normal bounds are now only moved onto the window's display when they sit on a different display; same-display bounds persist unchanged as on main. Co-Authored-By: Claude Opus 5.5 --- apps/desktop/src/window/DesktopWindow.test.ts | 70 +++++++++++++++++-- apps/desktop/src/window/DesktopWindow.ts | 12 ++-- 2 files changed, 70 insertions(+), 12 deletions(-) diff --git a/apps/desktop/src/window/DesktopWindow.test.ts b/apps/desktop/src/window/DesktopWindow.test.ts index a0dac15cc2b4e..e672143704c31 100644 --- a/apps/desktop/src/window/DesktopWindow.test.ts +++ b/apps/desktop/src/window/DesktopWindow.test.ts @@ -818,10 +818,13 @@ describe("DesktopWindow", () => { }), ); - it.effect("persists bounds on the display a maximized window was moved to", () => + it.effect.each([ + { width: 1920, height: 1080 }, + { width: 1280, height: 720 }, + ])("persists bounds on the display a maximized window was moved to ($width x $height)", (size) => Effect.gen(function* () { const displayOne = { x: 0, y: 0, width: 1920, height: 1080 }; - const displayTwo = { x: 1920, y: 0, width: 1920, height: 1080 }; + const displayTwo = { x: 1920, y: 0, ...size }; const getAllDisplays = vi.mocked(Electron.screen.getAllDisplays); getAllDisplays.mockReturnValue([ { bounds: displayOne, workArea: displayOne }, @@ -917,7 +920,7 @@ describe("DesktopWindow", () => { }), ); - it.effect("flushes normal bounds when minimized before the debounce completes", () => + it.effect("does not persist bounds while the window is minimized", () => Effect.gen(function* () { const fakeWindow = makeFakeBrowserWindow(); fakeWindow.getBounds.mockReturnValue({ x: -32_000, y: -32_000, width: 160, height: 28 }); @@ -945,14 +948,69 @@ describe("DesktopWindow", () => { fakeWindow.isMinimized.mockReturnValue(true); yield* desktopWindow.flushMainWindowBounds; + resize(); + yield* TestClock.adjust(500); + yield* Effect.promise(() => Promise.resolve()); - assert.deepEqual(mainWindowBoundsUpdates, [{ x: 180, y: 120, width: 1440, height: 960 }]); - assert.equal(fakeWindow.getBounds.mock.calls.length, 0); - assert.equal(fakeWindow.getNormalBounds.mock.calls.length, 1); + assert.deepEqual(mainWindowBoundsUpdates, []); }).pipe(Effect.provide(layer)); }), ); + it.effect("keeps the moved display when a maximized window quits minimized", () => + Effect.gen(function* () { + const displayOne = { x: 0, y: 0, width: 1920, height: 1080 }; + const displayTwo = { x: 1920, y: 0, width: 1920, height: 1080 }; + const getAllDisplays = vi.mocked(Electron.screen.getAllDisplays); + getAllDisplays.mockReturnValue([ + { bounds: displayOne, workArea: displayOne }, + { bounds: displayTwo, workArea: displayTwo }, + ] as unknown as Electron.Display[]); + const fakeWindow = makeFakeBrowserWindow(); + fakeWindow.isMaximized.mockReturnValue(true); + fakeWindow.getBounds.mockReturnValue(displayTwo); + fakeWindow.getNormalBounds.mockReturnValue({ x: 220, y: 140, width: 1380, height: 920 }); + const createCount = yield* Ref.make(0); + const mainWindow = yield* Ref.make>(Option.none()); + const mainWindowBoundsUpdates: DesktopAppSettings.DesktopWindowBounds[] = []; + const mainWindowMaximizedUpdates: boolean[] = []; + const layer = layerTest({ + window: fakeWindow.window, + createCount, + mainWindow, + mainWindowBoundsUpdates, + mainWindowMaximizedUpdates, + }); + + yield* Effect.gen(function* () { + const desktopWindow = yield* DesktopWindow.DesktopWindow; + yield* desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773")); + + const move = fakeWindow.windowListeners.get("move"); + if (!move) { + return yield* Effect.die("window move listener was not registered"); + } + move(); + yield* TestClock.adjust(500); + yield* Effect.promise(() => Promise.resolve()); + const movedBounds = { x: 2140, y: 140, width: 1380, height: 920 }; + assert.deepEqual(mainWindowBoundsUpdates, [movedBounds]); + + // macOS reports a minimized window as unmaximized with its pre-move normal frame. + fakeWindow.isMinimized.mockReturnValue(true); + fakeWindow.isMaximized.mockReturnValue(false); + fakeWindow.getBounds.mockReturnValue({ x: 220, y: 140, width: 1380, height: 920 }); + yield* desktopWindow.flushMainWindowBounds; + + assert.deepEqual(mainWindowBoundsUpdates.at(-1), movedBounds); + assert.deepEqual(mainWindowMaximizedUpdates.at(-1), true); + }).pipe( + Effect.provide(layer), + Effect.ensuring(Effect.sync(() => getAllDisplays.mockReset())), + ); + }), + ); + it.effect("persists the current main window bounds before the window closes", () => Effect.gen(function* () { const fakeWindow = makeFakeBrowserWindow(); diff --git a/apps/desktop/src/window/DesktopWindow.ts b/apps/desktop/src/window/DesktopWindow.ts index d6bceb97e712d..ec308b481c28c 100644 --- a/apps/desktop/src/window/DesktopWindow.ts +++ b/apps/desktop/src/window/DesktopWindow.ts @@ -177,10 +177,10 @@ function normalBoundsOnWindowDisplay( displays: readonly DisplayBounds[], ): DisplayBounds { const target = displayMatching(windowBounds, displays); - if (target === undefined || windowFitsWithinDisplay(normalBounds, target)) { + const source = displayMatching(normalBounds, displays); + if (target === undefined || source === undefined || source === target) { return normalBounds; } - const source = displayMatching(normalBounds, displays) ?? target; const width = Math.min(normalBounds.width, target.width); const height = Math.min(normalBounds.height, target.height); const clamp = (value: number, min: number, max: number) => Math.min(Math.max(value, min), max); @@ -459,7 +459,9 @@ export const make = Effect.gen(function* () { let pendingBoundsPersistFiber: Fiber.Fiber | undefined; let boundsPersistenceEnabled = persistedBounds === null || restoredPersistedBounds; const readPersistableBounds = (): DesktopAppSettings.DesktopWindowBounds | null => { - if (window.isDestroyed()) { + // Minimized windows report stale state on some platforms (macOS: not maximized, the + // pre-move normal frame; Windows: -32000), so the last state saved before minimizing stays. + if (window.isDestroyed() || window.isMinimized()) { return null; } const bounds = @@ -469,9 +471,7 @@ export const make = Effect.gen(function* () { window.getBounds(), readDisplayBoundsOrEmpty(), ) - : window.isMinimized() - ? window.getNormalBounds() - : window.getBounds(); + : window.getBounds(); return DesktopAppSettings.normalizeMainWindowBounds({ x: Math.round(bounds.x), y: Math.round(bounds.y), From 5f78dfa4484356066e8ff85ef6e4974e38542c97 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 27 Oct 2026 05:59:42 +0000 Subject: [PATCH 4/5] fix(desktop): a change made just before minimizing is still saved Writing nothing while minimized dropped a move or resize made inside the 500 ms debounce before minimizing, for both normal and maximized windows. Keep the last bounds and maximized state seen while not minimized, refreshed on every bounds event and write, and persist that snapshot when a write runs while minimized instead of rereading stale native state. Co-Authored-By: Claude Opus 5.5 --- apps/desktop/src/window/DesktopWindow.test.ts | 76 ++++++++++++++++++- apps/desktop/src/window/DesktopWindow.ts | 50 +++++++----- 2 files changed, 103 insertions(+), 23 deletions(-) diff --git a/apps/desktop/src/window/DesktopWindow.test.ts b/apps/desktop/src/window/DesktopWindow.test.ts index e672143704c31..87a68edc54a1e 100644 --- a/apps/desktop/src/window/DesktopWindow.test.ts +++ b/apps/desktop/src/window/DesktopWindow.test.ts @@ -920,11 +920,10 @@ describe("DesktopWindow", () => { }), ); - it.effect("does not persist bounds while the window is minimized", () => + it.effect("flushes normal bounds when minimized before the debounce completes", () => Effect.gen(function* () { const fakeWindow = makeFakeBrowserWindow(); - fakeWindow.getBounds.mockReturnValue({ x: -32_000, y: -32_000, width: 160, height: 28 }); - fakeWindow.getNormalBounds.mockReturnValue({ x: 180, y: 120, width: 1440, height: 960 }); + fakeWindow.getBounds.mockReturnValue({ x: 180, y: 120, width: 1440, height: 960 }); const createCount = yield* Ref.make(0); const mainWindow = yield* Ref.make>(Option.none()); const mainWindowBoundsUpdates: DesktopAppSettings.DesktopWindowBounds[] = []; @@ -945,18 +944,87 @@ describe("DesktopWindow", () => { } resize(); yield* TestClock.adjust(250); + // Windows reports a minimized window at -32000. fakeWindow.isMinimized.mockReturnValue(true); + fakeWindow.getBounds.mockReturnValue({ x: -32_000, y: -32_000, width: 160, height: 28 }); yield* desktopWindow.flushMainWindowBounds; resize(); yield* TestClock.adjust(500); yield* Effect.promise(() => Promise.resolve()); - assert.deepEqual(mainWindowBoundsUpdates, []); + assert.deepEqual(mainWindowBoundsUpdates, [{ x: 180, y: 120, width: 1440, height: 960 }]); }).pipe(Effect.provide(layer)); }), ); + it.effect.each([ + { window: "normal", write: "flush" }, + { window: "normal", write: "debounce" }, + { window: "maximized", write: "flush" }, + { window: "maximized", write: "debounce" }, + ] as const)("persists a $window window change minimized before its $write", (input) => + Effect.gen(function* () { + const displayOne = { x: 0, y: 0, width: 1920, height: 1080 }; + const displayTwo = { x: 1920, y: 0, width: 1920, height: 1080 }; + const getAllDisplays = vi.mocked(Electron.screen.getAllDisplays); + getAllDisplays.mockReturnValue([ + { bounds: displayOne, workArea: displayOne }, + { bounds: displayTwo, workArea: displayTwo }, + ] as unknown as Electron.Display[]); + const maximized = input.window === "maximized"; + const fakeWindow = makeFakeBrowserWindow(); + fakeWindow.isMaximized.mockReturnValue(maximized); + fakeWindow.getBounds.mockReturnValue( + maximized ? displayTwo : { x: 220, y: 140, width: 1380, height: 920 }, + ); + fakeWindow.getNormalBounds.mockReturnValue({ x: 220, y: 140, width: 1380, height: 920 }); + const createCount = yield* Ref.make(0); + const mainWindow = yield* Ref.make>(Option.none()); + const mainWindowBoundsUpdates: DesktopAppSettings.DesktopWindowBounds[] = []; + const mainWindowMaximizedUpdates: boolean[] = []; + const layer = layerTest({ + window: fakeWindow.window, + createCount, + mainWindow, + mainWindowBoundsUpdates, + mainWindowMaximizedUpdates, + }); + + yield* Effect.gen(function* () { + const desktopWindow = yield* DesktopWindow.DesktopWindow; + yield* desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773")); + + const move = fakeWindow.windowListeners.get("move"); + if (!move) { + return yield* Effect.die("window move listener was not registered"); + } + move(); + yield* TestClock.adjust(250); + // Minimized windows report stale state: macOS unmaximized at the pre-move frame. + fakeWindow.isMinimized.mockReturnValue(true); + fakeWindow.isMaximized.mockReturnValue(false); + fakeWindow.getBounds.mockReturnValue({ x: 100, y: 100, width: 1100, height: 780 }); + fakeWindow.getNormalBounds.mockReturnValue({ x: 100, y: 100, width: 1100, height: 780 }); + if (input.write === "debounce") { + yield* TestClock.adjust(250); + yield* Effect.promise(() => Promise.resolve()); + } + yield* desktopWindow.flushMainWindowBounds; + + assert.deepEqual(mainWindowBoundsUpdates, [ + maximized + ? { x: 2140, y: 140, width: 1380, height: 920 } + : { x: 220, y: 140, width: 1380, height: 920 }, + ]); + assert.deepEqual(mainWindowMaximizedUpdates, [maximized]); + }).pipe( + Effect.provide(layer), + Effect.ensuring(Effect.sync(() => getAllDisplays.mockReset())), + ); + }), + ); + it.effect("keeps the moved display when a maximized window quits minimized", () => Effect.gen(function* () { const displayOne = { x: 0, y: 0, width: 1920, height: 1080 }; diff --git a/apps/desktop/src/window/DesktopWindow.ts b/apps/desktop/src/window/DesktopWindow.ts index ec308b481c28c..3daff0866b1cd 100644 --- a/apps/desktop/src/window/DesktopWindow.ts +++ b/apps/desktop/src/window/DesktopWindow.ts @@ -458,39 +458,52 @@ export const make = Effect.gen(function* () { let boundsPersistFiber: Fiber.Fiber | undefined; let pendingBoundsPersistFiber: Fiber.Fiber | undefined; let boundsPersistenceEnabled = persistedBounds === null || restoredPersistedBounds; - const readPersistableBounds = (): DesktopAppSettings.DesktopWindowBounds | null => { - // Minimized windows report stale state on some platforms (macOS: not maximized, the - // pre-move normal frame; Windows: -32000), so the last state saved before minimizing stays. - if (window.isDestroyed() || window.isMinimized()) { + let lastPersistableState: { + readonly bounds: DesktopAppSettings.DesktopWindowBounds; + readonly isMaximized: boolean; + } | null = null; + const readPersistableState = () => { + if (window.isDestroyed()) { return null; } + // Minimized windows report stale state on some platforms (macOS: not maximized, the + // pre-move normal frame; Windows: -32000), so they keep the state seen before minimizing. + if (window.isMinimized()) { + return lastPersistableState; + } + const isMaximized = window.isMaximized(); const bounds = - window.isFullScreen() || window.isMaximized() + window.isFullScreen() || isMaximized ? normalBoundsOnWindowDisplay( window.getNormalBounds(), window.getBounds(), readDisplayBoundsOrEmpty(), ) : window.getBounds(); - return DesktopAppSettings.normalizeMainWindowBounds({ + const persistableBounds = DesktopAppSettings.normalizeMainWindowBounds({ x: Math.round(bounds.x), y: Math.round(bounds.y), width: Math.round(bounds.width), height: Math.round(bounds.height), }); + lastPersistableState = + persistableBounds === null ? null : { bounds: persistableBounds, isMaximized }; + return lastPersistableState; }; - const fallbackWindowBounds = boundsPersistenceEnabled ? null : readPersistableBounds(); + const fallbackWindowBounds = boundsPersistenceEnabled + ? null + : (readPersistableState()?.bounds ?? null); const fallbackWindowMaximized = persistedSettings.mainWindowMaximized; const persistCurrentBounds = (): Fiber.Fiber | undefined => { if (!boundsPersistenceEnabled) { return pendingBoundsPersistFiber; } - const bounds = readPersistableBounds(); - if (bounds === null) { + const state = readPersistableState(); + if (state === null) { return pendingBoundsPersistFiber; } pendingBoundsPersistFiber = runFork( - desktopSettings.setMainWindowBounds(bounds, window.isMaximized()).pipe( + desktopSettings.setMainWindowBounds(state.bounds, state.isMaximized).pipe( Effect.asVoid, Effect.catch((error) => logWindowWarning("failed to persist main window bounds", { @@ -502,16 +515,15 @@ export const make = Effect.gen(function* () { return pendingBoundsPersistFiber; }; const scheduleBoundsPersist = () => { - if (!boundsPersistenceEnabled) { - const currentBounds = readPersistableBounds(); - if ( - currentBounds === null || + const state = readPersistableState(); + if ( + !boundsPersistenceEnabled && + (state === null || (fallbackWindowBounds !== null && - windowBoundsEqual(currentBounds, fallbackWindowBounds) && - window.isMaximized() === fallbackWindowMaximized) - ) { - return; - } + windowBoundsEqual(state.bounds, fallbackWindowBounds) && + state.isMaximized === fallbackWindowMaximized)) + ) { + return; } boundsPersistenceEnabled = true; if (boundsPersistFiber !== undefined) { From 6fd7ae506ed311d1fa89f51bdb3a33a5cbb9ee73 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 27 Oct 2026 06:13:10 +0000 Subject: [PATCH 5/5] fix(desktop): a maximized window whose normal bounds straddle displays keeps its display Co-Authored-By: Claude Opus 5.5 --- apps/desktop/src/window/DesktopWindow.test.ts | 113 +++++++++--------- apps/desktop/src/window/DesktopWindow.ts | 6 +- 2 files changed, 61 insertions(+), 58 deletions(-) diff --git a/apps/desktop/src/window/DesktopWindow.test.ts b/apps/desktop/src/window/DesktopWindow.test.ts index 87a68edc54a1e..4ec10232c32d1 100644 --- a/apps/desktop/src/window/DesktopWindow.test.ts +++ b/apps/desktop/src/window/DesktopWindow.test.ts @@ -819,65 +819,68 @@ describe("DesktopWindow", () => { ); it.effect.each([ - { width: 1920, height: 1080 }, - { width: 1280, height: 720 }, - ])("persists bounds on the display a maximized window was moved to ($width x $height)", (size) => - Effect.gen(function* () { - const displayOne = { x: 0, y: 0, width: 1920, height: 1080 }; - const displayTwo = { x: 1920, y: 0, ...size }; - const getAllDisplays = vi.mocked(Electron.screen.getAllDisplays); - getAllDisplays.mockReturnValue([ - { bounds: displayOne, workArea: displayOne }, - { bounds: displayTwo, workArea: displayTwo }, - ] as unknown as Electron.Display[]); - const fakeWindow = makeFakeBrowserWindow(); - fakeWindow.isMaximized.mockReturnValue(true); - fakeWindow.getBounds.mockReturnValue(displayTwo); - fakeWindow.getNormalBounds.mockReturnValue({ x: 220, y: 140, width: 1380, height: 920 }); - const createCount = yield* Ref.make(0); - const mainWindow = yield* Ref.make>(Option.none()); - const mainWindowBoundsUpdates: DesktopAppSettings.DesktopWindowBounds[] = []; - const mainWindowMaximizedUpdates: boolean[] = []; - const layer = layerTest({ - window: fakeWindow.window, - createCount, - mainWindow, - mainWindowBoundsUpdates, - mainWindowMaximizedUpdates, - }); + { normal: "on display 1", width: 1920, height: 1080, x: 220 }, + { normal: "on display 1", width: 1280, height: 720, x: 220 }, + { normal: "straddling both displays", width: 1920, height: 1080, x: 1800 }, + ])( + "persists bounds on the display a maximized window was moved to ($width x $height, normal $normal)", + ({ width, height, x }) => + Effect.gen(function* () { + const displayOne = { x: 0, y: 0, width: 1920, height: 1080 }; + const displayTwo = { x: 1920, y: 0, width, height }; + const getAllDisplays = vi.mocked(Electron.screen.getAllDisplays); + getAllDisplays.mockReturnValue([ + { bounds: displayOne, workArea: displayOne }, + { bounds: displayTwo, workArea: displayTwo }, + ] as unknown as Electron.Display[]); + const fakeWindow = makeFakeBrowserWindow(); + fakeWindow.isMaximized.mockReturnValue(true); + fakeWindow.getBounds.mockReturnValue(displayTwo); + fakeWindow.getNormalBounds.mockReturnValue({ x, y: 140, width: 1380, height: 920 }); + const createCount = yield* Ref.make(0); + const mainWindow = yield* Ref.make>(Option.none()); + const mainWindowBoundsUpdates: DesktopAppSettings.DesktopWindowBounds[] = []; + const mainWindowMaximizedUpdates: boolean[] = []; + const layer = layerTest({ + window: fakeWindow.window, + createCount, + mainWindow, + mainWindowBoundsUpdates, + mainWindowMaximizedUpdates, + }); - yield* Effect.gen(function* () { - const desktopWindow = yield* DesktopWindow.DesktopWindow; - yield* desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773")); + yield* Effect.gen(function* () { + const desktopWindow = yield* DesktopWindow.DesktopWindow; + yield* desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773")); - const move = fakeWindow.windowListeners.get("move"); - if (!move) { - return yield* Effect.die("window move listener was not registered"); - } - move(); - yield* TestClock.adjust(500); - yield* Effect.promise(() => Promise.resolve()); + const move = fakeWindow.windowListeners.get("move"); + if (!move) { + return yield* Effect.die("window move listener was not registered"); + } + move(); + yield* TestClock.adjust(500); + yield* Effect.promise(() => Promise.resolve()); - const persisted = mainWindowBoundsUpdates.at(-1); - assert.isDefined(persisted, "maximized window bounds were not persisted"); - const persistedDisplay = [displayOne, displayTwo].findIndex( - (display) => - persisted.x >= display.x && - persisted.y >= display.y && - persisted.x + persisted.width <= display.x + display.width && - persisted.y + persisted.height <= display.y + display.height, - ); - assert.equal( - persistedDisplay, - 1, - `window maximized on display 2 persisted bounds ${JSON.stringify(persisted)}, which restore on display ${persistedDisplay + 1}`, + const persisted = mainWindowBoundsUpdates.at(-1); + assert.isDefined(persisted, "maximized window bounds were not persisted"); + const persistedDisplay = [displayOne, displayTwo].findIndex( + (display) => + persisted.x >= display.x && + persisted.y >= display.y && + persisted.x + persisted.width <= display.x + display.width && + persisted.y + persisted.height <= display.y + display.height, + ); + assert.equal( + persistedDisplay, + 1, + `window maximized on display 2 persisted bounds ${JSON.stringify(persisted)}, which restore on display ${persistedDisplay + 1}`, + ); + assert.deepEqual(mainWindowMaximizedUpdates, [true]); + }).pipe( + Effect.provide(layer), + Effect.ensuring(Effect.sync(() => getAllDisplays.mockReset())), ); - assert.deepEqual(mainWindowMaximizedUpdates, [true]); - }).pipe( - Effect.provide(layer), - Effect.ensuring(Effect.sync(() => getAllDisplays.mockReset())), - ); - }), + }), ); it.effect("preserves unrestorable bounds until the user changes the window", () => diff --git a/apps/desktop/src/window/DesktopWindow.ts b/apps/desktop/src/window/DesktopWindow.ts index 3daff0866b1cd..0e4c3d0fd895e 100644 --- a/apps/desktop/src/window/DesktopWindow.ts +++ b/apps/desktop/src/window/DesktopWindow.ts @@ -170,17 +170,17 @@ function displayMatching( return match; } -// Normal bounds of a maximized window can sit on another display than the window itself. +// Normal bounds of a maximized window can sit on, or straddle into, another display than the window. function normalBoundsOnWindowDisplay( normalBounds: DisplayBounds, windowBounds: DisplayBounds, displays: readonly DisplayBounds[], ): DisplayBounds { const target = displayMatching(windowBounds, displays); - const source = displayMatching(normalBounds, displays); - if (target === undefined || source === undefined || source === target) { + if (target === undefined || windowFitsWithinDisplay(normalBounds, target)) { return normalBounds; } + const source = displayMatching(normalBounds, displays) ?? target; const width = Math.min(normalBounds.width, target.width); const height = Math.min(normalBounds.height, target.height); const clamp = (value: number, min: number, max: number) => Math.min(Math.max(value, min), max);