From efbfc27e0c813c31b570d72fded40eb91d07f2db Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Thu, 28 May 2026 21:01:19 +0800
Subject: [PATCH] Add AppKit tag navigation reordering

---
 Apptag/TagNavigationView.swift |  165 +++++++++++++++++++++++++++++++++++++++++++++++++++---
 1 files changed, 154 insertions(+), 11 deletions(-)

diff --git a/Apptag/TagNavigationView.swift b/Apptag/TagNavigationView.swift
index 2736f53..9f9941f 100644
--- a/Apptag/TagNavigationView.swift
+++ b/Apptag/TagNavigationView.swift
@@ -16,8 +16,14 @@
     let items: [TagNavigationItem]
     let orientation: Orientation
     let contentInsets: NSEdgeInsets
+    let dragModeActive: Bool
+    let draggingItemID: String?
     let onActivate: (String) -> Void
     let onHoverChange: (String, Bool) -> Void
+    let canReorder: (String) -> Bool
+    let onReorderBegan: (String) -> Void
+    let onReorderMoved: (String, String) -> Void
+    let onReorderEnded: () -> Void
 
     func makeNSView(context: Context) -> TagNavigationHostView {
         let view = TagNavigationHostView()
@@ -25,8 +31,14 @@
             items: items,
             orientation: orientation,
             contentInsets: contentInsets,
+            dragModeActive: dragModeActive,
+            draggingItemID: draggingItemID,
             onActivate: onActivate,
-            onHoverChange: onHoverChange
+            onHoverChange: onHoverChange,
+            canReorder: canReorder,
+            onReorderBegan: onReorderBegan,
+            onReorderMoved: onReorderMoved,
+            onReorderEnded: onReorderEnded
         )
         return view
     }
@@ -36,8 +48,14 @@
             items: items,
             orientation: orientation,
             contentInsets: contentInsets,
+            dragModeActive: dragModeActive,
+            draggingItemID: draggingItemID,
             onActivate: onActivate,
-            onHoverChange: onHoverChange
+            onHoverChange: onHoverChange,
+            canReorder: canReorder,
+            onReorderBegan: onReorderBegan,
+            onReorderMoved: onReorderMoved,
+            onReorderEnded: onReorderEnded
         )
     }
 }
@@ -62,15 +80,27 @@
         items: [TagNavigationItem],
         orientation: TagNavigationView.Orientation,
         contentInsets: NSEdgeInsets,
+        dragModeActive: Bool,
+        draggingItemID: String?,
         onActivate: @escaping (String) -> Void,
-        onHoverChange: @escaping (String, Bool) -> Void
+        onHoverChange: @escaping (String, Bool) -> Void,
+        canReorder: @escaping (String) -> Bool,
+        onReorderBegan: @escaping (String) -> Void,
+        onReorderMoved: @escaping (String, String) -> Void,
+        onReorderEnded: @escaping () -> Void
     ) {
         documentView.update(
             items: items,
             orientation: orientation,
             contentInsets: contentInsets,
+            dragModeActive: dragModeActive,
+            draggingItemID: draggingItemID,
             onActivate: onActivate,
-            onHoverChange: onHoverChange
+            onHoverChange: onHoverChange,
+            canReorder: canReorder,
+            onReorderBegan: onReorderBegan,
+            onReorderMoved: onReorderMoved,
+            onReorderEnded: onReorderEnded
         )
         scrollView.hasHorizontalScroller = orientation == .horizontal
         scrollView.hasVerticalScroller = orientation == .vertical
@@ -108,8 +138,14 @@
     private var orientation: TagNavigationView.Orientation = .horizontal
     private var contentInsets = NSEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
     private var buttons: [TagNavigationButton] = []
+    private var dragModeActive = false
+    private var draggingItemID: String?
     private var onActivate: (String) -> Void = { _ in }
     private var onHoverChange: (String, Bool) -> Void = { _, _ in }
+    private var canReorder: (String) -> Bool = { _ in false }
+    private var onReorderBegan: (String) -> Void = { _ in }
+    private var onReorderMoved: (String, String) -> Void = { _, _ in }
+    private var onReorderEnded: () -> Void = {}
 
     override var isFlipped: Bool { true }
 
@@ -117,23 +153,36 @@
         items: [TagNavigationItem],
         orientation: TagNavigationView.Orientation,
         contentInsets: NSEdgeInsets,
+        dragModeActive: Bool,
+        draggingItemID: String?,
         onActivate: @escaping (String) -> Void,
-        onHoverChange: @escaping (String, Bool) -> Void
+        onHoverChange: @escaping (String, Bool) -> Void,
+        canReorder: @escaping (String) -> Bool,
+        onReorderBegan: @escaping (String) -> Void,
+        onReorderMoved: @escaping (String, String) -> Void,
+        onReorderEnded: @escaping () -> Void
     ) {
-        let needsRebuild = self.items != items || self.orientation != orientation
+        let oldNames = self.items.map(\.name)
+        let newNames = items.map(\.name)
+        let needsRebuild = Set(oldNames) != Set(newNames) || self.orientation != orientation
         self.items = items
         self.orientation = orientation
         self.contentInsets = contentInsets
+        self.dragModeActive = dragModeActive
+        self.draggingItemID = draggingItemID
         self.onActivate = onActivate
         self.onHoverChange = onHoverChange
+        self.canReorder = canReorder
+        self.onReorderBegan = onReorderBegan
+        self.onReorderMoved = onReorderMoved
+        self.onReorderEnded = onReorderEnded
 
         if needsRebuild {
             rebuildButtons()
         } else {
-            for (button, item) in zip(buttons, items) {
-                button.configure(item: item, orientation: orientation)
-            }
+            reuseButtonsInCurrentOrder()
         }
+        updateButtonRuntimeState()
         needsLayout = true
     }
 
