-
Notifications
You must be signed in to change notification settings - Fork 16.2k
feat: save window state #47425
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: gsoc-2025
Are you sure you want to change the base?
feat: save window state #47425
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- C++ suggestions inline, mostly LGTM
- The new API needs to be documented in
docs/
- @dsanders11 does the new API need API WG review?
- Does there need to be a way to delete a saved window state?
- This should also have new tests in
spec/
to test this new code; however, I'm not sure how much is testable until the restoration logic lands?
Co-authored-by: Charles Kerr <charles@charleskerr.com>
Yes 🫡. I'll add to this pr.
I’ve proposed an API for
Tests for this pr are wip 🚧 |
Yes, please. This would be very helpful to have. I'm asking because I've implemented exactly this feature in my bachelor's thesis project, using a class that handles the window I'm still developing this project in my free time, and this PR would improve the runtime performance of the window position/state updates, if all this monitoring can be done natively by C++ instead of my JS class in So, I'm asking about the location of the saved data because I'm thinking of separating my application into 2 windows, a sign-in/up window and a separate window where the actual user content is worked with. This way, besides better security and data isolation, each user's window could be explicitly initialised with the positions it was last closed with. I'm interested in whether the API proposed by this PR could fit this use case. I could give each window a Either way, I'm very much looking for this to become a native feature. I will find a way to integrate it one way or another. Although it is not of any relevance to this PR, here is the TS code for the window watcher, in case someone wants to have this capability before this lands or on older versions. You should implement and provide the TypeScript WindowPositionWatcher codeimport { LogFunctions } from "electron-log";
import { BrowserWindow, Rectangle } from "electron/main";
export const WINDOW_STATES = {
FullScreen: "fullscreen",
Maximized: "maximized",
Minimized: "minimized"
} as const;
export type WindowStates = typeof WINDOW_STATES;
export type WindowState = WindowStates[keyof WindowStates];
export type WindowPosition = Rectangle | WindowState;
export class WindowPositionWatcher {
private readonly logger: LogFunctions;
private updateWindowPositionTimeout: null | NodeJS.Timeout;
private readonly UPDATE_WINDOW_POSITION_TIMEOUT_DELAY_MS: number;
public constructor(logger: LogFunctions, timeoutDelayMs = 500) {
this.logger = logger;
this.logger.debug(`Initialising new Window Position Watcher with timeout delay: ${timeoutDelayMs.toString()} ms.`);
this.updateWindowPositionTimeout = null;
this.UPDATE_WINDOW_POSITION_TIMEOUT_DELAY_MS = timeoutDelayMs;
}
public watchWindowPosition(window: BrowserWindow, onWindowPositionChange: (position: WindowPosition) => void): void {
this.logger.debug("Starting watching window.");
window.on("move", (): void => {
this.onWindowBoundsChanged(window, onWindowPositionChange);
});
window.on("resize", (): void => {
this.onWindowBoundsChanged(window, onWindowPositionChange);
});
}
private onWindowBoundsChanged(window: BrowserWindow, onWindowPositionChange: (position: WindowPosition) => void): void {
// Move and resize events fire very often while the window is moving
// Debounce window position updates
if (this.updateWindowPositionTimeout !== null) {
clearTimeout(this.updateWindowPositionTimeout);
}
this.updateWindowPositionTimeout = setTimeout((): void => {
const NEW_WINDOW_POSITION: WindowPosition = this.getNewWindowPosition(window);
this.logger.silly(
`New window position: ${typeof NEW_WINDOW_POSITION === "string" ? `"${NEW_WINDOW_POSITION}"` : JSON.stringify(NEW_WINDOW_POSITION, null, 2)}.`
);
onWindowPositionChange(NEW_WINDOW_POSITION);
this.updateWindowPositionTimeout = null;
}, this.UPDATE_WINDOW_POSITION_TIMEOUT_DELAY_MS);
}
private getNewWindowPosition(window: BrowserWindow): WindowPosition {
this.logger.debug("Getting new window position.");
let newWindowPosition: WindowPosition;
if (window.isFullScreen()) {
newWindowPosition = WINDOW_STATES.FullScreen;
} else if (window.isMaximized()) {
newWindowPosition = WINDOW_STATES.Maximized;
} else if (window.isMinimized()) {
newWindowPosition = WINDOW_STATES.Minimized;
} else {
newWindowPosition = window.getBounds();
}
return newWindowPosition;
}
} Additionally, this is what I use to handle the edge cases. It is not perfect, but it handles window overflow and avoids reducing the application's overall area as much as possible. TypeScript adjustWindowBounds functionimport { LogFunctions } from "electron-log";
import { Rectangle } from "electron/main";
export function adjustWindowBounds(screenBounds: Rectangle, windowBounds: Rectangle, logger: LogFunctions): Rectangle {
let newWidth: number;
let newHeight: number;
let newX: number;
let newY: number;
// Ensure window is not wider than the screen
if (windowBounds.width > screenBounds.width) {
logger.silly("Window width greater than screen width. Setting to screen width.");
newWidth = screenBounds.width;
} else {
logger.silly("Window width smaller than screen width. No change.");
newWidth = windowBounds.width;
}
// Ensure window is not taller than the screen
if (windowBounds.height > screenBounds.height) {
logger.silly("Window height greater than screen height. Setting to screen height.");
newHeight = screenBounds.height;
} else {
logger.silly("Window height smaller than screen height. No change.");
newHeight = windowBounds.height;
}
// Ensure no leftwards overflow
if (windowBounds.x < screenBounds.x) {
logger.silly("Left window border extends beyond left screen edge. Setting to left screen edge.");
newX = screenBounds.x;
} else {
logger.silly("Left window border inside screen area. No change.");
newX = windowBounds.x;
}
// Ensure no rightwards overflow
if (windowBounds.x + windowBounds.width > screenBounds.x + screenBounds.width) {
logger.silly("Right window border extends beyond right screen edge. Setting to right screen edge.");
newX = screenBounds.x + screenBounds.width - newWidth;
} else {
logger.silly("Right window border inside screen area. No change.");
}
// Ensure no upwards overflow
if (windowBounds.y < screenBounds.y) {
logger.silly("Top window border extends beyond top screen edge. Setting to top screen edge.");
newY = screenBounds.y;
} else {
logger.silly("Top window border inside screen area. No change.");
newY = windowBounds.y;
}
// Ensure no downwards overflow
if (windowBounds.y + windowBounds.height > screenBounds.y + screenBounds.height) {
logger.silly("Bottom window border extends beyond bottom screen edge. Setting to bottom screen edge.");
newY = screenBounds.y + screenBounds.height - newHeight;
} else {
logger.silly("Bottom window border inside screen area. No change.");
}
// Final adjustment to ensure dimensions are correctly set within the screen bounds
// This accounts for potential rounding issues
newWidth = Math.min(newWidth, screenBounds.width);
newHeight = Math.min(newHeight, screenBounds.height);
return { x: newX, y: newY, width: newWidth, height: newHeight };
} |
@TheOneTheOnlyJJ Yes, it will cover your use case.
You can check out the entire feature proposal here electron/rfcs#16 |
Co-authored-by: David Sanders <dsanders11@ucsbalum.com>
Minor updates in fa3cc9b and d150554 — cc @dsanders11 @VerteDinde @georgexu99 @erickzhao |
@@ -0,0 +1,3 @@ | |||
# WindowStateRestoreOptions Object |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we add a blurb explaining when/how window states are saved (ie. on move/resize) and provide users with more context
@@ -0,0 +1,3 @@ | |||
# WindowStateRestoreOptions Object | |||
|
|||
* `stateId` string - A unique identifier used for saving and restoring window state. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we make this something more friendly, don't change immediately this is meant to start a discussion thread here.
I'd be in favour of just a generic BaseWindow
constructor option for a window "name" which we can document as a unique identifier that apps can use to identify windows, and Electron can use internally for things like this state persistance.
Then change this options object to be just enabled: true/false
.
Apologies if this option has already been discussed or bikeshed elsewhere.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@erickzhao interested in your opinions here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd be in favour of just a generic
BaseWindow
constructor option for a window "name" which we can document as a unique identifier that apps can use to identify windows, and Electron can use internally for things like this state persistance.
Makes sense! Moving "name" out helps future-proof things.
Then change this options object to be just
enabled: true/false
.
We could have it take either boolean/object to let this behavior be configurable.
@@ -0,0 +1,3 @@ | |||
# WindowStateRestoreOptions Object | |||
|
|||
* `stateId` string - A unique identifier used for saving and restoring window state. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@erickzhao interested in your opinions here
Description of Change
Note
This PR is part of GSoC 2025 project. It only includes logic to save window state and does not include the restoration logic — that will be implemented in a separate PR.
Added a constructor option
windowStateRestoreOptions
toBaseWindowConstructorOptions
with a single paramstateId
. It saves window bounds continuously on resize, move, and close events. Does not restore.Variables saved internally are the same as https://source.chromium.org/chromium/chromium/src/+/main:chrome/browser/ui/views/chrome_views_delegate.cc;drc=1f9f3d7d4227fc2021915926b0e9f934cd610201;l=99
Checklist
npm test
passesRelease Notes
Notes: none