mirror of
https://github.com/BradGroux/veritas-kanban.git
synced 2026-10-09 05:07:53 +00:00
Merge pull request #1555 from BradGroux/fix/window-bounds-1530
fix: restore window bounds within available displays
This commit is contained in:
commit
6abea64f04
5 changed files with 136 additions and 9 deletions
|
|
@ -1,4 +1,4 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import path from 'node:path';
|
||||
import { mkdtemp } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
|
|
@ -6,6 +6,7 @@ import { tmpdir } from 'node:os';
|
|||
import { createDesktopPaths } from '../paths.js';
|
||||
import {
|
||||
applyDesktopWindowState,
|
||||
captureDesktopWindowState,
|
||||
readDesktopWindowState,
|
||||
writeDesktopWindowState,
|
||||
writeDesktopWindowStateSync,
|
||||
|
|
@ -68,3 +69,52 @@ describe('desktop window state', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('visible display restoration', () => {
|
||||
const primary = { x: 0, y: 25, width: 1440, height: 875 };
|
||||
const left = { x: -1920, y: -200, width: 1920, height: 1080 };
|
||||
it('moves disconnected-monitor windows to the primary work area', () => {
|
||||
expect(
|
||||
applyDesktopWindowState({ x: 5000, y: 300, width: 1200, height: 800 }, undefined, [primary])
|
||||
).toEqual({ x: 120, y: 63, width: 1200, height: 800 });
|
||||
});
|
||||
it('preserves valid negative monitor coordinates', () => {
|
||||
expect(
|
||||
applyDesktopWindowState({ x: -1800, y: -100, width: 1200, height: 800 }, undefined, [
|
||||
primary,
|
||||
left,
|
||||
])
|
||||
).toEqual({ x: -1800, y: -100, width: 1200, height: 800 });
|
||||
});
|
||||
it('clamps dimensions and titlebar to a smaller scaled work area', () => {
|
||||
expect(
|
||||
applyDesktopWindowState({ x: -300, y: -200, width: 4000, height: 4000 }, undefined, [
|
||||
{ x: 0, y: 24, width: 1024, height: 700 },
|
||||
])
|
||||
).toEqual({ x: 0, y: 24, width: 1024, height: 700 });
|
||||
});
|
||||
it('recovers malformed coordinates on a single screen', () => {
|
||||
expect(
|
||||
applyDesktopWindowState(
|
||||
{ x: Number.NaN, y: Number.POSITIVE_INFINITY, width: 0, height: Number.NaN },
|
||||
undefined,
|
||||
[primary]
|
||||
)
|
||||
).toEqual({ x: 130, y: 25, width: 1180, height: 875 });
|
||||
});
|
||||
it('captures normal bounds independently of maximized bounds', () => {
|
||||
const window = {
|
||||
getNormalBounds: vi.fn(() => ({ x: 30, y: 40, width: 1200, height: 800 })),
|
||||
getBounds: vi.fn(() => primary),
|
||||
isMaximized: () => true,
|
||||
};
|
||||
expect(captureDesktopWindowState(window as never)).toEqual({
|
||||
x: 30,
|
||||
y: 40,
|
||||
width: 1200,
|
||||
height: 800,
|
||||
maximized: true,
|
||||
});
|
||||
expect(window.getBounds).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,13 @@
|
|||
import { app, BrowserWindow, clipboard, ipcMain, Notification, safeStorage, shell } from 'electron';
|
||||
import {
|
||||
app,
|
||||
BrowserWindow,
|
||||
clipboard,
|
||||
ipcMain,
|
||||
Notification,
|
||||
safeStorage,
|
||||
shell,
|
||||
screen,
|
||||
} from 'electron';
|
||||
import path from 'node:path';
|
||||
import { mkdirSync } from 'node:fs';
|
||||
import { createRequire } from 'node:module';
|
||||
|
|
@ -97,12 +106,17 @@ if (!app.requestSingleInstanceLock()) {
|
|||
|
||||
function createMainWindow(savedState: DesktopWindowState): BrowserWindow {
|
||||
const preloadPath = path.join(__dirname, '../preload/index.cjs');
|
||||
const windowBounds = applyDesktopWindowState(savedState);
|
||||
const primary = screen.getPrimaryDisplay();
|
||||
const workAreas = [
|
||||
primary,
|
||||
...screen.getAllDisplays().filter((display) => display.id !== primary.id),
|
||||
].map((display) => display.workArea);
|
||||
const windowBounds = applyDesktopWindowState(savedState, undefined, workAreas);
|
||||
|
||||
const window = new BrowserWindow({
|
||||
title: DESKTOP_APP_NAME,
|
||||
minWidth: DESKTOP_MIN_WINDOW.width,
|
||||
minHeight: DESKTOP_MIN_WINDOW.height,
|
||||
minWidth: Math.min(DESKTOP_MIN_WINDOW.width, windowBounds.width),
|
||||
minHeight: Math.min(DESKTOP_MIN_WINDOW.height, windowBounds.height),
|
||||
...windowBounds,
|
||||
titleBarStyle: process.platform === 'darwin' ? 'hiddenInset' : 'default',
|
||||
trafficLightPosition: process.platform === 'darwin' ? { x: 16, y: 18 } : undefined,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|||
import path from 'node:path';
|
||||
|
||||
import type { DesktopPaths } from './types.js';
|
||||
import { DESKTOP_MIN_WINDOW } from './app-metadata.js';
|
||||
|
||||
export interface DesktopWindowState {
|
||||
width: number;
|
||||
|
|
@ -45,7 +46,7 @@ export function writeDesktopWindowStateSync(paths: DesktopPaths, state: DesktopW
|
|||
}
|
||||
|
||||
export function captureDesktopWindowState(window: BrowserWindow): DesktopWindowState {
|
||||
const bounds = window.getBounds();
|
||||
const bounds = window.getNormalBounds();
|
||||
return {
|
||||
...boundsToWindowState(bounds),
|
||||
maximized: window.isMaximized(),
|
||||
|
|
@ -54,15 +55,57 @@ export function captureDesktopWindowState(window: BrowserWindow): DesktopWindowS
|
|||
|
||||
export function applyDesktopWindowState(
|
||||
state: DesktopWindowState,
|
||||
fallback = DEFAULT_DESKTOP_WINDOW_STATE
|
||||
fallback = DEFAULT_DESKTOP_WINDOW_STATE,
|
||||
workAreas: readonly Rectangle[] = []
|
||||
): Required<Pick<DesktopWindowState, 'width' | 'height'>> & Pick<DesktopWindowState, 'x' | 'y'> {
|
||||
const sanitized = sanitizeWindowState(state);
|
||||
return {
|
||||
const bounds = {
|
||||
width: sanitized.width || fallback.width,
|
||||
height: sanitized.height || fallback.height,
|
||||
x: sanitized.x,
|
||||
y: sanitized.y,
|
||||
};
|
||||
const areas = workAreas.filter(
|
||||
(area) =>
|
||||
[area.x, area.y, area.width, area.height].every(Number.isFinite) &&
|
||||
area.width > 0 &&
|
||||
area.height > 0
|
||||
);
|
||||
if (!areas.length) return bounds;
|
||||
// Electron's bounds and display work areas both use device-independent pixels.
|
||||
// Keep the monitor containing the largest part of the saved window. With no
|
||||
// intersection (disconnected display), use the first, primary work area.
|
||||
let area = areas[0];
|
||||
let largest = 0;
|
||||
if (bounds.x !== undefined && bounds.y !== undefined) {
|
||||
for (const candidate of areas) {
|
||||
const overlap =
|
||||
Math.max(
|
||||
0,
|
||||
Math.min(bounds.x + bounds.width, candidate.x + candidate.width) -
|
||||
Math.max(bounds.x, candidate.x)
|
||||
) *
|
||||
Math.max(
|
||||
0,
|
||||
Math.min(bounds.y + bounds.height, candidate.y + candidate.height) -
|
||||
Math.max(bounds.y, candidate.y)
|
||||
);
|
||||
if (overlap > largest) {
|
||||
largest = overlap;
|
||||
area = candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
const width = Math.min(area.width, Math.max(DESKTOP_MIN_WINDOW.width, bounds.width));
|
||||
const height = Math.min(area.height, Math.max(DESKTOP_MIN_WINDOW.height, bounds.height));
|
||||
const x = largest > 0 && bounds.x !== undefined ? bounds.x : area.x + (area.width - width) / 2;
|
||||
const y = largest > 0 && bounds.y !== undefined ? bounds.y : area.y + (area.height - height) / 2;
|
||||
return {
|
||||
width,
|
||||
height,
|
||||
x: Math.round(Math.max(area.x, Math.min(x, area.x + area.width - width))),
|
||||
y: Math.round(Math.max(area.y, Math.min(y, area.y + area.height - height))),
|
||||
};
|
||||
}
|
||||
|
||||
function boundsToWindowState(bounds: Rectangle): DesktopWindowState {
|
||||
|
|
|
|||
|
|
@ -322,3 +322,7 @@ On macOS, Command-W closes the window while the managed server stays running.
|
|||
Reopen the window from the Dock. Command-Q quits and stops the managed server.
|
||||
Standard Hide, Hide Others, Show All and Services commands are available in the
|
||||
application menu; View provides text zoom and full screen.
|
||||
|
||||
Saved window bounds are fitted to the current display work areas at launch or
|
||||
reopen. If a monitor was disconnected, the window returns to the primary display.
|
||||
Maximized windows retain their previous normal bounds for unmaximizing.
|
||||
|
|
|
|||
|
|
@ -126,6 +126,15 @@ export async function verifyNativeWindowMenu(app, page) {
|
|||
app.evaluate(({ BrowserWindow }) => BrowserWindow.getAllWindows()[0].isFullScreen())
|
||||
)
|
||||
.toBe(false);
|
||||
const normalBounds = await app.evaluate(({ BrowserWindow }) => {
|
||||
const window = BrowserWindow.getAllWindows()[0];
|
||||
const bounds = window.getNormalBounds();
|
||||
window.maximize();
|
||||
return bounds;
|
||||
});
|
||||
await expect
|
||||
.poll(() => app.evaluate(({ BrowserWindow }) => BrowserWindow.getAllWindows()[0].isMaximized()))
|
||||
.toBe(true);
|
||||
const before = await page.evaluate(() => window.veritasDesktop.getConnectionStatus());
|
||||
const closed = page.waitForEvent('close');
|
||||
await clickRole('close');
|
||||
|
|
@ -141,5 +150,12 @@ export async function verifyNativeWindowMenu(app, page) {
|
|||
before.server.pid,
|
||||
'Closing the window restarted the managed server'
|
||||
);
|
||||
return { page: reopened, roles: items };
|
||||
await expect
|
||||
.poll(() => app.evaluate(({ BrowserWindow }) => BrowserWindow.getAllWindows()[0].isMaximized()))
|
||||
.toBe(true);
|
||||
await app.evaluate(({ BrowserWindow }) => BrowserWindow.getAllWindows()[0].unmaximize());
|
||||
await expect
|
||||
.poll(() => app.evaluate(({ BrowserWindow }) => BrowserWindow.getAllWindows()[0].getBounds()))
|
||||
.toEqual(normalBounds);
|
||||
return { page: reopened, roles: items, normalBounds };
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue