Repository: electron/electron
Pull request: fix: desktopCapturer breaks BrowserWindow resizable on macOS

#### Description of Change

Closes https://github.com/electron/electron/issues/43002.

Fixes an issue where calling `desktopCapturer.getSources` on macOS running 14.4 or later would make non-resizable windows resizable. This was happening due to a ScreenCaptureKit bug that modified a window's styleMask the first time it captured a non-resizable window. Fix this by recording each window's resizable status and potentially restoring it after the capture is complete.

#### Checklist
<!-- Remove items that do not apply. For completed items, change [ ] to [x]. -->

- [x] PR description included and stakeholders cc'd
- [x] `npm test` passes
- [x] tests are [changed or added](https://github.com/electron/electron/blob/main/docs/development/testing.md)
- [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: Fixed an issue where calling `desktopCapturer.getSources` on macOS running 14.4 or later would make non-resizable windows resizable. 

Changed files:
filename: lib/browser/api/desktop-capturer.ts
patch: @@ -1,3 +1,4 @@
+import { BrowserWindow } from 'electron/main';
 const { createDesktopCapturer } = process._linkedBinding('electron_browser_desktop_capturer');
 
 const deepEqual = (a: ElectronInternal.GetSourcesOptions, b: ElectronInternal.GetSourcesOptions) => JSON.stringify(a) === JSON.stringify(b);
@@ -15,6 +16,16 @@ function isValid (options: Electron.SourcesOptions) {
 export async function getSources (args: Electron.SourcesOptions) {
   if (!isValid(args)) throw new Error('Invalid options');
 
+  const resizableValues = new Map();
+  if (process.platform === 'darwin') {
+    // Fix for bug in ScreenCaptureKit that modifies a window's styleMask the first time
+    // it captures a non-resizable window. We record each non-resizable window's styleMask,
+    // and we restore modified styleMasks later, after the screen capture.
+    for (const win of BrowserWindow.getAllWindows()) {
+      resizableValues.set([win.id], win.resizable);
+    }
+  }
+
   const captureWindow = args.types.includes('window');
   const captureScreen = args.types.includes('screen');
 
@@ -44,6 +55,14 @@ export async function getSources (args: Electron.SourcesOptions) {
         delete capturer._onerror;
         delete capturer._onfinished;
         capturer = null;
+
+        if (process.platform === 'darwin') {
+          for (const win of BrowserWindow.getAllWindows()) {
+            if (resizableValues.has(win.id)) {
+              win.resizable = resizableValues.get(win.id);
+            }
+          };
+        }
       }
       // Remove from currentlyRunning once we resolve or reject
       currentlyRunning = currentlyRunning.filter(running => running.options !== options);

filename: shell/browser/feature_list_mac.mm
patch: @@ -22,7 +22,7 @@
     // kThumbnailCapturerMac,
     // chrome/browser/media/webrtc/thumbnail_capturer_mac.mm
 #if DCHECK_IS_ON()
-    return "";
+    return "ScreenCaptureKitPickerScreen,ScreenCaptureKitStreamPickerSonoma";
 #else
     return "ScreenCaptureKitPickerScreen,ScreenCaptureKitStreamPickerSonoma,"
            "ThumbnailCapturerMac:capture_mode/sc_screenshot_manager";

filename: spec/api-desktop-capturer-spec.ts
patch: @@ -148,6 +148,20 @@ ifdescribe(!process.arch.includes('arm') && process.platform !== 'win32')('deskt
     }
   });
 
+  // Regression test - see https://github.com/electron/electron/issues/43002
+  it('does not affect window resizable state', async () => {
+    w.resizable = false;
+
+    const wShown = once(w, 'show');
+    w.show();
+    await wShown;
+
+    const sources = await desktopCapturer.getSources({ types: ['window', 'screen'] });
+    expect(sources).to.be.an('array').that.is.not.empty();
+
+    expect(w.resizable).to.be.false();
+  });
+
   it('moveAbove should move the window at the requested place', async () => {
     // DesktopCapturer.getSources() is guaranteed to return in the correct
     // z-order from foreground to background.