From 4d4294f8c375a06ad87e12a2c73a98a19eb7ed7e Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Sun, 14 Jun 2026 18:46:39 +0800
Subject: [PATCH] Add Quick Switch view mode toggle

---
 C1.source/Sources/Aligner/QuickSwitchRootView.swift |  274 +++++++++++++++++++++++++++++++++++++++---------------
 1 files changed, 196 insertions(+), 78 deletions(-)

diff --git a/C1.source/Sources/Aligner/QuickSwitchRootView.swift b/C1.source/Sources/Aligner/QuickSwitchRootView.swift
index 40611ae..6675a87 100644
--- a/C1.source/Sources/Aligner/QuickSwitchRootView.swift
+++ b/C1.source/Sources/Aligner/QuickSwitchRootView.swift
@@ -24,9 +24,8 @@
     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 viewModeToggleButtonLayer = CALayer()
+    private let viewModeToggleIconLayer = CALayer()
     private let appIconProvider: any AppIconProviderProtocol = WorkspaceAppIconProvider()
     private let skeletonThumbnailProvider = NativeSkeletonThumbnailProvider(size: NSSize(width: 160, height: 96))
     private var screenshotSourcesByWindowID: [UInt32: ScreenshotResolutionSource] = [:]
@@ -52,7 +51,7 @@
     private var closeConfirmationRequired = true
     private var closeFeedback: CloseFeedback?
     private var closeFeedbackHideTask: Task<Void, Never>?
-    private var isSettingsButtonHovered = false
+    private var isViewModeToggleButtonHovered = false
     private var hoverTarget: HoverTarget = .none
     private var hoverTargetSource: HoverTargetSource = .event
     private var hoveredSpaceLaneID: UInt64?
@@ -95,7 +94,7 @@
     var onCommitSelection: ((QuickSwitchSelection, QuickSwitchCommitSource) -> Void)?
     var onSpaceLaneClick: ((UInt64, Int) -> Void)?
     var onBackgroundClick: (() -> Bool)?
-    var onOpenSettings: (() -> Void)?
+    var onToggleWaterfallViewMode: (() -> Void)?
     var onRequestClose: ((QuickSwitchCloseRequest) -> Void)?
     var onCloseConfirmationDisabled: (() -> Void)?
 
