import SwiftUI import AppKit enum DJPasteboardPayload { static func song(_ pasteboard: NSPasteboard) -> SongDragReference? { guard let text = pasteboard.string(forType:.string),text.hasPrefix("KJMix.song.v1:"), let data = Data(base64Encoded:String(text.dropFirst("KJMix.song.v1:".count))) else { return nil } return try? JSONDecoder().decode(SongDragReference.self,from:data) } static func files(_ pasteboard: NSPasteboard) -> [URL] { (pasteboard.readObjects(forClasses:[NSURL.self],options:[.urlReadingFileURLsOnly:true]) as? [URL] ?? []).filter(\.isFileURL) } } struct DJLibraryDragTitle: NSViewRepresentable { let title: String let reference: SongDragReference let fontSize: Double let select: () -> Void var doubleClick: (() -> Void)? = nil var selectAll: (() -> Void)? = nil func makeNSView(context: Context) -> DJLibraryDragView { DJLibraryDragView() } func updateNSView(_ view: DJLibraryDragView,context: Context) { let redraw = view.title != title || view.fontSize != fontSize view.title = title;view.reference = reference;view.fontSize = fontSize;view.select = select;view.doubleClick = doubleClick;view.selectAllTracks = selectAll if redraw { view.toolTip = "Double-click to load alternating decks, or drag \(title) to either deck";view.setAccessibilityLabel(title);view.needsDisplay = true } } func sizeThatFits(_ proposal: ProposedViewSize,nsView: DJLibraryDragView,context: Context) -> CGSize? { .init(width:proposal.width ?? 240,height:24) } } final class DJLibraryDragView: NSView { var title = "" var reference: SongDragReference? var fontSize: Double = 11 var select: (() -> Void)? var doubleClick: (() -> Void)? var selectAllTracks: (() -> Void)? override var acceptsFirstResponder: Bool { true } @objc override func selectAll(_ sender: Any?) { selectAllTracks?() } private var armed = false private var dragging = false override init(frame: NSRect) { super.init(frame:frame);setAccessibilityElement(true);setAccessibilityRole(.staticText) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func acceptsFirstMouse(for event: NSEvent?) -> Bool { true } override func draw(_ dirtyRect: NSRect) { let paragraph = NSMutableParagraphStyle();paragraph.lineBreakMode = .byTruncatingTail let font = NSFont.systemFont(ofSize:fontSize,weight:.medium) let rect = NSRect(x:0,y:(bounds.height-ceil(font.ascender-font.descender))/2,width:bounds.width,height:ceil(font.ascender-font.descender)+2) (title as NSString).draw(in:rect,withAttributes:[.font:font,.foregroundColor:NSColor.white,.paragraphStyle:paragraph]) } override func mouseDown(with event: NSEvent) { window?.makeFirstResponder(self);armed = true;dragging = false } override func mouseDragged(with event: NSEvent) { guard armed else { return } dragging = true NSCursor.closedHand.set() for target in DJNativeDropView.targets.allObjects where target.window === window { let inside = !target.isHiddenOrHasHiddenAncestor && target.bounds.contains(target.convert(event.locationInWindow,from:nil)) target.layer?.borderWidth = inside ? 2 : 0 target.layer?.borderColor = target.tint.cgColor } } override func mouseUp(with event: NSEvent) { defer { armed = false;dragging = false;NSCursor.arrow.set() for target in DJNativeDropView.targets.allObjects { target.layer?.borderWidth = 0 } } guard dragging,let reference,let window else { select?();if armed,event.clickCount == 2 { doubleClick?() };return } for target in DJNativeDropView.targets.allObjects where target.window === window && !target.isHiddenOrHasHiddenAncestor { if target.bounds.contains(target.convert(event.locationInWindow,from:nil)),target.validSong?(reference) == true { let receive = target.acceptSong DispatchQueue.main.async { receive?(reference) } break } } } } struct DJDeckDropZone: NSViewRepresentable { let system: DJSystem let letter: String let content: Content init(system: DJSystem,letter: String,@ViewBuilder content: () -> Content) { self.system = system;self.letter = letter;self.content = content() } func makeNSView(context: Context) -> DJNativeDropView { let view = DJNativeDropView();updateNSView(view,context:context);return view } func updateNSView(_ view: DJNativeDropView,context: Context) { view.host.rootView = AnyView(content.environment(\.colorScheme,context.environment.colorScheme)) view.tint = letter == "A" ? .systemCyan : .systemOrange view.acceptSong = { [weak system] reference in guard let system,reference.library == system.dragSession,let song = system.songs.first(where:{$0.id == reference.song}) else { return } if letter == "Queue" { system.automix.enqueue([song]) } else { system.load(song,into:letter) } } view.acceptFiles = letter == "Queue" ? nil : { [weak system] urls in system?.requestFileLoad(urls,into:letter) } view.validSong = { [weak system] reference in reference.library == system?.dragSession } } func sizeThatFits(_ proposal: ProposedViewSize,nsView: DJNativeDropView,context: Context) -> CGSize? { .init(width:proposal.width ?? nsView.host.fittingSize.width,height:proposal.height ?? nsView.host.fittingSize.height) } } final class DJNativeDropView: NSView { static let targets = NSHashTable.weakObjects() let host = DJDropHostingView(rootView:AnyView(EmptyView())) var tint = NSColor.systemCyan var acceptSong: ((SongDragReference) -> Void)? var acceptFiles: (([URL]) -> Void)? var validSong: ((SongDragReference) -> Bool)? override init(frame: NSRect) { super.init(frame:frame);Self.targets.add(self);registerForDraggedTypes([.string,.fileURL]);wantsLayer = true host.dropTarget = self;host.registerForDraggedTypes([.string,.fileURL]) host.translatesAutoresizingMaskIntoConstraints = false;addSubview(host) NSLayoutConstraint.activate([host.leadingAnchor.constraint(equalTo:leadingAnchor),host.trailingAnchor.constraint(equalTo:trailingAnchor),host.topAnchor.constraint(equalTo:topAnchor),host.bottomAnchor.constraint(equalTo:bottomAnchor)]) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } private func accepts(_ pasteboard: NSPasteboard) -> Bool { if let song = DJPasteboardPayload.song(pasteboard) { return validSong?(song) == true } return !DJPasteboardPayload.files(pasteboard).isEmpty } override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation { let allowed = accepts(sender.draggingPasteboard) layer?.borderWidth = allowed ? 2 : 0;layer?.borderColor = tint.cgColor return allowed ? .copy : [] } override func draggingUpdated(_ sender: NSDraggingInfo) -> NSDragOperation { accepts(sender.draggingPasteboard) ? .copy : [] } override func draggingExited(_ sender: NSDraggingInfo?) { layer?.borderWidth = 0 } override func prepareForDragOperation(_ sender: NSDraggingInfo) -> Bool { accepts(sender.draggingPasteboard) } override func performDragOperation(_ sender: NSDraggingInfo) -> Bool { layer?.borderWidth = 0 if let song = DJPasteboardPayload.song(sender.draggingPasteboard),validSong?(song) == true { let receive = acceptSong // Finish the native dragging session before a load rebuilds any SwiftUI rows. DispatchQueue.main.async { receive?(song) };return true } let files = DJPasteboardPayload.files(sender.draggingPasteboard) guard !files.isEmpty else { return false } let receive = acceptFiles;DispatchQueue.main.async { receive?(files) };return true } override func concludeDragOperation(_ sender: NSDraggingInfo?) { layer?.borderWidth = 0 } } // NSHostingView participates in AppKit drag destination lookup itself. Forward // its callbacks so embedded SwiftUI content cannot shadow the deck destination. final class DJDropHostingView: NSHostingView { weak var dropTarget: DJNativeDropView? override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation { dropTarget?.draggingEntered(sender) ?? [] } override func draggingUpdated(_ sender: NSDraggingInfo) -> NSDragOperation { dropTarget?.draggingUpdated(sender) ?? [] } override func draggingExited(_ sender: NSDraggingInfo?) { dropTarget?.draggingExited(sender) } override func prepareForDragOperation(_ sender: NSDraggingInfo) -> Bool { dropTarget?.prepareForDragOperation(sender) ?? false } override func performDragOperation(_ sender: NSDraggingInfo) -> Bool { dropTarget?.performDragOperation(sender) ?? false } override func concludeDragOperation(_ sender: NSDraggingInfo?) { dropTarget?.concludeDragOperation(sender) } }