diff --git a/Sources/WindowDragHandleView.swift b/Sources/WindowDragHandleView.swift index 8786dae8..d0ffd48b 100644 --- a/Sources/WindowDragHandleView.swift +++ b/Sources/WindowDragHandleView.swift @@ -254,11 +254,13 @@ func windowDragHandleShouldCaptureHit(_ point: NSPoint, in dragHandleView: NSVie } } + let siblingSnapshot = Array(superview.subviews.reversed()) + #if DEBUG - let siblingCount = superview.subviews.count + let siblingCount = siblingSnapshot.count #endif - for sibling in superview.subviews.reversed() { + for sibling in siblingSnapshot { guard sibling !== dragHandleView else { continue } guard !sibling.isHidden, sibling.alphaValue > 0 else { continue } diff --git a/cmuxTests/CmuxWebViewKeyEquivalentTests.swift b/cmuxTests/CmuxWebViewKeyEquivalentTests.swift index 8f8afdf6..63f78cde 100644 --- a/cmuxTests/CmuxWebViewKeyEquivalentTests.swift +++ b/cmuxTests/CmuxWebViewKeyEquivalentTests.swift @@ -145,7 +145,6 @@ final class CmuxWebViewKeyEquivalentTests: XCTestCase { set {} } } - func testCmdNRoutesToMainMenuWhenWebViewIsFirstResponder() { let spy = ActionSpy() installMenu(spy: spy, key: "n", modifiers: [.command]) @@ -529,7 +528,6 @@ final class CmuxWebViewKeyEquivalentTests: XCTestCase { } XCTAssertTrue(window.makeFirstResponder(responder)) } - private func installMenu(spy: ActionSpy, key: String, modifiers: NSEvent.ModifierFlags) { let mainMenu = NSMenu() @@ -5899,6 +5897,21 @@ final class WindowDragHandleHitTests: XCTestCase { } } + private final class MutatingSiblingView: NSView { + weak var container: NSView? + private var didMutate = false + + override func hitTest(_ point: NSPoint) -> NSView? { + guard bounds.contains(point) else { return nil } + guard !didMutate, let container else { return nil } + didMutate = true + let transient = NSView(frame: .zero) + container.addSubview(transient) + transient.removeFromSuperview() + return nil + } + } + func testDragHandleCapturesHitWhenNoSiblingClaimsPoint() { let container = NSView(frame: NSRect(x: 0, y: 0, width: 220, height: 36)) let dragHandle = NSView(frame: container.bounds) @@ -6040,6 +6053,21 @@ final class WindowDragHandleHitTests: XCTestCase { "Top-hit recursion in one window must not disable top-hit resolution in another window" ) } + + func testDragHandleRemainsStableWhenSiblingMutatesSubviewsDuringHitTest() { + let container = NSView(frame: NSRect(x: 0, y: 0, width: 220, height: 36)) + let dragHandle = NSView(frame: container.bounds) + container.addSubview(dragHandle) + + let mutatingSibling = MutatingSiblingView(frame: container.bounds) + mutatingSibling.container = container + container.addSubview(mutatingSibling) + + XCTAssertTrue( + windowDragHandleShouldCaptureHit(NSPoint(x: 180, y: 18), in: dragHandle), + "Subview mutations during hit testing should not crash or break drag-handle capture" + ) + } } @MainActor