Issue #951

Create a custom NSView that handles mouseDragged to beginDraggingSession

struct DraggingSourceViewWrapper: NSViewRepresentable {
    let fileUrls: [URL]
    let onEnd: () -> Void
    
    func makeNSView(context: Context) -> DraggingSourceView {
        let view = DraggingSourceView(fileUrls: fileUrls)
        view.onEnd = onEnd
        return view
    }
    
    func updateNSView(_ view: DraggingSourceView, context: Context) {
        view.onEnd = onEnd
    }
}

final class DraggingSourceView: NSView {
    let fileUrls: [URL]
    var onEnd: (() -> Void)?
    
    init(fileUrls: [URL]) {
        self.fileUrls = fileUrls
        super.init(frame: .zero)
    }
    
    required init?(coder: NSCoder) {
        fatalError()
    }
    
    override func mouseDown(with event: NSEvent) {
        // NOTE: Needed for mouseDragged to work
    }
    
    override func mouseDragged(with event: NSEvent) {
        let draggingItems = fileUrls.map { url in
            let pasteboardItem = NSPasteboardItem()
            pasteboardItem.setString(url.absoluteString, forType: .string)
            pasteboardItem.setDataProvider(self, forTypes: [.fileURL])
            
            let image = NSWorkspace.shared.icon(forFile: url.path)
            
            let draggingItem = NSDraggingItem(pasteboardWriter: pasteboardItem)
            draggingItem.setDraggingFrame(bounds, contents: image)
            return draggingItem
        }
        
        beginDraggingSession(with: draggingItems, event: event, source: self)
    }
}

extension DraggingSourceView: NSDraggingSource {
    func draggingSession(
        _ session: NSDraggingSession,
        sourceOperationMaskFor context: NSDraggingContext
    ) -> NSDragOperation {
        switch context {
        case .outsideApplication:
            return .move
            
        default:
            return .private
        }
    }
    
    func draggingSession(_ session: NSDraggingSession, endedAt screenPoint: NSPoint, operation: NSDragOperation) {
        onEnd?()
    }
}

extension DraggingSourceView: NSPasteboardItemDataProvider {
    nonisolated func pasteboard(
        _ pasteboard: NSPasteboard?,
        item: NSPasteboardItem,
        provideDataForType type: NSPasteboard.PasteboardType
    ) {
        guard
            let string = item.string(forType: .string),
            let url = URL(string: string)
        else { return }
        
        if let url = BookmarkService.shared.resolve(url: url) {
            url.access {
                let data = url.absoluteString.data(using: .utf8) ?? Data()
                item.setData(data, forType: .fileURL)
            }
        }
    }
}