feat: save window state by nilayarya · Pull Request #47425 · electron/electron · GitHub | Latest TMZ Celebrity News & Gossip | Watch TMZ Live
Skip to content

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

Open
wants to merge 35 commits into
base: gsoc-2025
Choose a base branch
from

Conversation

nilayarya
Copy link
Contributor

@nilayarya nilayarya commented Jun 9, 2025

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 to BaseWindowConstructorOptions with a single param stateId. 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

Release Notes

Notes: none

@electron-cation electron-cation bot added the new-pr 🌱 PR opened recently label Jun 9, 2025
@nilayarya nilayarya changed the title Save window state feat: save window state Jun 9, 2025
@dsanders11 dsanders11 added GSoC 2025 Google Summer of Code 2025 no-backport backport-check-skip Skip trop's backport validity checking labels Jun 11, 2025
@electron-cation electron-cation bot removed the new-pr 🌱 PR opened recently label Jun 11, 2025
@dsanders11 dsanders11 added the semver/minor backwards-compatible functionality label Jun 11, 2025
Copy link
Member

@ckerr ckerr left a 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?

@nilayarya
Copy link
Contributor Author

nilayarya commented Jun 12, 2025

  • The new API needs to be documented in docs/

Yes 🫡. I'll add to this pr.

  • Does there need to be a way to delete a saved window state?

I’ve proposed an API for app.clearWindowState([stateId]). Additionally, adding a command-line switch could offer developers a convenient way to clear prior states—though that detail isn’t finalized yet.*

  • 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?

Tests for this pr are wip 🚧

@ckerr

@TheOneTheOnlyJJ
Copy link

TheOneTheOnlyJJ commented Jun 13, 2025

  • Does there need to be a way to delete a saved window state?

Yes, please. This would be very helpful to have.
Does this stateId represent the saved/persisted window state? Also, where is this state data saved?

I'm asking because I've implemented exactly this feature in my bachelor's thesis project, using a class that handles the window move and resize events by updating an in-memory object in the main process. When the app quits, the object is saved locally as JSON, along with other specific application settings unrelated to the window. This JSON is read every time the application starts, and the BrowserWindow is initialised with the read settings, or sane defaults if the settings could not be read for any reason.

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 main.

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 stateId that is the user's ID.

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.
But please provide a way to delete the window state data (if users delete their account in my app, I should delete their window state data as well).

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 onWindowPositionChange handler as you see fit:

TypeScript WindowPositionWatcher code
import { 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 function
import { 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 };
}

@nilayarya
Copy link
Contributor Author

@TheOneTheOnlyJJ Yes, it will cover your use case.

  • An API for win.clearState() and app.clearWindowState([stateId]) will be provided
  • The window state will be saved as a JSON object into the already existing Preferences folder inside app.getPath('userData')

You can check out the entire feature proposal here electron/rfcs#16

@nilayarya nilayarya requested a review from ckerr June 18, 2025 12:05
Co-authored-by: David Sanders <dsanders11@ucsbalum.com>
@nilayarya
Copy link
Contributor Author

Minor updates in fa3cc9b and d150554 — cc @dsanders11 @VerteDinde @georgexu99 @erickzhao

@@ -0,0 +1,3 @@
# WindowStateRestoreOptions Object
Copy link
Contributor

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

@nilayarya nilayarya requested a review from georgexu99 June 29, 2025 00:38
@@ -0,0 +1,3 @@
# WindowStateRestoreOptions Object

* `stateId` string - A unique identifier used for saving and restoring window state.
Copy link
Member

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.

Copy link
Member

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

Copy link
Contributor Author

@nilayarya nilayarya Jul 2, 2025

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.
Copy link
Member

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
backport-check-skip Skip trop's backport validity checking GSoC 2025 Google Summer of Code 2025 no-backport semver/minor backwards-compatible functionality
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants

TMZ Celebrity News – Breaking Stories, Videos & Gossip

Looking for the latest TMZ celebrity news? You've come to the right place. From shocking Hollywood scandals to exclusive videos, TMZ delivers it all in real time.

Whether it’s a red carpet slip-up, a viral paparazzi moment, or a legal drama involving your favorite stars, TMZ news is always first to break the story. Stay in the loop with daily updates, insider tips, and jaw-dropping photos.

🎥 Watch TMZ Live

TMZ Live brings you daily celebrity news and interviews straight from the TMZ newsroom. Don’t miss a beat—watch now and see what’s trending in Hollywood.