From c1d6cfc2b59b2ce4f1ebf235397cdf43a4292996 Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Wed, 24 Jun 2026 20:41:45 +0800
Subject: [PATCH] Release 8.0.1 dark glass themes

---
 src/Apptag/AppGridCollectionView.swift |  707 +++++++++++++++++++++++++++++++++++++++++++++++++++-------
 1 files changed, 615 insertions(+), 92 deletions(-)

diff --git a/src/Apptag/AppGridCollectionView.swift b/src/Apptag/AppGridCollectionView.swift
index 788dc43..5d8d845 100644
--- a/src/Apptag/AppGridCollectionView.swift
+++ b/src/Apptag/AppGridCollectionView.swift
@@ -1,5 +1,6 @@
 import SwiftUI
 import AppKit
+import Carbon.HIToolbox
 
 fileprivate enum AppGridCollectionDisplayMode: Equatable {
     case flat
@@ -37,10 +38,11 @@
 }
 
 enum AppGridUsageTipsMetrics {
-    static let barHeight: CGFloat = 68
-    static let reservedHeight: CGFloat = 104
-    static let bottomMargin: CGFloat = 18
-    static let horizontalInset: CGFloat = AppGridCollectionMetrics.outerPadding
+    static let barHeight: CGFloat = 136
+    static let reservedHeight: CGFloat = 176
+    static let bottomMargin: CGFloat = 20
+    static let horizontalInset: CGFloat = 24
+    static let minWidth: CGFloat = 640
 }
 
 struct AppGridCollectionView: NSViewRepresentable {
@@ -49,6 +51,7 @@
     let displayMode: String
     let iconSize: CGFloat
     let showNames: Bool
+    let appGridTheme: AppGridTheme
     let bubbleDisabled: Bool
     let showUncommonAppBubbles: Bool
     let highlightedGroupName: String?
@@ -88,6 +91,7 @@
             displayMode: displayMode,
             iconSize: iconSize,
             showNames: showNames,
+            appGridTheme: appGridTheme,
             bubbleDisabled: bubbleDisabled,
             showUncommonAppBubbles: showUncommonAppBubbles,
             highlightedGroupName: highlightedGroupName,
@@ -119,6 +123,7 @@
         var displayMode = AppDefaults.displayMode
         var iconSize: CGFloat = AppDefaults.iconSize
         var showNames = true
+        var appGridTheme = AppGridTheme.fallback
         private var externalBubbleDisabled = false
         private var scrollBubbleDisabled = false
         var showUncommonAppBubbles = AppDefaults.showUncommonAppBubbles
@@ -159,6 +164,7 @@
             displayMode: String,
             iconSize: CGFloat,
             showNames: Bool,
+            appGridTheme: AppGridTheme,
             bubbleDisabled: Bool,
             showUncommonAppBubbles: Bool,
             highlightedGroupName: String?,
@@ -186,6 +192,7 @@
             self.displayMode = displayMode
             self.iconSize = iconSize
             self.showNames = showNames
+            self.appGridTheme = appGridTheme
             self.externalBubbleDisabled = bubbleDisabled
             self.showUncommonAppBubbles = showUncommonAppBubbles
             self.highlightedGroupName = highlightedGroupName
@@ -216,6 +223,7 @@
                 displayMode: displayMode,
                 iconSize: iconSize,
                 showNames: showNames,
+                appGridTheme: appGridTheme,
                 showUncommonAppBubbles: showUncommonAppBubbles,
                 bottomContentPadding: self.bottomContentPadding,
                 contentRevision: contentRevision
@@ -391,6 +399,7 @@
             displayMode: String,
             iconSize: CGFloat,
             showNames: Bool,
+            appGridTheme: AppGridTheme,
             showUncommonAppBubbles: Bool,
             bottomContentPadding: CGFloat,
             contentRevision: Int
@@ -403,6 +412,7 @@
                 displayMode,
                 "\(Int(iconSize.rounded()))",
                 showNames ? "names" : "nonames",
+                "theme=\(appGridTheme.rawValue)",
                 showUncommonAppBubbles ? "uncommon" : "allbubbles",
                 "bottom=\(Int(bottomContentPadding.rounded()))",
                 colorPart,
@@ -432,6 +442,7 @@
     private let scrollView = AppGridScrollView()
     private let collectionView = NSCollectionView()
     private let gridLayout = AppGridContainerCollectionLayout()
+    private let usageTipsShieldView = AppGridUsageTipsShieldView()
     private let usageTipsView = AppGridUsageTipsNSView()
     private weak var coordinator: AppGridCollectionView.Coordinator?
     private var scrollObserver: NSObjectProtocol?
@@ -439,6 +450,8 @@
     private var lastReportedBoundsOrigin: NSPoint?
     private var scrollUnfreezeWorkItem: DispatchWorkItem?
     private var scrollActivityIsActive = false
+    private var usageTipsEventRegionIsClaimed = false
+    private var usageTipsMouseMonitor: Any?
 
     override var isFlipped: Bool { true }
 
@@ -456,6 +469,7 @@
         if let scrollObserver {
             NotificationCenter.default.removeObserver(scrollObserver)
         }
+        removeUsageTipsMouseMonitor()
         scrollUnfreezeWorkItem?.cancel()
         coordinator?.cancelAppIconDrag()
         AppDragCoordinator.shared.unregisterEmptyDropTarget(id: emptyDropTargetID)
@@ -471,6 +485,7 @@
 
     func applyCoordinatorUpdate() {
         guard let coordinator else { return }
+        applyAppearance()
         if coordinator.needsReload {
             coordinator.needsReload = false
             collectionView.reloadData()
@@ -508,12 +523,23 @@
     override func viewDidMoveToWindow() {
         super.viewDidMoveToWindow()
         if window == nil {
+            removeUsageTipsMouseMonitor()
             AppDragCoordinator.shared.cancelDrag()
             coordinator?.cancelAppIconDrag()
             AppDragCoordinator.shared.unregisterEmptyDropTarget(id: emptyDropTargetID)
         } else {
+            installUsageTipsMouseMonitor()
             AppDragCoordinator.shared.registerEmptyDropTarget(id: emptyDropTargetID, view: self)
         }
+    }
+
+    override func hitTest(_ point: NSPoint) -> NSView? {
+        if !usageTipsView.isHidden,
+           usageTipsView.frame.contains(point) {
+            let tipsPoint = usageTipsView.convert(point, from: self)
+            return usageTipsView.hitTest(tipsPoint) ?? usageTipsView
+        }
+        return super.hitTest(point)
     }
 
     func performEmptyDrop(path: String, source: String, screenPoint: NSPoint, copy: Bool) {
@@ -552,8 +578,16 @@
         scrollView.borderType = .noBorder
         scrollView.documentView = collectionView
         addSubview(scrollView)
+        usageTipsShieldView.isHidden = true
+        usageTipsShieldView.onClaim = { [weak self] in
+            self?.claimUsageTipsEventRegion()
+        }
+        usageTipsShieldView.onRelease = { [weak self] in
+            self?.releaseUsageTipsEventRegion()
+        }
         usageTipsView.isHidden = true
-        addSubview(usageTipsView)
+        addSubview(usageTipsShieldView, positioned: .above, relativeTo: scrollView)
+        addSubview(usageTipsView, positioned: .above, relativeTo: usageTipsShieldView)
 
         scrollView.contentView.postsBoundsChangedNotifications = true
         scrollObserver = NotificationCenter.default.addObserver(
@@ -573,17 +607,63 @@
               coordinator.usageTipsVisible
         else {
             usageTipsView.isHidden = true
+            usageTipsShieldView.isHidden = true
             usageTipsView.clearHoverState()
+            releaseUsageTipsEventRegion()
             usageTipsView.frame = .zero
+            usageTipsShieldView.frame = .zero
             return
         }
 
         let inset = min(AppGridUsageTipsMetrics.horizontalInset, max(0, bounds.width / 4))
-        let width = max(1, bounds.width - inset * 2)
+        let availableWidth = max(1, bounds.width - inset * 2)
+        let width = usageTipsView.preferredWidth(maxAvailableWidth: availableWidth)
         let height = AppGridUsageTipsMetrics.barHeight
-        let y = max(0, bounds.height - AppGridUsageTipsMetrics.bottomMargin - height)
+        usageTipsShieldView.isHidden = false
+        usageTipsShieldView.frame = usageTipsEventRegion()
+        usageTipsView.configureVisualLayout(
+            width: width,
+            height: height,
+            bottomMargin: AppGridUsageTipsMetrics.bottomMargin
+        )
         usageTipsView.isHidden = false
-        usageTipsView.frame = NSRect(x: inset, y: y, width: width, height: height)
+        usageTipsView.frame = usageTipsEventRegion()
+    }
+
+    fileprivate func relayoutUsageTipsAfterContentChange() {
+        positionUsageTipsView()
+        usageTipsView.needsLayout = true
+    }
+
+    private func usageTipsEventRegion() -> NSRect {
+        guard let coordinator,
+              coordinator.usageTipsVisible
+        else { return .zero }
+        let height = min(bounds.height, AppGridUsageTipsMetrics.reservedHeight)
+        return NSRect(
+            x: 0,
+            y: max(0, bounds.height - height),
+            width: bounds.width,
+            height: height
+        )
+    }
+
+    private func claimUsageTipsEventRegion() {
+        usageTipsEventRegionIsClaimed = true
+        window?.makeFirstResponder(usageTipsView)
+        if coordinator?.setUsageTipsBubbleDisabled(true) == true {
+            refreshVisibleRuntimeState()
+        }
+        coordinator?.onUsageTipsHoverChange(true)
+    }
+
+    private func releaseUsageTipsEventRegion() {
+        guard usageTipsEventRegionIsClaimed else { return }
+        usageTipsEventRegionIsClaimed = false
+        if coordinator?.setUsageTipsBubbleDisabled(false) == true {
+            refreshVisibleRuntimeState()
+        }
+        coordinator?.onUsageTipsHoverChange(false)
     }
 
     private func handleScrollActivity() {
@@ -623,6 +703,58 @@
         refreshVisibleRuntimeState()
     }
 
+    func handleUsageTipsMouseDown(_ event: NSEvent) -> Bool {
+        handleUsageTipsMouseEvent(event, triggerButtons: true)
+    }
+
+    private func handleUsageTipsMouseEvent(_ event: NSEvent, triggerButtons: Bool) -> Bool {
+        let point = usageTipsHostPoint(for: event)
+        guard !usageTipsView.isHidden,
+              usageTipsView.frame.contains(point)
+        else { return false }
+
+        return usageTipsView.handleMouseEventFromHost(event, triggerButtons: triggerButtons)
+    }
+
+    private func installUsageTipsMouseMonitor() {
+        guard usageTipsMouseMonitor == nil else { return }
+        usageTipsMouseMonitor = NSEvent.addLocalMonitorForEvents(
+            matching: [
+                .leftMouseDown,
+                .leftMouseUp,
+                .rightMouseDown,
+                .rightMouseUp,
+                .otherMouseDown,
+                .otherMouseUp
+            ]
+        ) { [weak self] event in
+            guard let self,
+                  self.window != nil,
+                  self.handleUsageTipsMouseEvent(
+                    event,
+                    triggerButtons: event.type == .leftMouseDown
+                  )
+            else { return event }
+            return nil
+        }
+    }
+
+    private func usageTipsHostPoint(for event: NSEvent) -> NSPoint {
+        if event.window === window {
+            return convert(event.locationInWindow, from: nil)
+        }
+        guard let window else { return .zero }
+        let windowPoint = window.convertPoint(fromScreen: NSEvent.mouseLocation)
+        return convert(windowPoint, from: nil)
+    }
+
+    private func removeUsageTipsMouseMonitor() {
+        if let usageTipsMouseMonitor {
+            NSEvent.removeMonitor(usageTipsMouseMonitor)
+            self.usageTipsMouseMonitor = nil
+        }
+    }
+
     private func replayPointerHover() {
         guard let window else { return }
         let windowPoint = window.convertPoint(fromScreen: NSEvent.mouseLocation)
@@ -643,24 +775,88 @@
         }
         return didScroll
     }
+
+    private func applyAppearance() {
+        let usesDarkGlass = coordinator?.appGridTheme.usesDarkGlass == true
+        layer?.backgroundColor = NSColor.clear.cgColor
+        collectionView.appearance = usesDarkGlass ? NSAppearance(named: .darkAqua) : nil
+    }
+}
+
+private final class AppGridUsageTipsShieldView: NSView {
+    var onClaim: (() -> Void)?
+    var onRelease: (() -> Void)?
+
+    private var trackingAreaRef: NSTrackingArea?
+
+    override var isFlipped: Bool { true }
+    override var acceptsFirstResponder: Bool { false }
+    override var mouseDownCanMoveWindow: Bool { false }
+
+    override func acceptsFirstMouse(for event: NSEvent?) -> Bool {
+        true
+    }
+
+    override func updateTrackingAreas() {
+        super.updateTrackingAreas()
+        if let trackingAreaRef {
+            removeTrackingArea(trackingAreaRef)
+        }
+        let area = NSTrackingArea(
+            rect: .zero,
+            options: [.mouseEnteredAndExited, .mouseMoved, .activeAlways, .inVisibleRect],
+            owner: self,
+            userInfo: nil
+        )
+        addTrackingArea(area)
+        trackingAreaRef = area
+    }
+
+    override func hitTest(_ point: NSPoint) -> NSView? {
+        guard !isHidden, bounds.contains(point) else { return nil }
+        onClaim?()
+        return self
+    }
+
+    override func mouseEntered(with event: NSEvent) { onClaim?() }
+    override func mouseMoved(with event: NSEvent) { onClaim?() }
+    override func mouseDragged(with event: NSEvent) { onClaim?() }
+    override func mouseExited(with event: NSEvent) { onRelease?() }
+    override func mouseDown(with event: NSEvent) {
+        Diagnostics.log("usageTips.shield.mouseDown")
+        onClaim?()
+    }
+    override func mouseUp(with event: NSEvent) {}
+    override func rightMouseDown(with event: NSEvent) { onClaim?() }
+    override func rightMouseUp(with event: NSEvent) {}
+    override func otherMouseDown(with event: NSEvent) { onClaim?() }
+    override func otherMouseUp(with event: NSEvent) {}
+    override func scrollWheel(with event: NSEvent) { onClaim?() }
 }
 
 private final class AppGridUsageTipsNSView: NSView {
     weak var coordinator: AppGridCollectionView.Coordinator?
 
-    private let titleBackgroundView = NSView()
+    private let backgroundView = NSVisualEffectView()
+    private let iconView = AppGridDecorativeImageView()
     private let titleLabel = NSTextField(labelWithString: "")
     private let detailScrollView = NSScrollView()
     private let detailLabel = NSTextField(labelWithString: "")
-    private let controlsView = NSView()
     private let previousButton = AppGridUsageTipIconButton(systemImage: "chevron.left")
     private let nextButton = AppGridUsageTipIconButton(systemImage: "chevron.right")
     private let dotsView = AppGridUsageTipDotsView()
     private var trackingAreaRef: NSTrackingArea?
     private var isPointerInside = false
+    private var visualWidth: CGFloat = AppGridUsageTipsMetrics.minWidth
+    private var visualHeight: CGFloat = AppGridUsageTipsMetrics.barHeight
+    private var visualBottomMargin: CGFloat = AppGridUsageTipsMetrics.bottomMargin
+
+    private let titleFont = NSFont.systemFont(ofSize: 24, weight: .semibold)
+    private let detailFont = NSFont.systemFont(ofSize: 24, weight: .regular)
 
     override var isFlipped: Bool { true }
-    override var acceptsFirstResponder: Bool { false }
+    override var acceptsFirstResponder: Bool { true }
+    override var mouseDownCanMoveWindow: Bool { false }
 
     override init(frame frameRect: NSRect) {
         super.init(frame: frameRect)
@@ -670,6 +866,10 @@
     required init?(coder: NSCoder) {
         super.init(coder: coder)
         setup()
+    }
+
+    override func acceptsFirstMouse(for event: NSEvent?) -> Bool {
+        true
     }
 
     override func viewDidMoveToWindow() {
@@ -686,7 +886,7 @@
         }
         let area = NSTrackingArea(
             rect: .zero,
-            options: [.mouseEnteredAndExited, .activeAlways, .inVisibleRect],
+            options: [.mouseEnteredAndExited, .mouseMoved, .activeAlways, .inVisibleRect],
             owner: self,
             userInfo: nil
         )
@@ -696,11 +896,20 @@
 
     override func hitTest(_ point: NSPoint) -> NSView? {
         guard !isHidden, bounds.contains(point) else { return nil }
-        return super.hitTest(point) ?? self
+        setPointerInside(true)
+        let hitView = super.hitTest(point)
+        if hitView == nil || hitView === backgroundView || hitView?.isDescendant(of: backgroundView) == true {
+            return self
+        }
+        return hitView
     }
 
     override func mouseEntered(with event: NSEvent) {
-        setPointerInside(true)
+        claimInteractionFocus()
+    }
+
+    override func mouseMoved(with event: NSEvent) {
+        claimInteractionFocus()
     }
 
     override func mouseExited(with event: NSEvent) {
@@ -708,12 +917,33 @@
     }
 
     override func mouseDown(with event: NSEvent) {
-        window?.makeKeyAndOrderFront(nil)
+        Diagnostics.log("usageTips.hud.mouseDown")
+        if routeButtonClickIfNeeded(event) {
+            return
+        }
+        claimInteractionFocus()
+    }
+
+    override func mouseDragged(with event: NSEvent) {
+        claimInteractionFocus()
     }
 
     override func mouseUp(with event: NSEvent) {}
-    override func rightMouseDown(with event: NSEvent) {}
+    override func rightMouseDown(with event: NSEvent) { claimInteractionFocus() }
     override func rightMouseUp(with event: NSEvent) {}
+    override func otherMouseDown(with event: NSEvent) { claimInteractionFocus() }
+    override func otherMouseUp(with event: NSEvent) {}
+
+    override func keyDown(with event: NSEvent) {
+        switch Int(event.keyCode) {
+        case kVK_LeftArrow:
+            selectUsageTip(offset: -1)
+        case kVK_RightArrow:
+            selectUsageTip(offset: 1)
+        default:
+            super.keyDown(with: event)
+        }
+    }
 
     override func scrollWheel(with event: NSEvent) {
         detailScrollView.scrollWheel(with: event)
@@ -721,21 +951,75 @@
 
     override func layout() {
         super.layout()
-        let titleWidth = min(320, max(240, bounds.width * 0.24))
-        let controlsWidth: CGFloat = 120
-        let detailWidth = max(1, bounds.width - titleWidth - controlsWidth)
+        let visualFrame = currentVisualFrame()
+        backgroundView.frame = visualFrame
 
-        titleBackgroundView.frame = NSRect(x: 0, y: 0, width: titleWidth, height: bounds.height)
-        titleLabel.frame = titleBackgroundView.bounds.insetBy(dx: 18, dy: 0)
-        detailScrollView.frame = NSRect(x: titleWidth, y: 0, width: detailWidth, height: bounds.height)
-        controlsView.frame = NSRect(x: titleWidth + detailWidth, y: 0, width: controlsWidth, height: bounds.height)
+        let paddingLeft: CGFloat = 22
+        let paddingRight: CGFloat = 16
+        let iconSize: CGFloat = 24
+        let iconTextGap: CGFloat = 10
+        let controlZoneWidth: CGFloat = 176
+        let buttonGap: CGFloat = 6
+        let buttonSize: CGFloat = 48
+        let buttonDotsGap: CGFloat = 6
+        let dotsHeight: CGFloat = 10
+        let dotsWidth = dotsView.preferredWidth
 
-        let buttonSize: CGFloat = 34
-        let buttonY: CGFloat = 9
-        previousButton.frame = NSRect(x: 22, y: buttonY, width: buttonSize, height: buttonSize)
-        nextButton.frame = NSRect(x: 64, y: buttonY, width: buttonSize, height: buttonSize)
-        dotsView.frame = NSRect(x: 18, y: 48, width: controlsWidth - 36, height: 10)
+        let titleX = visualFrame.minX + paddingLeft + iconSize + iconTextGap
+        let buttonsWidth = buttonSize * 2 + buttonGap
+        let buttonsGroupMinX = visualFrame.maxX - paddingRight - buttonsWidth
+        let textRight = visualFrame.maxX - controlZoneWidth
+        let availableTextWidth = max(1, textRight - titleX)
+        let titleWidth = availableTextWidth
+        let titleHeight: CGFloat = 32
+        let detailHeight: CGFloat = 68
+        let lineGap: CGFloat = 8
+        let textBlockHeight = titleHeight + lineGap + detailHeight
+        let textBlockY = visualFrame.minY + max(0, (visualFrame.height - textBlockHeight) / 2)
 
+        iconView.frame = NSRect(
+            x: visualFrame.minX + paddingLeft,
+            y: visualFrame.midY - iconSize / 2,
+            width: iconSize,
+            height: iconSize
+        )
+
+        titleLabel.frame = NSRect(
+            x: titleX,
+            y: textBlockY,
+            width: titleWidth,
+            height: titleHeight
+        )
+
+        let controlGroupHeight = buttonSize + buttonDotsGap + dotsHeight
+        let buttonY = visualFrame.minY + max(0, (visualFrame.height - controlGroupHeight) / 2)
+        nextButton.frame = NSRect(
+            x: buttonsGroupMinX + buttonSize + buttonGap,
+            y: buttonY,
+            width: buttonSize,
+            height: buttonSize
+        )
+        previousButton.frame = NSRect(
+            x: buttonsGroupMinX,
+            y: buttonY,
+            width: buttonSize,
+            height: buttonSize
+        )
+        dotsView.frame = NSRect(
+            x: buttonsGroupMinX + (buttonsWidth - dotsWidth) / 2,
+            y: previousButton.frame.maxY + buttonDotsGap,
+            width: dotsWidth,
+            height: dotsHeight
+        )
+
+        let detailX = titleX
+        let detailRight = textRight
+        detailScrollView.frame = NSRect(
+            x: detailX,
+            y: titleLabel.frame.maxY + lineGap,
+            width: max(1, detailRight - detailX),
+            height: detailHeight
+        )
         layoutDetailLabel()
     }
 
@@ -753,12 +1037,13 @@
         let safeIndex = min(max(0, coordinator.selectedUsageTipIndex), coordinator.usageTips.count - 1)
         let tip = coordinator.usageTips[safeIndex]
         titleLabel.stringValue = tr(tip.titleKey)
-        detailLabel.stringValue = tr(tip.detailKey)
-        previousButton.accessibilityLabel = tr("usageTips.previous")
-        nextButton.accessibilityLabel = tr("usageTips.next")
+        detailLabel.stringValue = formattedTipDetail(tr(tip.detailKey))
+        previousButton.buttonAccessibilityLabel = tr("usageTips.previous")
+        nextButton.buttonAccessibilityLabel = tr("usageTips.next")
         dotsView.configure(count: coordinator.usageTips.count, selectedIndex: safeIndex)
         updateColors()
-        layoutDetailLabel()
+        needsLayout = true
+        enclosingHostView?.relayoutUsageTipsAfterContentChange()
     }
 
     func clearHoverState() {
@@ -766,29 +1051,49 @@
         setPointerInside(false)
     }
 
+    func configureVisualLayout(width: CGFloat, height: CGFloat, bottomMargin: CGFloat) {
+        visualWidth = max(1, width)
+        visualHeight = max(1, height)
+        visualBottomMargin = max(0, bottomMargin)
+        needsLayout = true
+    }
+
+    func preferredWidth(maxAvailableWidth: CGFloat) -> CGFloat {
+        let padding: CGFloat = 22 + 16
+        let fixedWidth: CGFloat = 24 + 10 + 24 + 48 + 6 + 48
+        let titleWidth = ceil(titleLabel.attributedStringValue.size().width)
+        let detailWidth = measuredDetailLineWidth()
+        let contentWidth = padding + fixedWidth + max(titleWidth, detailWidth)
+        let preferredWidth = max(AppGridUsageTipsMetrics.minWidth, contentWidth)
+        return min(maxAvailableWidth, preferredWidth)
+    }
+
     private func setup() {
         wantsLayer = true
-        layer?.cornerRadius = 8
-        layer?.masksToBounds = true
+        layer?.shadowColor = NSColor.black.cgColor
+        layer?.shadowOpacity = 0.14
+        layer?.shadowRadius = 12
+        layer?.shadowOffset = NSSize(width: 0, height: -2)
 
-        titleBackgroundView.wantsLayer = true
-        controlsView.wantsLayer = true
+        backgroundView.material = .popover
+        backgroundView.blendingMode = .withinWindow
+        backgroundView.state = .active
+        backgroundView.wantsLayer = true
+        backgroundView.layer?.cornerRadius = 10
+        backgroundView.layer?.masksToBounds = true
+        backgroundView.layer?.borderWidth = 1
+        addSubview(backgroundView)
 
-        titleLabel.isEditable = false
-        titleLabel.isSelectable = false
-        titleLabel.drawsBackground = false
-        titleLabel.isBordered = false
-        titleLabel.lineBreakMode = .byTruncatingTail
-        titleLabel.maximumNumberOfLines = 1
-        titleLabel.font = NSFont.systemFont(ofSize: 24, weight: .bold)
+        let iconConfig = NSImage.SymbolConfiguration(pointSize: 24, weight: .semibold)
+        iconView.image = NSImage(
+            systemSymbolName: "lightbulb.fill",
+            accessibilityDescription: nil
+        )?.withSymbolConfiguration(iconConfig)
+        iconView.imageScaling = .scaleProportionallyDown
+        addSubview(iconView)
 
-        detailLabel.isEditable = false
-        detailLabel.isSelectable = false
-        detailLabel.drawsBackground = false
-        detailLabel.isBordered = false
-        detailLabel.lineBreakMode = .byClipping
-        detailLabel.maximumNumberOfLines = 1
-        detailLabel.font = NSFont.systemFont(ofSize: 24, weight: .bold)
+        configureLabel(titleLabel, font: titleFont, lineBreakMode: .byClipping)
+        configureDetailLabel()
 
         detailScrollView.drawsBackground = false
         detailScrollView.hasVerticalScroller = false
@@ -800,21 +1105,84 @@
         previousButton.action = { [weak self] in self?.selectUsageTip(offset: -1) }
         nextButton.action = { [weak self] in self?.selectUsageTip(offset: 1) }
 
-        addSubview(titleBackgroundView)
-        titleBackgroundView.addSubview(titleLabel)
+        addSubview(titleLabel)
         addSubview(detailScrollView)
-        addSubview(controlsView)
-        controlsView.addSubview(previousButton)
-        controlsView.addSubview(nextButton)
-        controlsView.addSubview(dotsView)
+        addSubview(dotsView)
+        addSubview(previousButton)
+        addSubview(nextButton)
 
         setAccessibilityRole(.group)
         updateColors()
     }
 
+    private func configureLabel(_ label: NSTextField, font: NSFont, lineBreakMode: NSLineBreakMode) {
+        label.cell = AppGridCenteredTextFieldCell(textCell: "")
+        label.isEditable = false
+        label.isSelectable = false
+        label.drawsBackground = false
+        label.isBordered = false
+        label.lineBreakMode = lineBreakMode
+        label.maximumNumberOfLines = 1
+        label.alignment = .center
+        label.font = font
+    }
+
+    private func configureDetailLabel() {
+        detailLabel.cell = NSTextFieldCell(textCell: "")
+        detailLabel.isEditable = false
+        detailLabel.isSelectable = false
+        detailLabel.drawsBackground = false
+        detailLabel.isBordered = false
+        detailLabel.lineBreakMode = .byWordWrapping
+        detailLabel.maximumNumberOfLines = 2
+        detailLabel.font = detailFont
+        detailLabel.cell?.usesSingleLineMode = false
+        detailLabel.cell?.wraps = true
+    }
+
+    private func claimInteractionFocus() {
+        setPointerInside(true)
+        window?.makeFirstResponder(self)
+    }
+
+    func handleMouseEventFromHost(_ event: NSEvent, triggerButtons: Bool) -> Bool {
+        let point = usageTipsPoint(for: event)
+        guard bounds.contains(point) else { return false }
+        if triggerButtons, routeButtonClickIfNeeded(event) {
+            return true
+        }
+        claimInteractionFocus()
+        return true
+    }
+
     private func selectUsageTip(offset: Int) {
         coordinator?.selectUsageTip(offset: offset)
         applyCoordinatorState()
+    }
+
+    private func routeButtonClickIfNeeded(_ event: NSEvent) -> Bool {
+        let point = usageTipsPoint(for: event)
+        let hitOutset: CGFloat = 24
+        if nextButton.frame.insetBy(dx: -hitOutset, dy: -hitOutset).contains(point) {
+            Diagnostics.log("usageTips.hud.routedNext")
+            selectUsageTip(offset: 1)
+            return true
+        }
+        if previousButton.frame.insetBy(dx: -hitOutset, dy: -hitOutset).contains(point) {
+            Diagnostics.log("usageTips.hud.routedPrevious")
+            selectUsageTip(offset: -1)
+            return true
+        }
+        return false
+    }
+
+    private func usageTipsPoint(for event: NSEvent) -> NSPoint {
+        if event.window === window {
+            return convert(event.locationInWindow, from: nil)
+        }
+        guard let window else { return .zero }
+        let windowPoint = window.convertPoint(fromScreen: NSEvent.mouseLocation)
+        return convert(windowPoint, from: nil)
     }
 
     private func setPointerInside(_ inside: Bool) {
@@ -838,36 +1206,90 @@
     }
 
     private func layoutDetailLabel() {
-        let textWidth = detailLabel.attributedStringValue.size().width
-        let width = max(detailScrollView.contentView.bounds.width, ceil(textWidth) + 36)
-        detailLabel.frame = NSRect(x: 18, y: 0, width: width, height: bounds.height)
+        let width = max(1, detailScrollView.contentView.bounds.width)
+        detailLabel.preferredMaxLayoutWidth = width
+        detailLabel.frame = NSRect(x: 0, y: 0, width: width, height: detailScrollView.bounds.height)
+    }
+
+    private func measuredDetailLineWidth() -> CGFloat {
+        let attributes: [NSAttributedString.Key: Any] = [.font: detailFont]
+        let lineWidths = detailLabel.stringValue
+            .components(separatedBy: .newlines)
+            .map { NSAttributedString(string: $0, attributes: attributes).size().width }
+        return ceil(lineWidths.max() ?? detailLabel.attributedStringValue.size().width)
+    }
+
+    private func currentVisualFrame() -> NSRect {
+        let width = min(bounds.width, visualWidth)
+        let height = min(bounds.height, visualHeight)
+        return NSRect(
+            x: max(0, (bounds.width - width) / 2),
+            y: max(0, bounds.height - visualBottomMargin - height),
+            width: width,
+            height: height
+        )
+    }
+
+    private func formattedTipDetail(_ text: String) -> String {
+        text
+            .replacingOccurrences(of: "\\s*(?:-->|->|>|→|>|,)\\s*", with: "\n", options: .regularExpression)
+            .replacingOccurrences(of: "[ \\t]{2,}", with: " ", options: .regularExpression)
+            .replacingOccurrences(of: "\\n{2,}", with: "\n", options: .regularExpression)
+            .trimmingCharacters(in: .whitespacesAndNewlines)
     }
 
     private func updateColors() {
-        layer?.backgroundColor = NSColor.black.withAlphaComponent(0.88).cgColor
-        layer?.borderColor = NSColor.labelColor.withAlphaComponent(0.18).cgColor
-        layer?.borderWidth = 1
-        titleBackgroundView.layer?.backgroundColor = NSColor.controlBackgroundColor.withAlphaComponent(0.96).cgColor
-        controlsView.layer?.backgroundColor = NSColor.black.withAlphaComponent(0.88).cgColor
-        titleLabel.textColor = .labelColor
-        detailLabel.textColor = NSColor.white.withAlphaComponent(0.96)
+        backgroundView.layer?.borderColor = NSColor.separatorColor.withAlphaComponent(0.35).cgColor
+        iconView.contentTintColor = .systemYellow
+        titleLabel.textColor = .secondaryLabelColor
+        detailLabel.textColor = .labelColor
+        previousButton.tintColor = .labelColor
+        nextButton.tintColor = .labelColor
+        dotsView.needsDisplay = true
+    }
+}
+
+private final class AppGridCenteredTextFieldCell: NSTextFieldCell {
+    override func drawingRect(forBounds rect: NSRect) -> NSRect {
+        var drawingRect = super.drawingRect(forBounds: rect)
+        let textHeight = cellSize(forBounds: rect).height
+        drawingRect.origin.y = rect.origin.y + max(0, (rect.height - textHeight) / 2)
+        drawingRect.size.height = min(rect.height, textHeight + 2)
+        return drawingRect
+    }
+}
+
+private final class AppGridDecorativeImageView: NSImageView {
+    override func hitTest(_ point: NSPoint) -> NSView? {
+        nil
+    }
+
+    override func isAccessibilityElement() -> Bool {
+        false
     }
 }
 
 private final class AppGridUsageTipIconButton: NSView {
     var action: (() -> Void)?
-    var accessibilityLabel: String = "" {
+    var buttonAccessibilityLabel: String = "" {
         didSet {
-            setAccessibilityLabel(accessibilityLabel)
+            setAccessibilityLabel(buttonAccessibilityLabel)
+        }
+    }
+    var tintColor: NSColor = .labelColor {
+        didSet {
+            imageView.contentTintColor = tintColor
         }
     }
 
-    private let imageView = NSImageView()
+    private let imageView = AppGridDecorativeImageView()
     private var trackingAreaRef: NSTrackingArea?
     private var isHovered = false
+    private var isPressed = false
 
     override var isFlipped: Bool { true }
     override var acceptsFirstResponder: Bool { false }
+    override var mouseDownCanMoveWindow: Bool { false }
 
     init(systemImage: String) {
         super.init(frame: .zero)
@@ -876,9 +1298,30 @@
 
     required init?(coder: NSCoder) { fatalError() }
 
+    override func acceptsFirstMouse(for event: NSEvent?) -> Bool {
+        true
+    }
+
+    override func isAccessibilityElement() -> Bool {
+        true
+    }
+
+    override func accessibilityRole() -> NSAccessibility.Role? {
+        .button
+    }
+
+    override func accessibilityLabel() -> String? {
+        buttonAccessibilityLabel
+    }
+
+    override func accessibilityPerformPress() -> Bool {
+        action?()
+        return true
+    }
+
     override func layout() {
         super.layout()
-        let size: CGFloat = 24
+        let size: CGFloat = 26
         imageView.frame = NSRect(
             x: (bounds.width - size) / 2,
             y: (bounds.height - size) / 2,
@@ -913,28 +1356,38 @@
 
     override func mouseExited(with event: NSEvent) {
         isHovered = false
+        isPressed = false
         needsDisplay = true
     }
 
     override func mouseDown(with event: NSEvent) {
+        Diagnostics.log("usageTips.button.mouseDown", [
+            "label": buttonAccessibilityLabel
+        ])
+        isPressed = true
+        needsDisplay = true
         action?()
+        DispatchQueue.main.asyncAfter(deadline: .now() + 0.10) { [weak self] in
+            self?.isPressed = false
+            self?.needsDisplay = true
+        }
     }
 
     override func draw(_ dirtyRect: NSRect) {
-        guard isHovered else { return }
-        NSColor.white.withAlphaComponent(0.12).setFill()
-        NSBezierPath(roundedRect: bounds.insetBy(dx: 1, dy: 1), xRadius: 7, yRadius: 7).fill()
+        guard isHovered || isPressed else { return }
+        tintColor.withAlphaComponent(isPressed ? 0.14 : 0.08).setFill()
+        NSBezierPath(roundedRect: bounds.insetBy(dx: 1, dy: 1), xRadius: 6, yRadius: 6).fill()
     }
 
     private func setup(systemImage: String) {
         wantsLayer = true
-        let config = NSImage.SymbolConfiguration(pointSize: 24, weight: .bold)
+        let config = NSImage.SymbolConfiguration(pointSize: 26, weight: .semibold)
         imageView.image = NSImage(
             systemSymbolName: systemImage,
             accessibilityDescription: nil
         )?.withSymbolConfiguration(config)
         imageView.imageScaling = .scaleProportionallyDown
-        imageView.contentTintColor = .white
+        imageView.contentTintColor = tintColor
         addSubview(imageView)
         setAccessibilityRole(.button)
     }
@@ -946,6 +1399,11 @@
 
     override var isFlipped: Bool { true }
 
+    var preferredWidth: CGFloat {
+        guard count > 0 else { return 0 }
+        return CGFloat(count) * 5 + CGFloat(max(0, count - 1)) * 5 + 2
+    }
+
     func configure(count: Int, selectedIndex: Int) {
         self.count = count
         self.selectedIndex = selectedIndex
@@ -954,23 +1412,28 @@
 
     override func draw(_ dirtyRect: NSRect) {
         guard count > 0 else { return }
-        let dotSize: CGFloat = 5
         let spacing: CGFloat = 5
-        let totalWidth = CGFloat(count) * dotSize + CGFloat(max(0, count - 1)) * spacing
+        let totalWidth = preferredWidth
         var x = (bounds.width - totalWidth) / 2
-        let y = (bounds.height - dotSize) / 2
+        let centerY = bounds.midY
 
         for index in 0..<count {
-            let color = index == selectedIndex
-                ? NSColor.controlAccentColor
-                : NSColor.white.withAlphaComponent(0.58)
+            let selected = index == selectedIndex
+            let dotSize: CGFloat = selected ? 6 : 5
+            let color = NSColor.labelColor.withAlphaComponent(selected ? 0.85 : 0.28)
             color.setFill()
             NSBezierPath(
-                ovalIn: NSRect(x: x, y: y, width: dotSize, height: dotSize)
+                ovalIn: NSRect(
+                    x: x,
+                    y: centerY - dotSize / 2,
+                    width: dotSize,
+                    height: dotSize
+                )
             ).fill()
-            x += dotSize + spacing
+            x += 5 + spacing
         }
     }
+
 }
 
 private final class AppGridContainerCollectionLayout: NSCollectionViewLayout {
@@ -1535,15 +1998,24 @@
         if displayStyle.usesCardSurface {
             let rect = bounds.insetBy(dx: 0.5, dy: 0.5)
             let path = NSBezierPath(roundedRect: rect, xRadius: 14, yRadius: 14)
-            cardSurfaceColor().setFill()
-            path.fill()
-            if coordinator.isColoredContainerMode || isColorlessActive {
-                tagColor.withAlphaComponent(0.30).setFill()
+            if coordinator.appGridTheme.usesDarkGlass {
+                drawDarkCardSurface(
+                    path: path,
+                    tagColor: tagColor,
+                    isActive: isHovered || isNavigationHighlighted,
+                    isTinted: coordinator.isColoredContainerMode || isColorlessActive
+                )
+            } else {
+                cardSurfaceColor().setFill()
                 path.fill()
+                if coordinator.isColoredContainerMode || isColorlessActive {
+                    tagColor.withAlphaComponent(0.30).setFill()
+                    path.fill()
+                }
+                NSColor.labelColor.withAlphaComponent(0.08).setStroke()
+                path.lineWidth = 1
+                path.stroke()
             }
-            NSColor.labelColor.withAlphaComponent(0.08).setStroke()
-            path.lineWidth = 1
-            path.stroke()
         }
 
         drawHeader(title: group.name, displayStyle: displayStyle)
@@ -1763,6 +2235,17 @@
             && (isHovered || isNavigationHighlighted)
         let shouldShadow = coordinator.displayStyle.usesCardSurface
             && ((coordinator.isColoredContainerMode && (isHovered || isNavigationHighlighted)) || isColorlessActive)
+        if coordinator.appGridTheme.usesDarkGlass && coordinator.displayStyle.usesCardSurface {
+            let tagColor = TagColor.nsColor(for: coordinator.tagColors[group?.name ?? ""] ?? 0)
+            let active = isHovered || isNavigationHighlighted
+            layer?.shadowColor = active
+                ? tagColor.withAlphaComponent(0.82).cgColor
+                : NSColor.black.withAlphaComponent(0.72).cgColor
+            layer?.shadowOpacity = active ? 0.50 : 0.28
+            layer?.shadowRadius = active ? 24 : 14
+            layer?.shadowOffset = NSSize(width: 0, height: active ? -8 : -4)
+            return
+        }
         layer?.shadowColor = NSColor.black.cgColor
         layer?.shadowOpacity = shouldShadow ? 0.22 : 0
         layer?.shadowRadius = shouldShadow ? 8 : 0
@@ -1798,6 +2281,40 @@
         return NSColor.white.withAlphaComponent(0.62)
     }
 
+    private func drawDarkCardSurface(
+        path: NSBezierPath,
+        tagColor: NSColor,
+        isActive: Bool,
+        isTinted: Bool
+    ) {
+        NSGraphicsContext.saveGraphicsState()
+        if isActive {
+            let glow = NSShadow()
+            glow.shadowColor = tagColor.withAlphaComponent(0.36)
+            glow.shadowBlurRadius = 28
+            glow.shadowOffset = NSSize(width: 0, height: -8)
+            glow.set()
+        }
+        NSColor(calibratedRed: 0.12, green: 0.18, blue: 0.25, alpha: 0.58).setFill()
+        path.fill()
+        NSGraphicsContext.restoreGraphicsState()
+
+        if isTinted {
+            tagColor.withAlphaComponent(isActive ? 0.18 : 0.10).setFill()
+            path.fill()
+        }
+
+        NSColor.white.withAlphaComponent(isActive ? 0.08 : 0.045).setFill()
+        path.fill()
+
+        let strokeColor = isActive
+            ? tagColor.withAlphaComponent(0.72)
+            : NSColor.white.withAlphaComponent(0.18)
+        strokeColor.setStroke()
+        path.lineWidth = isActive ? 1.4 : 1.0
+        path.stroke()
+    }
+
     private func drawHeader(title: String, displayStyle: AppGridCollectionDisplayMode) {
         let horizontalInset = displayStyle.usesCardSurface ? AppGridCollectionMetrics.cardPadding : 0
         let verticalInset = displayStyle.usesCardSurface ? AppGridCollectionMetrics.cardPadding : 0
@@ -1807,9 +2324,13 @@
             width: max(1, bounds.width - horizontalInset * 2),
             height: AppGridCollectionMetrics.headerHeight
         )
+        let isDarkGrid = coordinator?.appGridTheme.usesDarkGlass == true
+        let headerTextColor = isDarkGrid
+            ? NSColor.white.withAlphaComponent(0.74)
+            : NSColor.secondaryLabelColor
         let attributes: [NSAttributedString.Key: Any] = [
             .font: NSFont.systemFont(ofSize: 18, weight: .semibold),
-            .foregroundColor: NSColor.secondaryLabelColor,
+            .foregroundColor: headerTextColor,
             .paragraphStyle: centeredParagraph(lineBreak: .byTruncatingMiddle)
         ]
         let titleSize = title.size(withAttributes: attributes)
@@ -1821,7 +2342,9 @@
             height: headerRect.height - 6
         )
         let lineY = headerRect.midY
-        NSColor.secondaryLabelColor.withAlphaComponent(0.25).setStroke()
+        (isDarkGrid ? NSColor.white : NSColor.secondaryLabelColor)
+            .withAlphaComponent(isDarkGrid ? 0.16 : 0.25)
+            .setStroke()
         let leftLine = NSBezierPath()
         leftLine.move(to: NSPoint(x: headerRect.minX, y: lineY))
         leftLine.line(to: NSPoint(x: max(headerRect.minX, titleRect.minX - 2), y: lineY))

--
Gitblit v1.9.3