@@ -485,12 +484,12 @@
     override func mouseMoved(with event: NSEvent) {
         guard debugHoveredAppGroupIndex == nil else { return }
 
-        let settingsHovered = settingsButtonContains(windowLocation: event.locationInWindow)
-        if isSettingsButtonHovered != settingsHovered {
-            isSettingsButtonHovered = settingsHovered
+        let toggleHovered = viewModeToggleButtonContains(windowLocation: event.locationInWindow)
+        if isViewModeToggleButtonHovered != toggleHovered {
+            isViewModeToggleButtonHovered = toggleHovered
             needsLayout = true
         }
-        if settingsHovered {
+        if toggleHovered {
             hoveredCloseTarget = nil
             setHoverTarget(.none)
             return
@@ -506,7 +505,7 @@
 
     override func mouseExited(with event: NSEvent) {
         guard debugHoveredAppGroupIndex == nil else { return }
-        isSettingsButtonHovered = false
+        isViewModeToggleButtonHovered = false
         hoveredCloseTarget = nil
         setHoverTarget(.none)
     }
@@ -525,8 +524,8 @@
             return
         }
 
-        if settingsButtonContains(windowLocation: event.locationInWindow) {
-            clickSettingsButton(source: "mouse")
+        if viewModeToggleButtonContains(windowLocation: event.locationInWindow) {
+            clickViewModeToggleButton(source: "mouse")
             return
         }
 
@@ -1081,9 +1080,14 @@
             "activeSpaceFocusID": activeSpaceFocusID ?? NSNull(),
             "spaceFilterLockedSpaceID": currentViewModel?.lockedSpaceID ?? NSNull(),
             "spaceFilterActive": currentViewModel?.lockedSpaceID != nil,
-            "settingsButtonVisible": !settingsButtonLayer.isHidden,
-            "settingsButtonHovered": isSettingsButtonHovered,
-            "settingsButtonFrame": dictionary(from: settingsButtonLayer.frame),
+            "viewModeToggleButtonVisible": !viewModeToggleButtonLayer.isHidden,
+            "viewModeToggleButtonHovered": isViewModeToggleButtonHovered,
+            "viewModeToggleButtonFrame": dictionary(from: viewModeToggleButtonLayer.frame),
+            "viewModeToggleCurrentMode": waterfallViewMode.rawValue,
+            "viewModeToggleTargetMode": nextWaterfallViewMode.rawValue,
+            "viewModeToggleIconKind": viewModeToggleIconKind,
+            "viewModeToggleAccessibilityLabel": viewModeToggleAccessibilityLabel,
+            "settingsButtonVisible": false,
             "spaceFocusAppGroupIndexes": spaceFocusedAppGroupIndexes(),
             "appHoverAssociatedSpaceIDs": appHoverAssociatedSpaceIDs.sorted(),
             "appHoverAssociatedSpaceLabels": appHoverAssociatedSpaceLabels,
@@ -1326,9 +1330,9 @@
         case "click-background":
             recordMouseCommand("click-background")
             _ = onBackgroundClick?()
-        case "click-settings":
-            recordMouseCommand("click-settings")
-            clickSettingsButton(source: "debug")
+        case "click-view-toggle", "click-settings":
+            recordMouseCommand("click-view-toggle")
+            clickViewModeToggleButton(source: "debug")
         case "hover-card" where parts.count == 3:
             guard let appGroupIndex = Int(parts[1]), let windowIndex = Int(parts[2]) else {
                 recordMouseCommand("invalid:\(command)")
@@ -2281,25 +2285,15 @@
         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)
+        viewModeToggleButtonLayer.cornerRadius = 10
+        viewModeToggleButtonLayer.masksToBounds = false
+        viewModeToggleButtonLayer.borderWidth = 1
+        viewModeToggleButtonLayer.shadowColor = NSColor.black.cgColor
+        viewModeToggleButtonLayer.shadowOffset = CGSize(width: 0, height: -2)
+        viewModeToggleButtonLayer.zPosition = 170
+        viewModeToggleIconLayer.contentsScale = backingScaleFactor
+        viewModeToggleButtonLayer.addSublayer(viewModeToggleIconLayer)
+        glassLayer.addSublayer(viewModeToggleButtonLayer)
     }
 
     private func rebuildAppShelfItems() {
@@ -2650,12 +2644,12 @@
         layoutWaterfallColumns()
         layoutCloseConfirmation()
         layoutCloseFeedback()
-        layoutSettingsButton()
+        layoutViewModeToggleButton()
 
         CATransaction.commit()
     }
 
-    private func layoutSettingsButton() {
+    private func layoutViewModeToggleButton() {
         let size: CGFloat = 34
         let topMargin = topSafeAreaInset + RootLayoutMetrics.topContentMargin
         let x = max(
@@ -2666,24 +2660,139 @@
             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)
+        viewModeToggleButtonLayer.frame = CGRect(x: x, y: y, width: size, height: size)
+        viewModeToggleButtonLayer.backgroundColor = NSColor.windowBackgroundColor
+            .withAlphaComponent(isViewModeToggleButtonHovered ? 0.78 : 0.54)
             .cgColor
-        settingsButtonLayer.borderColor = NSColor.separatorColor
-            .withAlphaComponent(isSettingsButtonHovered ? 0.42 : 0.26)
+        viewModeToggleButtonLayer.borderColor = NSColor.separatorColor
+            .withAlphaComponent(isViewModeToggleButtonHovered ? 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
+        viewModeToggleButtonLayer.shadowOpacity = isViewModeToggleButtonHovered ? 0.14 : 0.08
+        viewModeToggleButtonLayer.shadowRadius = isViewModeToggleButtonHovered ? 9 : 6
+        viewModeToggleIconLayer.contentsScale = backingScaleFactor
+        let iconInset: CGFloat = 7
+        viewModeToggleIconLayer.frame = viewModeToggleButtonLayer.bounds.insetBy(dx: iconInset, dy: iconInset)
+        layoutViewModeToggleIcon()
+    }
+
+    private func layoutViewModeToggleIcon() {
+        CATransaction.begin()
+        CATransaction.setDisableActions(true)
+        viewModeToggleIconLayer.sublayers?.forEach { $0.removeFromSuperlayer() }
+
+        let bounds = viewModeToggleIconLayer.bounds
+        guard bounds.width > 0, bounds.height > 0 else {
+            CATransaction.commit()
+            return
+        }
+
+        let activeStroke = (isViewModeToggleButtonHovered
+            ? NSColor.controlAccentColor
             : NSColor.secondaryLabelColor
-        ).cgColor
+        ).withAlphaComponent(isViewModeToggleButtonHovered ? 0.88 : 0.82)
+        let quietStroke = (isViewModeToggleButtonHovered
+            ? NSColor.controlAccentColor
+            : NSColor.secondaryLabelColor
+        ).withAlphaComponent(isViewModeToggleButtonHovered ? 0.34 : 0.24)
+        let subtleFill = (isViewModeToggleButtonHovered
+            ? NSColor.controlAccentColor
+            : NSColor.labelColor
+        ).withAlphaComponent(isViewModeToggleButtonHovered ? 0.06 : 0.025)
+
+        addViewModeToggleRoundedRect(
+            bounds.insetBy(dx: 0.65, dy: 0.65),
+            cornerRadius: 4.8,
+            strokeColor: quietStroke,
+            fillColor: NSColor.clear,
+            lineWidth: 1.05
+        )
+
+        switch nextWaterfallViewMode {
+        case .verticalColumns:
+            addVerticalColumnsToggleGlyph(
+                in: bounds.insetBy(dx: 4.15, dy: 3.25),
+                strokeColor: activeStroke,
+                fillColor: subtleFill
+            )
+        case .horizontalMasonry:
+            addHorizontalMasonryToggleGlyph(
+                in: bounds.insetBy(dx: 3.95, dy: 3.95),
+                strokeColor: activeStroke,
+                fillColor: subtleFill
+            )
+        }
+
+        CATransaction.commit()
+    }
+
+    private func addVerticalColumnsToggleGlyph(
+        in rect: CGRect,
+        strokeColor: NSColor,
+        fillColor: NSColor
+    ) {
+        let columnWidth: CGFloat = 3.35
+        let gap = max(1.7, (rect.width - columnWidth * 3) / 2)
+        for index in 0..<3 {
+            let x = rect.minX + CGFloat(index) * (columnWidth + gap)
+            let columnRect = CGRect(x: x, y: rect.minY, width: columnWidth, height: rect.height)
+            addViewModeToggleRoundedRect(
+                columnRect,
+                cornerRadius: 1.6,
+                strokeColor: strokeColor,
+                fillColor: fillColor,
+                lineWidth: 1.22
+            )
+        }
+    }
+
+    private func addHorizontalMasonryToggleGlyph(
+        in rect: CGRect,
+        strokeColor: NSColor,
+        fillColor: NSColor
+    ) {
+        let cellSize: CGFloat = 3.65
+        let gapX = max(1.65, (rect.width - cellSize * 3) / 2)
+        let gapY = max(1.65, (rect.height - cellSize * 3) / 2)
+        for row in 0..<3 {
+            for column in 0..<3 {
+                let cellRect = CGRect(
+                    x: rect.minX + CGFloat(column) * (cellSize + gapX),
+                    y: rect.minY + CGFloat(row) * (cellSize + gapY),
+                    width: cellSize,
+                    height: cellSize
+                )
+                addViewModeToggleRoundedRect(
+                    cellRect,
+                    cornerRadius: 1.15,
+                    strokeColor: strokeColor,
+                    fillColor: fillColor,
+                    lineWidth: 1.18
+                )
+            }
+        }
+    }
+
+    private func addViewModeToggleRoundedRect(
+        _ rect: CGRect,
+        cornerRadius: CGFloat,
+        strokeColor: NSColor,
+        fillColor: NSColor,
+        lineWidth: CGFloat
+    ) {
+        let layer = CAShapeLayer()
+        layer.contentsScale = backingScaleFactor
+        layer.frame = viewModeToggleIconLayer.bounds
+        layer.path = CGPath(
+            roundedRect: rect,
+            cornerWidth: cornerRadius,
+            cornerHeight: cornerRadius,
+            transform: nil
+        )
+        layer.fillColor = fillColor.cgColor
+        layer.strokeColor = strokeColor.cgColor
+        layer.lineWidth = lineWidth
+        layer.lineJoin = .round
+        viewModeToggleIconLayer.addSublayer(layer)
     }
 
     private var topSafeAreaInset: CGFloat {
@@ -3035,24 +3144,26 @@
         spaceLaneSegments.first { $0.space.id == spaceID }
     }
 
-    private func settingsButtonContains(windowLocation: CGPoint) -> Bool {
+    private func viewModeToggleButtonContains(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)
+        return viewModeToggleButtonLayer.frame.contains(locationInGlass)
     }
 
-    private func clickSettingsButton(source: String) {
-        DevelopmentDiagnostics.log("quickSwitch.view.clickSettings", [
-            "source": source
+    private func clickViewModeToggleButton(source: String) {
+        DevelopmentDiagnostics.log("quickSwitch.view.clickWaterfallViewModeToggle", [
+            "source": source,
+            "currentMode": waterfallViewMode.rawValue,
+            "targetMode": nextWaterfallViewMode.rawValue
         ])
-        lastMouseCommand = "click-settings"
-        if !mouseCommandsApplied.contains("click-settings") {
-            mouseCommandsApplied.append("click-settings")
+        lastMouseCommand = "click-view-toggle"
+        if !mouseCommandsApplied.contains("click-view-toggle") {
+            mouseCommandsApplied.append("click-view-toggle")
         }
-        onOpenSettings?()
+        onToggleWaterfallViewMode?()
     }
 
     private func appShelfItem(at windowLocation: CGPoint) -> AppShelfItemLayers? {
@@ -4448,6 +4559,28 @@
         waterfallViewMode == .horizontalMasonry
     }
 
+    private var nextWaterfallViewMode: QuickSwitchWaterfallViewMode {
+        isHorizontalMasonryMode ? .verticalColumns : .horizontalMasonry
+    }
+
+    private var viewModeToggleIconKind: String {
+        switch nextWaterfallViewMode {
+        case .verticalColumns:
+            return "verticalColumnsTarget"
+        case .horizontalMasonry:
+            return "horizontalMasonryTarget"
+        }
+    }
+
+    private var viewModeToggleAccessibilityLabel: String {
+        switch nextWaterfallViewMode {
+        case .verticalColumns:
+            return "切换到纵栏瀑布"
+        case .horizontalMasonry:
+            return "切换到横栏瀑布"
+        }
+    }
+
     private var waterfallMaxScrollOffset: CGFloat {
         guard !isHorizontalMasonryMode else { return 0 }
         return max(0, waterfallContentWidth - waterfallLayer.bounds.width)
@@ -4565,21 +4698,6 @@
         shapeLayer.lineCap = .round
         shapeLayer.lineJoin = .round
         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(

--
Gitblit v1.9.3