Skip to content

Commit 9296dd3

Browse files
committed
feat: restore window sizes
1 parent 472a553 commit 9296dd3

3 files changed

Lines changed: 229 additions & 6 deletions

File tree

src-electron/main-window-ipc.js

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ const path = require('path');
33
const { spawn } = require('child_process');
44
const { cleanupWindowTrust } = require('./main-cred-ipc');
55
const { isTrustedOrigin, updateTrustStatus, cleanupTrust, assertTrusted } = require('./ipc-security');
6+
const { DEFAULTS } = require('./window-state');
67

78
const PHOENIX_WINDOW_PREFIX = 'phcode-';
89
const PHOENIX_EXTENSION_WINDOW_PREFIX = 'extn-';
@@ -96,11 +97,29 @@ function registerWindowIpcHandlers() {
9697
webPreferences.preload = path.join(__dirname, 'preload.js');
9798
}
9899

100+
let windowConfig;
101+
if (isExtension) {
102+
// Extensions manage their own sizing
103+
windowConfig = {
104+
width: width || 800,
105+
height: height || 600,
106+
minWidth: minWidth,
107+
minHeight: minHeight
108+
};
109+
} else {
110+
// Phoenix windows: use defaults and ensure dimensions are at least the minimums
111+
const actualMinWidth = Math.max(minWidth || DEFAULTS.minWidth, DEFAULTS.minWidth);
112+
const actualMinHeight = Math.max(minHeight || DEFAULTS.minHeight, DEFAULTS.minHeight);
113+
windowConfig = {
114+
width: Math.max(width || DEFAULTS.width, actualMinWidth),
115+
height: Math.max(height || DEFAULTS.height, actualMinHeight),
116+
minWidth: actualMinWidth,
117+
minHeight: actualMinHeight
118+
};
119+
}
120+
99121
const win = new BrowserWindow({
100-
width: width || 1366,
101-
height: height || 900,
102-
minWidth: minWidth || 800,
103-
minHeight: minHeight || 600,
122+
...windowConfig,
104123
fullscreen: fullscreen || false,
105124
resizable: resizable !== false,
106125
title: windowTitle || label,

src-electron/main.js

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const { registerFsIpcHandlers, getAppDataDir } = require('./main-fs-ipc');
77
const { registerCredIpcHandlers } = require('./main-cred-ipc');
88
const { registerWindowIpcHandlers, registerWindow } = require('./main-window-ipc');
99
const { assertTrusted } = require('./ipc-security');
10+
const { getWindowOptions, trackWindowState, DEFAULTS } = require('./window-state');
1011

1112
// Request single instance lock - only one instance of the app should run at a time
1213
const gotTheLock = app.requestSingleInstanceLock();
@@ -21,9 +22,15 @@ if (!gotTheLock) {
2122
const sharedStorageMap = new Map();
2223

2324
async function createWindow() {
25+
// Get window options with restored state or defaults
26+
const windowOptions = getWindowOptions();
27+
const wasMaximized = windowOptions._wasMaximized;
28+
delete windowOptions._wasMaximized;
29+
2430
const win = new BrowserWindow({
25-
width: 1200,
26-
height: 800,
31+
...windowOptions,
32+
minWidth: DEFAULTS.minWidth,
33+
minHeight: DEFAULTS.minHeight,
2734
webPreferences: {
2835
preload: path.join(__dirname, 'preload.js'),
2936
contextIsolation: true,
@@ -32,6 +39,14 @@ async function createWindow() {
3239
icon: path.join(__dirname, '..', 'src-tauri', 'icons', 'icon.png')
3340
});
3441

42+
// Track window state for persistence
43+
trackWindowState(win);
44+
45+
// Restore maximized state after window is ready
46+
if (wasMaximized) {
47+
win.maximize();
48+
}
49+
3550
// Register main window with label 'main' (mirrors Tauri's window labeling)
3651
// Trust cleanup is handled by registerWindow's closed handler
3752
registerWindow(win, 'main');

src-electron/window-state.js

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
/**
2+
* Window State Manager
3+
*
4+
* Persists and restores window position, size, and maximized state.
5+
* Handles multi-monitor setups and gracefully handles disconnected monitors.
6+
*/
7+
8+
const { screen } = require('electron');
9+
const path = require('path');
10+
const fs = require('fs');
11+
const { getAppDataDir } = require('./main-fs-ipc');
12+
13+
const STATE_FILE = 'window-state.json';
14+
15+
// Default window dimensions
16+
const DEFAULTS = {
17+
width: 1366,
18+
height: 900,
19+
minWidth: 800,
20+
minHeight: 600
21+
};
22+
23+
/**
24+
* Get the path to the window state file.
25+
*/
26+
function getStateFilePath() {
27+
return path.join(getAppDataDir(), STATE_FILE);
28+
}
29+
30+
/**
31+
* Load saved window state from disk.
32+
* Returns null if no saved state or file is corrupted.
33+
*/
34+
function loadWindowState() {
35+
try {
36+
const filePath = getStateFilePath();
37+
if (!fs.existsSync(filePath)) {
38+
return null;
39+
}
40+
const data = fs.readFileSync(filePath, 'utf8');
41+
return JSON.parse(data);
42+
} catch (err) {
43+
console.warn('Failed to load window state:', err.message);
44+
return null;
45+
}
46+
}
47+
48+
/**
49+
* Save window state to disk.
50+
*/
51+
function saveWindowState(state) {
52+
try {
53+
const filePath = getStateFilePath();
54+
// Ensure directory exists
55+
const dir = path.dirname(filePath);
56+
if (!fs.existsSync(dir)) {
57+
fs.mkdirSync(dir, { recursive: true });
58+
}
59+
fs.writeFileSync(filePath, JSON.stringify(state, null, 2));
60+
} catch (err) {
61+
console.warn('Failed to save window state:', err.message);
62+
}
63+
}
64+
65+
/**
66+
* Check if a rectangle is visible on any display.
67+
* Returns true if at least a portion of the window would be visible.
68+
*/
69+
function isVisibleOnAnyDisplay(bounds) {
70+
const displays = screen.getAllDisplays();
71+
const minVisibleArea = 100; // At least 100px visible
72+
73+
for (const display of displays) {
74+
const { x, y, width, height } = display.workArea;
75+
76+
// Calculate overlap between window bounds and display work area
77+
const overlapX = Math.max(0, Math.min(bounds.x + bounds.width, x + width) - Math.max(bounds.x, x));
78+
const overlapY = Math.max(0, Math.min(bounds.y + bounds.height, y + height) - Math.max(bounds.y, y));
79+
const overlapArea = overlapX * overlapY;
80+
81+
if (overlapArea >= minVisibleArea) {
82+
return true;
83+
}
84+
}
85+
return false;
86+
}
87+
88+
/**
89+
* Get the display nearest to a point.
90+
*/
91+
function getNearestDisplay(x, y) {
92+
return screen.getDisplayNearestPoint({ x, y });
93+
}
94+
95+
/**
96+
* Get window options with restored state or defaults.
97+
* Validates saved position is on a visible display.
98+
*/
99+
function getWindowOptions() {
100+
const savedState = loadWindowState();
101+
102+
const options = {
103+
width: DEFAULTS.width,
104+
height: DEFAULTS.height,
105+
minWidth: DEFAULTS.minWidth,
106+
minHeight: DEFAULTS.minHeight
107+
};
108+
109+
if (savedState) {
110+
// Restore size (clamped to minimums)
111+
options.width = Math.max(savedState.width || DEFAULTS.width, DEFAULTS.minWidth);
112+
options.height = Math.max(savedState.height || DEFAULTS.height, DEFAULTS.minHeight);
113+
114+
// Check if saved position is visible on any current display
115+
if (savedState.x !== undefined && savedState.y !== undefined) {
116+
const bounds = {
117+
x: savedState.x,
118+
y: savedState.y,
119+
width: options.width,
120+
height: options.height
121+
};
122+
123+
if (isVisibleOnAnyDisplay(bounds)) {
124+
// Position is valid, use it
125+
options.x = savedState.x;
126+
options.y = savedState.y;
127+
} else {
128+
// Position is off-screen (monitor disconnected?), center on nearest display
129+
const nearestDisplay = getNearestDisplay(savedState.x, savedState.y);
130+
const { x, y, width, height } = nearestDisplay.workArea;
131+
options.x = x + Math.round((width - options.width) / 2);
132+
options.y = y + Math.round((height - options.height) / 2);
133+
console.log('Window position was off-screen, repositioned to nearest display');
134+
}
135+
}
136+
137+
// Track if we need to maximize after window is created
138+
options._wasMaximized = savedState.isMaximized || false;
139+
}
140+
141+
return options;
142+
}
143+
144+
/**
145+
* Track window state changes and save on close.
146+
* Call this after creating the BrowserWindow.
147+
*/
148+
function trackWindowState(win) {
149+
let windowState = {
150+
width: DEFAULTS.width,
151+
height: DEFAULTS.height,
152+
x: undefined,
153+
y: undefined,
154+
isMaximized: false
155+
};
156+
157+
// Update state from current window bounds
158+
function updateState() {
159+
if (!win.isMaximized() && !win.isMinimized() && !win.isFullScreen()) {
160+
const bounds = win.getBounds();
161+
windowState.width = bounds.width;
162+
windowState.height = bounds.height;
163+
windowState.x = bounds.x;
164+
windowState.y = bounds.y;
165+
}
166+
windowState.isMaximized = win.isMaximized();
167+
}
168+
169+
// Listen for state changes
170+
win.on('resize', updateState);
171+
win.on('move', updateState);
172+
win.on('maximize', updateState);
173+
win.on('unmaximize', updateState);
174+
175+
// Save state before window closes
176+
win.on('close', () => {
177+
updateState();
178+
saveWindowState(windowState);
179+
});
180+
181+
// Initialize state
182+
updateState();
183+
}
184+
185+
module.exports = {
186+
DEFAULTS,
187+
getWindowOptions,
188+
trackWindowState
189+
};

0 commit comments

Comments
 (0)