Skip to content

rfc: Save/Restore Window-State API #16

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 12 commits into
base: main
Choose a base branch
from
Open

Conversation

nilayarya
Copy link

@nilayarya nilayarya commented May 17, 2025

Note

This RFC is part of GSoC 2025

This proposal aims to implement a save/restore window state API for Electron by providing a simple but powerful configuration object windowStateRestoreOptions that handles complex edge cases automatically. This approach offers a declarative way to configure window persistence behavior while maintaining flexibility for different application needs. The object would be optional in the BaseWindowConstructorOptions.

Related: electron/electron#526

@nilayarya nilayarya requested a review from a team as a code owner May 17, 2025 00:26
@itsananderson itsananderson added the pending-review Waiting for reviewers to give feedback on the PR specifications label May 22, 2025
Copy link
Member

@itsananderson itsananderson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I typed up some comments while reviewing last week, but forgot to hit submit 🤦‍♂️

- A window can never be restored out of reach. I am presuming no apps would want this behavior. `fallbackBehaviour` will take effect if the window is entirely off screen. We are still providing the option to relaunch in a minimized state.
- If window (width, height) reopens on a different display and does not fit on screen auto adjust to fit and resave the value (even if allowOverflow=true). This would reduce the number of edge cases significantly and I highly doubt that any app would want to preserve an overflow when opened on a different display.
- Not handling scaling as Windows, macOS, and Linux support multimonitor window scaling by default.
- Not handling other `displayModes` such as split screen because it's innapropriate I believe. We can restore back to the same bounds in 'normal' mode. I've excluded 'normal' from the options for `displayMode` as it is the default.
Copy link
Member

@itsananderson itsananderson May 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would this also include things like "Window Snapping"?

Copy link
Author

@nilayarya nilayarya Jun 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Snapping a window does not make the window enter any special displayMode on windows and linux. We can handle it as 'normal' state only. I've updated the RFC to reflect the same.

On macOS the 'tile window to the left/right of the screen' makes it enter fullscreen mode officially. We can treat it as a fullscreen state. If you think that's inappropriate we can restore it in a normal state to where it was before the split screen. This is something that needs to be finalized.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@electron/api-wg


#### Developer-Facing Events

I’m also considering emitting events such as: `will-save-window-state`, `saved-window-state`, `will-restore-window-state`, `restored-window-state`. However, I’m unsure about their practical usefulness for developers and hope to finalize them through this RFC.
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@electron/api-wg any thoughts on these?

Copy link

@TheOneTheOnlyJJ TheOneTheOnlyJJ Jun 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@electron/api-wg any thoughts on these?

If these events allow for intercepting and overriding the state save, I can see them being used to redirect the new state to other places. The same applies to reading the window state, as it could be read or loaded from a different source.

For example, in my comment on electron/electron#47425 (comment) (4th paragraph), I am thinking of saving the users' window states in the cloud and attempting to read them from there on window initialization. This way, their preferred window position would persist even across different machines and instances of the application executable. If these events allow for that use case, they would be very welcome.

Edit: If this is not possible, maybe it could be done by providing a loader and saver handler to windowStateRestoreOptions but if this is not planned to be supported as of now, I suppose it would complicate the API and implementation a lot. Flexibility is always good, but it may not be worth pursuing this at this stage of this RFC.

Edit 2: The option in my first edit seems very cumbersome. Maybe the will-save-window-state and saved-window-state can pass the data to be saved as a parameter to their handler? And also allow for intercepting and cancelling the save in the default format. Then, maybe the will-restore-window-state could restore the window saved with custom data if the event handler returns such custom data, while also allowing for the cancellation of the default behaviour? I imagine I could integrate cloud persistence of the window state on a per-user basis if these features were available.

Copy link
Author

@nilayarya nilayarya Jun 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Edit: If this is not possible, maybe it could be done by providing a loader and saver handler to windowStateRestoreOptions but if this is not planned to be supported as of now, I suppose it would complicate the API and implementation a lot. Flexibility is always good, but it may not be worth pursuing this at this stage of this RFC.

The issue with this is that const win = new BrowserWindow() is synchronous. Custom loader functions could introduce async operations during window construction. Also, Custom loader/saver handlers would completely defeat the purpose of having a declarative API.

