From 65b66f723294dd02174d30a67a05b88139f274ee Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Fri, 29 May 2026 05:12:04 +0800
Subject: [PATCH] Extract overlay window controller

---
 Apptag/ContentView.swift |   56 ++++++++++++++++++++++++++++++++++++++++++--------------
 1 files changed, 42 insertions(+), 14 deletions(-)

diff --git a/Apptag/ContentView.swift b/Apptag/ContentView.swift
index c060b8b..900cf4d 100644
--- a/Apptag/ContentView.swift
+++ b/Apptag/ContentView.swift
@@ -664,7 +664,7 @@
                     }
                     .zIndex(899)
 
-                QuickSearchOverlayView(
+                QuickSearchPanelPresentationView(
                     query: $quickSearchQuery,
                     results: quickSearchResults,
                     selectedID: quickSearchSelectedID,
@@ -672,16 +672,15 @@
                     selectionScrollToken: quickSearchSelectionScrollToken,
                     isLoading: quickSearchDocuments.isEmpty && refreshInProgress,
                     maxVisibleRows: quickSearchMaxVisibleRows(in: proxy.size),
+                    panelTopY: quickSearchPanelTopY(in: proxy.size),
+                    panelHeight: quickSearchPanelContentHeight(in: proxy.size),
                     errorMessage: quickSearchErrorMessage,
                     onCommand: handleQuickSearchCommand,
                     onHover: selectQuickSearchResult,
                     onLaunch: launchQuickSearchResult
                 )
-                .position(
-                    x: proxy.size.width / 2,
-                    y: quickSearchPanelCenterY(in: proxy.size)
-                )
-                .transition(.scale(scale: 0.98).combined(with: .opacity))
+                .frame(width: 0, height: 0)
+                .allowsHitTesting(false)
                 .zIndex(900)
             }
         }
@@ -689,10 +688,15 @@
         .animation(.easeOut(duration: 0.12), value: quickSearchVisible)
     }
 
-    private func quickSearchPanelCenterY(in size: CGSize) -> CGFloat {
-        let visibleRows = max(1, min(quickSearchResults.isEmpty ? 1 : quickSearchResults.count, quickSearchMaxVisibleRows(in: size)))
-        let estimatedPanelHeight = CGFloat(visibleRows) * 76 + 122
-        return quickSearchPanelTopY(in: size) + estimatedPanelHeight / 2
+    private func quickSearchPanelContentHeight(in size: CGSize) -> CGFloat {
+        let hasResultList = !(quickSearchDocuments.isEmpty && refreshInProgress)
+            && quickSearchErrorMessage == nil
+            && !quickSearchResults.isEmpty
+        let visibleRows = min(max(1, quickSearchResults.count), quickSearchMaxVisibleRows(in: size))
+        return QuickSearchPanelMetrics.contentHeight(
+            hasResultList: hasResultList,
+            visibleRows: visibleRows
+        )
     }
 
     private func quickSearchPanelTopY(in size: CGSize) -> CGFloat {
@@ -701,8 +705,11 @@
 
     private func quickSearchMaxVisibleRows(in size: CGSize) -> Int {
         let bottomClearance: CGFloat = 84
-        let chromeHeight: CGFloat = 122
-        let rowHeightWithSpacing: CGFloat = 76
+        let chromeHeight = QuickSearchPanelMetrics.headerHeight
+            + QuickSearchPanelMetrics.dividerHeight
+            + QuickSearchPanelMetrics.resultListVerticalInset * 2
+        let rowHeightWithSpacing = QuickSearchPanelMetrics.rowHeight
+            + QuickSearchPanelMetrics.rowSpacing
         let availableHeight = max(0, size.height - quickSearchPanelTopY(in: size) - bottomClearance - chromeHeight)
         return max(1, min(8, Int(floor(availableHeight / rowHeightWithSpacing))))
     }
@@ -934,11 +941,25 @@
             contentInsets: isHorizontal
                 ? NSEdgeInsets(top: 3, left: 24, bottom: 3, right: tagPosition == "top" ? floatingControlsReservedWidth : 24)
                 : NSEdgeInsets(top: 12, left: 12, bottom: 12, right: 12),
+            dragModeActive: tagNavDragModeActive,
+            draggingItemID: tagNavDragItem,
             onActivate: { tagID in
                 activateTagNavigation(tagID)
             },
             onHoverChange: { tagID, active in
                 handleTagNavigationHover(tagID, active: active)
+            },
+            canReorder: { tagID in
+                canReorderTag(tagID)
+            },
+            onReorderBegan: { tagID in
+                beginTagNavReorder(tagID)
+            },
+            onReorderMoved: { tagID, targetID in
+                reorderTagNavItem(fromName: tagID, to: targetID)
+            },
+            onReorderEnded: {
+                endTagNavReorder()
             }
         )
     }
@@ -2054,11 +2075,18 @@
     private func reorderTagNavItem(at location: CGPoint) {
         guard let fromName = tagNavDragItem,
               let targetName = tagNavReorderFrames.first(where: { $0.value.contains(location) })?.key,
-              fromName != targetName,
+              fromName != targetName
+        else { return }
+        reorderTagNavItem(fromName: fromName, to: targetName)
+    }
+
+    private func reorderTagNavItem(fromName: String, to targetName: String) {
+        guard fromName != targetName,
+              canReorderTag(fromName),
+              canReorderTag(targetName),
               let fromIndex = draggedTagNames.firstIndex(of: fromName),
               let toIndex = draggedTagNames.firstIndex(of: targetName)
         else { return }
-
         tagNavReorderDidMove = true
         withAnimation(.spring(response: 0.22, dampingFraction: 0.82)) {
             let destination = toIndex > fromIndex ? toIndex + 1 : toIndex

--
Gitblit v1.9.3