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