| Apptag/AppGridCollectionView.swift | ●●●●● patch | view | raw | blame | history | |
| Apptag/ContentView.swift | ●●●●● patch | view | raw | blame | history | |
| Apptag/TagNavigationView.swift | ●●●●● patch | view | raw | blame | history | |
| TODO.md | ●●●●● patch | view | raw | blame | history |
Apptag/AppGridCollectionView.swift
@@ -158,7 +158,6 @@ iconSize: iconSize, showNames: showNames, showUncommonAppBubbles: showUncommonAppBubbles, highlightedGroupName: highlightedGroupName, contentRevision: contentRevision ) if nextSignature != contentSignature { @@ -235,7 +234,6 @@ iconSize: CGFloat, showNames: Bool, showUncommonAppBubbles: Bool, highlightedGroupName: String?, contentRevision: Int ) -> String { let colorPart = tagColors @@ -247,7 +245,6 @@ "\(Int(iconSize.rounded()))", showNames ? "names" : "nonames", showUncommonAppBubbles ? "uncommon" : "allbubbles", highlightedGroupName ?? "", colorPart, "rev=\(contentRevision)" ].joined(separator: "|") @@ -951,8 +948,9 @@ guard let coordinator, let group else { return } let displayStyle = coordinator.displayStyle let tagColor = TagColor.nsColor(for: coordinator.tagColors[group.name] ?? 0) let isNavigationHighlighted = coordinator.highlightedGroupName == group.name let isColorlessActive = coordinator.isColorlessContainerMode && (isHovered || coordinator.highlightedGroupName == group.name) && (isHovered || isNavigationHighlighted) if displayStyle.usesCardSurface { let rect = bounds.insetBy(dx: 0.5, dy: 0.5) @@ -1033,10 +1031,11 @@ private func updateCardShadow() { guard let coordinator else { return } let isNavigationHighlighted = coordinator.highlightedGroupName == group?.name let isColorlessActive = coordinator.isColorlessContainerMode && (isHovered || coordinator.highlightedGroupName == group?.name) && (isHovered || isNavigationHighlighted) let shouldShadow = coordinator.displayStyle.usesCardSurface && ((coordinator.isColoredContainerMode && isHovered) || isColorlessActive) && ((coordinator.isColoredContainerMode && (isHovered || isNavigationHighlighted)) || isColorlessActive) layer?.shadowColor = NSColor.black.cgColor layer?.shadowOpacity = shouldShadow ? 0.22 : 0 layer?.shadowRadius = shouldShadow ? 8 : 0 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 { @@ -430,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 @@ -929,8 +937,8 @@ onActivate: { tagID in activateTagNavigation(tagID) }, onHover: { tagID in handleTagNavigationHover(tagID) onHoverChange: { tagID, active in handleTagNavigationHover(tagID, active: active) } ) } @@ -949,9 +957,7 @@ .zIndex(tagNavDragItem == tag.name ? 1 : 0) .highPriorityGesture(tagNavReorderGesture(for: tag.name)) .onHover { hovering in if hovering { handleTagNavigationHover(tag.id) } handleTagNavigationHover(tag.id, active: hovering) } } } @@ -978,9 +984,7 @@ .zIndex(tagNavDragItem == tag.name ? 1 : 0) .highPriorityGesture(tagNavReorderGesture(for: tag.name)) .onHover { hovering in if hovering { handleTagNavigationHover(tag.id) } handleTagNavigationHover(tag.id, active: hovering) } } } @@ -1011,7 +1015,7 @@ showNames: !hideAppNames, bubbleDisabled: appBubbleDisabled, showUncommonAppBubbles: showUncommonAppBubbles, highlightedGroupName: filledColorlessContainer, highlightedGroupName: appGridHighlightedGroupName, contentRevision: groupLayoutVersion, scrollTargetID: appGridScrollTargetID, scrollRequestToken: appGridScrollRequestToken, @@ -1945,8 +1949,28 @@ scrollTo(id) } private func handleTagNavigationHover(_ id: String) { 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) } Apptag/TagNavigationView.swift
@@ -17,7 +17,7 @@ let orientation: Orientation let contentInsets: NSEdgeInsets let onActivate: (String) -> Void let onHover: (String) -> Void let onHoverChange: (String, Bool) -> Void func makeNSView(context: Context) -> TagNavigationHostView { let view = TagNavigationHostView() @@ -26,7 +26,7 @@ orientation: orientation, contentInsets: contentInsets, onActivate: onActivate, onHover: onHover onHoverChange: onHoverChange ) return view } @@ -37,7 +37,7 @@ orientation: orientation, contentInsets: contentInsets, onActivate: onActivate, onHover: onHover onHoverChange: onHoverChange ) } } @@ -63,14 +63,14 @@ orientation: TagNavigationView.Orientation, contentInsets: NSEdgeInsets, onActivate: @escaping (String) -> Void, onHover: @escaping (String) -> Void onHoverChange: @escaping (String, Bool) -> Void ) { documentView.update( items: items, orientation: orientation, contentInsets: contentInsets, onActivate: onActivate, onHover: onHover onHoverChange: onHoverChange ) scrollView.hasHorizontalScroller = orientation == .horizontal scrollView.hasVerticalScroller = orientation == .vertical @@ -109,7 +109,7 @@ private var contentInsets = NSEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) private var buttons: [TagNavigationButton] = [] private var onActivate: (String) -> Void = { _ in } private var onHover: (String) -> Void = { _ in } private var onHoverChange: (String, Bool) -> Void = { _, _ in } override var isFlipped: Bool { true } @@ -118,14 +118,14 @@ orientation: TagNavigationView.Orientation, contentInsets: NSEdgeInsets, onActivate: @escaping (String) -> Void, onHover: @escaping (String) -> Void onHoverChange: @escaping (String, Bool) -> Void ) { let needsRebuild = self.items != items || self.orientation != orientation self.items = items self.orientation = orientation self.contentInsets = contentInsets self.onActivate = onActivate self.onHover = onHover self.onHoverChange = onHoverChange if needsRebuild { rebuildButtons() @@ -170,7 +170,7 @@ buttons = items.map { item in let button = TagNavigationButton(item: item, orientation: orientation) button.onActivate = { [weak self] tagID in self?.onActivate(tagID) } button.onHover = { [weak self] tagID in self?.onHover(tagID) } button.onHoverChange = { [weak self] tagID, active in self?.onHoverChange(tagID, active) } addSubview(button) return button } @@ -200,11 +200,12 @@ final class TagNavigationButton: NSButton { var onActivate: (String) -> Void = { _ in } var onHover: (String) -> Void = { _ in } var onHoverChange: (String, Bool) -> Void = { _, _ in } private var item: TagNavigationItem private var orientation: TagNavigationView.Orientation private var trackingAreaRef: NSTrackingArea? private var isMouseInside = false var preferredSize: NSSize { let font = NSFont.systemFont(ofSize: 13, weight: .medium) @@ -262,7 +263,16 @@ override func mouseEntered(with event: NSEvent) { super.mouseEntered(with: event) onHover(item.id) guard !isMouseInside else { return } isMouseInside = true onHoverChange(item.id, true) } override func mouseExited(with event: NSEvent) { super.mouseExited(with: event) guard isMouseInside else { return } isMouseInside = false onHoverChange(item.id, false) } @objc private func performActivation() { TODO.md
@@ -11,21 +11,14 @@ ## In Progress - [2026-05-28] 第二批 AppKit 化第 2 项:标签栏 AppKit 化第一阶段,只读导航。 - 分支: `codex/post-freeze-new-requirement` - 目标: 抽 `TagNavItem` / callbacks 边界,新增可回滚的 AppKit 标签栏。 - 范围: 顶部、左侧、右侧标签栏显示;点击标签滚动到对应分组。 - 暂不做: 标签拖拽排序。 - 回滚: 保留旧 SwiftUI 标签栏作为开关。 - 验收: 三种标签位置、点击滚动、长标签、多语言、全屏和 Split View。 - QA: 构建、签名、本地化 JSON、三种位置屏幕点击复核、SwiftUI 回滚开关、窗口 8 逻辑 QA 已通过;待用户最终体验验收后移动到 `Done`。 - 结果: 默认启用 AppKit 标签导航,`useAppKitTagNavigation=false` 可回滚到旧 SwiftUI 标签栏。 ## Todo - [2026-05-28] 第二批 AppKit 化第 3 项:标签栏 AppKit 化第二阶段,hover 与容器高亮。 - 分支: `codex/post-freeze-new-requirement` - 目标: hover 标签时高亮对应容器,保留 colorless container 的填充/取消逻辑。 - 验收: hover 触发频率、滚动不过度、视觉不闪烁、全屏/Split View 不跳 Space。 - QA: 构建、签名、本地化 JSON、hover 静态 QA、colorless grid 屏幕 hover/点击复核、窗口 8 逻辑 QA 已通过;待用户最终体验验收后移动到 `Done`。 - 结果: 标签 hover 进入/离开已接入 AppKit;hover 高亮只刷新可见运行态,不触发 App Grid reload;重复 hover 滚动已去抖。 ## Todo - [2026-05-28] 第二批 AppKit 化第 4 项:标签栏 AppKit 化第三阶段,拖拽排序。 - 目标: 用 AppKit hit-testing 替换 SwiftUI `GeometryReader + preference` 的标签排序命中逻辑。 @@ -61,6 +54,11 @@ ## Done - [2026-05-28] 第二批 AppKit 化第 2 项:标签栏 AppKit 化第一阶段,只读导航。 - 提交: `b4a1ba5` - 结果: 默认启用 AppKit 标签导航,顶部/左侧/右侧标签栏显示和点击滚动接入完成;`useAppKitTagNavigation=false` 可回滚到旧 SwiftUI 标签栏。 - 验证: 构建、签名、本地化 JSON、三种位置屏幕点击复核、SwiftUI 回滚开关、窗口 8 逻辑 QA 全部通过;用户已确认第 2 项 OK。 - [2026-05-28] 第二批 AppKit 化第 1 项:清理 `ContentView` 的 App Grid 交互职责。 - 提交: `561541e` - 结果: `ContentView` 中 App Grid 的 bubble、drag、drop、refresh toast 临时状态已收口到 `AppGridInteractionState`。