fix(desktop): leave full screen before hiding on macOS close (#3284)

This commit is contained in:
Hampus authored and GitHub committed 2026-10-08 17:28:58 +02:00
1 parent 46a356cc40
commit e7859fefb1
3 files changed
+158 -2

No files matched your search

@@ -0,0 +1,117 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import assert from 'node:assert/strict';
import {EventEmitter} from 'node:events';
import {readFileSync} from 'node:fs';
import {createRequire} from 'node:module';
import {describe, test} from 'node:test';
import {fileURLToPath} from 'node:url';
import vm from 'node:vm';
const require = createRequire(import.meta.url);
const esbuild = require('esbuild');
const sourcePath = fileURLToPath(new URL('./MacFullScreenHide.ts', import.meta.url));
const transformedSource = esbuild.transformSync(readFileSync(sourcePath, 'utf8'), {
loader: 'ts',
format: 'cjs',
platform: 'node',
target: 'node20',
}).code;
function loadMacFullScreenHide() {
const module = {exports: {}};
const context = vm.createContext({module, exports: module.exports, process: {platform: 'darwin'}});
vm.runInContext(transformedSource, context, {filename: sourcePath});
return module.exports;
}
class FakeWindow extends EventEmitter {
fullScreen;
visible = true;
destroyed = false;
calls = [];
constructor(fullScreen) {
super();
this.fullScreen = fullScreen;
}
isDestroyed() {
return this.destroyed;
}
isFullScreen() {
return this.fullScreen;
}
setFullScreen(flag) {
this.calls.push(`setFullScreen(${flag})`);
}
hide() {
this.calls.push('hide');
this.visible = false;
}
finishLeavingFullScreen() {
this.fullScreen = false;
this.emit('leave-full-screen');
}
}
describe('MacFullScreenHide', () => {
test('hides a windowed window at once', () => {
const {hideWindowLeavingFullScreen} = loadMacFullScreenHide();
const window = new FakeWindow(false);
hideWindowLeavingFullScreen(window, 'darwin');
assert.deepEqual(window.calls, ['hide']);
});
test('leaves full screen on macOS and hides only after the transition ends', () => {
const {hideWindowLeavingFullScreen} = loadMacFullScreenHide();
const window = new FakeWindow(true);
hideWindowLeavingFullScreen(window, 'darwin');
assert.deepEqual(window.calls, ['setFullScreen(false)']);
assert.equal(window.visible, true);
window.finishLeavingFullScreen();
assert.deepEqual(window.calls, ['setFullScreen(false)', 'hide']);
assert.equal(window.visible, false);
});
test('a second close during the transition does not queue another exit', () => {
const {hideWindowLeavingFullScreen} = loadMacFullScreenHide();
const window = new FakeWindow(true);
hideWindowLeavingFullScreen(window, 'darwin');
hideWindowLeavingFullScreen(window, 'darwin');
window.finishLeavingFullScreen();
assert.deepEqual(window.calls, ['setFullScreen(false)', 'hide']);
assert.equal(window.listenerCount('leave-full-screen'), 0);
});
test('showing the window during the transition cancels the pending hide', () => {
const {hideWindowLeavingFullScreen, cancelPendingFullScreenHide} = loadMacFullScreenHide();
const window = new FakeWindow(true);
hideWindowLeavingFullScreen(window, 'darwin');
cancelPendingFullScreenHide(window);
window.finishLeavingFullScreen();
assert.deepEqual(window.calls, ['setFullScreen(false)']);
assert.equal(window.visible, true);
});
test('does not touch a window destroyed before the transition ends', () => {
const {hideWindowLeavingFullScreen} = loadMacFullScreenHide();
const window = new FakeWindow(true);
hideWindowLeavingFullScreen(window, 'darwin');
window.destroyed = true;
window.finishLeavingFullScreen();
assert.deepEqual(window.calls, ['setFullScreen(false)']);
});
test('hides directly on other platforms', () => {
const {hideWindowLeavingFullScreen} = loadMacFullScreenHide();
const window = new FakeWindow(true);
hideWindowLeavingFullScreen(window, 'win32');
assert.deepEqual(window.calls, ['hide']);
});
});
@@ -0,0 +1,37 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
export interface FullScreenHideableWindow {
isDestroyed(): boolean;
isFullScreen(): boolean;
setFullScreen(flag: boolean): void;
hide(): void;
once(event: 'leave-full-screen', listener: () => void): unknown;
removeListener(event: 'leave-full-screen', listener: () => void): unknown;
}
const pendingHides = new WeakMap<FullScreenHideableWindow, () => void>();
export function hideWindowLeavingFullScreen(
window: FullScreenHideableWindow,
platform: NodeJS.Platform = process.platform,
): void {
if (platform !== 'darwin' || !window.isFullScreen()) {
window.hide();
return;
}
if (pendingHides.has(window)) return;
const hide = () => {
pendingHides.delete(window);
if (!window.isDestroyed()) window.hide();
};
pendingHides.set(window, hide);
window.once('leave-full-screen', hide);
window.setFullScreen(false);
}
export function cancelPendingFullScreenHide(window: FullScreenHideableWindow): void {
const hide = pendingHides.get(window);
if (!hide) return;
pendingHides.delete(window);
window.removeListener('leave-full-screen', hide);
}
+4 -2
View File
@@ -27,6 +27,7 @@ import {isNativeGatewayAvailable} from '@electron/main/GatewaySocketNativeBounda
import {shouldDisableV8CodeCache} from '@electron/main/LaunchOptions';
import {getDesktopLocalAppAuthorization} from '@electron/main/LocalAppProtocolAuthorization';
import {isLocalAppRendererDocumentURL, isLocalAppURL} from '@electron/main/LocalAppURL';
import {cancelPendingFullScreenHide, hideWindowLeavingFullScreen} from '@electron/main/MacFullScreenHide';
import {t} from '@electron/main/MainI18n';
import {MainWindowRevealGate, MainWindowRevealReason} from '@electron/main/MainWindowReveal';
import {signalMainWindowCreated, signalMainWindowReady} from '@electron/main/ModuleBootHandoff';
@@ -874,7 +875,7 @@ export function createWindow(options: CreateWindowOptions = {}): BrowserWindow {
? 'Window close hid the app. Use Quit to terminate the process'
: 'Window close hid the app to the tray. Use Quit to terminate the process',
);
mainWindow?.hide();
if (mainWindow) hideWindowLeavingFullScreen(mainWindow);
refreshDesktopTrayMenu();
}
});
@@ -1140,6 +1141,7 @@ export function showWindow(): void {
return;
}
if (mainWindow) {
cancelPendingFullScreenHide(mainWindow);
if (mainWindow.isMinimized()) {
mainWindow.restore();
}
@@ -1180,7 +1182,7 @@ export function showWindow(): void {
export function hideWindow(): void {
if (mainWindow) {
mainWindow.hide();
hideWindowLeavingFullScreen(mainWindow);
}
}