Repository: electron/electron
Pull request: fix: handle failing to enter fullscreen on macOS

#### Description of Change

<!--
Thank you for your Pull Request. Please provide a description above and review
the requirements below.

Contributors guide: https://github.com/electron/electron/blob/main/CONTRIBUTING.md
-->

On macOS, entering or exiting fullscreen mode can fail even after `windowWillEnterFullScreen` or `windowWillExitFullScreen` has run (see the documentation for [`windowDidFailToEnterFullScreen`](https://developer.apple.com/documentation/appkit/nswindowdelegate/1419591-windowdidfailtoenterfullscreen?language=objc) and [`windowDidFailToExitFullScreen`](https://developer.apple.com/documentation/appkit/nswindowdelegate/1419573-windowdidfailtoexitfullscreen?language=objc)). If this happens, it is necessary to properly restore the window's original state, but Electron currently does not do that. The most prominent bug caused by this is that if the window fails to enter fullscreen, `fullscreen_transition_state_` is "stuck" at `FullScreenTransitionState::kEntering`, and `app.quit()` does not work properly because the window refuses to close.

I don't know if it's possible to trigger these failures with automation, but `windowDidFailToEnterFullScreen` can be triggered manually on a Macbook without much effort:

1. Ensure the system has at least 2 virtual desktops (swipe up with 4 fingers and click the plus icon on the upper right).
2. Run the below snippet.
3. Before the first 3-second timer fires, press 3 or 4 fingers on the touchpad and move them slightly to one side (as if to swipe to another desktop). Keep the fingers held down, so that the UI remains in the "halfway" state between the two desktops without being fully in either one or the other.
4. Entering fullscreen is apparently not allowed when swiping between desktops like this, so `windowWillEnterFullScreen` runs followed by `windowDidFailToEnterFullScreen`. Without this change, the app will refuse to quit when the `app.quit()` call eventually runs. With this change, it quits normally.

```node
const { app, BrowserWindow } = require('electron')

app.whenReady().then(() => {
  const mainWindow = new BrowserWindow({
    width: 800,
    height: 600,
    // Uncomment to test custom title bars, which require some special handling
    // titleBarStyle: "hidden",
    // trafficLightPosition: { x: 30, y: 30 },
  })

  mainWindow.loadFile("/dev/null");

  setTimeout(() => {
    mainWindow.setFullScreen(true)
    setTimeout(() => {
      mainWindow.setFullScreen(false)
      setTimeout(() => {
        app.quit()
      }, 3000)
    }, 3000)
  }, 3000)
})
```

Exiting fullscreen seems to behave oddly - when doing the same "swipe halfway into the other desktop" test with a window that is exiting fullscreen, `windowDidFailToExitFullScreen` does fire, but `windowDidExitFullScreen` also fires, and the window properly exits fullscreen once the user fully switches to a single desktop. To be safe, I tried to write this change such that it will work if either just `windowDidFailToExitFullScreen` runs _or_ if both run.

#### Checklist
<!-- Remove items that do not apply. For completed items, change [ ] to [x]. -->

- [x] PR description included and stakeholders cc'd
- [x] [PR release notes](https://github.com/electron/clerk/blob/main/README.md) describe the change in a way relevant to app developers, and are [capitalized, punctuated, and past tense](https://github.com/electron/clerk/blob/main/README.md#examples).

#### Release Notes

Notes: Fix behavior when entering/exiting fullscreen fails on macOS.

Changed files:
filename: shell/browser/native_window_mac.h
patch: @@ -167,6 +167,7 @@ class NativeWindowMac : public NativeWindow,
   void DetachChildren() override;
 
   void NotifyWindowWillEnterFullScreen();
+  void NotifyWindowDidFailToEnterFullScreen();
   void NotifyWindowWillLeaveFullScreen();
 
   // Cleanup observers when window is getting closed. Note that the destructor

filename: shell/browser/native_window_mac.mm
patch: @@ -1662,6 +1662,13 @@ void ReorderChildWindowAbove(NSWindow* child_window, NSWindow* other_window) {
   UpdateVibrancyRadii(true);
 }
 
+void NativeWindowMac::NotifyWindowDidFailToEnterFullScreen() {
+  UpdateVibrancyRadii(false);
+
+  if (buttons_proxy_)
+    [buttons_proxy_ redraw];
+}
+
 void NativeWindowMac::NotifyWindowWillLeaveFullScreen() {
   if (buttons_proxy_) {
     // Hide window title when leaving fullscreen.

filename: shell/browser/ui/cocoa/electron_ns_window_delegate.mm
patch: @@ -311,6 +311,18 @@ - (void)windowDidEnterFullScreen:(NSNotification*)notification {
   shell_->HandlePendingFullscreenTransitions();
 }
 
+- (void)windowDidFailToEnterFullScreen:(NSWindow*)window {
+  shell_->set_fullscreen_transition_state(FullScreenTransitionState::kNone);
+
+  shell_->SetResizable(is_resizable_);
+  shell_->NotifyWindowDidFailToEnterFullScreen();
+
+  if (shell_->HandleDeferredClose())
+    return;
+
+  shell_->HandlePendingFullscreenTransitions();
+}
+
 - (void)windowWillExitFullScreen:(NSNotification*)notification {
   shell_->set_fullscreen_transition_state(FullScreenTransitionState::kExiting);