Ariver
2026-07-02 d77d35981ccd3f64c34727b8d21ac599012007bb
src/Apptag/AppGridCollectionView.swift
@@ -48,6 +48,7 @@
struct AppGridCollectionView: NSViewRepresentable {
    let groups: [TagGroup]
    let tagColors: [String: Int]
    let tagCustomColors: [String: TagCustomColor]
    let displayMode: String
    let iconSize: CGFloat
    let showNames: Bool
@@ -89,6 +90,7 @@
        context.coordinator.update(
            groups: groups,
            tagColors: tagColors,
            tagCustomColors: tagCustomColors,
            displayMode: displayMode,
            iconSize: iconSize,
            showNames: showNames,
@@ -122,6 +124,7 @@
    final class Coordinator: NSObject, NSCollectionViewDataSource, NSCollectionViewDelegate {
        var groups: [TagGroup] = []
        var tagColors: [String: Int] = [:]
        var tagCustomColors: [String: TagCustomColor] = [:]
        var displayMode = AppDefaults.displayMode
        var iconSize: CGFloat = AppDefaults.iconSize
        var showNames = true
@@ -164,6 +167,7 @@
        func update(
            groups: [TagGroup],
            tagColors: [String: Int],
            tagCustomColors: [String: TagCustomColor],
            displayMode: String,
            iconSize: CGFloat,
            showNames: Bool,
@@ -193,6 +197,7 @@
        ) {
            self.groups = groups
            self.tagColors = tagColors
            self.tagCustomColors = tagCustomColors
            self.displayMode = displayMode
            self.iconSize = iconSize
            self.showNames = showNames
@@ -225,6 +230,7 @@
            let nextSignature = Self.signature(
                tagColors: tagColors,
                tagCustomColors: tagCustomColors,
                displayMode: displayMode,
                iconSize: iconSize,
                showNames: showNames,
@@ -401,6 +407,7 @@
        private static func signature(
            tagColors: [String: Int],
            tagCustomColors: [String: TagCustomColor],
            displayMode: String,
            iconSize: CGFloat,
            showNames: Bool,
@@ -413,6 +420,10 @@
                .sorted { $0.key < $1.key }
                .map { "\($0.key)=\($0.value)" }
                .joined(separator: ",")
            let customColorPart = tagCustomColors
                .sorted { $0.key < $1.key }
                .map { "\($0.key)=\($0.value.signature)" }
                .joined(separator: ",")
            return [
                displayMode,
                "\(Int(iconSize.rounded()))",
@@ -421,8 +432,16 @@
                showUncommonAppBubbles ? "uncommon" : "allbubbles",
                "bottom=\(Int(bottomContentPadding.rounded()))",
                colorPart,
                customColorPart,
                "rev=\(contentRevision)"
            ].joined(separator: "|")
        }
        func tagColor(for groupName: String) -> NSColor {
            TagColor.nsColor(
                for: tagColors[groupName] ?? 0,
                customColor: tagCustomColors[groupName]
            )
        }
        private static func clampedUsageTipIndex(_ index: Int, tips: [AppGridUsageTip]) -> Int {
@@ -562,6 +581,17 @@
        DispatchQueue.main.async {
            coordinator.onDropOutsideGroup(path, source, copy)
        }
    }
    func canPreviewEmptyDrop(path: String, source: String, screenPoint: NSPoint, copy: Bool) -> Bool {
        guard let coordinator,
              coordinator.displayStyle != .flat
        else { return false }
        return !coordinator.shouldCancelEmptyDropForActiveReorder(
            path: path,
            screenPoint: screenPoint,
            copy: copy
        )
    }
    private func setup() {
@@ -1134,7 +1164,7 @@
        detailScrollView.borderType = .noBorder
        detailScrollView.documentView = detailLabel
        closeButton.action = { [weak self] in self?.hideUsageTips() }
        closeButton.action = { [weak self] in self?.requestHideUsageTips() }
        previousButton.action = { [weak self] in self?.selectUsageTip(offset: -1) }
        nextButton.action = { [weak self] in self?.selectUsageTip(offset: 1) }
@@ -1194,10 +1224,9 @@
        applyCoordinatorState()
    }
    private func hideUsageTips() {
    private func requestHideUsageTips() {
        Diagnostics.log("usageTips.hud.close")
        clearHoverState()
        isHidden = true
        coordinator?.onHideUsageTips()
    }
@@ -1206,7 +1235,7 @@
        let closeHitOutset: CGFloat = 8
        if closeButton.frame.insetBy(dx: -closeHitOutset, dy: -closeHitOutset).contains(point) {
            closeButton.performPressFeedback()
            hideUsageTips()
            requestHideUsageTips()
            return true
        }
        let hitOutset: CGFloat = 6
@@ -1767,7 +1796,7 @@
    private struct LayoutItem {
        let index: Int
        let frame: NSRect
        var frame: NSRect
    }
    private struct LayoutPlan {
@@ -1903,8 +1932,20 @@
        let columnWidth = floor((availableWidth - gap * CGFloat(columnCount - 1)) / CGFloat(columnCount))
        var columnHeights = Array(repeating: outerPadding, count: columnCount)
        var items: [LayoutItem] = []
        var lastItemIndexForColumn = Array<Int?>(repeating: nil, count: columnCount)
        let bottomPinnedGroups = groups.enumerated()
            .filter { bottomPinnedPriority(for: $0.element.containerID) != nil }
            .sorted {
                let lhsPriority = bottomPinnedPriority(for: $0.element.containerID) ?? Int.max
                let rhsPriority = bottomPinnedPriority(for: $1.element.containerID) ?? Int.max
                if lhsPriority != rhsPriority { return lhsPriority < rhsPriority }
                return $0.offset < $1.offset
            }
        let bottomPinnedIndexes = Set(bottomPinnedGroups.map { $0.offset })
        for (index, group) in groups.enumerated() {
            if bottomPinnedIndexes.contains(index) { continue }
            let columnIndex = columnHeights.indices.min { columnHeights[$0] < columnHeights[$1] } ?? 0
            let x = outerPadding + CGFloat(columnIndex) * (columnWidth + gap)
            let y = columnHeights[columnIndex]
@@ -1919,16 +1960,54 @@
                    frame: NSRect(x: x, y: y, width: columnWidth, height: height)
                )
            )
            lastItemIndexForColumn[columnIndex] = items.count - 1
            columnHeights[columnIndex] = y + height + gap
        }
        let tallest = columnHeights.max() ?? outerPadding
        let contentHeight = (groups.isEmpty ? outerPadding * 2 : tallest - gap + outerPadding)
        var contentBottom = columnHeights.max() ?? outerPadding
        if !bottomPinnedGroups.isEmpty {
            let alignedBottom = max(outerPadding, contentBottom - gap)
            for itemIndex in lastItemIndexForColumn.compactMap({ $0 }) {
                let currentFrame = items[itemIndex].frame
                let alignedHeight = alignedBottom - currentFrame.minY
                if alignedHeight > currentFrame.height {
                    items[itemIndex].frame.size.height = alignedHeight
                }
            }
        }
        for (index, group) in bottomPinnedGroups {
            let y = max(outerPadding, contentBottom)
            let height = AppGridCollectionMetrics.cardHeight(
                appCount: group.apps.count,
                width: availableWidth,
                iconSize: iconSize
            )
            items.append(
                LayoutItem(
                    index: index,
                    frame: NSRect(x: outerPadding, y: y, width: availableWidth, height: height)
                )
            )
            contentBottom = y + height + gap
        }
        let contentHeight = (items.isEmpty ? outerPadding * 2 : contentBottom - gap + outerPadding)
            + max(0, bottomContentPadding)
        return LayoutPlan(
            items: items,
            contentSize: NSSize(width: boundedContentWidth, height: max(1, contentHeight))
        )
    }
    private static func bottomPinnedPriority(for containerID: String) -> Int? {
        switch containerID {
        case AppContainerID.uncategorized:
            return 0
        case AppContainerID.appleBuiltIn:
            return 1
        default:
            return nil
        }
    }
    private struct RowItem {
@@ -2265,7 +2344,7 @@
    override func draw(_ dirtyRect: NSRect) {
        guard let coordinator, let group else { return }
        let displayStyle = coordinator.displayStyle
        let tagColor = TagColor.nsColor(for: coordinator.tagColors[group.name] ?? 0)
        let tagColor = coordinator.tagColor(for: group.name)
        let isNavigationHighlighted = coordinator.highlightedGroupName == group.name
        let isColorlessActive = coordinator.isColorlessContainerMode
            && (isHovered || isNavigationHighlighted)
@@ -2511,7 +2590,7 @@
        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 tagColor = coordinator.tagColor(for: group?.name ?? "")
            let active = isHovered || isNavigationHighlighted
            layer?.shadowColor = active
                ? tagColor.withAlphaComponent(0.82).cgColor