| | |
| | | struct AppGridCollectionView: NSViewRepresentable { |
| | | let groups: [TagGroup] |
| | | let tagColors: [String: Int] |
| | | let tagCustomColors: [String: TagCustomColor] |
| | | let displayMode: String |
| | | let iconSize: CGFloat |
| | | let showNames: Bool |
| | |
| | | context.coordinator.update( |
| | | groups: groups, |
| | | tagColors: tagColors, |
| | | tagCustomColors: tagCustomColors, |
| | | displayMode: displayMode, |
| | | iconSize: iconSize, |
| | | showNames: showNames, |
| | |
| | | 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 |
| | |
| | | func update( |
| | | groups: [TagGroup], |
| | | tagColors: [String: Int], |
| | | tagCustomColors: [String: TagCustomColor], |
| | | displayMode: String, |
| | | iconSize: CGFloat, |
| | | showNames: Bool, |
| | |
| | | ) { |
| | | self.groups = groups |
| | | self.tagColors = tagColors |
| | | self.tagCustomColors = tagCustomColors |
| | | self.displayMode = displayMode |
| | | self.iconSize = iconSize |
| | | self.showNames = showNames |
| | |
| | | |
| | | let nextSignature = Self.signature( |
| | | tagColors: tagColors, |
| | | tagCustomColors: tagCustomColors, |
| | | displayMode: displayMode, |
| | | iconSize: iconSize, |
| | | showNames: showNames, |
| | |
| | | |
| | | private static func signature( |
| | | tagColors: [String: Int], |
| | | tagCustomColors: [String: TagCustomColor], |
| | | displayMode: String, |
| | | iconSize: CGFloat, |
| | | showNames: Bool, |
| | |
| | | .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()))", |
| | |
| | | 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 { |
| | |
| | | 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() { |
| | |
| | | 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) } |
| | | |
| | |
| | | applyCoordinatorState() |
| | | } |
| | | |
| | | private func hideUsageTips() { |
| | | private func requestHideUsageTips() { |
| | | Diagnostics.log("usageTips.hud.close") |
| | | clearHoverState() |
| | | isHidden = true |
| | | coordinator?.onHideUsageTips() |
| | | } |
| | | |
| | |
| | | let closeHitOutset: CGFloat = 8 |
| | | if closeButton.frame.insetBy(dx: -closeHitOutset, dy: -closeHitOutset).contains(point) { |
| | | closeButton.performPressFeedback() |
| | | hideUsageTips() |
| | | requestHideUsageTips() |
| | | return true |
| | | } |
| | | let hitOutset: CGFloat = 6 |
| | |
| | | |
| | | private struct LayoutItem { |
| | | let index: Int |
| | | let frame: NSRect |
| | | var frame: NSRect |
| | | } |
| | | |
| | | private struct LayoutPlan { |
| | |
| | | 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] |
| | |
| | | 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 { |
| | |
| | | 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) |
| | |
| | | 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 |