From 844e2f2459bb31d6903f1010f694089edabfd8ec Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Fri, 29 May 2026 02:18:55 +0800
Subject: [PATCH] Fix app icon transparency
---
Apptag/AppGridCollectionView.swift | 427 ++++++++++++++++++++++++++++++++++++++++++++++-------
1 files changed, 371 insertions(+), 56 deletions(-)
diff --git a/Apptag/AppGridCollectionView.swift b/Apptag/AppGridCollectionView.swift
index 2f46f71..013e915 100644
--- a/Apptag/AppGridCollectionView.swift
+++ b/Apptag/AppGridCollectionView.swift
@@ -1,6 +1,34 @@
import SwiftUI
import AppKit
+fileprivate enum AppGridCollectionDisplayMode: Equatable {
+ case flat
+ case masonryContainer
+ case gridContainer
+
+ init(_ rawValue: String) {
+ switch rawValue {
+ case "container", "coloredContainer":
+ self = .masonryContainer
+ case "gridContainer", "coloredGridContainer":
+ self = .gridContainer
+ default:
+ self = .flat
+ }
+ }
+
+ var usesCardSurface: Bool {
+ self != .flat
+ }
+}
+
+fileprivate struct AppGridBubbleSuppressionReasons: OptionSet {
+ let rawValue: Int
+
+ static let externalInteraction = AppGridBubbleSuppressionReasons(rawValue: 1 << 0)
+ static let scroll = AppGridBubbleSuppressionReasons(rawValue: 1 << 1)
+}
+
struct AppGridCollectionView: NSViewRepresentable {
let groups: [TagGroup]
let tagColors: [String: Int]
@@ -17,6 +45,7 @@
let onBubbleHover: (AppInfo, CGRect, AppBubbleHoverEvent) -> Void
let onEditNote: (AppInfo, CGRect) -> Void
let onDropApp: (String, String, String, Bool) -> Void
+ let onDropOutsideGroup: (String, String, Bool) -> Void
let onGroupActivate: (String) -> Void
let onScrollActivity: () -> Void
let onDragModeChange: (Bool) -> Void
@@ -48,6 +77,7 @@
onBubbleHover: onBubbleHover,
onEditNote: onEditNote,
onDropApp: onDropApp,
+ onDropOutsideGroup: onDropOutsideGroup,
onGroupActivate: onGroupActivate,
onScrollActivity: onScrollActivity,
onDragModeChange: onDragModeChange
@@ -61,7 +91,8 @@
var displayMode = AppDefaults.displayMode
var iconSize: CGFloat = AppDefaults.iconSize
var showNames = true
- var bubbleDisabled = false
+ private var externalBubbleDisabled = false
+ private var scrollBubbleDisabled = false
var showUncommonAppBubbles = AppDefaults.showUncommonAppBubbles
var highlightedGroupName: String?
var contentRevision = 0
@@ -75,6 +106,7 @@
var onBubbleHover: (AppInfo, CGRect, AppBubbleHoverEvent) -> Void = { _, _, _ in }
var onEditNote: (AppInfo, CGRect) -> Void = { _, _ in }
var onDropApp: (String, String, String, Bool) -> Void = { _, _, _, _ in }
+ var onDropOutsideGroup: (String, String, Bool) -> Void = { _, _, _ in }
var onGroupActivate: (String) -> Void = { _ in }
var onScrollActivity: () -> Void = {}
var onDragModeChange: (Bool) -> Void = { _ in }
@@ -95,6 +127,7 @@
onBubbleHover: @escaping (AppInfo, CGRect, AppBubbleHoverEvent) -> Void,
onEditNote: @escaping (AppInfo, CGRect) -> Void,
onDropApp: @escaping (String, String, String, Bool) -> Void,
+ onDropOutsideGroup: @escaping (String, String, Bool) -> Void,
onGroupActivate: @escaping (String) -> Void,
onScrollActivity: @escaping () -> Void,
onDragModeChange: @escaping (Bool) -> Void
@@ -104,7 +137,7 @@
self.displayMode = displayMode
self.iconSize = iconSize
self.showNames = showNames
- self.bubbleDisabled = bubbleDisabled
+ self.externalBubbleDisabled = bubbleDisabled
self.showUncommonAppBubbles = showUncommonAppBubbles
self.highlightedGroupName = highlightedGroupName
self.contentRevision = contentRevision
@@ -114,6 +147,7 @@
self.onBubbleHover = onBubbleHover
self.onEditNote = onEditNote
self.onDropApp = onDropApp
+ self.onDropOutsideGroup = onDropOutsideGroup
self.onGroupActivate = onGroupActivate
self.onScrollActivity = onScrollActivity
self.onDragModeChange = onDragModeChange
@@ -124,7 +158,6 @@
iconSize: iconSize,
showNames: showNames,
showUncommonAppBubbles: showUncommonAppBubbles,
- highlightedGroupName: highlightedGroupName,
contentRevision: contentRevision
)
if nextSignature != contentSignature {
@@ -161,13 +194,46 @@
groups.firstIndex { $0.id == tagID || $0.name == tagID }
}
+ fileprivate var displayStyle: AppGridCollectionDisplayMode {
+ AppGridCollectionDisplayMode(displayMode)
+ }
+
+ var isColoredContainerMode: Bool {
+ displayMode == "coloredContainer" || displayMode == "coloredGridContainer"
+ }
+
+ var isColorlessContainerMode: Bool {
+ displayMode == "container" || displayMode == "gridContainer"
+ }
+
+ fileprivate var bubbleSuppressionReasons: AppGridBubbleSuppressionReasons {
+ var reasons: AppGridBubbleSuppressionReasons = []
+ if externalBubbleDisabled {
+ reasons.insert(.externalInteraction)
+ }
+ if scrollBubbleDisabled {
+ reasons.insert(.scroll)
+ }
+ return reasons
+ }
+
+ var bubbleDisabled: Bool {
+ !bubbleSuppressionReasons.isEmpty
+ }
+
+ @discardableResult
+ func setScrollBubbleDisabled(_ disabled: Bool) -> Bool {
+ guard scrollBubbleDisabled != disabled else { return false }
+ scrollBubbleDisabled = disabled
+ return true
+ }
+
private static func signature(
tagColors: [String: Int],
displayMode: String,
iconSize: CGFloat,
showNames: Bool,
showUncommonAppBubbles: Bool,
- highlightedGroupName: String?,
contentRevision: Int
) -> String {
let colorPart = tagColors
@@ -179,7 +245,6 @@
"\(Int(iconSize.rounded()))",
showNames ? "names" : "nonames",
showUncommonAppBubbles ? "uncommon" : "allbubbles",
- highlightedGroupName ?? "",
colorPart,
"rev=\(contentRevision)"
].joined(separator: "|")
@@ -187,7 +252,8 @@
}
}
-final class AppGridCollectionHostView: NSView {
+final class AppGridCollectionHostView: NSView, AppEmptyDropReceivingView {
+ private let emptyDropTargetID = UUID()
private let scrollView = NSScrollView()
private let collectionView = NSCollectionView()
private let gridLayout = AppGridContainerCollectionLayout()
@@ -195,6 +261,7 @@
private var scrollObserver: NSObjectProtocol?
private var lastLayoutSize: NSSize = .zero
private var lastReportedBoundsOrigin: NSPoint?
+ private var scrollUnfreezeWorkItem: DispatchWorkItem?
override var isFlipped: Bool { true }
@@ -212,6 +279,8 @@
if let scrollObserver {
NotificationCenter.default.removeObserver(scrollObserver)
}
+ scrollUnfreezeWorkItem?.cancel()
+ AppDragCoordinator.shared.unregisterEmptyDropTarget(id: emptyDropTargetID)
}
func configure(coordinator: AppGridCollectionView.Coordinator) {
@@ -228,9 +297,7 @@
collectionView.reloadData()
gridLayout.invalidateLayout()
} else {
- collectionView.visibleItems().forEach { item in
- (item as? AppGridGroupCollectionItem)?.refreshRuntimeState()
- }
+ refreshVisibleRuntimeState()
}
if coordinator.scrollRequestToken != coordinator.lastHandledScrollRequestToken {
@@ -253,6 +320,24 @@
if lastLayoutSize != bounds.size {
lastLayoutSize = bounds.size
gridLayout.invalidateLayout()
+ }
+ }
+
+ override func viewDidMoveToWindow() {
+ super.viewDidMoveToWindow()
+ if window == nil {
+ AppDragCoordinator.shared.unregisterEmptyDropTarget(id: emptyDropTargetID)
+ } else {
+ AppDragCoordinator.shared.registerEmptyDropTarget(id: emptyDropTargetID, view: self)
+ }
+ }
+
+ func performEmptyDrop(path: String, source: String, screenPoint: NSPoint, copy: Bool) {
+ guard let coordinator,
+ coordinator.displayStyle != .flat
+ else { return }
+ DispatchQueue.main.async {
+ coordinator.onDropOutsideGroup(path, source, copy)
}
}
@@ -285,7 +370,39 @@
guard let self,
self.recordScrollIfNeeded()
else { return }
- self.coordinator?.onScrollActivity()
+ self.handleScrollActivity()
+ }
+ }
+
+ private func handleScrollActivity() {
+ if coordinator?.setScrollBubbleDisabled(true) == true {
+ refreshVisibleRuntimeState()
+ }
+ coordinator?.onScrollActivity()
+
+ scrollUnfreezeWorkItem?.cancel()
+ let workItem = DispatchWorkItem { [weak self] in
+ guard let self else { return }
+ self.replayPointerHover()
+ if self.coordinator?.setScrollBubbleDisabled(false) == true {
+ self.refreshVisibleRuntimeState()
+ }
+ }
+ scrollUnfreezeWorkItem = workItem
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.18, execute: workItem)
+ }
+
+ private func refreshVisibleRuntimeState() {
+ collectionView.visibleItems().forEach { item in
+ (item as? AppGridGroupCollectionItem)?.refreshRuntimeState()
+ }
+ }
+
+ private func replayPointerHover() {
+ guard let window else { return }
+ let windowPoint = window.convertPoint(fromScreen: NSEvent.mouseLocation)
+ collectionView.visibleItems().forEach { item in
+ (item as? AppGridGroupCollectionItem)?.replayPointerHover(windowPoint: windowPoint)
}
}
@@ -328,7 +445,8 @@
let plan = Self.makePlan(
groups: visibleGroups,
contentWidth: collectionView.bounds.width,
- iconSize: iconSize
+ iconSize: iconSize,
+ displayMode: coordinator?.displayMode ?? AppDefaults.displayMode
)
var nextAttributes: [IndexPath: NSCollectionViewLayoutAttributes] = [:]
@@ -373,6 +491,22 @@
private static func makePlan(
groups: [TagGroup],
contentWidth: CGFloat,
+ iconSize: CGFloat,
+ displayMode: String
+ ) -> LayoutPlan {
+ switch AppGridCollectionDisplayMode(displayMode) {
+ case .flat:
+ return makeFlatPlan(groups: groups, contentWidth: contentWidth, iconSize: iconSize)
+ case .masonryContainer:
+ return makeMasonryPlan(groups: groups, contentWidth: contentWidth, iconSize: iconSize)
+ case .gridContainer:
+ return makeGridPlan(groups: groups, contentWidth: contentWidth, iconSize: iconSize)
+ }
+ }
+
+ private static func makeGridPlan(
+ groups: [TagGroup],
+ contentWidth: CGFloat,
iconSize: CGFloat
) -> LayoutPlan {
let boundedContentWidth = max(1, contentWidth)
@@ -404,6 +538,79 @@
}
let contentHeight = rows.isEmpty ? outerPadding * 2 : y - gap + outerPadding
+ return LayoutPlan(
+ items: items,
+ contentSize: NSSize(width: boundedContentWidth, height: max(1, contentHeight))
+ )
+ }
+
+ private static func makeFlatPlan(
+ groups: [TagGroup],
+ contentWidth: CGFloat,
+ iconSize: CGFloat
+ ) -> LayoutPlan {
+ let boundedContentWidth = max(1, contentWidth)
+ let outerPadding = AppGridCollectionMetrics.outerPadding
+ let availableWidth = max(1, boundedContentWidth - outerPadding * 2)
+ var y = outerPadding
+ var items: [LayoutItem] = []
+
+ for (index, group) in groups.enumerated() {
+ let height = AppGridCollectionMetrics.flatGroupHeight(
+ appCount: group.apps.count,
+ width: availableWidth,
+ iconSize: iconSize
+ )
+ items.append(
+ LayoutItem(
+ index: index,
+ frame: NSRect(x: outerPadding, y: y, width: availableWidth, height: height)
+ )
+ )
+ y += height + AppGridCollectionMetrics.flatGroupGap
+ }
+
+ let contentHeight = groups.isEmpty ? outerPadding * 2 : y - AppGridCollectionMetrics.flatGroupGap + outerPadding
+ return LayoutPlan(
+ items: items,
+ contentSize: NSSize(width: boundedContentWidth, height: max(1, contentHeight))
+ )
+ }
+
+ private static func makeMasonryPlan(
+ groups: [TagGroup],
+ contentWidth: CGFloat,
+ iconSize: CGFloat
+ ) -> LayoutPlan {
+ let boundedContentWidth = max(1, contentWidth)
+ let outerPadding = AppGridCollectionMetrics.outerPadding
+ let gap = AppGridCollectionMetrics.cardGap
+ let availableWidth = max(1, boundedContentWidth - outerPadding * 2)
+ let columnCount = preferredMasonryColumnCount(availableWidth: availableWidth)
+ let columnWidth = floor((availableWidth - gap * CGFloat(columnCount - 1)) / CGFloat(columnCount))
+ var columnHeights = Array(repeating: outerPadding, count: columnCount)
+ var items: [LayoutItem] = []
+
+ for (index, group) in groups.enumerated() {
+ let columnIndex = columnHeights.indices.min { columnHeights[$0] < columnHeights[$1] } ?? 0
+ let x = outerPadding + CGFloat(columnIndex) * (columnWidth + gap)
+ let y = columnHeights[columnIndex]
+ let height = AppGridCollectionMetrics.cardHeight(
+ appCount: group.apps.count,
+ width: columnWidth,
+ iconSize: iconSize
+ )
+ items.append(
+ LayoutItem(
+ index: index,
+ frame: NSRect(x: x, y: y, width: columnWidth, height: height)
+ )
+ )
+ columnHeights[columnIndex] = y + height + gap
+ }
+
+ let tallest = columnHeights.max() ?? outerPadding
+ let contentHeight = groups.isEmpty ? outerPadding * 2 : tallest - gap + outerPadding
return LayoutPlan(
items: items,
contentSize: NSSize(width: boundedContentWidth, height: max(1, contentHeight))
@@ -505,6 +712,14 @@
return 1
}
+ private static func preferredMasonryColumnCount(availableWidth: CGFloat) -> Int {
+ let preferredColumnWidth: CGFloat = 280
+ return max(
+ 1,
+ Int((availableWidth + AppGridCollectionMetrics.cardGap) / (preferredColumnWidth + AppGridCollectionMetrics.cardGap))
+ )
+ }
+
private static func spanPatterns(trackCount: Int) -> [[Int]] {
switch trackCount {
case 3:
@@ -525,6 +740,7 @@
private enum AppGridCollectionMetrics {
static let outerPadding: CGFloat = 20
static let cardGap: CGFloat = 16
+ static let flatGroupGap: CGFloat = 24
static let cardPadding: CGFloat = 16
static let headerHeight: CGFloat = 28
static let headerBottomGap: CGFloat = 6
@@ -541,10 +757,14 @@
iconSize * hoverScale + labelHeight + 22
}
- static func columns(width: CGFloat, iconSize: CGFloat) -> Int {
- let inner = max(1, width - cardPadding * 2)
+ static func columnsForIconArea(width: CGFloat, iconSize: CGFloat) -> Int {
+ let inner = max(1, width)
let itemW = iconCellWidth(iconSize: iconSize)
return max(1, Int((inner + iconColumnGap) / (itemW + iconColumnGap)))
+ }
+
+ static func columns(width: CGFloat, iconSize: CGFloat) -> Int {
+ columnsForIconArea(width: width - cardPadding * 2, iconSize: iconSize)
}
static func cardHeight(appCount: Int, width: CGFloat, iconSize: CGFloat) -> CGFloat {
@@ -563,6 +783,15 @@
static func iconRows(appCount: Int, width: CGFloat, iconSize: CGFloat) -> Int {
let cols = columns(width: width, iconSize: iconSize)
return max(1, (appCount + cols - 1) / cols)
+ }
+
+ static func flatGroupHeight(appCount: Int, width: CGFloat, iconSize: CGFloat) -> CGFloat {
+ let cols = columnsForIconArea(width: width, iconSize: iconSize)
+ let rows = max(1, (appCount + cols - 1) / cols)
+ return headerHeight
+ + headerBottomGap
+ + CGFloat(rows) * iconCellHeight(iconSize: iconSize)
+ + CGFloat(max(0, rows - 1)) * iconRowGap
}
}
@@ -585,6 +814,10 @@
cardView.refreshRuntimeState()
}
+ func replayPointerHover(windowPoint: NSPoint) {
+ cardView.replayPointerHover(windowPoint: windowPoint)
+ }
+
override func prepareForReuse() {
super.prepareForReuse()
cardView.prepareForReuse()
@@ -596,6 +829,7 @@
private weak var coordinator: AppGridCollectionView.Coordinator?
private var group: TagGroup?
private var iconViews: [AppGridIconNSView] = []
+ private var isMouseInside = false
private var isHovered = false
private var trackingAreaRef: NSTrackingArea?
@@ -629,6 +863,9 @@
AppDragCoordinator.shared.unregister(id: dropTargetID)
group = nil
coordinator = nil
+ isMouseInside = false
+ isHovered = false
+ layer?.shadowOpacity = 0
iconViews.forEach {
$0.prepareForReuse()
$0.removeFromSuperview()
@@ -638,10 +875,11 @@
func refreshRuntimeState() {
let runtime = AppGridIconRuntimeState(
- bubbleDisabled: coordinator?.bubbleDisabled ?? false,
+ bubbleSuppressionReasons: coordinator?.bubbleSuppressionReasons ?? [],
showUncommonAppBubbles: coordinator?.showUncommonAppBubbles ?? AppDefaults.showUncommonAppBubbles
)
iconViews.forEach { $0.runtimeState = runtime }
+ updateHoverPresentation()
needsDisplay = true
}
@@ -670,13 +908,11 @@
}
override func mouseEntered(with event: NSEvent) {
- isHovered = true
- needsDisplay = true
+ setPointerInside(true)
}
override func mouseExited(with event: NSEvent) {
- isHovered = false
- needsDisplay = true
+ setPointerInside(false)
}
override func mouseUp(with event: NSEvent) {
@@ -689,48 +925,48 @@
override func layout() {
super.layout()
guard let coordinator, let group else { return }
- let cols = AppGridCollectionMetrics.columns(width: bounds.width, iconSize: coordinator.iconSize)
- let innerWidth = max(1, bounds.width - AppGridCollectionMetrics.cardPadding * 2)
+ let contentRect = iconContentRect(displayStyle: coordinator.displayStyle)
+ let cols = AppGridCollectionMetrics.columnsForIconArea(width: contentRect.width, iconSize: coordinator.iconSize)
let cellWidth = max(
1,
- (innerWidth - AppGridCollectionMetrics.iconColumnGap * CGFloat(cols - 1)) / CGFloat(cols)
+ (contentRect.width - AppGridCollectionMetrics.iconColumnGap * CGFloat(cols - 1)) / CGFloat(cols)
)
let cellHeight = AppGridCollectionMetrics.iconCellHeight(iconSize: coordinator.iconSize)
- let startY = AppGridCollectionMetrics.cardPadding
- + AppGridCollectionMetrics.headerHeight
- + AppGridCollectionMetrics.headerBottomGap
for index in group.apps.indices {
guard index < iconViews.count else { continue }
let row = index / cols
let col = index % cols
- let x = AppGridCollectionMetrics.cardPadding
+ let x = contentRect.minX
+ CGFloat(col) * (cellWidth + AppGridCollectionMetrics.iconColumnGap)
- let y = startY + CGFloat(row) * (cellHeight + AppGridCollectionMetrics.iconRowGap)
+ let y = contentRect.minY + CGFloat(row) * (cellHeight + AppGridCollectionMetrics.iconRowGap)
iconViews[index].frame = NSRect(x: x, y: y, width: cellWidth, height: cellHeight)
}
}
override func draw(_ dirtyRect: NSRect) {
guard let coordinator, let group else { return }
- let rect = bounds.insetBy(dx: 0.5, dy: 0.5)
- let path = NSBezierPath(roundedRect: rect, xRadius: 14, yRadius: 14)
+ let displayStyle = coordinator.displayStyle
let tagColor = TagColor.nsColor(for: coordinator.tagColors[group.name] ?? 0)
- let isColored = coordinator.displayMode == "coloredGridContainer"
- let isColorlessActive = coordinator.displayMode == "gridContainer"
- && (isHovered || coordinator.highlightedGroupName == group.name)
+ let isNavigationHighlighted = coordinator.highlightedGroupName == group.name
+ let isColorlessActive = coordinator.isColorlessContainerMode
+ && (isHovered || isNavigationHighlighted)
- cardSurfaceColor().setFill()
- path.fill()
- if isColored || isColorlessActive {
- tagColor.withAlphaComponent(0.30).setFill()
+ 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()
+ 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)
+ drawHeader(title: group.name, displayStyle: displayStyle)
}
func performDrop(path: String, source: String, copy: Bool) {
@@ -739,6 +975,11 @@
if source != group.name || copy {
coordinator?.onDragModeChange(false)
}
+ }
+
+ func replayPointerHover(windowPoint: NSPoint) {
+ setPointerInside(bounds.contains(convert(windowPoint, from: nil)))
+ iconViews.forEach { $0.replayPointerHover(windowPoint: windowPoint) }
}
private func rebuildIconViews() {
@@ -769,6 +1010,60 @@
AppDragCoordinator.shared.register(id: dropTargetID, view: self, tag: group.name)
}
+ private func setPointerInside(_ inside: Bool) {
+ isMouseInside = inside
+ updateHoverPresentation()
+ }
+
+ private func updateHoverPresentation() {
+ guard let coordinator else { return }
+ let shouldHover = coordinator.displayStyle.usesCardSurface
+ && isMouseInside
+ && !coordinator.bubbleDisabled
+ guard isHovered != shouldHover else {
+ updateCardShadow()
+ return
+ }
+ isHovered = shouldHover
+ updateCardShadow()
+ needsDisplay = true
+ }
+
+ private func updateCardShadow() {
+ guard let coordinator else { return }
+ let isNavigationHighlighted = coordinator.highlightedGroupName == group?.name
+ let isColorlessActive = coordinator.isColorlessContainerMode
+ && (isHovered || isNavigationHighlighted)
+ let shouldShadow = coordinator.displayStyle.usesCardSurface
+ && ((coordinator.isColoredContainerMode && (isHovered || isNavigationHighlighted)) || isColorlessActive)
+ layer?.shadowColor = NSColor.black.cgColor
+ layer?.shadowOpacity = shouldShadow ? 0.22 : 0
+ layer?.shadowRadius = shouldShadow ? 8 : 0
+ layer?.shadowOffset = NSSize(width: 0, height: -3)
+ }
+
+ private func iconContentRect(displayStyle: AppGridCollectionDisplayMode) -> NSRect {
+ switch displayStyle {
+ case .flat:
+ return NSRect(
+ x: 0,
+ y: AppGridCollectionMetrics.headerHeight + AppGridCollectionMetrics.headerBottomGap,
+ width: bounds.width,
+ height: max(1, bounds.height - AppGridCollectionMetrics.headerHeight - AppGridCollectionMetrics.headerBottomGap)
+ )
+ case .masonryContainer, .gridContainer:
+ let startY = AppGridCollectionMetrics.cardPadding
+ + AppGridCollectionMetrics.headerHeight
+ + AppGridCollectionMetrics.headerBottomGap
+ return NSRect(
+ x: AppGridCollectionMetrics.cardPadding,
+ y: startY,
+ width: max(1, bounds.width - AppGridCollectionMetrics.cardPadding * 2),
+ height: max(1, bounds.height - startY - AppGridCollectionMetrics.cardPadding)
+ )
+ }
+ }
+
private func cardSurfaceColor() -> NSColor {
if effectiveAppearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua {
return NSColor.white.withAlphaComponent(0.055)
@@ -776,11 +1071,13 @@
return NSColor.white.withAlphaComponent(0.62)
}
- private func drawHeader(title: String) {
+ private func drawHeader(title: String, displayStyle: AppGridCollectionDisplayMode) {
+ let horizontalInset = displayStyle.usesCardSurface ? AppGridCollectionMetrics.cardPadding : 0
+ let verticalInset = displayStyle.usesCardSurface ? AppGridCollectionMetrics.cardPadding : 0
let headerRect = NSRect(
- x: AppGridCollectionMetrics.cardPadding,
- y: AppGridCollectionMetrics.cardPadding,
- width: max(1, bounds.width - AppGridCollectionMetrics.cardPadding * 2),
+ x: horizontalInset,
+ y: verticalInset,
+ width: max(1, bounds.width - horizontalInset * 2),
height: AppGridCollectionMetrics.headerHeight
)
let attributes: [NSAttributedString.Key: Any] = [
@@ -818,8 +1115,12 @@
}
private struct AppGridIconRuntimeState {
- var bubbleDisabled: Bool
+ var bubbleSuppressionReasons: AppGridBubbleSuppressionReasons
var showUncommonAppBubbles: Bool
+
+ var bubbleDisabled: Bool {
+ !bubbleSuppressionReasons.isEmpty
+ }
}
private final class AppGridIconNSView: NSView {
@@ -828,6 +1129,7 @@
private var sourceTag = ""
private var iconSize: CGFloat = AppDefaults.iconSize
private var showName = true
+ private var isMouseInside = false
private var isHovered = false
private var trackingAreaRef: NSTrackingArea?
private var mouseDownEvent: NSEvent?
@@ -836,13 +1138,11 @@
private var longPressWorkItem: DispatchWorkItem?
var runtimeState = AppGridIconRuntimeState(
- bubbleDisabled: false,
+ bubbleSuppressionReasons: [],
showUncommonAppBubbles: AppDefaults.showUncommonAppBubbles
) {
didSet {
- if runtimeState.bubbleDisabled {
- setHover(false, notify: true)
- }
+ updateHoverPresentation(notify: true)
}
}
@@ -861,7 +1161,7 @@
self.showName = showName
self.coordinator = coordinator
runtimeState = AppGridIconRuntimeState(
- bubbleDisabled: coordinator.bubbleDisabled,
+ bubbleSuppressionReasons: coordinator.bubbleSuppressionReasons,
showUncommonAppBubbles: coordinator.showUncommonAppBubbles
)
needsDisplay = true
@@ -872,6 +1172,7 @@
if isHovered {
setHover(false, notify: true)
}
+ isMouseInside = false
mouseDownEvent = nil
didStartDrag = false
isLongPressActive = false
@@ -935,17 +1236,16 @@
.foregroundColor: NSColor.labelColor.withAlphaComponent(alpha),
.paragraphStyle: paragraph
]
- app.name.draw(with: labelRect, options: [.usesLineFragmentOrigin], attributes: attributes)
+ app.displayName.draw(with: labelRect, options: [.usesLineFragmentOrigin], attributes: attributes)
}
}
override func mouseEntered(with event: NSEvent) {
- guard !runtimeState.bubbleDisabled else { return }
- setHover(true, notify: true)
+ setPointerInside(true, notify: true)
}
override func mouseExited(with event: NSEvent) {
- setHover(false, notify: true)
+ setPointerInside(false, notify: true)
}
override func mouseDown(with event: NSEvent) {
@@ -956,7 +1256,7 @@
guard let self, self.mouseDownEvent != nil else { return }
self.isLongPressActive = true
self.coordinator?.onDragModeChange(true)
- self.setHover(false, notify: true)
+ self.updateHoverPresentation(notify: true, forceHidden: true)
}
longPressWorkItem = workItem
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: workItem)
@@ -1007,11 +1307,12 @@
}
override func rightMouseDown(with event: NSEvent) {
- guard let app else { return }
+ guard !runtimeState.bubbleDisabled, let app else { return }
coordinator?.onEditNote(app, rootLocalFrame())
}
override func menu(for event: NSEvent) -> NSMenu? {
+ guard !runtimeState.bubbleDisabled else { return nil }
let menu = NSMenu()
let item = NSMenuItem(title: tr("appNote.edit"), action: #selector(editNoteFromMenu), keyEquivalent: "")
item.target = self
@@ -1020,8 +1321,12 @@
}
@objc private func editNoteFromMenu() {
- guard let app else { return }
+ guard !runtimeState.bubbleDisabled, let app else { return }
coordinator?.onEditNote(app, rootLocalFrame())
+ }
+
+ func replayPointerHover(windowPoint: NSPoint) {
+ setPointerInside(bounds.contains(convert(windowPoint, from: nil)), notify: true)
}
private var shouldShowAppBubble: Bool {
@@ -1050,6 +1355,16 @@
}
}
+ private func setPointerInside(_ inside: Bool, notify: Bool) {
+ isMouseInside = inside
+ updateHoverPresentation(notify: notify)
+ }
+
+ private func updateHoverPresentation(notify: Bool, forceHidden: Bool = false) {
+ let shouldHover = isMouseInside && !runtimeState.bubbleDisabled && !forceHidden
+ setHover(shouldHover, notify: notify)
+ }
+
private func makeDragImage() -> NSImage {
guard let app else { return NSImage(size: .zero) }
let scale: CGFloat = AppGridCollectionMetrics.hoverScale * 1.5
--
Gitblit v1.9.3