@@ -171,9 +220,45 @@
             let button = TagNavigationButton(item: item, orientation: orientation)
             button.onActivate = { [weak self] tagID in self?.onActivate(tagID) }
             button.onHoverChange = { [weak self] tagID, active in self?.onHoverChange(tagID, active) }
+            button.canReorder = { [weak self] tagID in self?.canReorder(tagID) ?? false }
+            button.onReorderBegan = { [weak self] tagID in self?.onReorderBegan(tagID) }
+            button.onReorderMoved = { [weak self] tagID, screenPoint in
+                self?.handleReorderMove(tagID: tagID, screenPoint: screenPoint)
+            }
+            button.onReorderEnded = { [weak self] in self?.onReorderEnded() }
             addSubview(button)
             return button
         }
+    }
+
+    private func reuseButtonsInCurrentOrder() {
+        let existing = Dictionary(uniqueKeysWithValues: buttons.map { ($0.itemName, $0) })
+        buttons = items.compactMap { item in
+            guard let button = existing[item.name] else { return nil }
+            button.configure(item: item, orientation: orientation)
+            return button
+        }
+    }
+
+    private func updateButtonRuntimeState() {
+        for button in buttons {
+            button.configureRuntime(
+                dragModeActive: dragModeActive && canReorder(button.itemName),
+                isDragging: draggingItemID == button.itemName
+            )
+        }
+    }
+
+    private func handleReorderMove(tagID: String, screenPoint: NSPoint) {
+        guard let window else { return }
+        let windowPoint = window.convertPoint(fromScreen: screenPoint)
+        let point = convert(windowPoint, from: nil)
+        guard let target = buttons.first(where: { button in
+            button.itemName != tagID
+                && canReorder(button.itemName)
+                && button.frame.insetBy(dx: -4, dy: -4).contains(point)
+        }) else { return }
+        onReorderMoved(tagID, target.itemName)
     }
 
     private func layoutHorizontal() {
@@ -201,11 +286,19 @@
 final class TagNavigationButton: NSButton {
     var onActivate: (String) -> Void = { _ in }
     var onHoverChange: (String, Bool) -> Void = { _, _ in }
+    var canReorder: (String) -> Bool = { _ in false }
+    var onReorderBegan: (String) -> Void = { _ in }
+    var onReorderMoved: (String, NSPoint) -> Void = { _, _ in }
+    var onReorderEnded: () -> Void = {}
 
     private var item: TagNavigationItem
     private var orientation: TagNavigationView.Orientation
     private var trackingAreaRef: NSTrackingArea?
     private var isMouseInside = false
+    private var dragModeActive = false
+    private var isDragging = false
+
+    var itemName: String { item.name }
 
     var preferredSize: NSSize {
         let font = NSFont.systemFont(ofSize: 13, weight: .medium)
@@ -246,6 +339,12 @@
         needsLayout = true
     }
 
+    func configureRuntime(dragModeActive: Bool, isDragging: Bool) {
+        self.dragModeActive = dragModeActive
+        self.isDragging = isDragging
+        updateAppearance()
+    }
+
     override func updateTrackingAreas() {
         super.updateTrackingAreas()
         if let trackingAreaRef {
@@ -275,6 +374,49 @@
         onHoverChange(item.id, false)
     }
 
+    override func mouseDown(with event: NSEvent) {
+        guard canReorder(item.id) else {
+            onActivate(item.id)
+            return
+        }
+
+        let start = Date()
+        let longPressDuration: TimeInterval = 0.35
+        var reorderActive = false
+        var finished = false
+
+        while !finished {
+            if !reorderActive && Date().timeIntervalSince(start) >= longPressDuration {
+                reorderActive = true
+                onReorderBegan(item.id)
+            }
+
+            let nextEvent = window?.nextEvent(
+                matching: [.leftMouseDragged, .leftMouseUp],
+                until: Date().addingTimeInterval(0.04),
+                inMode: .eventTracking,
+                dequeue: true
+            )
+
+            guard let nextEvent else { continue }
+            switch nextEvent.type {
+            case .leftMouseDragged:
+                if reorderActive {
+                    onReorderMoved(item.id, NSEvent.mouseLocation)
+                }
+            case .leftMouseUp:
+                if reorderActive {
+                    onReorderEnded()
+                } else {
+                    onActivate(item.id)
+                }
+                finished = true
+            default:
+                break
+            }
+        }
+    }
+
     @objc private func performActivation() {
         onActivate(item.id)
     }
@@ -285,8 +427,9 @@
         layer?.cornerRadius = orientation == .horizontal ? 7 : 6
         layer?.shadowColor = NSColor.black.withAlphaComponent(0.20).cgColor
         layer?.shadowOpacity = 1
-        layer?.shadowRadius = 3
-        layer?.shadowOffset = NSSize(width: 0, height: -1)
+        layer?.shadowRadius = isDragging ? 8 : 3
+        layer?.shadowOffset = NSSize(width: 0, height: isDragging ? -4 : -1)
+        alphaValue = dragModeActive ? (isDragging ? 1.0 : 0.62) : 1.0
 
         let textColor: NSColor = (item.colorIndex == 0 || item.colorIndex == 5) ? .labelColor : .white
         let paragraph = NSMutableParagraphStyle()

--
Gitblit v1.9.3