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
197 changes: 192 additions & 5 deletions apps/desktop/src/window/DesktopWindow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -814,11 +814,75 @@ 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));
}),
);

it.effect.each([
{ 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.Option<Electron.BrowserWindow>>(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();
Expand Down Expand Up @@ -862,8 +926,7 @@ describe("DesktopWindow", () => {
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.Option<Electron.BrowserWindow>>(Option.none());
const mainWindowBoundsUpdates: DesktopAppSettings.DesktopWindowBounds[] = [];
Expand All @@ -884,17 +947,141 @@ 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, [{ x: 180, y: 120, width: 1440, height: 960 }]);
assert.equal(fakeWindow.getBounds.mock.calls.length, 0);
assert.equal(fakeWindow.getNormalBounds.mock.calls.length, 1);
}).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.Option<Electron.BrowserWindow>>(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 };
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.Option<Electron.BrowserWindow>>(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();
Expand Down
104 changes: 87 additions & 17 deletions apps/desktop/src/window/DesktopWindow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, or straddle into, another display than the window.
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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium window/DesktopWindow.ts:185

On a display shorter than 620px, this returns a normalBounds.height below the settings minimum; for example, a 600px display produces height 600. normalizeMainWindowBounds then rejects the bounds, causing readPersistableState to return null and persistCurrentBounds to skip saving the new window state, including the maximized flag. Preserve settings-valid bounds when the target display is smaller than the minimum so the state is still persisted.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/desktop/src/window/DesktopWindow.ts around line 185:

On a display shorter than 620px, this returns a `normalBounds.height` below the settings minimum; for example, a 600px display produces height 600. `normalizeMainWindowBounds` then rejects the bounds, causing `readPersistableState` to return `null` and `persistCurrentBounds` to skip saving the new window state, including the maximized flag. Preserve settings-valid bounds when the target display is smaller than the minimum so the state is still persisted.

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,
Expand Down Expand Up @@ -406,33 +458,52 @@ export const make = Effect.gen(function* () {
let boundsPersistFiber: Fiber.Fiber<void, never> | undefined;
let pendingBoundsPersistFiber: Fiber.Fiber<void, never> | undefined;
let boundsPersistenceEnabled = persistedBounds === null || restoredPersistedBounds;
const readPersistableBounds = (): DesktopAppSettings.DesktopWindowBounds | null => {
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.isMinimized()
? window.getNormalBounds()
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<void, never> | 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", {
Expand All @@ -444,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) {
Expand Down
Loading