Skip to content

Commit 1b1705e

Browse files
committed
fix(macos): stop HUD dialogs painting a grey rectangle over the desktop
On macOS an owned message box is a sheet, and AppKit dims the whole owning window behind it. The HUD is a ~907x696 transparent window around a ~60px bar, so every dialog it owned greyed a large invisible area of the desktop. On macOS the transparent overlays (HUD, source selector, countdown) no longer own their message boxes: an unowned alert is app-modal and sits above their floating level. Windows and Linux keep the owner, where an unowned dialog opens behind the always-on-top HUD. The two permission dialogs this first covered in ipc/handlers.ts are gone since #735 replaced them with the permissions window; what remains are main.ts's dialogs (About, updates, diagnostics), all of which fall back to the HUD as their owner.
1 parent f418465 commit 1b1705e

4 files changed

Lines changed: 88 additions & 3 deletions

File tree

‎electron/main.ts‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import {
55
app,
66
BrowserWindow,
77
clipboard,
8-
dialog,
98
ipcMain,
109
Menu,
1110
nativeImage,
@@ -59,6 +58,7 @@ import {
5958
registerIpcHandlers,
6059
} from "./ipc/handlers";
6160
import { installMainProcessErrorGuards } from "./main-process-errors";
61+
import { showMessageBoxOver } from "./messageBox";
6262
import {
6363
registerPermissionsIpc,
6464
showPermissionsWindow,
@@ -465,14 +465,15 @@ function channelAllowsUpdateCheck(): boolean {
465465
/** Message boxes must be owned by a window. The HUD is `alwaysOnTop` and `skipTaskbar`
466466
* (electron/windows.ts), so an unowned dialog opens *behind* it on Windows and most Linux
467467
* WMs, with no taskbar entry to recover it — the user sees a button flash and nothing else.
468-
* Mirrors what ipc/handlers.ts already does for its own dialogs. */
468+
* Mirrors what ipc/handlers.ts already does for its own dialogs. On macOS the HUD is
469+
* skipped as an owner (see messageBox.ts). */
469470
function showMessageBox(options: Electron.MessageBoxOptions) {
470471
const visible = (win: BrowserWindow | null) =>
471472
win && !win.isDestroyed() && win.isVisible() ? win : null;
472473
// A modal owned by a hidden window may never be drawn, so an unowned dialog is the safer
473474
// fallback when the HUD has been closed to the tray.
474475
const parent = visible(BrowserWindow.getFocusedWindow()) ?? visible(mainWindow);
475-
return parent ? dialog.showMessageBox(parent, options) : dialog.showMessageBox(options);
476+
return showMessageBoxOver(parent, options);
476477
}
477478

478479
function aboutFacts(): AboutFacts {

‎electron/messageBox.test.ts‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { describe, expect, it, vi } from "vitest";
2+
3+
vi.mock("electron", () => ({ dialog: { showMessageBox: vi.fn() } }));
4+
5+
import type { BrowserWindow } from "electron";
6+
import { markSheetless, messageBoxOwner } from "./messageBox";
7+
8+
function fakeWindow(destroyed = false) {
9+
return { isDestroyed: () => destroyed } as unknown as BrowserWindow;
10+
}
11+
12+
describe("messageBoxOwner", () => {
13+
it("shows a transparent overlay's dialog unowned on macOS, where a sheet greys the whole window", () => {
14+
const hud = fakeWindow();
15+
markSheetless(hud);
16+
17+
expect(messageBoxOwner(hud, "darwin")).toBeNull();
18+
});
19+
20+
it("keeps the overlay as owner elsewhere, where an unowned dialog opens behind it", () => {
21+
const hud = fakeWindow();
22+
markSheetless(hud);
23+
24+
expect(messageBoxOwner(hud, "win32")).toBe(hud);
25+
expect(messageBoxOwner(hud, "linux")).toBe(hud);
26+
});
27+
28+
it("keeps an opaque window as owner on macOS", () => {
29+
const editor = fakeWindow();
30+
31+
expect(messageBoxOwner(editor, "darwin")).toBe(editor);
32+
});
33+
34+
it("never attaches to a missing or destroyed window", () => {
35+
expect(messageBoxOwner(null, "win32")).toBeNull();
36+
expect(messageBoxOwner(fakeWindow(true), "win32")).toBeNull();
37+
});
38+
});

‎electron/messageBox.ts‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { type BrowserWindow, dialog } from "electron";
2+
3+
/**
4+
* Transparent windows that must not own a dialog on macOS.
5+
*
6+
* On macOS an owned message box is a sheet, and AppKit dims the whole owning window behind
7+
* it. The HUD and the other overlays are mostly invisible padding around what they draw
8+
* (the HUD is ~900x700 for a bar ~60 px tall), so the dim paints a grey rectangle over the
9+
* desktop, far larger than anything the user sees of the app.
10+
*
11+
* Elsewhere they keep owning their dialogs: on Windows and most Linux WMs an unowned
12+
* dialog opens behind these `alwaysOnTop` windows, with no taskbar entry to recover it.
13+
* macOS has no such problem -- an unowned alert is app-modal and sits at the modal-panel
14+
* level, above the HUD's floating one.
15+
*/
16+
const sheetlessWindows = new WeakSet<BrowserWindow>();
17+
18+
export function markSheetless(win: BrowserWindow): void {
19+
sheetlessWindows.add(win);
20+
}
21+
22+
/** The window a message box should be attached to, or null to show it unowned. */
23+
export function messageBoxOwner(
24+
parent: BrowserWindow | null | undefined,
25+
platform: NodeJS.Platform = process.platform,
26+
): BrowserWindow | null {
27+
if (!parent || parent.isDestroyed()) {
28+
return null;
29+
}
30+
if (platform === "darwin" && sheetlessWindows.has(parent)) {
31+
return null;
32+
}
33+
return parent;
34+
}
35+
36+
export function showMessageBoxOver(
37+
parent: BrowserWindow | null | undefined,
38+
options: Electron.MessageBoxOptions,
39+
): Promise<Electron.MessageBoxReturnValue> {
40+
const owner = messageBoxOwner(parent);
41+
return owner ? dialog.showMessageBox(owner, options) : dialog.showMessageBox(options);
42+
}

‎electron/windows.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
sameRect,
2020
} from "./hudWindowBounds";
2121
import { followAcrossSpaces } from "./macSpaces";
22+
import { markSheetless } from "./messageBox";
2223

2324
const __dirname = path.dirname(fileURLToPath(import.meta.url));
2425

@@ -447,6 +448,7 @@ export function createHudOverlayWindow(): BrowserWindow {
447448
});
448449
}
449450

451+
markSheetless(win);
450452
return win;
451453
}
452454

@@ -600,6 +602,7 @@ export function createSourceSelectorWindow(): BrowserWindow {
600602
});
601603
}
602604

605+
markSheetless(win);
603606
return win;
604607
}
605608

@@ -651,6 +654,7 @@ export function createCountdownOverlayWindow(): BrowserWindow {
651654
});
652655
}
653656

657+
markSheetless(win);
654658
return win;
655659
}
656660

0 commit comments

Comments
 (0)