From 0754f2706a79128e87b65867e1d33096d53687fa Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Fri, 12 Jun 2026 17:19:21 +0800
Subject: [PATCH] Use fixed height horizontal waterfall cards
---
C1.source/Sources/Aligner/QuickSwitchRootView.swift | 180 ++++++++++++++++++++++++++++++++++++++++++------------------
1 files changed, 126 insertions(+), 54 deletions(-)
diff --git a/C1.source/Sources/Aligner/QuickSwitchRootView.swift b/C1.source/Sources/Aligner/QuickSwitchRootView.swift
index e001bc4..d7f02fc 100644
--- a/C1.source/Sources/Aligner/QuickSwitchRootView.swift
+++ b/C1.source/Sources/Aligner/QuickSwitchRootView.swift
@@ -24,6 +24,9 @@
private lazy var closeConfirmationLayers = makeCloseConfirmationLayers()
private let closeFeedbackLayer = CALayer()
private let closeFeedbackTextLayer = CATextLayer()
+ private let settingsButtonLayer = CALayer()
+ private let settingsButtonIconLayer = CALayer()
+ private let settingsButtonFallbackTextLayer = CATextLayer()
private let appIconProvider: any AppIconProviderProtocol = WorkspaceAppIconProvider()
private let skeletonThumbnailProvider = NativeSkeletonThumbnailProvider(size: NSSize(width: 160, height: 96))
private var screenshotSourcesByWindowID: [UInt32: ScreenshotResolutionSource] = [:]
@@ -49,6 +52,7 @@
private var closeConfirmationRequired = true
private var closeFeedback: CloseFeedback?
private var closeFeedbackHideTask: Task<Void, Never>?
+ private var isSettingsButtonHovered = false
private var hoverTarget: HoverTarget = .none
private var hoverTargetSource: HoverTargetSource = .event
private var hoveredSpaceLaneID: UInt64?
@@ -91,6 +95,7 @@
var onCommitSelection: ((QuickSwitchSelection, QuickSwitchCommitSource) -> Void)?
var onSpaceLaneClick: ((UInt64, Int) -> Void)?
var onBackgroundClick: (() -> Bool)?
+ var onOpenSettings: (() -> Void)?
var onRequestClose: ((QuickSwitchCloseRequest) -> Void)?
var onCloseConfirmationDisabled: (() -> Void)?
@@ -322,13 +327,11 @@
private enum HorizontalMasonryMetrics {
static let horizontalPadding: CGFloat = 12
static let verticalPadding: CGFloat = 0
- static let cardGap: CGFloat = 16
+ static let horizontalGap: CGFloat = 16
+ static let verticalGap: CGFloat = WaterfallMetrics.cardGap
static let cardMinWidth: CGFloat = 220
static let cardMaxWidth: CGFloat = 320
- static let titleBarHeight: CGFloat = 26
- static let titleGap: CGFloat = 6
- static let minThumbnailHeight: CGFloat = 92
- static let maxThumbnailHeightFraction: CGFloat = 0.40
+ static let cardHeight: CGFloat = WaterfallMetrics.cardHeight
}
private enum WaterfallHorizontalIntent {
@@ -481,6 +484,17 @@
override func mouseMoved(with event: NSEvent) {
guard debugHoveredAppGroupIndex == nil else { return }
+ let settingsHovered = settingsButtonContains(windowLocation: event.locationInWindow)
+ if isSettingsButtonHovered != settingsHovered {
+ isSettingsButtonHovered = settingsHovered
+ needsLayout = true
+ }
+ if settingsHovered {
+ hoveredCloseTarget = nil
+ setHoverTarget(.none)
+ return
+ }
+
let closeTarget = closeButtonTarget(at: event.locationInWindow)
if hoveredCloseTarget != closeTarget {
hoveredCloseTarget = closeTarget
@@ -491,6 +505,7 @@
override func mouseExited(with event: NSEvent) {
guard debugHoveredAppGroupIndex == nil else { return }
+ isSettingsButtonHovered = false
hoveredCloseTarget = nil
setHoverTarget(.none)
}
@@ -506,6 +521,11 @@
}
if handleCloseConfirmationMouseDown(at: event.locationInWindow) {
+ return
+ }
+
+ if settingsButtonContains(windowLocation: event.locationInWindow) {
+ clickSettingsButton(source: "mouse")
return
}
@@ -1060,6 +1080,9 @@
"activeSpaceFocusID": activeSpaceFocusID ?? NSNull(),
"spaceFilterLockedSpaceID": currentViewModel?.lockedSpaceID ?? NSNull(),
"spaceFilterActive": currentViewModel?.lockedSpaceID != nil,
+ "settingsButtonVisible": !settingsButtonLayer.isHidden,
+ "settingsButtonHovered": isSettingsButtonHovered,
+ "settingsButtonFrame": dictionary(from: settingsButtonLayer.frame),
"spaceFocusAppGroupIndexes": spaceFocusedAppGroupIndexes(),
"appHoverAssociatedSpaceIDs": appHoverAssociatedSpaceIDs.sorted(),
"appHoverAssociatedSpaceLabels": appHoverAssociatedSpaceLabels,
@@ -1302,6 +1325,9 @@
case "click-background":
recordMouseCommand("click-background")
_ = onBackgroundClick?()
+ case "click-settings":
+ recordMouseCommand("click-settings")
+ clickSettingsButton(source: "debug")
case "hover-card" where parts.count == 3:
guard let appGroupIndex = Int(parts[1]), let windowIndex = Int(parts[2]) else {
recordMouseCommand("invalid:\(command)")
@@ -2244,6 +2270,26 @@
closeFeedbackTextLayer.truncationMode = .end
closeFeedbackLayer.addSublayer(closeFeedbackTextLayer)
glassLayer.addSublayer(closeFeedbackLayer)
+
+ settingsButtonLayer.cornerRadius = 10
+ settingsButtonLayer.masksToBounds = false
+ settingsButtonLayer.borderWidth = 1
+ settingsButtonLayer.shadowColor = NSColor.black.cgColor
+ settingsButtonLayer.shadowOffset = CGSize(width: 0, height: -2)
+ settingsButtonLayer.zPosition = 170
+ settingsButtonIconLayer.contentsGravity = .resizeAspect
+ settingsButtonIconLayer.contentsScale = backingScaleFactor
+ settingsButtonIconLayer.contents = settingsIconImage()
+ settingsButtonFallbackTextLayer.contentsScale = backingScaleFactor
+ settingsButtonFallbackTextLayer.string = "⚙"
+ settingsButtonFallbackTextLayer.font = NSFont.systemFont(ofSize: 18, weight: .regular)
+ settingsButtonFallbackTextLayer.fontSize = 18
+ settingsButtonFallbackTextLayer.alignmentMode = .center
+ settingsButtonFallbackTextLayer.foregroundColor = NSColor.secondaryLabelColor.cgColor
+ settingsButtonFallbackTextLayer.isHidden = settingsButtonIconLayer.contents != nil
+ settingsButtonLayer.addSublayer(settingsButtonIconLayer)
+ settingsButtonLayer.addSublayer(settingsButtonFallbackTextLayer)
+ glassLayer.addSublayer(settingsButtonLayer)
}
private func rebuildAppShelfItems() {
@@ -2581,8 +2627,40 @@
layoutWaterfallColumns()
layoutCloseConfirmation()
layoutCloseFeedback()
+ layoutSettingsButton()
CATransaction.commit()
+ }
+
+ private func layoutSettingsButton() {
+ let size: CGFloat = 34
+ let topMargin = topSafeAreaInset + RootLayoutMetrics.topContentMargin
+ let x = max(
+ RootLayoutMetrics.horizontalMargin,
+ glassLayer.bounds.width - RootLayoutMetrics.horizontalMargin - size
+ )
+ let y = max(
+ RootLayoutMetrics.bottomMargin,
+ glassLayer.bounds.height - topMargin - size
+ )
+ settingsButtonLayer.frame = CGRect(x: x, y: y, width: size, height: size)
+ settingsButtonLayer.backgroundColor = NSColor.windowBackgroundColor
+ .withAlphaComponent(isSettingsButtonHovered ? 0.78 : 0.54)
+ .cgColor
+ settingsButtonLayer.borderColor = NSColor.separatorColor
+ .withAlphaComponent(isSettingsButtonHovered ? 0.42 : 0.26)
+ .cgColor
+ settingsButtonLayer.shadowOpacity = isSettingsButtonHovered ? 0.14 : 0.08
+ settingsButtonLayer.shadowRadius = isSettingsButtonHovered ? 9 : 6
+ settingsButtonIconLayer.contentsScale = backingScaleFactor
+ settingsButtonFallbackTextLayer.contentsScale = backingScaleFactor
+ let iconInset: CGFloat = 8
+ settingsButtonIconLayer.frame = settingsButtonLayer.bounds.insetBy(dx: iconInset, dy: iconInset)
+ settingsButtonFallbackTextLayer.frame = settingsButtonLayer.bounds.insetBy(dx: 0, dy: 6)
+ settingsButtonFallbackTextLayer.foregroundColor = (isSettingsButtonHovered
+ ? NSColor.labelColor
+ : NSColor.secondaryLabelColor
+ ).cgColor
}
private var topSafeAreaInset: CGFloat {
@@ -2896,6 +2974,26 @@
private func spaceLaneSegment(spaceID: UInt64) -> SpaceLaneSegmentLayers? {
spaceLaneSegments.first { $0.space.id == spaceID }
+ }
+
+ private func settingsButtonContains(windowLocation: CGPoint) -> Bool {
+ let locationInView = convert(windowLocation, from: nil)
+ let locationInGlass = CGPoint(
+ x: locationInView.x - glassLayer.frame.minX,
+ y: locationInView.y - glassLayer.frame.minY
+ )
+ return settingsButtonLayer.frame.contains(locationInGlass)
+ }
+
+ private func clickSettingsButton(source: String) {
+ DevelopmentDiagnostics.log("quickSwitch.view.clickSettings", [
+ "source": source
+ ])
+ lastMouseCommand = "click-settings"
+ if !mouseCommandsApplied.contains("click-settings") {
+ mouseCommandsApplied.append("click-settings")
+ }
+ onOpenSettings?()
}
private func appShelfItem(at windowLocation: CGPoint) -> AppShelfItemLayers? {
@@ -3406,14 +3504,14 @@
1,
Int(
floor(
- (availableWidth + HorizontalMasonryMetrics.cardGap)
- / (HorizontalMasonryMetrics.cardMinWidth + HorizontalMasonryMetrics.cardGap)
+ (availableWidth + HorizontalMasonryMetrics.horizontalGap)
+ / (HorizontalMasonryMetrics.cardMinWidth + HorizontalMasonryMetrics.horizontalGap)
)
)
)
let laneCount = min(maximumLaneCount, max(1, cards.count))
let fittingWidth = (
- availableWidth - CGFloat(max(0, laneCount - 1)) * HorizontalMasonryMetrics.cardGap
+ availableWidth - CGFloat(max(0, laneCount - 1)) * HorizontalMasonryMetrics.horizontalGap
) / CGFloat(laneCount)
let cardWidth = floor(
min(
@@ -3422,7 +3520,7 @@
)
)
let usedWidth = CGFloat(laneCount) * cardWidth
- + CGFloat(max(0, laneCount - 1)) * HorizontalMasonryMetrics.cardGap
+ + CGFloat(max(0, laneCount - 1)) * HorizontalMasonryMetrics.horizontalGap
let startX = max(
HorizontalMasonryMetrics.horizontalPadding,
(viewportWidth - usedWidth) / 2
@@ -3438,12 +3536,8 @@
}
return lhs.offset < rhs.offset
}?.offset ?? 0
- let cardHeight = horizontalMasonryCardHeight(
- for: card,
- width: cardWidth,
- viewportHeight: waterfallLayer.bounds.height
- )
- let x = startX + CGFloat(laneIndex) * (cardWidth + HorizontalMasonryMetrics.cardGap)
+ let cardHeight = HorizontalMasonryMetrics.cardHeight
+ let x = startX + CGFloat(laneIndex) * (cardWidth + HorizontalMasonryMetrics.horizontalGap)
let top = HorizontalMasonryMetrics.verticalPadding + laneHeights[laneIndex]
plannedFrames.append((
windowID: card.item.window.id,
@@ -3452,11 +3546,11 @@
width: cardWidth,
height: cardHeight
))
- laneHeights[laneIndex] += cardHeight + HorizontalMasonryMetrics.cardGap
+ laneHeights[laneIndex] += cardHeight + HorizontalMasonryMetrics.verticalGap
}
let tallestLane = laneHeights.max().map {
- max(0, $0 - HorizontalMasonryMetrics.cardGap)
+ max(0, $0 - HorizontalMasonryMetrics.verticalGap)
} ?? 0
let contentHeight = max(
waterfallLayer.bounds.height,
@@ -3477,43 +3571,6 @@
)
return HorizontalMasonryLayout(contentHeight: contentHeight, cardFramesByWindowID: frames)
- }
-
- private func horizontalMasonryCardHeight(
- for card: WaterfallCardLayers,
- width: CGFloat,
- viewportHeight: CGFloat
- ) -> CGFloat {
- let thumbnailHeight = min(
- max(
- HorizontalMasonryMetrics.minThumbnailHeight,
- width / horizontalMasonryThumbnailAspectRatio(for: card)
- ),
- max(1, viewportHeight * HorizontalMasonryMetrics.maxThumbnailHeightFraction)
- )
- return HorizontalMasonryMetrics.titleBarHeight
- + HorizontalMasonryMetrics.titleGap
- + thumbnailHeight
- }
-
- private func horizontalMasonryThumbnailAspectRatio(for card: WaterfallCardLayers) -> CGFloat {
- if card.item.window.isFullscreen {
- return 16.0 / 9.0
- }
- if card.item.window.isMinimized {
- return 4.0 / 3.0
- }
-
- switch Int(card.item.window.id % 4) {
- case 0:
- return 16.0 / 9.0
- case 1:
- return 3.0 / 2.0
- case 2:
- return 4.0 / 3.0
- default:
- return 5.0 / 4.0
- }
}
private func horizontalMasonryContentFrame() -> CGRect {
@@ -4381,6 +4438,21 @@
return shapeLayer
}
+ private func settingsIconImage() -> CGImage? {
+ let names = [
+ NSImage.Name("NSPreferencesGeneral"),
+ NSImage.Name("NSAdvanced")
+ ]
+ for name in names {
+ guard let image = NSImage(named: name) else { continue }
+ var rect = CGRect(origin: .zero, size: image.size)
+ if let cgImage = image.cgImage(forProposedRect: &rect, context: nil, hints: nil) {
+ return cgImage
+ }
+ }
+ return nil
+ }
+
private func makeTextLayer(
string: String,
fontSize: CGFloat,
--
Gitblit v1.9.3