Edit 2: The option in my first edit seems very cumbersome. Maybe the will-save-window-state and saved-window-state can pass the data to be saved as a parameter to their handler? And also allow for intercepting and cancelling the save in the default format. Then, maybe the will-restore-window-state could restore the window saved with custom data if the event handler returns such custom data, while also allowing for the cancellation of the default behaviour? I imagine I could integrate cloud persistence of the window state on a per-user basis if these features were available.

On second thought there isn't much utility for these events beyond just telemetry. Adding these events with the ability to intercept and modify saved values, add custom restoration logic, use a custom format would unnecessarily increase the implementation complexity and maintainability. Performance implications of running custom logic for hundreds of events per second during window manipulation needs to be considered.

We can definitely reconsider this upon further discussion. Thanks for the suggestion.

Let me know if you can think of any use case that cannot be accomplished via app.setWindowState() app.getWindowState()

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For your cloud sync use case, a simpler approach might be for electron to provide an API like app.setWindowState([stateId], [stateObj]) that allows you to synchronously modify saved states before new BrowserWindow() initialization.
This would enable you to leverage electron's restoration logic for all the edge cases (display changes, bounds validation, etc.) rather than requiring you to reimplement that complexity yourself.

For saving to the cloud you could use app.getWindowState([stateId]) at your leisure.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that the API should be kept as simple as possible, while allowing versatility.

For your cloud sync use case, a simpler approach might be for electron to provide an API like app.setWindowState([stateId], [stateObj]) that allows you to synchronously modify saved states before new BrowserWindow() initialization.

Would app.setWindowState([stateId], [stateObj]) overwrite the locally saved window states for the specific stateIds provided? This would make it possible to support my use case, because I could pull the latest state from the cloud and update it locally upon each app launch.

For saving to the cloud you could use app.getWindowState([stateId]) at your leisure.

I would suggest having a separate BrowserWindow.getState() or BrowserWindow.getCurrentState() that avoids reading the window state from the disk and returns the BrowserWindow's current state computed directly from memory. This would avoid a file read if I understand correctly, right?

Furthermore, this API would make the events you suggest very useful for my cloud use case. It would enable getting the new window state via BrowserWindow.getState() and pushing it to the cloud only when will-save-window-state fires (provided that BrowserWindow.getState() would return the same state that will be saved when will-save-window-state fires).

