From 9d87dbed3f81d07a81b1484a1317d7f7c0258610 Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Thu, 28 May 2026 19:58:24 +0800
Subject: [PATCH] Stabilize tag navigation hover highlighting
---
Apptag/ContentView.swift | 126 +++++++++++++++++++++++++++++++----------
1 files changed, 94 insertions(+), 32 deletions(-)
diff --git a/Apptag/ContentView.swift b/Apptag/ContentView.swift
index f94de57..c060b8b 100644
--- a/Apptag/ContentView.swift
+++ b/Apptag/ContentView.swift
@@ -316,6 +316,9 @@
var tagRemovalDropSuppressFuturePrompt = false
var appDragResetToken = 0
var bubbleDraftNote = ""
+ var tagNavigationHoveredGroupName: String? = nil
+ var tagNavigationLastHoverScrollID: String? = nil
+ var tagNavigationLastHoverScrollAt: Date? = nil
}
private struct EditActionFeedback: Identifiable {
@@ -418,6 +421,7 @@
@AppStorage("displayMode") private var displayMode = AppDefaults.displayMode
@AppStorage("hideAppNames") private var hideAppNames = AppDefaults.hideAppNames
@AppStorage("showUncommonAppBubbles") private var showUncommonAppBubbles = AppDefaults.showUncommonAppBubbles
+ @AppStorage("useAppKitTagNavigation") private var useAppKitTagNavigation = AppDefaults.useAppKitTagNavigation
@AppStorage("skipTagRemovalDropConfirm") private var skipTagRemovalDropConfirm = false
private let editSidebarWidth: CGFloat = 188
@@ -429,7 +433,12 @@
|| appGridInteraction.pendingUncategorizedDrop != nil
|| appGridInteraction.pendingTagRemovalDrop != nil
}
+ private var appGridHighlightedGroupName: String? {
+ appGridInteraction.tagNavigationHoveredGroupName
+ ?? (isColorlessContainerMode ? filledColorlessContainer : nil)
+ }
private let rightSidebarFloatingClearance: CGFloat = 44
+ private let tagNavigationHoverScrollInterval: TimeInterval = 0.22
private var floatingButtonSurfaceColor: Color {
colorScheme == .dark
@@ -865,7 +874,7 @@
private var topLayout: some View {
VStack(spacing: 0) {
Spacer().frame(height: notchHeight > 0 ? notchHeight + 14 : 28)
- if !tagLabels.isEmpty { tagBar.padding(.bottom, 8) }
+ if !tagLabels.isEmpty { topTagNavigation.padding(.bottom, 8) }
Divider().opacity(0.3)
appGridContent
}
@@ -876,14 +885,65 @@
Spacer().frame(height: notchHeight > 0 ? notchHeight + 14 : 28)
Divider().opacity(0.3)
HStack(spacing: 0) {
- if tagPosition == "left" { tagSidebar; sideDivider }
+ if tagPosition == "left" { leftTagSidebar; sideDivider }
appGridContent
if tagPosition == "right" { sideDivider; rightTagSidebar }
}
}
}
- private var tagBar: some View {
+ private var topTagNavigation: some View {
+ Group {
+ if useAppKitTagNavigation {
+ appKitTagNavigation(orientation: .horizontal)
+ .frame(height: 34)
+ } else {
+ swiftUITopTagBar
+ }
+ }
+ }
+
+ private var leftTagSidebar: some View {
+ Group {
+ if useAppKitTagNavigation {
+ appKitTagNavigation(orientation: .vertical)
+ } else {
+ swiftUITagSidebarList
+ }
+ }
+ .frame(width: 135)
+ }
+
+ private var rightTagSidebar: some View {
+ Group {
+ if useAppKitTagNavigation {
+ appKitTagNavigation(orientation: .vertical)
+ } else {
+ swiftUITagSidebarList
+ }
+ }
+ .padding(.top, rightSidebarFloatingClearance)
+ .frame(width: 135)
+ }
+
+ private func appKitTagNavigation(orientation: TagNavigationView.Orientation) -> some View {
+ let isHorizontal = orientation == .horizontal
+ return TagNavigationView(
+ items: tagLabels,
+ orientation: orientation,
+ contentInsets: isHorizontal
+ ? NSEdgeInsets(top: 3, left: 24, bottom: 3, right: tagPosition == "top" ? floatingControlsReservedWidth : 24)
+ : NSEdgeInsets(top: 12, left: 12, bottom: 12, right: 12),
+ onActivate: { tagID in
+ activateTagNavigation(tagID)
+ },
+ onHoverChange: { tagID, active in
+ handleTagNavigationHover(tagID, active: active)
+ }
+ )
+ }
+
+ private var swiftUITopTagBar: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 8) {
ForEach(tagLabels) { tag in
@@ -897,10 +957,7 @@
.zIndex(tagNavDragItem == tag.name ? 1 : 0)
.highPriorityGesture(tagNavReorderGesture(for: tag.name))
.onHover { hovering in
- if hovering {
- fillColorlessContainer(tag.id)
- scrollTo(tag.id)
- }
+ handleTagNavigationHover(tag.id, active: hovering)
}
}
}
@@ -913,18 +970,7 @@
}
}
- private var tagSidebar: some View {
- tagSidebarList
- .frame(width: 135)
- }
-
- private var rightTagSidebar: some View {
- tagSidebarList
- .padding(.top, rightSidebarFloatingClearance)
- .frame(width: 135)
- }
-
- private var tagSidebarList: some View {
+ private var swiftUITagSidebarList: some View {
ScrollView(.vertical, showsIndicators: false) {
VStack(spacing: 6) {
ForEach(tagLabels) { tag in
@@ -938,10 +984,7 @@
.zIndex(tagNavDragItem == tag.name ? 1 : 0)
.highPriorityGesture(tagNavReorderGesture(for: tag.name))
.onHover { hovering in
- if hovering {
- fillColorlessContainer(tag.id)
- scrollTo(tag.id)
- }
+ handleTagNavigationHover(tag.id, active: hovering)
}
}
}
@@ -972,7 +1015,7 @@
showNames: !hideAppNames,
bubbleDisabled: appBubbleDisabled,
showUncommonAppBubbles: showUncommonAppBubbles,
- highlightedGroupName: filledColorlessContainer,
+ highlightedGroupName: appGridHighlightedGroupName,
contentRevision: groupLayoutVersion,
scrollTargetID: appGridScrollTargetID,
scrollRequestToken: appGridScrollRequestToken,
@@ -1732,8 +1775,8 @@
groupLayoutVersion &+= 1
}
- private var tagLabels: [TagLabel] {
- displayGroups.map { TagLabel(name: $0.name, colorIndex: tagColors[$0.name] ?? 0) }
+ private var tagLabels: [TagNavigationItem] {
+ displayGroups.map { TagNavigationItem(name: $0.name, colorIndex: tagColors[$0.name] ?? 0) }
}
private var editGroups: [TagGroup] {
@@ -1903,6 +1946,31 @@
if isColorlessContainerMode {
toggleColorlessFill(id)
}
+ scrollTo(id)
+ }
+
+ private func handleTagNavigationHover(_ id: String, active: Bool) {
+ guard active else {
+ if appGridInteraction.tagNavigationHoveredGroupName == id {
+ appGridInteraction.tagNavigationHoveredGroupName = nil
+ }
+ return
+ }
+
+ appGridInteraction.tagNavigationHoveredGroupName = id
+ fillColorlessContainer(id)
+ scrollToTagFromHover(id)
+ }
+
+ private func scrollToTagFromHover(_ id: String) {
+ let now = Date()
+ if appGridInteraction.tagNavigationLastHoverScrollID == id,
+ let lastScrollAt = appGridInteraction.tagNavigationLastHoverScrollAt,
+ now.timeIntervalSince(lastScrollAt) < tagNavigationHoverScrollInterval {
+ return
+ }
+ appGridInteraction.tagNavigationLastHoverScrollID = id
+ appGridInteraction.tagNavigationLastHoverScrollAt = now
scrollTo(id)
}
@@ -2360,12 +2428,6 @@
static func reduce(value: inout [String: CGRect], nextValue: () -> [String: CGRect]) {
value.merge(nextValue(), uniquingKeysWith: { _, new in new })
}
-}
-
-private struct TagLabel: Identifiable {
- var id: String { name }
- let name: String
- let colorIndex: Int
}
// MARK: - NSVisualEffectView bridge
--
Gitblit v1.9.3