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
51 changes: 32 additions & 19 deletions Sources/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ final class SidebarState: ObservableObject {
/// embedded terminal views. This overlay sits above the entire content view hierarchy and
/// intercepts file drags, forwarding drops to the GhosttyNSView under the cursor.
///
/// Mouse events are forwarded to the views below via a hide-send-unhide pattern so clicks,
/// Mouse events are forwarded to the view below the overlay via hit-testing so clicks,
/// scrolls, and other interactions pass through normally.
final class FileDropOverlayView: NSView {
/// Fallback handler when no terminal is found under the drop point.
Expand Down Expand Up @@ -194,25 +194,32 @@ final class FileDropOverlayView: NSView {
return nil
}

// MARK: Mouse forwarding — safety net for the rare case where stale drag pasteboard
// data causes hitTest to return self when no drag is actually active.
// MARK: Mouse forwarding – resolve the view under the overlay and dispatch directly.
//
// Calling the target view's mouseDown can propagate back through AppKit's event dispatch
// (NSWindow.sendEvent → _reallySendEvent) which hit-tests the overlay again, causing
// infinite recursion / stack overflow. The re-entrancy guard breaks the cycle.

private func forwardEvent(_ event: NSEvent) {
isHidden = true
window?.sendEvent(event)
isHidden = false
private var isForwarding = false

private func forwardEvent(_ event: NSEvent, dispatch: (NSView, NSEvent) -> Void) {
guard !isForwarding else { return }
isForwarding = true
defer { isForwarding = false }
guard let target = viewUnderOverlay(at: event.locationInWindow) else { return }
dispatch(target, event)
}

override func mouseDown(with event: NSEvent) { forwardEvent(event) }
override func mouseUp(with event: NSEvent) { forwardEvent(event) }
override func mouseDragged(with event: NSEvent) { forwardEvent(event) }
override func rightMouseDown(with event: NSEvent) { forwardEvent(event) }
override func rightMouseUp(with event: NSEvent) { forwardEvent(event) }
override func rightMouseDragged(with event: NSEvent) { forwardEvent(event) }
override func otherMouseDown(with event: NSEvent) { forwardEvent(event) }
override func otherMouseUp(with event: NSEvent) { forwardEvent(event) }
override func otherMouseDragged(with event: NSEvent) { forwardEvent(event) }
override func scrollWheel(with event: NSEvent) { forwardEvent(event) }
override func mouseDown(with event: NSEvent) { forwardEvent(event) { $0.mouseDown(with: $1) } }
override func mouseUp(with event: NSEvent) { forwardEvent(event) { $0.mouseUp(with: $1) } }
override func mouseDragged(with event: NSEvent) { forwardEvent(event) { $0.mouseDragged(with: $1) } }
override func rightMouseDown(with event: NSEvent) { forwardEvent(event) { $0.rightMouseDown(with: $1) } }
override func rightMouseUp(with event: NSEvent) { forwardEvent(event) { $0.rightMouseUp(with: $1) } }
override func rightMouseDragged(with event: NSEvent) { forwardEvent(event) { $0.rightMouseDragged(with: $1) } }
override func otherMouseDown(with event: NSEvent) { forwardEvent(event) { $0.otherMouseDown(with: $1) } }
override func otherMouseUp(with event: NSEvent) { forwardEvent(event) { $0.otherMouseUp(with: $1) } }
override func otherMouseDragged(with event: NSEvent) { forwardEvent(event) { $0.otherMouseDragged(with: $1) } }
override func scrollWheel(with event: NSEvent) { forwardEvent(event) { $0.scrollWheel(with: $1) } }

// MARK: NSDraggingDestination – only accept file drops over terminal views.

Expand All @@ -238,14 +245,20 @@ final class FileDropOverlayView: NSView {
return .copy
}

/// Temporarily hides self, hit-tests the window to find the GhosttyNSView under the cursor.
private func terminalUnderPoint(_ windowPoint: NSPoint) -> GhosttyNSView? {
/// Temporarily hides self and hit-tests content to find the view under the cursor.
private func viewUnderOverlay(at windowPoint: NSPoint) -> NSView? {
guard let window, let contentView = window.contentView,
let themeFrame = contentView.superview else { return nil }
isHidden = true
let point = themeFrame.convert(windowPoint, from: nil)
let hitView = contentView.hitTest(point)
isHidden = false
return hitView
}

/// Temporarily hides self, hit-tests the window to find the GhosttyNSView under the cursor.
private func terminalUnderPoint(_ windowPoint: NSPoint) -> GhosttyNSView? {
guard let hitView = viewUnderOverlay(at: windowPoint) else { return nil }

var current: NSView? = hitView
while let view = current {
Expand Down
2 changes: 1 addition & 1 deletion homebrew-cmux
Submodule homebrew-cmux updated 1 files
+4 −4 Casks/cmux.rb