diff --git a/Sources/ContentView.swift b/Sources/ContentView.swift
index e5b5bea3..7e77f333 100644
--- a/Sources/ContentView.swift
+++ b/Sources/ContentView.swift
@@ -3,6 +3,7 @@ import Bonsplit
import SwiftUI
import ObjectiveC
import UniformTypeIdentifiers
+import WebKit
struct ShortcutHintPillBackground: View {
var emphasis: Double = 1.0
@@ -172,6 +173,9 @@ final class FileDropOverlayView: NSView {
/// Fallback handler when no terminal is found under the drop point.
var onDrop: (([URL]) -> Bool)?
private var isForwardingMouseEvent = false
+ /// The WKWebView currently receiving forwarded drag events, so we can
+ /// synthesize draggingExited/draggingEntered as the cursor moves.
+ private weak var activeDragWebView: WKWebView?
override var acceptsFirstResponder: Bool { false }
@@ -248,30 +252,82 @@ final class FileDropOverlayView: NSView {
override func otherMouseDragged(with event: NSEvent) { forwardEvent(event) }
override func scrollWheel(with event: NSEvent) { forwardEvent(event) }
- // MARK: NSDraggingDestination – only accept file drops over terminal views.
+ // MARK: NSDraggingDestination – accept file drops over terminal and browser views.
+ //
+ // AppKit sends draggingEntered once when the drag enters this overlay, then
+ // draggingUpdated as the cursor moves within it. We track which WKWebView (if
+ // any) is under the cursor and synthesize enter/exit calls so the browser's
+ // HTML5 drag events (dragenter, dragleave, drop) fire correctly.
override func draggingEntered(_ sender: any NSDraggingInfo) -> NSDragOperation {
- return dragOperationForSender(sender)
+ return updateDragTarget(sender)
}
override func draggingUpdated(_ sender: any NSDraggingInfo) -> NSDragOperation {
- return dragOperationForSender(sender)
+ return updateDragTarget(sender)
+ }
+
+ override func draggingExited(_ sender: (any NSDraggingInfo)?) {
+ if let prev = activeDragWebView {
+ prev.draggingExited(sender)
+ activeDragWebView = nil
+ }
}
override func performDragOperation(_ sender: any NSDraggingInfo) -> Bool {
+ let webView = activeDragWebView
+ activeDragWebView = nil
+ if let webView {
+ return webView.performDragOperation(sender)
+ }
guard let terminal = terminalUnderPoint(sender.draggingLocation) else { return false }
return terminal.performDragOperation(sender)
}
- private func dragOperationForSender(_ sender: any NSDraggingInfo) -> NSDragOperation {
+ private func updateDragTarget(_ sender: any NSDraggingInfo) -> NSDragOperation {
+ let loc = sender.draggingLocation
+ let webView = webViewUnderPoint(loc)
+
+ // Cursor moved away from the previous web view.
+ if let prev = activeDragWebView, prev !== webView {
+ prev.draggingExited(sender)
+ activeDragWebView = nil
+ }
+
+ if let webView {
+ if activeDragWebView !== webView {
+ // Cursor entered a (new) web view — send draggingEntered.
+ activeDragWebView = webView
+ return webView.draggingEntered(sender)
+ }
+ return webView.draggingUpdated(sender)
+ }
+
+ // Over a terminal (or nothing).
guard let types = sender.draggingPasteboard.types,
types.contains(.fileURL),
- terminalUnderPoint(sender.draggingLocation) != nil else {
+ terminalUnderPoint(loc) != nil else {
return []
}
return .copy
}
+ /// Hit-tests the window to find a WKWebView (browser panel) under the cursor.
+ private func webViewUnderPoint(_ windowPoint: NSPoint) -> WKWebView? {
+ guard let window, let contentView = window.contentView else { return nil }
+ isHidden = true
+ defer { isHidden = false }
+ let point = contentView.convert(windowPoint, from: nil)
+ let hitView = contentView.hitTest(point)
+
+ var current: NSView? = hitView
+ while let view = current {
+ if let webView = view as? WKWebView { return webView }
+ current = view.superview
+ }
+ return nil
+ }
+
/// Hit-tests the window to find the GhosttyNSView under the cursor.
func terminalUnderPoint(_ windowPoint: NSPoint) -> GhosttyNSView? {
if let window,
diff --git a/Sources/Panels/BrowserPanel.swift b/Sources/Panels/BrowserPanel.swift
index 59c23491..f2b86d0c 100644
--- a/Sources/Panels/BrowserPanel.swift
+++ b/Sources/Panels/BrowserPanel.swift
@@ -2183,4 +2183,20 @@ private class BrowserUIDelegate: NSObject, WKUIDelegate {
}
return nil
}
+
+ /// Handle elements by presenting the native file picker.
+ func webView(
+ _ webView: WKWebView,
+ runOpenPanelWith parameters: WKOpenPanelParameters,
+ initiatedByFrame frame: WKFrameInfo,
+ completionHandler: @escaping ([URL]?) -> Void
+ ) {
+ let panel = NSOpenPanel()
+ panel.allowsMultipleSelection = parameters.allowsMultipleSelection
+ panel.canChooseDirectories = parameters.allowsDirectories
+ panel.canChooseFiles = true
+ panel.begin { result in
+ completionHandler(result == .OK ? panel.urls : nil)
+ }
+ }
}
diff --git a/Sources/Panels/CmuxWebView.swift b/Sources/Panels/CmuxWebView.swift
index e0131b9f..08843c0f 100644
--- a/Sources/Panels/CmuxWebView.swift
+++ b/Sources/Panels/CmuxWebView.swift
@@ -117,10 +117,21 @@ final class CmuxWebView: WKWebView {
// of SwiftUI's sibling .onDrop overlays. Rejecting in draggingEntered doesn't help because
// AppKit only bubbles up through superviews, not siblings.
//
- // Fix: prevent WKWebView from registering as a drag destination entirely. AppKit won't
- // route drags here, so they reach the SwiftUI overlay drop zones as intended.
+ // Fix: filter out text-based types that conflict with bonsplit tab drags, but keep
+ // file URL types so Finder file drops and HTML drag-and-drop work.
+ private static let blockedDragTypes: Set = [
+ .string, // public.utf8-plain-text — matches bonsplit's NSString tab drags
+ NSPasteboard.PasteboardType("public.text"),
+ NSPasteboard.PasteboardType("public.plain-text"),
+ NSPasteboard.PasteboardType("com.splittabbar.tabtransfer"),
+ NSPasteboard.PasteboardType("com.cmux.sidebar-tab-reorder"),
+ ]
+
override func registerForDraggedTypes(_ newTypes: [NSPasteboard.PasteboardType]) {
- // No-op: suppress WKWebView's automatic drag type registration.
+ let filtered = newTypes.filter { !Self.blockedDragTypes.contains($0) }
+ if !filtered.isEmpty {
+ super.registerForDraggedTypes(filtered)
+ }
}
override func willOpenMenu(_ menu: NSMenu, with event: NSEvent) {