Having will-save-window-state would allow for 1) intercepting the new state even if, for any reason, the file write fails and 2) avoid adding handlers to the resize and move events, which fire very often when the window is moving and need manual debouncing to avoid performance issues (I've faced this in my JS soultion, handling it using a setTimeout as in electron/electron#47425 (comment)).

While the events would mainly serve for telemetry purposes, I suppose other developers who want to do quirky things like my cloud use case will find them useful in other contexts. I'd suggest keeping them unless they add complexity that is a maintenance burden.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would suggest having a separate BrowserWindow.getState() or BrowserWindow.getCurrentState() that avoids reading the window state from the disk and returns the BrowserWindow's current state computed directly from memory. This would avoid a file read if I understand correctly, right?

Yes, it would avoid a file read. BrowserWindow.getState() or BrowserWindow.getCurrentState() are planned but not finalized yet.

While the events would mainly serve for telemetry purposes, I suppose other developers who want to do quirky things like my cloud use case will find them useful in other contexts. I'd suggest keeping them unless they add complexity that is a maintenance burden.

Implementation complexity and maintenance for these events should be fine. I've added them to the rfc.

- A window can never be restored out of reach. I am presuming no apps would want this behavior. `fallbackBehaviour` will take effect if the window is entirely off screen. We are still providing the option to relaunch in a minimized state.
- If window (width, height) reopens on a different display and does not fit on screen auto adjust to fit and resave the value (even if allowOverflow=true). This would reduce the number of edge cases significantly and I highly doubt that any app would want to preserve an overflow when opened on a different display.
- Not handling scaling as Windows, macOS, and Linux support multimonitor window scaling by default.
- Not handling other `displayModes` such as split screen because it's innapropriate I believe. We can restore back to the same bounds in 'normal' mode. I've excluded 'normal' from the options for `displayMode` as it is the default.
Copy link
Author

@nilayarya nilayarya Jun 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Snapping a window does not make the window enter any special displayMode on windows and linux. We can handle it as 'normal' state only. I've updated the RFC to reflect the same.

On macOS the 'tile window to the left/right of the screen' makes it enter fullscreen mode officially. We can treat it as a fullscreen state. If you think that's inappropriate we can restore it in a normal state to where it was before the split screen. This is something that needs to be finalized.

- A window can never be restored out of reach. I am presuming no apps would want this behavior. `fallbackBehaviour` will take effect if the window is entirely off screen. We are still providing the option to relaunch in a minimized state.
- If window (width, height) reopens on a different display and does not fit on screen auto adjust to fit and resave the value (even if allowOverflow=true). This would reduce the number of edge cases significantly and I highly doubt that any app would want to preserve an overflow when opened on a different display.
- Not handling scaling as Windows, macOS, and Linux support multimonitor window scaling by default.
- Not handling other `displayModes` such as split screen because it's innapropriate I believe. We can restore back to the same bounds in 'normal' mode. I've excluded 'normal' from the options for `displayMode` as it is the default.
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@electron/api-wg

@TheOneTheOnlyJJ
Copy link

TheOneTheOnlyJJ commented Jun 17, 2025

I'm not trying to be disruptive here, but after giving my proposed cloud sync use case some thought, I would like to put forward a few ideas, based on my opinions of how such a use case (and other potential non-standard use cases) could be supported most cleanly.

Would it be possible for BrowserWindow to accept a full window state object directly, in addition to the proposed stateId? This would let developers load state from custom sources (like the cloud) before constructing the window, while still allowing Electron to run its position restoration logic.

Alternatively, the window state loading could be fully decoupled into a dedicated API (app.getWindowState([stateId])), allowing developers to implement custom loaders. The constructor would then accept only a window state object, which would be validated and corrected as usual during the window creation process. To skip local persistence in these cases, developers could pass stateId: null (or undefined for easily having the default BrowserWindow constructor not persist the state).

To complete this flow, two instance-level events could be added.
A new state-adjusted event could be emitted when the initial window state object passed to the BrowserWindow constructor is modified by Electron’s internal restoration logic (to avoid overflow or off-screen placement).
Separately, a new state-updated event could be emitted whenever the window state changes due to user or programmatic actions (such as moving or resizing) and would normally be persisted. If stateId is set to null (not persisting the state on disk with the default API proposed as of now), this event would allow developers to handle custom persistence by calling BrowserWindow.getState() and syncing the result to the cloud.

EDIT: This new state-updated event I proposed is functionally equivalent to will-save-window-state, provided it fires even when the window state should not be persisted for that specific window (stateId: null or undefined).

What I try to achieve with these proposals is the ability to pass the window state to a new window and to get it for external saving, when it is updated, by completely bypassing the local JSON file persistence functionality. The current way the API is proposed would not allow for that and would require additional code to clean up window states periodically from disk.

I'm eager to hear your feedback and opinions.

@nilayarya
Copy link
Author

I'm not trying to be disruptive here, but after giving my proposed cloud sync use case some thought, I would like to put forward a few ideas, based on my opinions of how such a use case (and other potential non-standard use cases) could be supported most cleanly.

Would it be possible for BrowserWindow to accept a full window state object directly, in addition to the proposed stateId? This would let developers load state from custom sources (like the cloud) before constructing the window, while still allowing Electron to run its position restoration logic.

This can be considered as an alternative to app.setWindowState([stateObj]).

Alternatively, the window state loading could be fully decoupled into a dedicated API (app.getWindowState([stateId])), allowing developers to implement custom loaders. The constructor would then accept only a window state object, which would be validated and corrected as usual during window creation. To skip local persistence in these cases, developers could simply pass stateId: null.

To skip local persistence developers can skip passing the windowStateRestoreOptions object entirely.

To complete this flow, two instance-level events could be added. A new state-adjusted event could be emitted when the initial window state object passed to the BrowserWindow constructor is modified by Electron’s internal restoration logic (to avoid overflow or off-screen placement). Separately, a new state-updated event could be emitted whenever the window state changes due to user or programmatic actions (such as moving or resizing) and would normally be persisted. If stateId is set to null (not persisting the state on disk with the default API proposed as of now), this event would allow developers to handle custom persistence by calling BrowserWindow.getState() and syncing the result to the cloud.

This can be moved to future possibilities.

@TheOneTheOnlyJJ
Copy link

This can be considered as an alternative to app.setWindowState([stateObj]).

But I suppose app.setWindowState([stateObj]), as it is right now, is doing a write to disk of the passed state object.
If this is the case, passing the window state object to the window as an argument directly is different because it would not do a file write.

To skip local persistence developers can skip passing the windowStateRestoreOptions object entirely.

While this is true, setting the window position manually with state from an external source would bypass the proposed repositioning algorithm that avoids overflow and excessive clipping.
Could this window state correction algorithm be exposed with its own API, something like app.adjustWindowState([windowState])?
If this were exposed, it would allow getting the state object from an external source and properly adjusting it to avoid all the edge cases mentioned in the discussions until now.
It may be a little cumbersome to map the state object data to the different window APIs to set the size, position, maximized/minimized/fullscreen/kiosk mode, but that is not a real problem.

This can be moved to future possibilities.

Yes, but is will-save-window-state intended to be fired when state persistence is disabled?
The name of the event does not suggest that. this is why I suggested the new event to be named as state-updated, because it does not imply that it will be persisted.

If state persistence is disabled (by not passing the windowStateRestoreOptions object) and this event does not fire, then I cannot know when new state should be handled.

If there were a way to add a callback to the moment the window would normally persist the state (even when it does not do it), it could be intercepted by a user function and processed further (i.e., sent to the cloud).

@TheOneTheOnlyJJ
Copy link

I just noticed that will-save-window-state can be prevented with e.preventDefault(), per your latest commit. This is amazing, it's exactly the interception and redirection hook I've been looking for.

All that is left is the ability to have the state passed to/used by the window be read from any source rather than the local disk, thus my initial suggestion to allow passing the state directly to the window, avoiding app.setWindowState([stateObj]).

I feel like I'm disrupting your work with my specific, non-standard use case, and I don't want to impede the development of this feature.

As such, I will sum up my final ideas as concisely as possible here:

  • The ability to have the preventable will-save-window-state (maybe named state-updated?) be emitted even when not passing the windowStateRestoreOptions object to the window. This is the equivalent of intercepting the disk write and redirecting the state to an external destination. This would avoid listening for the move and resize events, which fire very often and cause real performance issues if not handled properly with a setTimeout. Maybe state-updated could be fired always, and will-save-window-state could fire specifically when the window was passed windowStateRestoreOptions in its constructor.
  • The ability to invoke the position/window state adjustment algorithm (that avoids overflow and excessive clipping) on arbitrary window state objects (such as those read/fetched from external sources). This would make it possible to avoid passing windowStateRestoreOptions object to the window while still enjoying the benefits of edge case correction. This API could be named app.adjustWindowState([windowState]) or something similar.

With just these 2 features, non-standard use cases such as mine could be supported while avoiding file reads and writes completely. For example, my cloud sync use case could fetch the window state from the cloud, adjust it using app.adjustWindowState([windowState]), then map it to the desired BrowserWindow instance after its initialisation, using the current APIs for setting the window size and position (could be further enhanced by having the BrowserWindow accept an initialWindowState prop that does not imply persistence), and have state updates pushed to the cloud by adding a hook to the state-updated event. This would completely avoid writing or reading the window state to/from disk, maintaining the best performance possible.

I hope you don't mind my comments, I'm just trying to help make this feature as flexible as possible, the best I can.


I'm thinking we could make these events simple telemetry events. Simple telemetry events would provide observability without the performance cost of running the custom logic for hundreds of events per second during window manipulation. Adding these events with the ability to intercept and modify values that are going to be saved, ability to intercept with custom restoration logic, would increase the implementation complexity and maintainability.

With the help of APIs `app.setWindowState([stateObj])` and `app.getWindowState([stateId])` (these could be made static as well) we can modify and manipulate saved states synchronously to accomplish our goals.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: We don't have an App class here to attach static functions on.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I meant static over BaseWindow. So BaseWindow.getWindowState([stateId]) BaseWindow.setWindowState([stateObj]).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moving these APIs to future possibilities as it adds complications to the restoration process as discussed yesterday during the weekly gsoc meeting.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moving these APIs to future possibilities as it adds complications to the restoration process as discussed yesterday during the weekly gsoc meeting.

Are there public notes or details of these meetings?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, we don't post public transcripts/minutes from our Summer of Code meetings. 😅

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
pending-review Waiting for reviewers to give feedback on the PR specifications
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants