From 71bfdd4087d0b4d94bd5345cb45bb335617d737c Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Thu, 28 May 2026 15:27:05 +0800
Subject: [PATCH] Confirm single-tag removal on empty grid drop

---
 Apptag/AppDragCoordinator.swift |   48 +++++++++++++++++++++++++++++++++++++++++++++++-
 1 files changed, 47 insertions(+), 1 deletions(-)

diff --git a/Apptag/AppDragCoordinator.swift b/Apptag/AppDragCoordinator.swift
index 070e2ad..e767f95 100644
--- a/Apptag/AppDragCoordinator.swift
+++ b/Apptag/AppDragCoordinator.swift
@@ -10,7 +10,12 @@
         var tag: String
     }
 
+    struct EmptyDropTarget {
+        weak var view: AppEmptyDropReceivingView?
+    }
+
     private var targets: [UUID: DropTarget] = [:]
+    private var emptyTargets: [UUID: EmptyDropTarget] = [:]
     private weak var dragHostWindow: NSWindow?
     private weak var dragLayerHostView: NSView?
     private var dragLayer: CALayer?
@@ -39,6 +44,17 @@
 
     func unregister(id: UUID) {
         targets.removeValue(forKey: id)
+    }
+
+    func registerEmptyDropTarget(id: UUID, view: AppEmptyDropReceivingView) {
+        if emptyTargets.count > 64 {
+            pruneDeadTargets()
+        }
+        emptyTargets[id] = EmptyDropTarget(view: view)
+    }
+
+    func unregisterEmptyDropTarget(id: UUID) {
+        emptyTargets.removeValue(forKey: id)
     }
 
     func beginDrag(image: NSImage, payload: String, at screenPoint: NSPoint, copy: Bool, in hostWindow: NSWindow?) {
@@ -142,7 +158,23 @@
             .sorted { $0.1 < $1.1 }
             .first?.0
 
-        hitTarget?.performDrop(path: path, source: source, copy: copy)
+        if let hitTarget {
+            hitTarget.performDrop(path: path, source: source, copy: copy)
+            return
+        }
+
+        let emptyTarget = emptyTargets.values
+            .compactMap { target -> (AppEmptyDropReceivingView, CGFloat)? in
+                guard let view = target.view,
+                      let frame = view.screenFrame(),
+                      frame.contains(screenPoint)
+                else { return nil }
+                return (view, frame.width * frame.height)
+            }
+            .sorted { $0.1 < $1.1 }
+            .first?.0
+
+        emptyTarget?.performEmptyDrop(path: path, source: source, screenPoint: screenPoint, copy: copy)
     }
 
     func cancelDrag() {
@@ -174,6 +206,7 @@
 
     private func pruneDeadTargets() {
         targets = targets.filter { $0.value.view != nil }
+        emptyTargets = emptyTargets.filter { $0.value.view != nil }
     }
 
     private static func cgImage(from image: NSImage) -> CGImage? {
@@ -231,6 +264,11 @@
     func performDrop(path: String, source: String, copy: Bool)
 }
 
+protocol AppEmptyDropReceivingView: AnyObject {
+    func screenFrame() -> NSRect?
+    func performEmptyDrop(path: String, source: String, screenPoint: NSPoint, copy: Bool)
+}
+
 extension AppDropTargetReceivingView where Self: NSView {
     func screenFrame() -> NSRect? {
         guard let window else { return nil }
@@ -239,6 +277,14 @@
     }
 }
 
+extension AppEmptyDropReceivingView where Self: 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

--
Gitblit v1.9.3