diff --git a/desktop/macos/Desktop/Sources/MainWindow/ShellClickThrough.swift b/desktop/macos/Desktop/Sources/MainWindow/ShellClickThrough.swift index 2a07c97e4a0..a063a503a3f 100644 --- a/desktop/macos/Desktop/Sources/MainWindow/ShellClickThrough.swift +++ b/desktop/macos/Desktop/Sources/MainWindow/ShellClickThrough.swift @@ -49,6 +49,7 @@ enum ShellClickThroughPolicy { @MainActor final class ShellMouseInterceptionSync { private nonisolated(unsafe) var monitors: [Any] = [] + private var visibilityObservation: NSKeyValueObservation? private var pollingCancellable: AnyCancellable? private weak var window: NSWindow? @@ -72,6 +73,9 @@ final class ShellMouseInterceptionSync { { monitors.append(local) } + visibilityObservation = window.observe(\.isVisible, options: [.new]) { [weak self] _, _ in + MainActor.assumeIsolated { self?.sync() } + } sync() } @@ -86,6 +90,7 @@ final class ShellMouseInterceptionSync { window?.ignoresMouseEvents = false window = nil pollingCancellable = nil + visibilityObservation = nil } func sync() { diff --git a/desktop/macos/Desktop/Tests/ShellClickThroughPolicyTests.swift b/desktop/macos/Desktop/Tests/ShellClickThroughPolicyTests.swift index 173f3bf44c0..f1f5378833f 100644 --- a/desktop/macos/Desktop/Tests/ShellClickThroughPolicyTests.swift +++ b/desktop/macos/Desktop/Tests/ShellClickThroughPolicyTests.swift @@ -7,6 +7,7 @@ import XCTest /// frame — the reserved title-bar band, lane margins, and gaps between panels swallowed clicks /// aimed at other apps, which never activated (the shell-window dead zone). The policy passes the /// pointer through everywhere except visible content, the modal barrier's host, and the resize rim. +@MainActor final class ShellClickThroughPolicyTests: XCTestCase { private let windowSize = NSSize(width: 960, height: 712) @@ -60,4 +61,33 @@ final class ShellClickThroughPolicyTests: XCTestCase { contentContains: { _ in false }), "a fixed-size window has no resize affordance to preserve") } + + /// The shell reuses one window across dismiss/summon. Reconciliation while that window is ordered + /// out must not leave it ignoring mouse events when AppKit puts it back on screen: while ignored, + /// neither its visible controls nor its local mouse monitor can recover the window. + func testOrderingShellBackOnScreenRestoresMouseInterception() { + let mouse = NSEvent.mouseLocation + let window = NSWindow( + contentRect: NSRect(x: mouse.x + 5_000, y: mouse.y + 5_000, width: 320, height: 240), + styleMask: [.borderless], + backing: .buffered, + defer: false) + window.orderFront(nil) + let sync = ShellMouseInterceptionSync(window: window) + defer { + sync.detach() + window.orderOut(nil) + } + + XCTAssertFalse(window.ignoresMouseEvents) + window.orderOut(nil) + sync.sync() + XCTAssertTrue(window.ignoresMouseEvents, "the hidden shell previously entered pass-through mode") + + window.orderFront(nil) + + XCTAssertFalse( + window.ignoresMouseEvents, + "a visible shell must recover before the first click, without waiting for pointer movement") + } } diff --git a/desktop/macos/changelog/unreleased/20260826-shell-click-recovery.json b/desktop/macos/changelog/unreleased/20260826-shell-click-recovery.json new file mode 100644 index 00000000000..82660ef796c --- /dev/null +++ b/desktop/macos/changelog/unreleased/20260826-shell-click-recovery.json @@ -0,0 +1,3 @@ +{ + "change": "Fixed the main window occasionally ignoring clicks after reopening" +}