import SwiftUI
import AppKit
import QuartzCore
final class AppDragCoordinator {
static let shared = AppDragCoordinator()
struct DropTarget {
weak var view: NSView?
var tag: String
var onDrop: (String, String, Bool) -> Void
}
private var targets: [UUID: DropTarget] = [:]
private weak var dragHostWindow: NSWindow?
private weak var dragLayerHostView: NSView?
private var dragLayer: CALayer?
private var normalDragImage: CGImage?
private var copyDragImage: CGImage?
private var currentCopyMode = false
private var dragWindow: NSWindow?
private var dragImageSize: NSSize = .zero
private var activePayload = ""
private init() {}
func register(id: UUID, view: NSView, tag: String, onDrop: @escaping (String, String, Bool) -> Void) {
targets[id] = DropTarget(view: view, tag: tag, onDrop: onDrop)
}
func unregister(id: UUID) {
targets.removeValue(forKey: id)
}
func beginDrag(image: NSImage, payload: String, at screenPoint: NSPoint, copy: Bool, in hostWindow: NSWindow?) {
endDragVisuals()
activePayload = payload
dragImageSize = image.size
dragHostWindow = hostWindow
currentCopyMode = copy
normalDragImage = Self.cgImage(from: image)
copyDragImage = Self.cgImage(from: Self.copyBadgeImage(from: image))
let hostView: NSView
if let contentView = hostWindow?.contentView {
hostView = contentView
} else {
let panel = NSPanel(
contentRect: NSRect(origin: .zero, size: image.size),
styleMask: [.borderless, .nonactivatingPanel],
backing: .buffered,
defer: false
)
panel.isOpaque = false
panel.backgroundColor = .clear
panel.hasShadow = false
panel.isFloatingPanel = true
panel.hidesOnDeactivate = false
panel.ignoresMouseEvents = true
panel.level = NSWindow.Level(rawValue: Int(CGWindowLevelForKey(.maximumWindow)))
panel.collectionBehavior = [.moveToActiveSpace, .fullScreenAuxiliary, .stationary, .transient, .ignoresCycle]
panel.isReleasedWhenClosed = false
let contentView = NSView(frame: NSRect(origin: .zero, size: image.size))
panel.contentView = contentView
dragWindow = panel
dragHostWindow = panel
hostView = contentView
panel.setFrameOrigin(NSPoint(x: screenPoint.x - image.size.width / 2, y: screenPoint.y - image.size.height / 2))
panel.orderFrontRegardless()
}
hostView.wantsLayer = true
guard let rootLayer = hostView.layer else { return }
let layer = CALayer()
layer.bounds = CGRect(origin: .zero, size: image.size)
layer.anchorPoint = CGPoint(x: 0.5, y: 0.5)
layer.contentsGravity = .resizeAspect
layer.contentsScale = NSScreen.main?.backingScaleFactor ?? 2
layer.contents = copy ? copyDragImage : normalDragImage
layer.zPosition = 1_000_000
layer.actions = [
"position": NSNull(),
"contents": NSNull(),
"bounds": NSNull(),
"opacity": NSNull()
]
rootLayer.addSublayer(layer)
dragLayerHostView = hostView
dragLayer = layer
updateDragLocation(screenPoint)
}
func updateDragLocation(_ screenPoint: NSPoint, copy: Bool? = nil) {
if let dragLayer, let dragHostWindow, let hostView = dragLayerHostView {
if let copy {
updateCopyMode(copy)
}
let windowPoint = dragHostWindow.convertPoint(fromScreen: screenPoint)
let contentPoint = hostView.convert(windowPoint, from: nil)
CATransaction.begin()
CATransaction.setDisableActions(true)
dragLayer.position = contentPoint
CATransaction.commit()
return
}
guard let dragWindow else { return }
let origin = NSPoint(
x: screenPoint.x - dragImageSize.width / 2,
y: screenPoint.y - dragImageSize.height / 2
)
dragWindow.setFrameOrigin(origin)
}
func finishDrag(at screenPoint: NSPoint, copy: Bool) {
defer { endDragVisuals() }
let parts = activePayload.components(separatedBy: "\n")
guard let path = parts.first, !path.isEmpty else { return }
let source = parts.dropFirst().first ?? ""
let hitTarget = targets.values
.compactMap { target -> (DropTarget, CGFloat)? in
guard let frame = target.view?.screenFrame(), frame.contains(screenPoint) else { return nil }
return (target, frame.width * frame.height)
}
.sorted { $0.1 < $1.1 }
.first?.0
hitTarget?.onDrop(path, source, copy)
}
func cancelDrag() {
endDragVisuals()
}
private func updateCopyMode(_ copy: Bool) {
guard currentCopyMode != copy else { return }
currentCopyMode = copy
CATransaction.begin()
CATransaction.setDisableActions(true)
dragLayer?.contents = copy ? copyDragImage : normalDragImage
CATransaction.commit()
}
private func endDragVisuals() {
dragLayer?.removeFromSuperlayer()
dragLayer = nil
dragLayerHostView = nil
dragHostWindow = nil
normalDragImage = nil
copyDragImage = nil
currentCopyMode = false
dragWindow?.orderOut(nil)
dragWindow = nil
activePayload = ""
dragImageSize = .zero
}
private static func cgImage(from image: NSImage) -> CGImage? {
var rect = NSRect(origin: .zero, size: image.size)
return image.cgImage(forProposedRect: &rect, context: nil, hints: [
.interpolation: NSImageInterpolation.high
])
}
private static func copyBadgeImage(from baseImage: NSImage) -> NSImage {
let image = NSImage(size: baseImage.size)
image.lockFocus()
NSGraphicsContext.current?.imageInterpolation = .high
baseImage.draw(in: NSRect(origin: .zero, size: baseImage.size), from: .zero, operation: .sourceOver, fraction: 1)
let bounds = NSRect(origin: .zero, size: baseImage.size)
let badgeSize = min(bounds.width, bounds.height) * 0.28
let badgeRect = NSRect(
x: bounds.maxX - badgeSize - badgeSize * 0.22,
y: bounds.maxY - badgeSize - badgeSize * 0.22,
width: badgeSize,
height: badgeSize
)
NSGraphicsContext.saveGraphicsState()
let shadow = NSShadow()
shadow.shadowColor = NSColor.black.withAlphaComponent(0.30)
shadow.shadowBlurRadius = 8
shadow.shadowOffset = NSSize(width: 0, height: -2)
shadow.set()
NSColor.systemGreen.setFill()
NSBezierPath(ovalIn: badgeRect).fill()
NSGraphicsContext.restoreGraphicsState()
let plus = "+"
let attrs: [NSAttributedString.Key: Any] = [
.font: NSFont.systemFont(ofSize: badgeSize * 0.78, weight: .bold),
.foregroundColor: NSColor.white
]
let plusSize = plus.size(withAttributes: attrs)
plus.draw(
at: NSPoint(
x: badgeRect.midX - plusSize.width / 2,
y: badgeRect.midY - plusSize.height / 2 + badgeSize * 0.03
),
withAttributes: attrs
)
image.unlockFocus()
return image
}
}
private extension NSView {
func screenFrame() -> NSRect? {
guard let window else { return nil }
let rectInWindow = convert(bounds, to: nil)
return window.convertToScreen(rectInWindow)
}
}
struct AppDropTargetView: NSViewRepresentable {
let targetTag: String
let onDropApp: (String, String, Bool) -> Void
func makeNSView(context: Context) -> AppDropTargetNSView {
let view = AppDropTargetNSView()
view.targetTag = targetTag
view.onDropApp = onDropApp
return view
}
func updateNSView(_ view: AppDropTargetNSView, context: Context) {
view.targetTag = targetTag
view.onDropApp = onDropApp
view.registerTarget()
}
static func dismantleNSView(_ view: AppDropTargetNSView, coordinator: ()) {
AppDragCoordinator.shared.unregister(id: view.id)
}
}
final class AppDropTargetNSView: NSView {
let id = UUID()
var targetTag = ""
var onDropApp: ((String, String, Bool) -> Void)?
override func viewDidMoveToWindow() {
super.viewDidMoveToWindow()
registerTarget()
}
override func layout() {
super.layout()
registerTarget()
}
func registerTarget() {
guard window != nil, let onDropApp else { return }
AppDragCoordinator.shared.register(id: id, view: self, tag: targetTag, onDrop: onDropApp)
}
}