Skip to content
12 changes: 12 additions & 0 deletions apps/desktop/src/settings/DesktopAppSettings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,18 @@ describe("DesktopSettings", () => {
),
);

it.effect("persists and reloads compositor bounds below the preferred minimum", () =>
withSettings(
Effect.gen(function* () {
const settings = yield* DesktopAppSettings.DesktopAppSettings;
const mainWindowBounds = { x: 12, y: 563, width: 280, height: 240 };
yield* settings.setMainWindowBounds(mainWindowBounds, false);
const loaded = yield* settings.load;
assert.deepEqual(loaded.mainWindowBounds, mainWindowBounds);
}),
),
);

it.effect("rejects window bounds that do not satisfy the domain schema", () =>
withSettings(
Effect.gen(function* () {
Expand Down
9 changes: 3 additions & 6 deletions apps/desktop/src/settings/DesktopAppSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,12 @@ export interface DesktopSettingsChange {
}

const DEFAULT_TAILSCALE_SERVE_PORT = 443;
const MIN_MAIN_WINDOW_SIZE = {
width: 840,
height: 620,
} as const;
// Compositors can assign bounds smaller than the requested native minimum.
export const DesktopWindowBoundsSchema = Schema.Struct({
x: Schema.Int,
y: Schema.Int,
width: Schema.Int.check(Schema.isGreaterThanOrEqualTo(MIN_MAIN_WINDOW_SIZE.width)),
height: Schema.Int.check(Schema.isGreaterThanOrEqualTo(MIN_MAIN_WINDOW_SIZE.height)),
width: Schema.Int.check(Schema.isGreaterThan(0)),
height: Schema.Int.check(Schema.isGreaterThan(0)),
});
export type DesktopWindowBounds = typeof DesktopWindowBoundsSchema.Type;
export const DEFAULT_MAIN_WINDOW_SIZE = {
Expand Down
90 changes: 90 additions & 0 deletions apps/desktop/src/window/DesktopWindow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,14 @@ vi.mock("electron", async (importOriginal) => ({
})),
},
screen: {
getPrimaryDisplay: vi.fn(() => ({
bounds: { x: 0, y: 0, width: 1920, height: 1080 },
workArea: { x: 0, y: 0, width: 1920, height: 1080 },
})),
getAllDisplays: vi.fn(() => [
{
bounds: { x: 0, y: 0, width: 1920, height: 1080 },
workArea: { x: 0, y: 0, width: 1920, height: 1080 },
},
]),
},
Expand Down Expand Up @@ -895,6 +900,91 @@ describe("DesktopWindow", () => {
}),
);

it.effect.each([
{
scenario: "missing saved bounds and a taskbar on a small display",
workArea: { x: -1280, y: 26, width: 1280, height: 694 },
savedBounds: null,
expected: { x: -1190, y: 26, width: 1100, height: 694, minWidth: 360, minHeight: 320 },
},
{
scenario: "missing saved bounds and default size that just fits above the Dock",
workArea: { x: 0, y: 25, width: 1440, height: 800 },
savedBounds: null,
expected: { x: 170, y: 35, width: 1100, height: 780, minWidth: 360, minHeight: 320 },
},
{
scenario: "offscreen saved bounds and a work area below the preferred minimum",
workArea: { x: 40, y: 26, width: 280, height: 240 },
savedBounds: { x: 2040, y: 80, width: 1320, height: 880 },
expected: { x: 40, y: 26, width: 280, height: 240, minWidth: 280, minHeight: 240 },
},
{
scenario: "saved bounds below the preferred minimum on a large display",
workArea: { x: 0, y: 0, width: 1920, height: 1080 },
savedBounds: { x: 12, y: 563, width: 280, height: 240 },
expected: { x: 12, y: 563, width: 280, height: 240, minWidth: 280, minHeight: 240 },
},
{
scenario: "saved bounds overlapping the taskbar within the full display",
workArea: { x: 0, y: 0, width: 1920, height: 1040 },
savedBounds: { x: 100, y: 286, width: 1100, height: 780 },
expected: { x: 100, y: 286, width: 1100, height: 780, minWidth: 360, minHeight: 320 },
},
{
scenario: "a small primary display listed after a large secondary display",
workArea: { x: -800, y: 26, width: 800, height: 574 },
secondaryWorkArea: { x: 0, y: 0, width: 1920, height: 1080 },
savedBounds: null,
expected: { x: -800, y: 26, width: 800, height: 574, minWidth: 360, minHeight: 320 },
},
])(
"opens a window that fits with $scenario",
({ workArea, secondaryWorkArea, savedBounds, expected }) =>
Effect.gen(function* () {
const previousPrimary = Electron.screen.getPrimaryDisplay();
const display = {
...Electron.screen.getAllDisplays()[0]!,
bounds: { x: workArea.x, y: 0, width: workArea.width, height: workArea.height + 26 },
workArea,
};
vi.mocked(Electron.screen.getAllDisplays).mockReturnValueOnce([
...(secondaryWorkArea
? [{ ...display, bounds: secondaryWorkArea, workArea: secondaryWorkArea }]
: []),
display,
]);
vi.mocked(Electron.screen.getPrimaryDisplay).mockReturnValue(display);
yield* Effect.addFinalizer(() =>
Effect.sync(() => {
vi.mocked(Electron.screen.getPrimaryDisplay).mockReturnValue(previousPrimary);
}),
);
const fakeWindow = makeFakeBrowserWindow();
const createCount = yield* Ref.make(0);
const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
const createdWindowOptions: Electron.BrowserWindowConstructorOptions[] = [];
const layer = layerTest({
window: fakeWindow.window,
createCount,
mainWindow,
createdWindowOptions,
desktopSettings: {
...DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS,
mainWindowBounds: savedBounds,
},
});

yield* Effect.gen(function* () {
const desktopWindow = yield* DesktopWindow.DesktopWindow;
yield* desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773"));
}).pipe(Effect.provide(layer));

assert.equal(createdWindowOptions.length, 1);
assert.containsSubset(createdWindowOptions[0], expected);
}).pipe(Effect.scoped),
);

it.effect("persists the current main window bounds before the window closes", () =>
Effect.gen(function* () {
const fakeWindow = makeFakeBrowserWindow();
Expand Down
28 changes: 23 additions & 5 deletions apps/desktop/src/window/DesktopWindow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ function syncMacosWindowButtons(window: Electron.BrowserWindow): void {
const TITLEBAR_COLOR = "#01000000"; // #00000000 does not work correctly on Linux
const TITLEBAR_LIGHT_SYMBOL_COLOR = "#1f2937";
const TITLEBAR_DARK_SYMBOL_COLOR = "#f8fafc";
const MIN_MAIN_WINDOW_SIZE = { width: 360, height: 320 } as const;
const MAIN_WINDOW_BOUNDS_PERSIST_DEBOUNCE_MS = 500;
const DEVELOPMENT_LOAD_RETRY_DELAYS_MS = [100, 250, 500, 1_000, 2_000] as const;
const RENDERER_RECOVERY_RELOAD_DELAY_MS = 500;
Expand Down Expand Up @@ -163,14 +164,26 @@ function windowBoundsEqual(
export function resolveInitialMainWindowBounds(
persistedBounds: DesktopAppSettings.DesktopWindowBounds | null,
displays: readonly DisplayBounds[],
primaryWorkArea: DisplayBounds | null = null,
): DesktopAppSettings.DesktopWindowBounds | typeof DesktopAppSettings.DEFAULT_MAIN_WINDOW_SIZE {
if (
persistedBounds !== null &&
displays.some((display) => windowFitsWithinDisplay(persistedBounds, display))
) {
return persistedBounds;
}
return DesktopAppSettings.DEFAULT_MAIN_WINDOW_SIZE;
const display = primaryWorkArea;
const defaults = DesktopAppSettings.DEFAULT_MAIN_WINDOW_SIZE;
// Electron centers an unpositioned macOS window on the full screen, which can put it under the Dock.
if (!display) return defaults;
const width = Math.min(defaults.width, display.width);
Comment thread
MatthewFeroz marked this conversation as resolved.
Resolved
const height = Math.min(defaults.height, display.height);
return {
x: display.x + Math.floor((display.width - width) / 2),
y: display.y + Math.floor((display.height - height) / 2),
width,
height,
};
}

function buildConnectingSplashDataUrl(shouldUseDarkColors: boolean): string {
Expand Down Expand Up @@ -354,6 +367,7 @@ export const make = Effect.gen(function* () {
return {
_tag: "Success" as const,
bounds: Electron.screen.getAllDisplays().map((display) => display.bounds),
primaryWorkArea: Electron.screen.getPrimaryDisplay().workArea,
};
} catch (cause) {
return { _tag: "Failure" as const, cause };
Expand All @@ -365,16 +379,20 @@ export const make = Effect.gen(function* () {
: yield* logWindowWarning("failed to read connected displays; using defaults", {
cause: displayBoundsResult.cause,
}).pipe(Effect.as<readonly Electron.Rectangle[]>([]));
const initialBounds = resolveInitialMainWindowBounds(persistedBounds, displayBounds);
const initialBounds = resolveInitialMainWindowBounds(
persistedBounds,
displayBounds,
displayBoundsResult._tag === "Success" ? displayBoundsResult.primaryWorkArea : null,
);
const restoredPersistedBounds = persistedBounds !== null && initialBounds === persistedBounds;
if (persistedBounds !== null && initialBounds === DesktopAppSettings.DEFAULT_MAIN_WINDOW_SIZE) {
if (persistedBounds !== null && !restoredPersistedBounds) {
yield* logWindowWarning("saved main window bounds could not be restored; using defaults");
}
if (yield* Ref.get(desktopState.quitting)) return Option.none();
const window = yield* electronWindow.create({
...initialBounds,
minWidth: 840,
minHeight: 620,
minWidth: Math.min(MIN_MAIN_WINDOW_SIZE.width, initialBounds.width),
minHeight: Math.min(MIN_MAIN_WINDOW_SIZE.height, initialBounds.height),
show: false,
autoHideMenuBar: true,
...(environment.platform === "darwin" ? { disableAutoHideCursor: true } : {}),
Expand Down
Loading