Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Sources/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
79 changes: 74 additions & 5 deletions Sources/WindowDragHandleView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,79 @@ 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 {
dispatchPrecondition(condition: .onQueue(.main))
return 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
}
return WindowDragHandleEventDispatchState.containsActiveDispatch(for: event)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/// Runs the same action macOS titlebars use for double-click:
/// zoom by default, or minimize when the user preference is set.
enum StandardTitlebarDoubleClickAction: Equatable {
Expand Down Expand Up @@ -1180,11 +1253,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(
Expand Down
73 changes: 73 additions & 0 deletions cmuxTests/WindowAndDragTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -774,6 +774,35 @@ final class WindowDragHandleHitTests: XCTestCase {
return nil
}

private static func makeMouseEvent(
type: NSEvent.EventType,
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,
modifierFlags: [],
timestamp: ProcessInfo.processInfo.systemUptime,
windowNumber: window.windowNumber,
context: nil,
eventNumber: 0,
clickCount: isButtonEvent ? 1 : 0,
pressure: isButtonEvent ? 1.0 : 0.0
) else {
fatalError("Failed to create \(type) mouse event")
}
return event
}
Comment thread
austinywang marked this conversation as resolved.

func testDragHandleCapturesHitWhenNoSiblingClaimsPoint() {
let container = NSView(frame: NSRect(x: 0, y: 0, width: 220, height: 36))
let dragHandle = NSView(frame: container.bounds)
Expand Down Expand Up @@ -975,6 +1004,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),
Expand Down
Loading