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 | 193 ++++++++++++++++++++++++++++++++++++------------
1 files changed, 145 insertions(+), 48 deletions(-)
diff --git a/src/Apptag/AppGridCollectionView.swift b/src/Apptag/AppGridCollectionView.swift
index e5df3a5..5d8d845 100644
--- a/src/Apptag/AppGridCollectionView.swift
+++ b/src/Apptag/AppGridCollectionView.swift
@@ -38,8 +38,8 @@
}
enum AppGridUsageTipsMetrics {
- static let barHeight: CGFloat = 84
- static let reservedHeight: CGFloat = 128
+ 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
@@ -51,6 +51,7 @@
let displayMode: String
let iconSize: CGFloat
let showNames: Bool
+ let appGridTheme: AppGridTheme
let bubbleDisabled: Bool
let showUncommonAppBubbles: Bool
let highlightedGroupName: String?
@@ -90,6 +91,7 @@
displayMode: displayMode,
iconSize: iconSize,
showNames: showNames,
+ appGridTheme: appGridTheme,
bubbleDisabled: bubbleDisabled,
showUncommonAppBubbles: showUncommonAppBubbles,
highlightedGroupName: highlightedGroupName,
@@ -121,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
@@ -161,6 +164,7 @@
displayMode: String,
iconSize: CGFloat,
showNames: Bool,
+ appGridTheme: AppGridTheme,
bubbleDisabled: Bool,
showUncommonAppBubbles: Bool,
highlightedGroupName: String?,
@@ -188,6 +192,7 @@
self.displayMode = displayMode
self.iconSize = iconSize
self.showNames = showNames
+ self.appGridTheme = appGridTheme
self.externalBubbleDisabled = bubbleDisabled
self.showUncommonAppBubbles = showUncommonAppBubbles
self.highlightedGroupName = highlightedGroupName
@@ -218,6 +223,7 @@
displayMode: displayMode,
iconSize: iconSize,
showNames: showNames,
+ appGridTheme: appGridTheme,
showUncommonAppBubbles: showUncommonAppBubbles,
bottomContentPadding: self.bottomContentPadding,
contentRevision: contentRevision
@@ -393,6 +399,7 @@
displayMode: String,
iconSize: CGFloat,
showNames: Bool,
+ appGridTheme: AppGridTheme,
showUncommonAppBubbles: Bool,
bottomContentPadding: CGFloat,
contentRevision: Int
@@ -405,6 +412,7 @@
displayMode,
"\(Int(iconSize.rounded()))",
showNames ? "names" : "nonames",
+ "theme=\(appGridTheme.rawValue)",
showUncommonAppBubbles ? "uncommon" : "allbubbles",
"bottom=\(Int(bottomContentPadding.rounded()))",
colorPart,
@@ -477,6 +485,7 @@
func applyCoordinatorUpdate() {
guard let coordinator else { return }
+ applyAppearance()
if coordinator.needsReload {
coordinator.needsReload = false
collectionView.reloadData()
@@ -766,6 +775,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 {
@@ -943,44 +958,41 @@
let paddingRight: CGFloat = 16
let iconSize: CGFloat = 24
let iconTextGap: CGFloat = 10
- let titleDetailGap: CGFloat = 22
- let detailControlsGap: CGFloat = 24
+ 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 fullTitleWidth = ceil(titleLabel.attributedStringValue.size().width)
- let centerY = visualFrame.midY
+ 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: centerY - iconSize / 2,
+ y: visualFrame.midY - iconSize / 2,
width: iconSize,
height: iconSize
)
- let titleX = iconView.frame.maxX + iconTextGap
- let buttonsWidth = buttonSize * 2 + buttonGap
- let trailingWidth = paddingRight + buttonsWidth + detailControlsGap
- let availableTextWidth = max(1, visualFrame.maxX - titleX - titleDetailGap - trailingWidth)
- let minimumReadableDetailWidth: CGFloat = 220
- let titleWidth: CGFloat
- if availableTextWidth >= fullTitleWidth + minimumReadableDetailWidth {
- titleWidth = fullTitleWidth
- } else if availableTextWidth >= fullTitleWidth {
- titleWidth = fullTitleWidth
- } else {
- titleWidth = max(1, availableTextWidth)
- }
-
titleLabel.frame = NSRect(
x: titleX,
- y: visualFrame.minY,
+ y: textBlockY,
width: titleWidth,
- height: visualFrame.height
+ height: titleHeight
)
- let buttonY = visualFrame.minY + 10
- let buttonsGroupMinX = visualFrame.maxX - paddingRight - buttonsWidth
+ let controlGroupHeight = buttonSize + buttonDotsGap + dotsHeight
+ let buttonY = visualFrame.minY + max(0, (visualFrame.height - controlGroupHeight) / 2)
nextButton.frame = NSRect(
x: buttonsGroupMinX + buttonSize + buttonGap,
y: buttonY,
@@ -995,18 +1007,18 @@
)
dotsView.frame = NSRect(
x: buttonsGroupMinX + (buttonsWidth - dotsWidth) / 2,
- y: previousButton.frame.maxY + 6,
+ y: previousButton.frame.maxY + buttonDotsGap,
width: dotsWidth,
- height: 10
+ height: dotsHeight
)
- let detailX = titleLabel.frame.maxX + titleDetailGap
- let detailRight = previousButton.frame.minX - detailControlsGap
+ let detailX = titleX
+ let detailRight = textRight
detailScrollView.frame = NSRect(
x: detailX,
- y: visualFrame.minY,
+ y: titleLabel.frame.maxY + lineGap,
width: max(1, detailRight - detailX),
- height: visualFrame.height
+ height: detailHeight
)
layoutDetailLabel()
}
@@ -1048,10 +1060,10 @@
func preferredWidth(maxAvailableWidth: CGFloat) -> CGFloat {
let padding: CGFloat = 22 + 16
- let fixedWidth: CGFloat = 24 + 10 + 22 + 24 + 48 + 6 + 48
+ let fixedWidth: CGFloat = 24 + 10 + 24 + 48 + 6 + 48
let titleWidth = ceil(titleLabel.attributedStringValue.size().width)
- let detailWidth = ceil(detailLabel.attributedStringValue.size().width)
- let contentWidth = padding + fixedWidth + titleWidth + detailWidth
+ let detailWidth = measuredDetailLineWidth()
+ let contentWidth = padding + fixedWidth + max(titleWidth, detailWidth)
let preferredWidth = max(AppGridUsageTipsMetrics.minWidth, contentWidth)
return min(maxAvailableWidth, preferredWidth)
}
@@ -1081,7 +1093,7 @@
addSubview(iconView)
configureLabel(titleLabel, font: titleFont, lineBreakMode: .byClipping)
- configureLabel(detailLabel, font: detailFont, lineBreakMode: .byClipping)
+ configureDetailLabel()
detailScrollView.drawsBackground = false
detailScrollView.hasVerticalScroller = false
@@ -1111,7 +1123,21 @@
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() {
@@ -1180,9 +1206,17 @@
}
private func layoutDetailLabel() {
- let textWidth = detailLabel.attributedStringValue.size().width
- let width = max(detailScrollView.contentView.bounds.width, ceil(textWidth) + 18)
+ 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 {
@@ -1198,8 +1232,10 @@
private func formattedTipDetail(_ text: String) -> String {
text
- .replacingOccurrences(of: " > ", with: " -> ")
- .replacingOccurrences(of: ">", with: "->")
+ .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() {
@@ -1397,6 +1433,7 @@
x += 5 + spacing
}
}
+
}
private final class AppGridContainerCollectionLayout: NSCollectionViewLayout {
@@ -1961,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)
@@ -2189,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
@@ -2224,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
@@ -2233,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)
@@ -2247,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