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 | 320 +++++++++++++++++++++++++++++++++++++++--------------
1 files changed, 234 insertions(+), 86 deletions(-)
diff --git a/C1.source/Sources/Aligner/QuickSwitchRootView.swift b/C1.source/Sources/Aligner/QuickSwitchRootView.swift
index c04faf7..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? {
@@ -3338,6 +3449,14 @@
.cards
.compactMap(\.item.primarySpaceID) ?? []
)
+ }
+
+ private var appIconHoverLinkedAppGroupIndex: Int? {
+ if let debugHoveredAppGroupIndex {
+ return debugHoveredAppGroupIndex
+ }
+ guard case .app(let appGroupIndex) = hoverTarget else { return nil }
+ return appGroupIndex
}
private var appHoverAssociatedSpaceLabels: [String] {
@@ -3767,10 +3886,13 @@
let bounds = card.containerLayer.bounds
let isSelected = card.item.window.id == effectiveSelection?.windowID
let isHovered = card.item.window.id == hoveredWindowID
+ let isAppLinked = isHorizontalMasonryMode
+ && card.item.appGroupIndex == appIconHoverLinkedAppGroupIndex
applyWaterfallCardVisualState(
card,
selected: shouldShowSelectedVisual(isSelected: isSelected, isHovered: isHovered),
- hovered: isHovered
+ hovered: isHovered,
+ appLinked: isAppLinked
)
let outerInset: CGFloat = 0
@@ -3833,12 +3955,19 @@
)
}
- private func applyWaterfallCardVisualState(_ card: WaterfallCardLayers, selected: Bool, hovered: Bool) {
+ private func applyWaterfallCardVisualState(
+ _ card: WaterfallCardLayers,
+ selected: Bool,
+ hovered: Bool,
+ appLinked: Bool
+ ) {
let isSpaceFocused = isWaterfallCardSpaceFocused(card)
card.containerLayer.backgroundColor = (selected
? NSColor.windowBackgroundColor.withAlphaComponent(0.94)
: hovered
? accentTintedWindowBackground(fraction: 0.09, alpha: 0.95)
+ : appLinked
+ ? accentTintedWindowBackground(fraction: 0.08, alpha: 0.93)
: isSpaceFocused
? accentTintedWindowBackground(fraction: 0.10, alpha: 0.94)
: NSColor.windowBackgroundColor.withAlphaComponent(0.82)
@@ -3847,27 +3976,31 @@
? NSColor.controlAccentColor.withAlphaComponent(0.52)
: hovered
? NSColor.controlAccentColor.withAlphaComponent(0.44)
+ : appLinked
+ ? NSColor.controlAccentColor.withAlphaComponent(0.38)
: isSpaceFocused
? NSColor.controlAccentColor.withAlphaComponent(0.42)
: NSColor.separatorColor.withAlphaComponent(0.28)
).cgColor
- card.containerLayer.borderWidth = (selected || hovered || isSpaceFocused) ? 1.5 : 1
+ card.containerLayer.borderWidth = (selected || hovered || appLinked || isSpaceFocused) ? 1.5 : 1
card.containerLayer.shadowColor = NSColor.black.cgColor
- card.containerLayer.shadowOpacity = selected ? 0.24 : hovered ? 0.14 : isSpaceFocused ? 0.20 : 0
- card.containerLayer.shadowRadius = selected ? 22 : hovered ? 17 : isSpaceFocused ? 24 : 0
+ card.containerLayer.shadowOpacity = selected ? 0.24 : hovered ? 0.14 : appLinked ? 0.16 : isSpaceFocused ? 0.20 : 0
+ card.containerLayer.shadowRadius = selected ? 22 : hovered ? 17 : appLinked ? 18 : isSpaceFocused ? 24 : 0
card.containerLayer.shadowOffset = selected
? CGSize(width: 0, height: -6)
: hovered
? CGSize(width: 0, height: -2.5)
+ : appLinked
+ ? CGSize(width: 0, height: -3)
: isSpaceFocused
? CGSize(width: 0, height: -5)
: .zero
- card.containerLayer.zPosition = selected ? 20 : hovered ? 10 : isSpaceFocused ? 8 : 0
+ card.containerLayer.zPosition = selected ? 20 : hovered ? 12 : appLinked ? 10 : isSpaceFocused ? 8 : 0
let focusedScale: CGFloat = selected ? 1.015 : isSpaceFocused ? 1.012 : 1.008
card.containerLayer.transform = (selected || hovered || isSpaceFocused)
? CATransform3DMakeScale(focusedScale, focusedScale, 1)
: CATransform3DIdentity
- card.containerLayer.shadowPath = (selected || hovered || isSpaceFocused)
+ card.containerLayer.shadowPath = (selected || hovered || appLinked || isSpaceFocused)
? CGPath(
roundedRect: card.containerLayer.bounds,
cornerWidth: card.containerLayer.cornerRadius,
@@ -3875,7 +4008,9 @@
transform: nil
)
: nil
- card.titleBarLayer.backgroundColor = NSColor.clear.cgColor
+ card.titleBarLayer.backgroundColor = appLinked
+ ? spaceLaneOccupiedColor.withAlphaComponent(0.18).cgColor
+ : NSColor.clear.cgColor
card.titleBarLayer.borderColor = NSColor.clear.cgColor
card.titleBarLayer.borderWidth = 0
card.shineLayer.opacity = selected ? 1 : 0
@@ -4424,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)
@@ -4541,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(
@@ -5002,6 +5144,7 @@
"shadowRadius": Double(card.containerLayer.shadowRadius),
"thumbnailFrame": dictionary(from: card.thumbnailLayer.frame),
"titleBarFrame": dictionary(from: card.titleBarLayer.frame),
+ "titleBarBackgroundColor": colorDictionary(from: card.titleBarLayer.backgroundColor),
"titleBarBorderWidth": Double(card.titleBarLayer.borderWidth),
"titleFrame": dictionary(from: card.titleLayer.frame),
"metaFrame": dictionary(from: card.metaLayer.frame),
@@ -5109,6 +5252,8 @@
var states: [String] = []
let isSelected = card.item.window.id == effectiveSelection?.windowID
let isHovered = card.item.window.id == hoveredWindowID
+ let isAppLinked = isHorizontalMasonryMode
+ && card.item.appGroupIndex == appIconHoverLinkedAppGroupIndex
if shouldShowSelectedVisual(isSelected: isSelected, isHovered: isHovered) {
states.append("selected")
} else if isSelected {
@@ -5117,6 +5262,9 @@
if isHovered {
states.append("hover")
}
+ if isAppLinked {
+ states.append("appLinked")
+ }
if isWaterfallCardSpaceFocused(card) {
states.append("spaceFocused")
}
--
Gitblit v1.9.3