From d9e0da176530b70a18565bd1ed0eb10c1dc4237b Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Tue, 26 May 2026 11:44:20 -0700 Subject: [PATCH 1/5] test: cover stale titlebar drag-handle event dispatch --- cmuxTests/WindowAndDragTests.swift | 65 ++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/cmuxTests/WindowAndDragTests.swift b/cmuxTests/WindowAndDragTests.swift index be0cc85871e1..6a221dce45cd 100644 --- a/cmuxTests/WindowAndDragTests.swift +++ b/cmuxTests/WindowAndDragTests.swift @@ -774,6 +774,27 @@ final class WindowDragHandleHitTests: XCTestCase { return nil } + private static func makeMouseEvent( + type: NSEvent.EventType, + location: NSPoint, + window: NSWindow + ) -> NSEvent { + guard let event = NSEvent.mouseEvent( + with: type, + location: location, + modifierFlags: [], + timestamp: ProcessInfo.processInfo.systemUptime, + windowNumber: window.windowNumber, + context: nil, + eventNumber: 0, + clickCount: 1, + pressure: 1.0 + ) else { + fatalError("Failed to create \(type) mouse event") + } + return event + } + func testDragHandleCapturesHitWhenNoSiblingClaimsPoint() { let container = NSView(frame: NSRect(x: 0, y: 0, width: 220, height: 36)) let dragHandle = NSView(frame: container.bounds) @@ -975,6 +996,50 @@ final class WindowDragHandleHitTests: XCTestCase { XCTAssertTrue(windowDragHandleShouldCaptureHit(point, in: dragHandle, eventType: .leftMouseDown)) } + func testDragHandleViewHitTestingRequiresActiveWindowEventDispatch() { + let window = NSWindow( + contentRect: NSRect(x: 0, y: 0, width: 220, height: 36), + styleMask: [.titled, .closable], + backing: .buffered, + defer: false + ) + defer { window.orderOut(nil) } + + let event = Self.makeMouseEvent(type: .leftMouseDown, location: NSPoint(x: 180, y: 18), window: window) + + XCTAssertFalse( + windowDragHandleViewHitTestingAllowsCurrentEvent(event), + "A stale leftMouseDown in NSApp.currentEvent must not make layout/passive hit-testing enter the drag handle." + ) + + let token = beginWindowDragHandleEventDispatch(window: window, event: event) + defer { endWindowDragHandleEventDispatch(token) } + + XCTAssertTrue( + windowDragHandleViewHitTestingAllowsCurrentEvent(event), + "A real leftMouseDown may resolve drag-handle hits while AppKit is dispatching that window event." + ) + } + + func testDragHandleViewHitTestingStillRejectsPassiveEventsDuringDispatch() { + let window = NSWindow( + contentRect: NSRect(x: 0, y: 0, width: 220, height: 36), + styleMask: [.titled, .closable], + backing: .buffered, + defer: false + ) + defer { window.orderOut(nil) } + + let event = Self.makeMouseEvent(type: .mouseMoved, location: NSPoint(x: 180, y: 18), window: window) + let token = beginWindowDragHandleEventDispatch(window: window, event: event) + defer { endWindowDragHandleEventDispatch(token) } + + XCTAssertFalse( + windowDragHandleViewHitTestingAllowsCurrentEvent(event), + "Hover dispatch must stay transparent to the titlebar drag handle." + ) + } + func testDragHandleNeverCapturesRegisteredBonsplitPaneTab() { let window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 320, height: 120), From cb441e83625c97d71bf2cb44e4d3f64b445fd056 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Tue, 26 May 2026 11:45:52 -0700 Subject: [PATCH 2/5] fix: scope titlebar drag hit testing to event dispatch --- Sources/AppDelegate.swift | 4 ++ Sources/WindowDragHandleView.swift | 76 ++++++++++++++++++++++++++++-- 2 files changed, 75 insertions(+), 5 deletions(-) diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index fefd060ca08f..914369de8368 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -15579,6 +15579,10 @@ private extension NSWindow { CmuxTypingTiming.logEventDelay(path: "window.sendEvent", event: event) } #endif + let dragHandleDispatchToken = beginWindowDragHandleEventDispatch(window: self, event: event) + defer { + endWindowDragHandleEventDispatch(dragHandleDispatchToken) + } // recordTypingActivity must run in all builds so runSessionAutosaveTick // can honor the typing quiet period in release. if event.type == .keyDown, let app = AppDelegate.shared, cmuxCloseFocusedTerminalFindForEscape(event: event, appDelegate: app) { return } diff --git a/Sources/WindowDragHandleView.swift b/Sources/WindowDragHandleView.swift index 8d6b353da8ae..add30de069e7 100644 --- a/Sources/WindowDragHandleView.swift +++ b/Sources/WindowDragHandleView.swift @@ -144,6 +144,76 @@ private func windowDragHandleShouldResolveActiveHitCapture( return eventWindow === dragHandleWindow } +struct WindowDragHandleEventDispatchToken: Equatable { + fileprivate let id: Int +} + +private struct WindowDragHandleEventDispatchFrame { + let token: WindowDragHandleEventDispatchToken + weak var window: NSWindow? + let windowNumber: Int + let eventType: NSEvent.EventType +} + +private enum WindowDragHandleEventDispatchState { + // AppKit event dispatch is main-thread only. This stack lets drag-handle + // NSViews distinguish real event delivery from stale NSApp.currentEvent reads + // during SwiftUI/AppKit layout hit-testing. + private nonisolated(unsafe) static var nextTokenId = 0 + private nonisolated(unsafe) static var frames: [WindowDragHandleEventDispatchFrame] = [] + + static func begin(window: NSWindow, event: NSEvent) -> WindowDragHandleEventDispatchToken { + nextTokenId += 1 + let token = WindowDragHandleEventDispatchToken(id: nextTokenId) + frames.append( + WindowDragHandleEventDispatchFrame( + token: token, + window: window, + windowNumber: window.windowNumber, + eventType: event.type + ) + ) + return token + } + + static func end(_ token: WindowDragHandleEventDispatchToken) { + if let index = frames.lastIndex(where: { $0.token == token }) { + frames.remove(at: index) + } + } + + static func containsActiveDispatch(for event: NSEvent) -> Bool { + frames.contains { frame in + guard frame.eventType == event.type, + frame.windowNumber == event.windowNumber else { + return false + } + if let eventWindow = event.window { + return frame.window === eventWindow + } + return true + } + } +} + +func beginWindowDragHandleEventDispatch( + window: NSWindow, + event: NSEvent +) -> WindowDragHandleEventDispatchToken { + WindowDragHandleEventDispatchState.begin(window: window, event: event) +} + +func endWindowDragHandleEventDispatch(_ token: WindowDragHandleEventDispatchToken) { + WindowDragHandleEventDispatchState.end(token) +} + +func windowDragHandleViewHitTestingAllowsCurrentEvent(_ event: NSEvent?) -> Bool { + guard let event, event.type == .leftMouseDown else { + return false + } + return WindowDragHandleEventDispatchState.containsActiveDispatch(for: event) +} + /// Runs the same action macOS titlebars use for double-click: /// zoom by default, or minimize when the user preference is set. enum StandardTitlebarDoubleClickAction: Equatable { @@ -1180,11 +1250,7 @@ struct WindowDragHandleView: NSViewRepresentable { override func hitTest(_ point: NSPoint) -> NSView? { let currentEvent = NSApp.currentEvent - // Fast bail-out: only claim hits for left-mouse-down events. - // For mouseMoved / mouseEntered / etc., return nil immediately - // to avoid re-entering SwiftUI view state during layout passes, - // which causes exclusive-access crashes. - guard currentEvent?.type == .leftMouseDown else { + guard windowDragHandleViewHitTestingAllowsCurrentEvent(currentEvent) else { return nil } let shouldCapture = windowDragHandleShouldCaptureHit( From 6ec7079a3121a58ee22f94c04506fabd16fe7cf4 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Tue, 26 May 2026 11:55:21 -0700 Subject: [PATCH 3/5] test: align synthetic hover event shape --- cmuxTests/WindowAndDragTests.swift | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/cmuxTests/WindowAndDragTests.swift b/cmuxTests/WindowAndDragTests.swift index 6a221dce45cd..7a2dd41d1f35 100644 --- a/cmuxTests/WindowAndDragTests.swift +++ b/cmuxTests/WindowAndDragTests.swift @@ -779,6 +779,14 @@ final class WindowDragHandleHitTests: XCTestCase { location: NSPoint, window: NSWindow ) -> NSEvent { + let isButtonEvent: Bool + switch type { + case .leftMouseDown, .leftMouseUp, .rightMouseDown, .rightMouseUp, .otherMouseDown, .otherMouseUp: + isButtonEvent = true + default: + isButtonEvent = false + } + guard let event = NSEvent.mouseEvent( with: type, location: location, @@ -787,8 +795,8 @@ final class WindowDragHandleHitTests: XCTestCase { windowNumber: window.windowNumber, context: nil, eventNumber: 0, - clickCount: 1, - pressure: 1.0 + clickCount: isButtonEvent ? 1 : 0, + pressure: isButtonEvent ? 1.0 : 0.0 ) else { fatalError("Failed to create \(type) mouse event") } From 5e30f6544a1480750b6d123d8aaa6355deda84c1 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Tue, 26 May 2026 12:16:11 -0700 Subject: [PATCH 4/5] fix: assert drag-handle dispatch state on main --- Sources/WindowDragHandleView.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Sources/WindowDragHandleView.swift b/Sources/WindowDragHandleView.swift index add30de069e7..74ced72cdef3 100644 --- a/Sources/WindowDragHandleView.swift +++ b/Sources/WindowDragHandleView.swift @@ -200,14 +200,17 @@ func beginWindowDragHandleEventDispatch( window: NSWindow, event: NSEvent ) -> WindowDragHandleEventDispatchToken { + dispatchPrecondition(condition: .onQueue(.main)) WindowDragHandleEventDispatchState.begin(window: window, event: event) } func endWindowDragHandleEventDispatch(_ token: WindowDragHandleEventDispatchToken) { + dispatchPrecondition(condition: .onQueue(.main)) WindowDragHandleEventDispatchState.end(token) } func windowDragHandleViewHitTestingAllowsCurrentEvent(_ event: NSEvent?) -> Bool { + dispatchPrecondition(condition: .onQueue(.main)) guard let event, event.type == .leftMouseDown else { return false } From 68f1924d3c0d3aca37939be9a60aa12b28de89c8 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Tue, 26 May 2026 12:20:22 -0700 Subject: [PATCH 5/5] fix: return drag dispatch token after precondition --- Sources/WindowDragHandleView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/WindowDragHandleView.swift b/Sources/WindowDragHandleView.swift index 74ced72cdef3..4eb14d7dab03 100644 --- a/Sources/WindowDragHandleView.swift +++ b/Sources/WindowDragHandleView.swift @@ -201,7 +201,7 @@ func beginWindowDragHandleEventDispatch( event: NSEvent ) -> WindowDragHandleEventDispatchToken { dispatchPrecondition(condition: .onQueue(.main)) - WindowDragHandleEventDispatchState.begin(window: window, event: event) + return WindowDragHandleEventDispatchState.begin(window: window, event: event) } func endWindowDragHandleEventDispatch(_ token: WindowDragHandleEventDispatchToken) {