From e13cd43ebda2ecd0e71e82a7c3d7e0da270f9702 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 28 Dec 2025 15:04:27 -0800 Subject: [PATCH] macos: convert the transferable to a nsdraggingitem --- macos/Ghostty.xcodeproj/project.pbxproj | 1 + .../Surface View/SurfaceDragSource.swift | 6 +- .../Extensions/Transferable+Extension.swift | 58 +++++++++++++++++++ 3 files changed, 61 insertions(+), 4 deletions(-) create mode 100644 macos/Sources/Helpers/Extensions/Transferable+Extension.swift diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index cc15248a8..a3abaa4a0 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -168,6 +168,7 @@ "Helpers/Extensions/NSView+Extension.swift", "Helpers/Extensions/NSWindow+Extension.swift", "Helpers/Extensions/NSWorkspace+Extension.swift", + "Helpers/Extensions/Transferable+Extension.swift", "Helpers/Extensions/UndoManager+Extension.swift", "Helpers/Extensions/View+Extension.swift", Helpers/Fullscreen.swift, diff --git a/macos/Sources/Ghostty/Surface View/SurfaceDragSource.swift b/macos/Sources/Ghostty/Surface View/SurfaceDragSource.swift index 534834af3..e40a1fc9b 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceDragSource.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceDragSource.swift @@ -129,10 +129,8 @@ extension Ghostty { override func mouseDragged(with event: NSEvent) { guard !isTracking, let surfaceView = surfaceView else { return } - // Create the pasteboard item with the surface ID - let data = withUnsafeBytes(of: surfaceView.id.uuid) { Data($0) } - let pasteboardItem = NSPasteboardItem() - pasteboardItem.setData(data, forType: .ghosttySurfaceId) + // Create our dragging item from our transferable + guard let pasteboardItem = surfaceView.pasteboardItem() else { return } let item = NSDraggingItem(pasteboardWriter: pasteboardItem) // Create a scaled preview image from the surface snapshot diff --git a/macos/Sources/Helpers/Extensions/Transferable+Extension.swift b/macos/Sources/Helpers/Extensions/Transferable+Extension.swift new file mode 100644 index 000000000..3bcc9057f --- /dev/null +++ b/macos/Sources/Helpers/Extensions/Transferable+Extension.swift @@ -0,0 +1,58 @@ +import AppKit +import CoreTransferable +import UniformTypeIdentifiers + +extension Transferable { + /// Converts this Transferable to an NSPasteboardItem with lazy data loading. + /// Data is only fetched when the pasteboard consumer requests it. This allows + /// bridging a Transferable to NSDraggingSource. + func pasteboardItem() -> NSPasteboardItem? { + let itemProvider = NSItemProvider() + itemProvider.register(self) + + let types = itemProvider.registeredTypeIdentifiers.compactMap { UTType($0) } + guard !types.isEmpty else { return nil } + + let item = NSPasteboardItem() + let dataProvider = TransferableDataProvider(itemProvider: itemProvider) + let pasteboardTypes = types.map { NSPasteboard.PasteboardType($0.identifier) } + item.setDataProvider(dataProvider, forTypes: pasteboardTypes) + + return item + } +} + +private final class TransferableDataProvider: NSObject, NSPasteboardItemDataProvider { + private let itemProvider: NSItemProvider + + init(itemProvider: NSItemProvider) { + self.itemProvider = itemProvider + super.init() + } + + func pasteboard( + _ pasteboard: NSPasteboard?, + item: NSPasteboardItem, + provideDataForType type: NSPasteboard.PasteboardType + ) { + // NSPasteboardItemDataProvider requires synchronous data return, but + // NSItemProvider.loadDataRepresentation is async. We use a semaphore + // to block until the async load completes. This is safe because AppKit + // calls this method on a background thread during drag operations. + let semaphore = DispatchSemaphore(value: 0) + + var result: Data? + itemProvider.loadDataRepresentation(forTypeIdentifier: type.rawValue) { data, _ in + result = data + semaphore.signal() + } + + // Wait for the data to load + semaphore.wait() + + // Set it. I honestly don't know what happens here if this fails. + if let data = result { + item.setData(data, forType: type) + } + } +}