Fix bonsplit drag routing and pin submodule commit

This commit is contained in:
Lawrence Chen 2026-02-20 18:47:34 -08:00
parent 23979d8c02
commit cf767cf9af
7 changed files with 623 additions and 62 deletions

View file

@ -165,17 +165,32 @@ enum DragOverlayRoutingPolicy {
static let bonsplitTabTransferType = NSPasteboard.PasteboardType("com.splittabbar.tabtransfer")
static let sidebarTabReorderType = NSPasteboard.PasteboardType(SidebarTabDragPayload.typeIdentifier)
static func hasBonsplitTabTransfer(_ pasteboardTypes: [NSPasteboard.PasteboardType]?) -> Bool {
guard let pasteboardTypes else { return false }
return pasteboardTypes.contains(bonsplitTabTransferType)
}
static func hasSidebarTabReorder(_ pasteboardTypes: [NSPasteboard.PasteboardType]?) -> Bool {
guard let pasteboardTypes else { return false }
return pasteboardTypes.contains(sidebarTabReorderType)
}
static func hasFileURL(_ pasteboardTypes: [NSPasteboard.PasteboardType]?) -> Bool {
guard let pasteboardTypes else { return false }
return pasteboardTypes.contains(.fileURL)
}
static func shouldCaptureFileDropDestination(
pasteboardTypes: [NSPasteboard.PasteboardType]?,
hasLocalDraggingSource: Bool
) -> Bool {
guard !hasLocalDraggingSource else { return false }
guard let pasteboardTypes, pasteboardTypes.contains(.fileURL) else { return false }
guard hasFileURL(pasteboardTypes) else { return false }
// Prefer explicit non-file drag types so stale fileURL entries cannot hijack
// Bonsplit tab drags or sidebar tab reorder drags.
if pasteboardTypes.contains(bonsplitTabTransferType) { return false }
if pasteboardTypes.contains(sidebarTabReorderType) { return false }
if hasBonsplitTabTransfer(pasteboardTypes) { return false }
if hasSidebarTabReorder(pasteboardTypes) { return false }
return true
}
@ -197,13 +212,30 @@ enum DragOverlayRoutingPolicy {
return true
}
static func shouldCaptureSidebarExternalOverlay(
hasSidebarDragState: Bool,
pasteboardTypes: [NSPasteboard.PasteboardType]?
) -> Bool {
guard hasSidebarDragState else { return false }
return hasSidebarTabReorder(pasteboardTypes)
}
static func shouldCaptureSidebarExternalOverlay(
draggedTabId: UUID?,
pasteboardTypes: [NSPasteboard.PasteboardType]?
) -> Bool {
guard draggedTabId != nil else { return false }
guard let pasteboardTypes else { return false }
return pasteboardTypes.contains(sidebarTabReorderType)
shouldCaptureSidebarExternalOverlay(
hasSidebarDragState: draggedTabId != nil,
pasteboardTypes: pasteboardTypes
)
}
static func shouldPassThroughPortalHitTesting(
pasteboardTypes: [NSPasteboard.PasteboardType]?,
eventType: NSEvent.EventType?
) -> Bool {
guard isPortalDragEvent(eventType) else { return false }
return hasBonsplitTabTransfer(pasteboardTypes) || hasSidebarTabReorder(pasteboardTypes)
}
private static func isDragMouseEvent(_ eventType: NSEvent.EventType?) -> Bool {
@ -211,6 +243,27 @@ enum DragOverlayRoutingPolicy {
|| eventType == .rightMouseDragged
|| eventType == .otherMouseDragged
}
private static func isPortalDragEvent(_ eventType: NSEvent.EventType?) -> Bool {
// NSDraggingDestination hit-testing can occur with no current NSEvent.
// Treat nil as drag-routing context so portal-hosted terminals do not
// swallow Bonsplit/sidebar drag payloads.
guard let eventType else { return true }
switch eventType {
case .leftMouseDragged, .rightMouseDragged, .otherMouseDragged:
return true
case .flagsChanged:
// Real tab drags can briefly report flagsChanged while modifiers
// are sampled; still treat as drag-routing context.
return true
case .mouseMoved, .mouseEntered, .mouseExited, .cursorUpdate:
return true
case .appKitDefined, .systemDefined, .applicationDefined, .periodic:
return true
default:
return false
}
}
}
/// Transparent NSView installed on the window's theme frame (above the NSHostingView) to
@ -228,6 +281,8 @@ final class FileDropOverlayView: NSView {
/// The WKWebView currently receiving forwarded drag events, so we can
/// synthesize draggingExited/draggingEntered as the cursor moves.
private weak var activeDragWebView: WKWebView?
private var lastHitTestLogSignature: String?
private var lastDragRouteLogSignatureByPhase: [String: String] = [:]
override var acceptsFirstResponder: Bool { false }
@ -243,10 +298,19 @@ final class FileDropOverlayView: NSView {
override func hitTest(_ point: NSPoint) -> NSView? {
let pb = NSPasteboard(name: .drag)
guard DragOverlayRoutingPolicy.shouldCaptureFileDropOverlay(
let eventType = NSApp.currentEvent?.type
let shouldCapture = DragOverlayRoutingPolicy.shouldCaptureFileDropOverlay(
pasteboardTypes: pb.types,
eventType: NSApp.currentEvent?.type
) else { return nil }
eventType: eventType
)
#if DEBUG
logHitTestDecision(
pasteboardTypes: pb.types,
eventType: eventType,
shouldCapture: shouldCapture
)
#endif
guard shouldCapture else { return nil }
return super.hitTest(point)
}
@ -328,19 +392,22 @@ final class FileDropOverlayView: NSView {
)
let webView = activeDragWebView
activeDragWebView = nil
let terminal = terminalUnderPoint(sender.draggingLocation)
let hasTerminalTarget = terminal != nil
#if DEBUG
dlog(
"overlay.fileDrop.perform capture=\(shouldCapture ? 1 : 0) " +
"localSource=\(hasLocalDraggingSource ? 1 : 0) " +
"hasWebView=\(webView != nil ? 1 : 0) " +
"types=\(debugPasteboardTypes(types))"
logDragRouteDecision(
phase: "perform",
pasteboardTypes: types,
shouldCapture: shouldCapture,
hasLocalDraggingSource: hasLocalDraggingSource,
hasTerminalTarget: hasTerminalTarget
)
#endif
guard shouldCapture else { return false }
if let webView {
return webView.performDragOperation(sender)
}
guard let terminal = terminalUnderPoint(sender.draggingLocation) else { return false }
guard let terminal else { return false }
return terminal.performDragOperation(sender)
}
@ -369,12 +436,12 @@ final class FileDropOverlayView: NSView {
let hasTerminalTarget = terminalUnderPoint(loc) != nil
#if DEBUG
dlog(
"overlay.fileDrop.\(phase) capture=\(shouldCapture ? 1 : 0) " +
"localSource=\(hasLocalDraggingSource ? 1 : 0) " +
"hasWebView=\(webView != nil ? 1 : 0) " +
"hasTerminal=\(hasTerminalTarget ? 1 : 0) " +
"types=\(debugPasteboardTypes(types))"
logDragRouteDecision(
phase: phase,
pasteboardTypes: types,
shouldCapture: shouldCapture,
hasLocalDraggingSource: hasLocalDraggingSource,
hasTerminalTarget: hasTerminalTarget
)
#endif
guard shouldCapture, hasTerminalTarget else { return [] }
@ -402,6 +469,135 @@ final class FileDropOverlayView: NSView {
return nil
}
private func debugTopHitViewForCurrentEvent() -> String {
guard let window,
let currentEvent = NSApp.currentEvent,
let contentView = window.contentView,
let themeFrame = contentView.superview else { return "-" }
let pointInTheme = themeFrame.convert(currentEvent.locationInWindow, from: nil)
isHidden = true
defer { isHidden = false }
guard let hit = themeFrame.hitTest(pointInTheme) else { return "nil" }
var chain: [String] = []
var current: NSView? = hit
var depth = 0
while let view = current, depth < 6 {
chain.append(debugHitViewDescriptor(view))
current = view.superview
depth += 1
}
return chain.joined(separator: "->")
}
private func debugHitViewDescriptor(_ view: NSView) -> String {
let className = String(describing: type(of: view))
let ptr = String(describing: Unmanaged.passUnretained(view).toOpaque())
let dragTypes = debugRegisteredDragTypes(view)
return "\(className)@\(ptr){dragTypes=\(dragTypes)}"
}
private func debugRegisteredDragTypes(_ view: NSView) -> String {
let types = view.registeredDraggedTypes
guard !types.isEmpty else { return "-" }
let interestingTypes = types.filter { type in
let raw = type.rawValue
return raw == NSPasteboard.PasteboardType.fileURL.rawValue
|| raw == DragOverlayRoutingPolicy.bonsplitTabTransferType.rawValue
|| raw == DragOverlayRoutingPolicy.sidebarTabReorderType.rawValue
|| raw.contains("public.text")
|| raw.contains("public.url")
|| raw.contains("public.data")
}
let selected = interestingTypes.isEmpty ? Array(types.prefix(3)) : interestingTypes
let rendered = selected.map(\.rawValue).joined(separator: ",")
if selected.count < types.count {
return "\(rendered),+\(types.count - selected.count)"
}
return rendered
}
private func hasRelevantDragTypes(_ types: [NSPasteboard.PasteboardType]?) -> Bool {
guard let types else { return false }
return types.contains(.fileURL)
|| types.contains(DragOverlayRoutingPolicy.bonsplitTabTransferType)
|| types.contains(DragOverlayRoutingPolicy.sidebarTabReorderType)
}
private func debugEventName(_ eventType: NSEvent.EventType?) -> String {
guard let eventType else { return "none" }
switch eventType {
case .cursorUpdate: return "cursorUpdate"
case .appKitDefined: return "appKitDefined"
case .systemDefined: return "systemDefined"
case .applicationDefined: return "applicationDefined"
case .periodic: return "periodic"
case .mouseMoved: return "mouseMoved"
case .mouseEntered: return "mouseEntered"
case .mouseExited: return "mouseExited"
case .flagsChanged: return "flagsChanged"
case .leftMouseDown: return "leftMouseDown"
case .leftMouseUp: return "leftMouseUp"
case .leftMouseDragged: return "leftMouseDragged"
case .rightMouseDown: return "rightMouseDown"
case .rightMouseUp: return "rightMouseUp"
case .rightMouseDragged: return "rightMouseDragged"
case .otherMouseDown: return "otherMouseDown"
case .otherMouseUp: return "otherMouseUp"
case .otherMouseDragged: return "otherMouseDragged"
case .scrollWheel: return "scrollWheel"
default: return "other(\(eventType.rawValue))"
}
}
#if DEBUG
private func logHitTestDecision(
pasteboardTypes: [NSPasteboard.PasteboardType]?,
eventType: NSEvent.EventType?,
shouldCapture: Bool
) {
let isDragEvent = eventType == .leftMouseDragged
|| eventType == .rightMouseDragged
|| eventType == .otherMouseDragged
guard shouldCapture || isDragEvent || hasRelevantDragTypes(pasteboardTypes) else { return }
let signature = "\(shouldCapture ? 1 : 0)|\(debugEventName(eventType))|\(debugPasteboardTypes(pasteboardTypes))"
guard lastHitTestLogSignature != signature else { return }
lastHitTestLogSignature = signature
dlog(
"overlay.fileDrop.hitTest capture=\(shouldCapture ? 1 : 0) " +
"event=\(debugEventName(eventType)) " +
"topHit=\(debugTopHitViewForCurrentEvent()) " +
"types=\(debugPasteboardTypes(pasteboardTypes))"
)
}
private func logDragRouteDecision(
phase: String,
pasteboardTypes: [NSPasteboard.PasteboardType]?,
shouldCapture: Bool,
hasLocalDraggingSource: Bool,
hasTerminalTarget: Bool
) {
guard shouldCapture || hasRelevantDragTypes(pasteboardTypes) else { return }
let signature = [
shouldCapture ? "1" : "0",
hasLocalDraggingSource ? "1" : "0",
hasTerminalTarget ? "1" : "0",
debugPasteboardTypes(pasteboardTypes)
].joined(separator: "|")
guard lastDragRouteLogSignatureByPhase[phase] != signature else { return }
lastDragRouteLogSignatureByPhase[phase] = signature
dlog(
"overlay.fileDrop.\(phase) capture=\(shouldCapture ? 1 : 0) " +
"localSource=\(hasLocalDraggingSource ? 1 : 0) " +
"hasTerminal=\(hasTerminalTarget ? 1 : 0) " +
"types=\(debugPasteboardTypes(pasteboardTypes))"
)
}
#endif
/// Hit-tests the window to find the GhosttyNSView under the cursor.
func terminalUnderPoint(_ windowPoint: NSPoint) -> GhosttyNSView? {
if let window,
@ -1522,13 +1718,21 @@ private struct SidebarExternalDropOverlay: View {
draggedTabId: draggedTabId,
pasteboardTypes: dragPasteboardTypes
)
Color.clear
.contentShape(Rectangle())
.allowsHitTesting(shouldCapture)
.onDrop(
of: [SidebarTabDragPayload.typeIdentifier],
delegate: SidebarExternalDropDelegate(draggedTabId: draggedTabId)
)
Group {
if shouldCapture {
Color.clear
.contentShape(Rectangle())
.allowsHitTesting(true)
.onDrop(
of: [SidebarTabDragPayload.typeIdentifier],
delegate: SidebarExternalDropDelegate(draggedTabId: draggedTabId)
)
} else {
Color.clear
.contentShape(Rectangle())
.allowsHitTesting(false)
}
}
}
}

View file

@ -2818,6 +2818,10 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations {
}
override func draggingUpdated(_ sender: any NSDraggingInfo) -> NSDragOperation {
#if DEBUG
let types = sender.draggingPasteboard.types ?? []
dlog("terminal.draggingUpdated surface=\(terminalSurface?.id.uuidString.prefix(5) ?? "nil") types=\(types.map(\.rawValue))")
#endif
guard let types = sender.draggingPasteboard.types else { return [] }
if Set(types).isDisjoint(with: Self.dropTypes) {
return []

View file

@ -496,12 +496,21 @@ class TerminalController {
case "drop_hit_test":
return dropHitTest(args)
case "drag_hit_chain":
return dragHitChain(args)
case "overlay_hit_gate":
return overlayHitGate(args)
case "overlay_drop_gate":
return overlayDropGate(args)
case "portal_hit_gate":
return portalHitGate(args)
case "sidebar_overlay_gate":
return sidebarOverlayGate(args)
case "activate_app":
return activateApp()
@ -6966,8 +6975,11 @@ class TerminalController {
seed_drag_pasteboard_types <types> - Seed NSDrag pasteboard with comma/space-separated types (fileurl, tabtransfer, sidebarreorder, or raw UTI)
clear_drag_pasteboard - Clear NSDrag pasteboard (test-only)
drop_hit_test <x 0-1> <y 0-1> - Hit-test file-drop overlay at normalised coords (test-only)
drag_hit_chain <x 0-1> <y 0-1> - Return hit-view chain at normalised coords (test-only)
overlay_hit_gate <event|none> - Return true/false if file-drop overlay would capture hit-testing for event type (test-only)
overlay_drop_gate [external|local] - Return true/false if file-drop overlay would capture drag destination routing (test-only)
portal_hit_gate <event|none> - Return true/false if terminal portal should pass hit-testing to SwiftUI drag targets (test-only)
sidebar_overlay_gate [active|inactive] - Return true/false if sidebar outside-drop overlay would capture (test-only)
activate_app - Bring app + main window to front (test-only)
is_terminal_focused <id|idx> - Return true/false if terminal surface is first responder (test-only)
read_terminal_text [id|idx] - Read visible terminal text (base64, test-only)
@ -7246,36 +7258,14 @@ class TerminalController {
private func overlayHitGate(_ args: String) -> String {
let token = args.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
guard !token.isEmpty else {
return "ERROR: Usage: overlay_hit_gate <leftMouseDragged|rightMouseDragged|otherMouseDragged|leftMouseDown|leftMouseUp|rightMouseDown|rightMouseUp|otherMouseDown|otherMouseUp|scrollWheel|none>"
return "ERROR: Usage: overlay_hit_gate <leftMouseDragged|rightMouseDragged|otherMouseDragged|mouseMoved|mouseEntered|mouseExited|flagsChanged|cursorUpdate|appKitDefined|systemDefined|applicationDefined|periodic|leftMouseDown|leftMouseUp|rightMouseDown|rightMouseUp|otherMouseDown|otherMouseUp|scrollWheel|none>"
}
let eventType: NSEvent.EventType?
switch token {
case "leftmousedragged":
eventType = .leftMouseDragged
case "rightmousedragged":
eventType = .rightMouseDragged
case "othermousedragged":
eventType = .otherMouseDragged
case "leftmousedown":
eventType = .leftMouseDown
case "leftmouseup":
eventType = .leftMouseUp
case "rightmousedown":
eventType = .rightMouseDown
case "rightmouseup":
eventType = .rightMouseUp
case "othermousedown":
eventType = .otherMouseDown
case "othermouseup":
eventType = .otherMouseUp
case "scrollwheel":
eventType = .scrollWheel
case "none":
eventType = nil
default:
let parsedEvent = parseOverlayEventType(token)
guard parsedEvent.isKnown else {
return "ERROR: Unknown event type '\(args.trimmingCharacters(in: .whitespacesAndNewlines))'"
}
let eventType = parsedEvent.eventType
var shouldCapture = false
DispatchQueue.main.sync {
@ -7312,6 +7302,98 @@ class TerminalController {
return shouldCapture ? "true" : "false"
}
private func portalHitGate(_ args: String) -> String {
let token = args.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
guard !token.isEmpty else {
return "ERROR: Usage: portal_hit_gate <leftMouseDragged|rightMouseDragged|otherMouseDragged|mouseMoved|mouseEntered|mouseExited|flagsChanged|cursorUpdate|appKitDefined|systemDefined|applicationDefined|periodic|leftMouseDown|leftMouseUp|rightMouseDown|rightMouseUp|otherMouseDown|otherMouseUp|scrollWheel|none>"
}
let parsedEvent = parseOverlayEventType(token)
guard parsedEvent.isKnown else {
return "ERROR: Unknown event type '\(args.trimmingCharacters(in: .whitespacesAndNewlines))'"
}
let eventType = parsedEvent.eventType
var shouldPassThrough = false
DispatchQueue.main.sync {
let pb = NSPasteboard(name: .drag)
shouldPassThrough = DragOverlayRoutingPolicy.shouldPassThroughPortalHitTesting(
pasteboardTypes: pb.types,
eventType: eventType
)
}
return shouldPassThrough ? "true" : "false"
}
private func sidebarOverlayGate(_ args: String) -> String {
let token = args.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
let hasSidebarDragState: Bool
switch token {
case "", "active":
hasSidebarDragState = true
case "inactive":
hasSidebarDragState = false
default:
return "ERROR: Usage: sidebar_overlay_gate [active|inactive]"
}
var shouldCapture = false
DispatchQueue.main.sync {
let pb = NSPasteboard(name: .drag)
shouldCapture = DragOverlayRoutingPolicy.shouldCaptureSidebarExternalOverlay(
hasSidebarDragState: hasSidebarDragState,
pasteboardTypes: pb.types
)
}
return shouldCapture ? "true" : "false"
}
private func parseOverlayEventType(_ token: String) -> (isKnown: Bool, eventType: NSEvent.EventType?) {
switch token {
case "leftmousedragged":
return (true, .leftMouseDragged)
case "rightmousedragged":
return (true, .rightMouseDragged)
case "othermousedragged":
return (true, .otherMouseDragged)
case "mousemove", "mousemoved":
return (true, .mouseMoved)
case "mouseentered":
return (true, .mouseEntered)
case "mouseexited":
return (true, .mouseExited)
case "flagschanged":
return (true, .flagsChanged)
case "cursorupdate":
return (true, .cursorUpdate)
case "appkitdefined":
return (true, .appKitDefined)
case "systemdefined":
return (true, .systemDefined)
case "applicationdefined":
return (true, .applicationDefined)
case "periodic":
return (true, .periodic)
case "leftmousedown":
return (true, .leftMouseDown)
case "leftmouseup":
return (true, .leftMouseUp)
case "rightmousedown":
return (true, .rightMouseDown)
case "rightmouseup":
return (true, .rightMouseUp)
case "othermousedown":
return (true, .otherMouseDown)
case "othermouseup":
return (true, .otherMouseUp)
case "scrollwheel":
return (true, .scrollWheel)
case "none":
return (true, nil)
default:
return (false, nil)
}
}
private func dragPasteboardType(from token: String) -> NSPasteboard.PasteboardType? {
let normalized = token.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
switch normalized {
@ -7371,6 +7453,71 @@ class TerminalController {
return result
}
/// Return the hit-test chain at normalized (0-1) coordinates in the main window's
/// content area. Used by regression tests to detect root-level drag destinations
/// shadowing pane-local Bonsplit drop targets.
private func dragHitChain(_ args: String) -> String {
let parts = args.split(separator: " ").map(String.init)
guard parts.count == 2,
let nx = Double(parts[0]), let ny = Double(parts[1]),
(0...1).contains(nx), (0...1).contains(ny) else {
return "ERROR: Usage: drag_hit_chain <x 0-1> <y 0-1>"
}
var result = "ERROR: No window"
DispatchQueue.main.sync {
guard let window = NSApp.mainWindow
?? NSApp.keyWindow
?? NSApp.windows.first(where: { win in
guard let raw = win.identifier?.rawValue else { return false }
return raw == "cmux.main" || raw.hasPrefix("cmux.main.")
}),
let contentView = window.contentView,
let themeFrame = contentView.superview else { return }
let pointInTheme = NSPoint(
x: contentView.frame.minX + (contentView.bounds.width * nx),
y: contentView.frame.maxY - (contentView.bounds.height * ny)
)
let overlay = objc_getAssociatedObject(window, &fileDropOverlayKey) as? NSView
if let overlay { overlay.isHidden = true }
defer { overlay?.isHidden = false }
guard let hit = themeFrame.hitTest(pointInTheme) else {
result = "none"
return
}
var chain: [String] = []
var current: NSView? = hit
var depth = 0
while let view = current, depth < 8 {
chain.append(debugDragHitViewDescriptor(view))
current = view.superview
depth += 1
}
result = chain.joined(separator: "->")
}
return result
}
private func debugDragHitViewDescriptor(_ view: NSView) -> String {
let className = String(describing: type(of: view))
let pointer = String(describing: Unmanaged.passUnretained(view).toOpaque())
let types = view.registeredDraggedTypes
let renderedTypes: String
if types.isEmpty {
renderedTypes = "-"
} else {
let raw = types.map(\.rawValue)
renderedTypes = raw.count <= 4
? raw.joined(separator: ",")
: raw.prefix(4).joined(separator: ",") + ",+\(raw.count - 4)"
}
return "\(className)@\(pointer){dragTypes=\(renderedTypes)}"
}
private func unescapeSocketText(_ input: String) -> String {
var out = ""
var escaping = false

View file

@ -21,12 +21,42 @@ private func portalDebugFrame(_ rect: NSRect) -> String {
final class WindowTerminalHostView: NSView {
override var isOpaque: Bool { false }
#if DEBUG
private var lastDragRouteSignature: String?
#endif
override func hitTest(_ point: NSPoint) -> NSView? {
if shouldPassThroughToSplitDivider(at: point) {
return nil
}
let dragPasteboardTypes = NSPasteboard(name: .drag).types
let eventType = NSApp.currentEvent?.type
let shouldPassThrough = DragOverlayRoutingPolicy.shouldPassThroughPortalHitTesting(
pasteboardTypes: dragPasteboardTypes,
eventType: eventType
)
if shouldPassThrough {
#if DEBUG
logDragRouteDecision(
passThrough: true,
eventType: eventType,
pasteboardTypes: dragPasteboardTypes,
hitView: nil
)
#endif
return nil
}
let hitView = super.hitTest(point)
#if DEBUG
logDragRouteDecision(
passThrough: false,
eventType: eventType,
pasteboardTypes: dragPasteboardTypes,
hitView: hitView
)
#endif
return hitView === self ? nil : hitView
}
@ -87,6 +117,65 @@ final class WindowTerminalHostView: NSView {
return false
}
#if DEBUG
private func logDragRouteDecision(
passThrough: Bool,
eventType: NSEvent.EventType?,
pasteboardTypes: [NSPasteboard.PasteboardType]?,
hitView: NSView?
) {
let hasRelevantTypes = DragOverlayRoutingPolicy.hasBonsplitTabTransfer(pasteboardTypes)
|| DragOverlayRoutingPolicy.hasSidebarTabReorder(pasteboardTypes)
guard passThrough || hasRelevantTypes else { return }
let targetClass = hitView.map { NSStringFromClass(type(of: $0)) } ?? "nil"
let signature = [
passThrough ? "1" : "0",
debugEventName(eventType),
debugPasteboardTypes(pasteboardTypes),
targetClass,
].joined(separator: "|")
guard lastDragRouteSignature != signature else { return }
lastDragRouteSignature = signature
dlog(
"portal.dragRoute passThrough=\(passThrough ? 1 : 0) " +
"event=\(debugEventName(eventType)) target=\(targetClass) " +
"types=\(debugPasteboardTypes(pasteboardTypes))"
)
}
private func debugPasteboardTypes(_ types: [NSPasteboard.PasteboardType]?) -> String {
guard let types, !types.isEmpty else { return "-" }
return types.map(\.rawValue).joined(separator: ",")
}
private func debugEventName(_ eventType: NSEvent.EventType?) -> String {
guard let eventType else { return "none" }
switch eventType {
case .cursorUpdate: return "cursorUpdate"
case .appKitDefined: return "appKitDefined"
case .systemDefined: return "systemDefined"
case .applicationDefined: return "applicationDefined"
case .periodic: return "periodic"
case .mouseMoved: return "mouseMoved"
case .mouseEntered: return "mouseEntered"
case .mouseExited: return "mouseExited"
case .flagsChanged: return "flagsChanged"
case .leftMouseDragged: return "leftMouseDragged"
case .rightMouseDragged: return "rightMouseDragged"
case .otherMouseDragged: return "otherMouseDragged"
case .leftMouseDown: return "leftMouseDown"
case .leftMouseUp: return "leftMouseUp"
case .rightMouseDown: return "rightMouseDown"
case .rightMouseUp: return "rightMouseUp"
case .otherMouseDown: return "otherMouseDown"
case .otherMouseUp: return "otherMouseUp"
default: return "other(\(eventType.rawValue))"
}
}
#endif
}
@MainActor