From 0f7ee6e29f70017dd4fdfe2dd3a6106f7204f04f Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Thu, 02 Jul 2026 02:40:40 +0800
Subject: [PATCH] Document Raycast extension backlog
---
src/Apptag/AppGridCollectionView.swift | 598 +++++++++++++++++++++++++++++++++++++++++++++++++++--------
1 files changed, 514 insertions(+), 84 deletions(-)
diff --git a/src/Apptag/AppGridCollectionView.swift b/src/Apptag/AppGridCollectionView.swift
index 2d41fc4..ba2b864 100644
--- a/src/Apptag/AppGridCollectionView.swift
+++ b/src/Apptag/AppGridCollectionView.swift
@@ -38,8 +38,8 @@
}
enum AppGridUsageTipsMetrics {
- static let barHeight: CGFloat = 136
- static let reservedHeight: CGFloat = 176
+ static let barHeight: CGFloat = 154
+ static let reservedHeight: CGFloat = 194
static let bottomMargin: CGFloat = 20
static let horizontalInset: CGFloat = 24
static let minWidth: CGFloat = 640
@@ -48,9 +48,11 @@
struct AppGridCollectionView: NSViewRepresentable {
let groups: [TagGroup]
let tagColors: [String: Int]
+ let tagCustomColors: [String: TagCustomColor]
let displayMode: String
let iconSize: CGFloat
let showNames: Bool
+ let appGridTheme: AppGridTheme
let bubbleDisabled: Bool
let showUncommonAppBubbles: Bool
let highlightedGroupName: String?
@@ -70,6 +72,7 @@
let onGroupActivate: (String) -> Void
let onScrollActivity: () -> Void
let onDragModeChange: (Bool) -> Void
+ let onHideUsageTips: () -> Void
let onUsageTipsHoverChange: (Bool) -> Void
func makeCoordinator() -> Coordinator {
@@ -87,9 +90,11 @@
context.coordinator.update(
groups: groups,
tagColors: tagColors,
+ tagCustomColors: tagCustomColors,
displayMode: displayMode,
iconSize: iconSize,
showNames: showNames,
+ appGridTheme: appGridTheme,
bubbleDisabled: bubbleDisabled,
showUncommonAppBubbles: showUncommonAppBubbles,
highlightedGroupName: highlightedGroupName,
@@ -109,6 +114,7 @@
onGroupActivate: onGroupActivate,
onScrollActivity: onScrollActivity,
onDragModeChange: onDragModeChange,
+ onHideUsageTips: onHideUsageTips,
onUsageTipIndexChange: { selectedUsageTipIndexBinding.wrappedValue = $0 },
onUsageTipsHoverChange: onUsageTipsHoverChange
)
@@ -118,9 +124,11 @@
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
+ var appGridTheme = AppGridTheme.fallback
private var externalBubbleDisabled = false
private var scrollBubbleDisabled = false
var showUncommonAppBubbles = AppDefaults.showUncommonAppBubbles
@@ -146,6 +154,7 @@
var onGroupActivate: (String) -> Void = { _ in }
var onScrollActivity: () -> Void = {}
var onDragModeChange: (Bool) -> Void = { _ in }
+ var onHideUsageTips: () -> Void = {}
var onUsageTipIndexChange: (Int) -> Void = { _ in }
var onUsageTipsHoverChange: (Bool) -> Void = { _ in }
@@ -158,9 +167,11 @@
func update(
groups: [TagGroup],
tagColors: [String: Int],
+ tagCustomColors: [String: TagCustomColor],
displayMode: String,
iconSize: CGFloat,
showNames: Bool,
+ appGridTheme: AppGridTheme,
bubbleDisabled: Bool,
showUncommonAppBubbles: Bool,
highlightedGroupName: String?,
@@ -180,14 +191,17 @@
onGroupActivate: @escaping (String) -> Void,
onScrollActivity: @escaping () -> Void,
onDragModeChange: @escaping (Bool) -> Void,
+ onHideUsageTips: @escaping () -> Void,
onUsageTipIndexChange: @escaping (Int) -> Void,
onUsageTipsHoverChange: @escaping (Bool) -> Void
) {
self.groups = groups
self.tagColors = tagColors
+ self.tagCustomColors = tagCustomColors
self.displayMode = displayMode
self.iconSize = iconSize
self.showNames = showNames
+ self.appGridTheme = appGridTheme
self.externalBubbleDisabled = bubbleDisabled
self.showUncommonAppBubbles = showUncommonAppBubbles
self.highlightedGroupName = highlightedGroupName
@@ -207,6 +221,7 @@
self.onGroupActivate = onGroupActivate
self.onScrollActivity = onScrollActivity
self.onDragModeChange = onDragModeChange
+ self.onHideUsageTips = onHideUsageTips
self.onUsageTipIndexChange = onUsageTipIndexChange
self.onUsageTipsHoverChange = onUsageTipsHoverChange
if !self.usageTipsVisible {
@@ -215,9 +230,11 @@
let nextSignature = Self.signature(
tagColors: tagColors,
+ tagCustomColors: tagCustomColors,
displayMode: displayMode,
iconSize: iconSize,
showNames: showNames,
+ appGridTheme: appGridTheme,
showUncommonAppBubbles: showUncommonAppBubbles,
bottomContentPadding: self.bottomContentPadding,
contentRevision: contentRevision
@@ -390,9 +407,11 @@
private static func signature(
tagColors: [String: Int],
+ tagCustomColors: [String: TagCustomColor],
displayMode: String,
iconSize: CGFloat,
showNames: Bool,
+ appGridTheme: AppGridTheme,
showUncommonAppBubbles: Bool,
bottomContentPadding: CGFloat,
contentRevision: Int
@@ -401,15 +420,28 @@
.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()))",
showNames ? "names" : "nonames",
+ "theme=\(appGridTheme.rawValue)",
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 {
@@ -477,6 +509,7 @@
func applyCoordinatorUpdate() {
guard let coordinator else { return }
+ applyAppearance()
if coordinator.needsReload {
coordinator.needsReload = false
collectionView.reloadData()
@@ -548,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() {
@@ -766,6 +810,12 @@
}
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 {
@@ -823,10 +873,12 @@
weak var coordinator: AppGridCollectionView.Coordinator?
private let backgroundView = NSVisualEffectView()
+ private let titlePanelView = NSView()
private let iconView = AppGridDecorativeImageView()
private let titleLabel = NSTextField(labelWithString: "")
private let detailScrollView = NSScrollView()
private let detailLabel = NSTextField(labelWithString: "")
+ private let closeButton = AppGridUsageTipIconButton(systemImage: "xmark")
private let previousButton = AppGridUsageTipIconButton(systemImage: "chevron.left")
private let nextButton = AppGridUsageTipIconButton(systemImage: "chevron.right")
private let dotsView = AppGridUsageTipDotsView()
@@ -835,9 +887,10 @@
private var visualWidth: CGFloat = AppGridUsageTipsMetrics.minWidth
private var visualHeight: CGFloat = AppGridUsageTipsMetrics.barHeight
private var visualBottomMargin: CGFloat = AppGridUsageTipsMetrics.bottomMargin
+ private var currentDetailText = ""
- private let titleFont = NSFont.systemFont(ofSize: 24, weight: .semibold)
- private let detailFont = NSFont.systemFont(ofSize: 24, weight: .regular)
+ private let titleFont = NSFont.systemFont(ofSize: 25, weight: .bold)
+ private let detailFont = NSFont.systemFont(ofSize: 24, weight: .medium)
override var isFlipped: Bool { true }
override var acceptsFirstResponder: Bool { true }
@@ -939,42 +992,65 @@
let visualFrame = currentVisualFrame()
backgroundView.frame = visualFrame
- 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 outerPaddingX: CGFloat = 20
+ let outerPaddingY: CGFloat = 19
+ let titlePanelWidth = min(
+ max(350, visualFrame.width * 0.24),
+ min(460, visualFrame.width * 0.36)
+ )
+ let titlePanelHeight = max(1, min(116, visualFrame.height - outerPaddingY * 2))
+ let titlePanelFrame = NSRect(
+ x: visualFrame.minX + outerPaddingX,
+ y: visualFrame.midY - titlePanelHeight / 2,
+ width: titlePanelWidth,
+ height: titlePanelHeight
+ )
+ titlePanelView.frame = titlePanelFrame
+
+ let iconSize: CGFloat = 26
+ let controlZoneWidth: CGFloat = min(148, max(128, visualFrame.width * 0.08))
+ let buttonGap: CGFloat = 12
+ let buttonSize: CGFloat = 52
+ let buttonDotsGap: CGFloat = 10
let dotsHeight: CGFloat = 10
let dotsWidth = dotsView.preferredWidth
- let fullTitleWidth = ceil(titleLabel.attributedStringValue.size().width)
- let titleX = visualFrame.minX + paddingLeft + iconSize + iconTextGap
+ let titleIconGap: CGFloat = 20
+ let titleSidePadding: CGFloat = 28
+ let titleX = titlePanelFrame.minX + titleSidePadding + iconSize + titleIconGap
+ let titleWidth = max(1, titlePanelFrame.maxX - titleX - titleSidePadding)
+ let titleHeight: CGFloat = 68
+ let titleY = titlePanelFrame.midY - titleHeight / 2
+ let detailGap = visualFrame.width < 980 ? CGFloat(30) : CGFloat(52)
+ let controlGap = visualFrame.width < 980 ? CGFloat(24) : CGFloat(36)
+ let detailX = titlePanelFrame.maxX + detailGap
+ let controlsMaxX = visualFrame.maxX - 54
+ let textRight = controlsMaxX - controlZoneWidth - controlGap
+ let detailHeight: CGFloat = 88
+ let detailY = visualFrame.minY + max(0, (visualFrame.height - detailHeight) / 2)
+
let buttonsWidth = buttonSize * 2 + buttonGap
- let buttonsGroupMinX = visualFrame.maxX - paddingRight - buttonsWidth
- let textRight = visualFrame.maxX - controlZoneWidth
- let availableTextWidth = max(1, textRight - titleX)
- let titleWidth = min(fullTitleWidth, 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)
+ let buttonsGroupMinX = controlsMaxX - buttonsWidth
iconView.frame = NSRect(
- x: visualFrame.minX + paddingLeft,
- y: visualFrame.midY - iconSize / 2,
+ x: titlePanelFrame.minX + titleSidePadding,
+ y: titlePanelFrame.midY - iconSize / 2,
width: iconSize,
height: iconSize
)
titleLabel.frame = NSRect(
x: titleX,
- y: textBlockY,
+ y: titleY,
width: titleWidth,
height: titleHeight
+ )
+
+ closeButton.frame = NSRect(
+ x: visualFrame.maxX - 40,
+ y: visualFrame.minY + 10,
+ width: 30,
+ height: 30
)
let controlGroupHeight = buttonSize + buttonDotsGap + dotsHeight
@@ -998,12 +1074,10 @@
height: dotsHeight
)
- let detailX = titleX
- let detailRight = textRight
detailScrollView.frame = NSRect(
x: detailX,
- y: titleLabel.frame.maxY + lineGap,
- width: max(1, detailRight - detailX),
+ y: detailY,
+ width: max(1, textRight - detailX),
height: detailHeight
)
layoutDetailLabel()
@@ -1023,7 +1097,9 @@
let safeIndex = min(max(0, coordinator.selectedUsageTipIndex), coordinator.usageTips.count - 1)
let tip = coordinator.usageTips[safeIndex]
titleLabel.stringValue = tr(tip.titleKey)
- detailLabel.stringValue = formattedTipDetail(tr(tip.detailKey))
+ updateTipIcon(id: tip.id)
+ currentDetailText = formattedTipDetail(tr(tip.detailKey))
+ closeButton.buttonAccessibilityLabel = tr("usageTips.close")
previousButton.buttonAccessibilityLabel = tr("usageTips.previous")
nextButton.buttonAccessibilityLabel = tr("usageTips.next")
dotsView.configure(count: coordinator.usageTips.count, selectedIndex: safeIndex)
@@ -1045,13 +1121,7 @@
}
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)
+ max(1, maxAvailableWidth)
}
private func setup() {
@@ -1070,7 +1140,13 @@
backgroundView.layer?.borderWidth = 1
addSubview(backgroundView)
- let iconConfig = NSImage.SymbolConfiguration(pointSize: 24, weight: .semibold)
+ titlePanelView.wantsLayer = true
+ titlePanelView.layer?.cornerRadius = 18
+ titlePanelView.layer?.masksToBounds = true
+ titlePanelView.layer?.borderWidth = 1
+ addSubview(titlePanelView)
+
+ let iconConfig = NSImage.SymbolConfiguration(pointSize: 26, weight: .bold)
iconView.image = NSImage(
systemSymbolName: "lightbulb.fill",
accessibilityDescription: nil
@@ -1078,7 +1154,7 @@
iconView.imageScaling = .scaleProportionallyDown
addSubview(iconView)
- configureLabel(titleLabel, font: titleFont, lineBreakMode: .byClipping)
+ configureLabel(titleLabel, font: titleFont, lineBreakMode: .byWordWrapping)
configureDetailLabel()
detailScrollView.drawsBackground = false
@@ -1088,12 +1164,14 @@
detailScrollView.borderType = .noBorder
detailScrollView.documentView = detailLabel
+ 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) }
addSubview(titleLabel)
addSubview(detailScrollView)
addSubview(dotsView)
+ addSubview(closeButton)
addSubview(previousButton)
addSubview(nextButton)
@@ -1108,12 +1186,13 @@
label.drawsBackground = false
label.isBordered = false
label.lineBreakMode = lineBreakMode
- label.maximumNumberOfLines = 1
+ label.maximumNumberOfLines = 2
+ label.alignment = .left
label.font = font
}
private func configureDetailLabel() {
- detailLabel.cell = NSTextFieldCell(textCell: "")
+ detailLabel.cell = AppGridCenteredMultilineTextFieldCell(textCell: "")
detailLabel.isEditable = false
detailLabel.isSelectable = false
detailLabel.drawsBackground = false
@@ -1145,9 +1224,21 @@
applyCoordinatorState()
}
+ private func requestHideUsageTips() {
+ Diagnostics.log("usageTips.hud.close")
+ clearHoverState()
+ coordinator?.onHideUsageTips()
+ }
+
private func routeButtonClickIfNeeded(_ event: NSEvent) -> Bool {
let point = usageTipsPoint(for: event)
- let hitOutset: CGFloat = 24
+ let closeHitOutset: CGFloat = 8
+ if closeButton.frame.insetBy(dx: -closeHitOutset, dy: -closeHitOutset).contains(point) {
+ closeButton.performPressFeedback()
+ requestHideUsageTips()
+ return true
+ }
+ let hitOutset: CGFloat = 6
if nextButton.frame.insetBy(dx: -hitOutset, dy: -hitOutset).contains(point) {
Diagnostics.log("usageTips.hud.routedNext")
selectUsageTip(offset: 1)
@@ -1196,14 +1287,6 @@
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)
@@ -1216,21 +1299,197 @@
}
private func formattedTipDetail(_ text: String) -> String {
- text
+ let normalized = 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)
+ return orderedTipDetail(normalized)
+ }
+
+ private func orderedTipDetail(_ text: String) -> String {
+ let lines = text
+ .components(separatedBy: .newlines)
+ .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
+ .filter { !$0.isEmpty }
+ guard lines.count > 1 else { return text }
+ return lines
+ .enumerated()
+ .map { "\($0.offset + 1). \($0.element)" }
+ .joined(separator: "\n")
+ }
+
+ private func updateTipIcon(id: Int) {
+ let symbolName: String
+ switch id {
+ case 1:
+ symbolName = "tag.fill"
+ case 2:
+ symbolName = "checkmark.seal.fill"
+ case 3:
+ symbolName = "arrow.up.and.down.and.arrow.left.and.right"
+ case 4:
+ symbolName = "doc.on.doc.fill"
+ case 5:
+ symbolName = "minus.circle.fill"
+ case 6:
+ symbolName = "arrow.up.arrow.down.circle.fill"
+ case 7:
+ symbolName = "square.grid.2x2.fill"
+ case 8:
+ symbolName = "note.text"
+ default:
+ symbolName = "lightbulb.fill"
+ }
+ let iconConfig = NSImage.SymbolConfiguration(pointSize: 26, weight: .bold)
+ iconView.image = NSImage(
+ systemSymbolName: symbolName,
+ accessibilityDescription: nil
+ )?.withSymbolConfiguration(iconConfig)
+ ?? NSImage(
+ systemSymbolName: "lightbulb.fill",
+ accessibilityDescription: nil
+ )?.withSymbolConfiguration(iconConfig)
}
private func updateColors() {
- 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
+ let usesDarkGlass = coordinator?.appGridTheme.usesDarkGlass == true
+
+ backgroundView.material = usesDarkGlass ? .underWindowBackground : .popover
+ backgroundView.appearance = usesDarkGlass ? NSAppearance(named: .darkAqua) : nil
+ backgroundView.layer?.backgroundColor = (
+ usesDarkGlass
+ ? NSColor.black.withAlphaComponent(0.46)
+ : NSColor.white.withAlphaComponent(0.54)
+ ).cgColor
+ backgroundView.layer?.borderColor = (
+ usesDarkGlass
+ ? NSColor.white.withAlphaComponent(0.22)
+ : NSColor.black.withAlphaComponent(0.18)
+ ).cgColor
+
+ titlePanelView.layer?.backgroundColor = (
+ usesDarkGlass
+ ? NSColor.white.withAlphaComponent(0.08)
+ : NSColor.white.withAlphaComponent(0.42)
+ ).cgColor
+ titlePanelView.layer?.borderColor = (
+ usesDarkGlass
+ ? NSColor.white.withAlphaComponent(0.12)
+ : NSColor.black.withAlphaComponent(0.08)
+ ).cgColor
+
+ let theme = coordinator?.appGridTheme ?? .fallback
+ let accentColor = usageTipAccentColor(for: theme)
+ iconView.contentTintColor = accentColor
+ titleLabel.textColor = usesDarkGlass
+ ? NSColor.white.withAlphaComponent(0.92)
+ : accentColor.withAlphaComponent(0.94)
+ detailLabel.textColor = usesDarkGlass
+ ? NSColor.white.withAlphaComponent(0.95)
+ : NSColor(calibratedRed: 0.08, green: 0.10, blue: 0.13, alpha: 0.94)
+ applyDetailTextStyle()
+
+ let buttonTint = usesDarkGlass
+ ? NSColor.white.withAlphaComponent(0.90)
+ : NSColor(calibratedWhite: 0.08, alpha: 0.92)
+ let buttonHoverTint = usesDarkGlass
+ ? NSColor.white
+ : NSColor(calibratedWhite: 0.04, alpha: 1.00)
+ let buttonPressedTint = usesDarkGlass
+ ? NSColor.white
+ : NSColor.black
+ let buttonSurface = usesDarkGlass
+ ? NSColor.white.withAlphaComponent(0.10)
+ : NSColor.white.withAlphaComponent(0.38)
+ let buttonHoverSurface = usesDarkGlass
+ ? NSColor.white.withAlphaComponent(0.20)
+ : NSColor.white.withAlphaComponent(0.70)
+ let buttonPressedSurface = usesDarkGlass
+ ? NSColor.white.withAlphaComponent(0.28)
+ : NSColor.white.withAlphaComponent(0.86)
+ let buttonBorder = usesDarkGlass
+ ? NSColor.white.withAlphaComponent(0.14)
+ : NSColor.black.withAlphaComponent(0.10)
+ [previousButton, nextButton].forEach { button in
+ button.tintColor = buttonTint
+ button.hoveredTintColor = buttonHoverTint
+ button.pressedTintColor = buttonPressedTint
+ button.surfaceColor = buttonSurface
+ button.hoveredSurfaceColor = buttonHoverSurface
+ button.pressedSurfaceColor = buttonPressedSurface
+ button.borderColor = buttonBorder
+ }
+
+ closeButton.tintColor = usesDarkGlass
+ ? NSColor.white.withAlphaComponent(0.42)
+ : NSColor.black.withAlphaComponent(0.34)
+ closeButton.hoveredTintColor = usesDarkGlass
+ ? NSColor.white.withAlphaComponent(0.86)
+ : NSColor.black.withAlphaComponent(0.78)
+ closeButton.pressedTintColor = usesDarkGlass ? .white : .black
+ closeButton.surfaceColor = usesDarkGlass
+ ? NSColor.white.withAlphaComponent(0.04)
+ : NSColor.white.withAlphaComponent(0.08)
+ closeButton.hoveredSurfaceColor = usesDarkGlass
+ ? NSColor.white.withAlphaComponent(0.14)
+ : NSColor.white.withAlphaComponent(0.62)
+ closeButton.pressedSurfaceColor = usesDarkGlass
+ ? NSColor.white.withAlphaComponent(0.22)
+ : NSColor.white.withAlphaComponent(0.82)
+ closeButton.borderColor = usesDarkGlass
+ ? NSColor.white.withAlphaComponent(0.08)
+ : NSColor.black.withAlphaComponent(0.08)
+
+ dotsView.selectedColor = usesDarkGlass
+ ? NSColor.white.withAlphaComponent(0.88)
+ : NSColor.labelColor.withAlphaComponent(0.74)
+ dotsView.idleColor = usesDarkGlass
+ ? NSColor.white.withAlphaComponent(0.28)
+ : NSColor.labelColor.withAlphaComponent(0.24)
+
+ layer?.shadowOpacity = usesDarkGlass ? 0.28 : 0.14
+ layer?.shadowRadius = usesDarkGlass ? 18 : 12
+ needsDisplay = true
+ }
+
+ private func usageTipAccentColor(for theme: AppGridTheme) -> NSColor {
+ switch theme {
+ case .defaultLight:
+ return NSColor(calibratedRed: 0.34, green: 0.40, blue: 0.48, alpha: 1.00)
+ case .deepBlue:
+ return NSColor(calibratedRed: 0.42, green: 0.80, blue: 1.00, alpha: 1.00)
+ case .black:
+ return NSColor(calibratedRed: 0.36, green: 0.74, blue: 1.00, alpha: 1.00)
+ case .pink:
+ return NSColor(calibratedRed: 0.70, green: 0.18, blue: 0.45, alpha: 1.00)
+ case .purple:
+ return NSColor(calibratedRed: 0.40, green: 0.22, blue: 0.70, alpha: 1.00)
+ case .green:
+ return NSColor(calibratedRed: 0.08, green: 0.42, blue: 0.22, alpha: 1.00)
+ case .blue:
+ return NSColor(calibratedRed: 0.06, green: 0.34, blue: 0.74, alpha: 1.00)
+ case .colorful:
+ return NSColor(calibratedRed: 0.06, green: 0.44, blue: 0.78, alpha: 1.00)
+ }
+ }
+
+ private func applyDetailTextStyle() {
+ let paragraphStyle = NSMutableParagraphStyle()
+ paragraphStyle.alignment = .left
+ paragraphStyle.lineBreakMode = .byWordWrapping
+ paragraphStyle.minimumLineHeight = 31
+ paragraphStyle.maximumLineHeight = 34
+ paragraphStyle.lineSpacing = 6
+
+ detailLabel.attributedStringValue = NSAttributedString(
+ string: currentDetailText,
+ attributes: [
+ .font: detailFont,
+ .foregroundColor: detailLabel.textColor ?? NSColor.labelColor,
+ .paragraphStyle: paragraphStyle
+ ]
+ )
}
}
@@ -1240,6 +1499,20 @@
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 AppGridCenteredMultilineTextFieldCell: NSTextFieldCell {
+ override func drawingRect(forBounds rect: NSRect) -> NSRect {
+ var drawingRect = super.drawingRect(forBounds: rect)
+ let textHeight = attributedStringValue.boundingRect(
+ with: NSSize(width: max(1, rect.width), height: .greatestFiniteMagnitude),
+ options: [.usesLineFragmentOrigin, .usesFontLeading]
+ ).height
+ let centeredHeight = min(rect.height, ceil(textHeight) + 4)
+ drawingRect.origin.y = rect.origin.y + max(0, (rect.height - centeredHeight) / 2)
+ drawingRect.size.height = centeredHeight
return drawingRect
}
}
@@ -1263,8 +1536,26 @@
}
var tintColor: NSColor = .labelColor {
didSet {
- imageView.contentTintColor = tintColor
+ updateIconTint()
}
+ }
+ var hoveredTintColor: NSColor? {
+ didSet { updateIconTint() }
+ }
+ var pressedTintColor: NSColor? {
+ didSet { updateIconTint() }
+ }
+ var surfaceColor: NSColor = .clear {
+ didSet { needsDisplay = true }
+ }
+ var hoveredSurfaceColor: NSColor = NSColor.labelColor.withAlphaComponent(0.08) {
+ didSet { needsDisplay = true }
+ }
+ var pressedSurfaceColor: NSColor = NSColor.labelColor.withAlphaComponent(0.14) {
+ didSet { needsDisplay = true }
+ }
+ var borderColor: NSColor = .clear {
+ didSet { needsDisplay = true }
}
private let imageView = AppGridDecorativeImageView()
@@ -1304,9 +1595,20 @@
return true
}
+ func performPressFeedback() {
+ isPressed = true
+ needsDisplay = true
+ updateIconTint()
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.10) { [weak self] in
+ self?.isPressed = false
+ self?.needsDisplay = true
+ self?.updateIconTint()
+ }
+ }
+
override func layout() {
super.layout()
- let size: CGFloat = 26
+ let size = min(CGFloat(26), max(14, min(bounds.width, bounds.height) * 0.62))
imageView.frame = NSRect(
x: (bounds.width - size) / 2,
y: (bounds.height - size) / 2,
@@ -1337,31 +1639,32 @@
override func mouseEntered(with event: NSEvent) {
isHovered = true
needsDisplay = true
+ updateIconTint()
}
override func mouseExited(with event: NSEvent) {
isHovered = false
isPressed = false
needsDisplay = true
+ updateIconTint()
}
override func mouseDown(with event: NSEvent) {
Diagnostics.log("usageTips.button.mouseDown", [
"label": buttonAccessibilityLabel
])
- isPressed = true
- needsDisplay = true
+ performPressFeedback()
action?()
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.10) { [weak self] in
- self?.isPressed = false
- self?.needsDisplay = true
- }
}
override func draw(_ dirtyRect: NSRect) {
- 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()
+ let path = NSBezierPath(roundedRect: bounds.insetBy(dx: 1, dy: 1), xRadius: 8, yRadius: 8)
+ let fill = isPressed ? pressedSurfaceColor : isHovered ? hoveredSurfaceColor : surfaceColor
+ fill.setFill()
+ path.fill()
+ borderColor.setStroke()
+ path.lineWidth = 1
+ path.stroke()
}
private func setup(systemImage: String) {
@@ -1376,11 +1679,27 @@
addSubview(imageView)
setAccessibilityRole(.button)
}
+
+ private func updateIconTint() {
+ if isPressed, let pressedTintColor {
+ imageView.contentTintColor = pressedTintColor
+ } else if isHovered, let hoveredTintColor {
+ imageView.contentTintColor = hoveredTintColor
+ } else {
+ imageView.contentTintColor = tintColor
+ }
+ }
}
private final class AppGridUsageTipDotsView: NSView {
private var count = 0
private var selectedIndex = 0
+ var selectedColor: NSColor = .labelColor {
+ didSet { needsDisplay = true }
+ }
+ var idleColor: NSColor = NSColor.labelColor.withAlphaComponent(0.28) {
+ didSet { needsDisplay = true }
+ }
override var isFlipped: Bool { true }
@@ -1405,7 +1724,7 @@
for index in 0..<count {
let selected = index == selectedIndex
let dotSize: CGFloat = selected ? 6 : 5
- let color = NSColor.labelColor.withAlphaComponent(selected ? 0.85 : 0.28)
+ let color = selected ? selectedColor : idleColor
color.setFill()
NSBezierPath(
ovalIn: NSRect(
@@ -1418,6 +1737,7 @@
x += 5 + spacing
}
}
+
}
private final class AppGridContainerCollectionLayout: NSCollectionViewLayout {
@@ -1476,7 +1796,7 @@
private struct LayoutItem {
let index: Int
- let frame: NSRect
+ var frame: NSRect
}
private struct LayoutPlan {
@@ -1612,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]
@@ -1628,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 {
@@ -1974,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)
@@ -1982,15 +2352,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)
@@ -2210,6 +2589,17 @@
&& (isHovered || isNavigationHighlighted)
let shouldShadow = coordinator.displayStyle.usesCardSurface
&& ((coordinator.isColoredContainerMode && (isHovered || isNavigationHighlighted)) || isColorlessActive)
+ if coordinator.appGridTheme.usesDarkGlass && coordinator.displayStyle.usesCardSurface {
+ let tagColor = coordinator.tagColor(for: group?.name ?? "")
+ 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
@@ -2245,6 +2635,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
@@ -2254,9 +2678,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)
@@ -2268,7 +2696,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