Fix interaction crash in titlebar drag hit-testing (#600)

This commit is contained in:
ALPER 2026-02-27 07:02:53 +03:00 committed by GitHub
parent 570ed27b5d
commit 4cb278c854
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 2 additions and 88 deletions

View file

@ -747,11 +747,11 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
label: "com.cmuxterm.app.sessionPersistence",
qos: .utility
)
private static let launchServicesRegistrationQueue = DispatchQueue(
private nonisolated static let launchServicesRegistrationQueue = DispatchQueue(
label: "com.cmuxterm.app.launchServicesRegistration",
qos: .utility
)
private static func enqueueLaunchServicesRegistrationWork(_ work: @escaping @Sendable () -> Void) {
private nonisolated static func enqueueLaunchServicesRegistrationWork(_ work: @escaping @Sendable () -> Void) {
launchServicesRegistrationQueue.async(execute: work)
}
private var lastSessionAutosaveFingerprint: Int?

View file

@ -55,10 +55,8 @@ func performStandardTitlebarDoubleClick(window: NSWindow?) -> Bool {
private enum WindowDragHandleAssociatedObjectKeys {
private static let suppressionDepthToken = NSObject()
private static let topHitResolutionDepthToken = NSObject()
static let suppressionDepth = UnsafeRawPointer(Unmanaged.passUnretained(suppressionDepthToken).toOpaque())
static let topHitResolutionDepth = UnsafeRawPointer(Unmanaged.passUnretained(topHitResolutionDepthToken).toOpaque())
}
func beginWindowDragSuppression(window: NSWindow?) -> Int? {
@ -142,57 +140,6 @@ func withTemporaryWindowMovableEnabled(window: NSWindow?, _ body: () -> Void) ->
return previousMovableState
}
private enum WindowDragHandleHitTestState {
static func depth(window: NSWindow?) -> Int {
guard let window,
let value = objc_getAssociatedObject(
window,
WindowDragHandleAssociatedObjectKeys.topHitResolutionDepth
) as? NSNumber else {
return 0
}
return value.intValue
}
static func begin(window: NSWindow?) {
guard let window else { return }
let next = depth(window: window) + 1
objc_setAssociatedObject(
window,
WindowDragHandleAssociatedObjectKeys.topHitResolutionDepth,
NSNumber(value: next),
.OBJC_ASSOCIATION_RETAIN_NONATOMIC
)
}
@discardableResult
static func end(window: NSWindow?) -> Int {
guard let window else { return 0 }
let current = depth(window: window)
let next = max(0, current - 1)
if next == 0 {
objc_setAssociatedObject(
window,
WindowDragHandleAssociatedObjectKeys.topHitResolutionDepth,
nil,
.OBJC_ASSOCIATION_RETAIN_NONATOMIC
)
} else {
objc_setAssociatedObject(
window,
WindowDragHandleAssociatedObjectKeys.topHitResolutionDepth,
NSNumber(value: next),
.OBJC_ASSOCIATION_RETAIN_NONATOMIC
)
}
return next
}
static func isResolvingTopHit(window: NSWindow?) -> Bool {
depth(window: window) > 0
}
}
/// SwiftUI/AppKit hosting wrappers can appear as the top hit even for empty
/// titlebar space. Treat those as pass-through so explicit sibling checks decide.
func windowDragHandleShouldTreatTopHitAsPassiveHost(_ view: NSView) -> Bool {
@ -266,39 +213,6 @@ func windowDragHandleShouldCaptureHit(
return true
}
if let window = dragHandleView.window,
let contentView = window.contentView,
!WindowDragHandleHitTestState.isResolvingTopHit(window: window) {
let pointInWindow = dragHandleView.convert(point, to: nil)
let pointInContent = contentView.convert(pointInWindow, from: nil)
WindowDragHandleHitTestState.begin(window: window)
defer {
WindowDragHandleHitTestState.end(window: window)
}
let topHit = contentView.hitTest(pointInContent)
if let topHit {
let ownsTopHit = topHit === dragHandleView || topHit.isDescendant(of: dragHandleView)
let topHitBelongsToTitlebarOverlay = topHit === superview || topHit.isDescendant(of: superview)
let isPassiveHostHit = windowDragHandleShouldTreatTopHitAsPassiveHost(topHit)
#if DEBUG
dlog(
"titlebar.dragHandle.hitTest capture=\(ownsTopHit) strategy=windowTopHit point=\(windowDragHandleFormatPoint(point)) top=\(type(of: topHit)) inTitlebarOverlay=\(topHitBelongsToTitlebarOverlay) passiveHost=\(isPassiveHostHit)"
)
#endif
if ownsTopHit {
return true
}
// Underlay content can transiently overlap titlebar space (notably browser
// chrome/webview layers). Only let top-hits block capture when they belong
// to this titlebar overlay stack.
if topHitBelongsToTitlebarOverlay && !isPassiveHostHit {
return false
}
}
}
let siblingSnapshot = Array(superview.subviews.reversed())
#if DEBUG