From cb67d8b2fa9b9c9d30cd81d72b8737ee2baa44b6 Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Fri, 19 Jun 2026 14:09:49 +0800
Subject: [PATCH] Fix vertical keyboard window focus

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

diff --git a/C1.source/Sources/Aligner/QuickSwitchRootView.swift b/C1.source/Sources/Aligner/QuickSwitchRootView.swift
index 9fb63d3..f7cbfa2 100644
--- a/C1.source/Sources/Aligner/QuickSwitchRootView.swift
+++ b/C1.source/Sources/Aligner/QuickSwitchRootView.swift
@@ -70,6 +70,7 @@
     private var currentSelection: QuickSwitchSelection?
     private var columnSelectionHistory: [Int: Int] = [:]
     private var keyboardFocusedAppGroupIndex: Int?
+    private var keyboardFocusedWindowID: UInt32?
     private var keyboardCommandsApplied: [String] = []
     private var lastKeyboardCommand: String?
     private var lastKeyboardAppIndexCommand: String?
@@ -266,6 +267,7 @@
     private struct WaterfallColumnLayers {
         let column: QuickSwitchWaterfallColumnViewModel
         let containerLayer: CALayer
+        let cardsClipLayer: CALayer
         let headerLayer: CALayer
         let appNameLayer: CATextLayer
         let countLayer: CATextLayer
@@ -355,6 +357,8 @@
         static let cardGap: CGFloat = 12
         static let revealPadding: CGFloat = 24
         static let horizontalRevealPadding: CGFloat = 0
+        static let selectedCardScale: CGFloat = 1.015
+        static let selectedCardScaleOverflow: CGFloat = cardHeight * (selectedCardScale - 1) / 2
     }
 
     private enum HorizontalMasonryMetrics {
@@ -739,7 +743,8 @@
         if case .realScreenshot = resolution.source {
             let isSelected = card.item.window.id == effectiveSelection?.windowID
             let isHovered = card.item.window.id == hoveredWindowID
-            card.shineLayer.opacity = shouldShowSelectedVisual(isSelected: isSelected, isHovered: isHovered) ? 1 : 0
+            let isKeyboardFocused = card.item.window.id == keyboardFocusedWindowID
+            card.shineLayer.opacity = (shouldShowSelectedVisual(isSelected: isSelected, isHovered: isHovered) || isKeyboardFocused) ? 1 : 0
         }
         needsLayout = true
     }
@@ -815,6 +820,12 @@
                viewModel?.waterfallColumns.contains(where: { $0.appGroupIndex == keyboardFocusedAppGroupIndex }) != true {
                 self.keyboardFocusedAppGroupIndex = nil
             }
+            if let keyboardFocusedWindowID,
+               viewModel?.waterfallColumns.contains(where: { column in
+                   column.windows.contains { $0.window.id == keyboardFocusedWindowID }
+               }) != true {
+                self.keyboardFocusedWindowID = nil
+            }
             if let currentSelection {
                 columnSelectionHistory[currentSelection.appGroupIndex] = currentSelection.windowIndex
             }
@@ -830,6 +841,7 @@
             currentSelection = viewModel?.initialSelection
             columnSelectionHistory = [:]
             keyboardFocusedAppGroupIndex = nil
+            keyboardFocusedWindowID = nil
             keyboardCommandsApplied = []
             lastKeyboardCommand = nil
             lastKeyboardAppIndexCommand = nil
@@ -1256,6 +1268,7 @@
             "selectedWindowIndex": effectiveSelection?.windowIndex ?? NSNull(),
             "selectedWindowID": effectiveSelection?.windowID ?? NSNull(),
             "keyboardFocusedAppGroupIndex": keyboardFocusedAppGroupIndex ?? NSNull(),
+            "keyboardFocusedWindowID": keyboardFocusedWindowID ?? NSNull(),
             "keyboardCommandsApplied": keyboardCommandsApplied,
             "lastKeyboardCommand": lastKeyboardCommand ?? NSNull(),
             "lastKeyboardAppIndexCommand": lastKeyboardAppIndexCommand ?? NSNull(),
@@ -1450,6 +1463,7 @@
         }
 
         keyboardFocusedAppGroupIndex = appGroupIndex
+        keyboardFocusedWindowID = nil
         lastKeyboardAppIndexCommand = appIndexCommand
         selectionChangedByLastCommand = false
         clearKeyboardBoundaryFeedback()
@@ -1533,6 +1547,7 @@
 
         keyboardFocusedAppGroupIndex = targetAppGroupIndex
         currentSelection = nextSelection
+        keyboardFocusedWindowID = nextSelection.windowID
         columnSelectionHistory[nextSelection.appGroupIndex] = nextSelection.windowIndex
         selectionChangedByLastCommand = previousSelection != nextSelection
         clearKeyboardBoundaryFeedback()
@@ -2767,8 +2782,13 @@
             container.borderColor = NSColor.separatorColor.withAlphaComponent(0.34).cgColor
             container.borderWidth = 1
 
+            let cardsClip = CALayer()
+            cardsClip.masksToBounds = true
+            cardsClip.backgroundColor = NSColor.clear.cgColor
+
             let header = CALayer()
             header.backgroundColor = NSColor.controlBackgroundColor.withAlphaComponent(0.68).cgColor
+            header.zPosition = 40
 
             let appName = makeTextLayer(
                 string: column.app.name,
@@ -2785,6 +2805,10 @@
                 alignment: .right
             )
 
+            appName.zPosition = 42
+            count.zPosition = 42
+
+            container.addSublayer(cardsClip)
             container.addSublayer(header)
             container.addSublayer(appName)
             container.addSublayer(count)
@@ -2851,7 +2875,7 @@
                 card.addSublayer(state)
                 card.addSublayer(shine)
                 card.addSublayer(closeButton.containerLayer)
-                container.addSublayer(card)
+                cardsClip.addSublayer(card)
 
                 return WaterfallCardLayers(
                     item: window,
@@ -2872,6 +2896,7 @@
             return WaterfallColumnLayers(
                 column: column,
                 containerLayer: container,
+                cardsClipLayer: cardsClip,
                 headerLayer: header,
                 appNameLayer: appName,
                 countLayer: count,
@@ -3591,7 +3616,14 @@
                 x: locationInWaterfallContent.x - column.containerLayer.frame.minX,
                 y: locationInWaterfallContent.y - column.containerLayer.frame.minY
             )
-            if let card = column.cards.first(where: { $0.containerLayer.frame.contains(locationInColumn) }) {
+            guard column.cardsClipLayer.frame.contains(locationInColumn) else {
+                continue
+            }
+            let locationInCardsClip = CGPoint(
+                x: locationInColumn.x - column.cardsClipLayer.frame.minX,
+                y: locationInColumn.y - column.cardsClipLayer.frame.minY
+            )
+            if let card = column.cards.first(where: { $0.containerLayer.frame.contains(locationInCardsClip) }) {
                 return card
             }
         }
@@ -4154,6 +4186,8 @@
             column.containerLayer.shadowOpacity = 0
             column.containerLayer.shadowRadius = 0
             column.containerLayer.shadowOffset = .zero
+            column.cardsClipLayer.frame = column.containerLayer.bounds
+            column.cardsClipLayer.masksToBounds = false
             column.headerLayer.isHidden = true
             column.appNameLayer.isHidden = true
             column.countLayer.isHidden = true
@@ -4293,6 +4327,14 @@
         column.countLayer.isHidden = false
 
         let bounds = column.containerLayer.bounds
+        let cardsClipFrame = CGRect(
+            x: 0,
+            y: 0,
+            width: bounds.width,
+            height: max(1, bounds.height - WaterfallMetrics.headerHeight)
+        )
+        column.cardsClipLayer.frame = cardsClipFrame
+        column.cardsClipLayer.masksToBounds = true
         let isSpaceFocused = spaceFocusWindowCount(for: column.column.appGroupIndex) > 0
         let isHovered = column.column.appGroupIndex == effectiveHoveredAppGroupIndex
         column.containerLayer.borderColor = (isSpaceFocused
@@ -4335,8 +4377,8 @@
         waterfallColumnScrollOffsets[appGroupIndex] = verticalOffset
 
         let cardWidth = max(1, bounds.width - WaterfallMetrics.columnHorizontalPadding * 2)
-        let firstCardTop = bounds.height
-            - WaterfallMetrics.headerHeight
+        let firstCardTop = cardsClipFrame.height
+            - WaterfallMetrics.selectedCardScaleOverflow
             - WaterfallMetrics.columnVerticalPadding
             + verticalOffset
 
@@ -4360,10 +4402,11 @@
         let bounds = card.containerLayer.bounds
         let isSelected = card.item.window.id == effectiveSelection?.windowID
         let isHovered = card.item.window.id == hoveredWindowID
+        let isKeyboardFocused = card.item.window.id == keyboardFocusedWindowID
         let isAppLinked = isWaterfallCardAppLinked(card)
         applyWaterfallCardVisualState(
             card,
-            selected: shouldShowSelectedVisual(isSelected: isSelected, isHovered: isHovered),
+            selected: shouldShowSelectedVisual(isSelected: isSelected, isHovered: isHovered) || isKeyboardFocused,
             hovered: isHovered,
             appLinked: isAppLinked
         )
@@ -4469,7 +4512,7 @@
                     ? CGSize(width: 0, height: -5)
                     : .zero
         card.containerLayer.zPosition = selected ? 20 : hovered ? 12 : appLinked ? 10 : isSpaceFocused ? 8 : 0
-        let focusedScale: CGFloat = selected ? 1.015 : isSpaceFocused ? 1.012 : 1.008
+        let focusedScale: CGFloat = selected ? WaterfallMetrics.selectedCardScale : isSpaceFocused ? 1.012 : 1.008
         card.containerLayer.transform = (selected || hovered || isSpaceFocused)
             ? CATransform3DMakeScale(focusedScale, focusedScale, 1)
             : CATransform3DIdentity
@@ -4945,11 +4988,18 @@
                 x: locationInWaterfallContent.x - column.containerLayer.frame.minX,
                 y: locationInWaterfallContent.y - column.containerLayer.frame.minY
             )
-            for card in column.cards where card.containerLayer.frame.contains(locationInColumn) {
+            guard column.cardsClipLayer.frame.contains(locationInColumn) else {
+                continue
+            }
+            let locationInCardsClip = CGPoint(
+                x: locationInColumn.x - column.cardsClipLayer.frame.minX,
+                y: locationInColumn.y - column.cardsClipLayer.frame.minY
+            )
+            for card in column.cards where card.containerLayer.frame.contains(locationInCardsClip) {
                 guard card.closeButton.containerLayer.opacity > 0 else { continue }
                 let locationInCard = CGPoint(
-                    x: locationInColumn.x - card.containerLayer.frame.minX,
-                    y: locationInColumn.y - card.containerLayer.frame.minY
+                    x: locationInCardsClip.x - card.containerLayer.frame.minX,
+                    y: locationInCardsClip.y - card.containerLayer.frame.minY
                 )
                 if card.closeButton.containerLayer.frame.insetBy(dx: -8, dy: -8).contains(locationInCard) {
                     return .window(
@@ -4991,11 +5041,13 @@
                 x: waterfallLayer.frame.minX
                     + waterfallContentLayer.frame.minX
                     + column.containerLayer.frame.minX
+                    + column.cardsClipLayer.frame.minX
                     + card.containerLayer.frame.minX
                     + card.closeButton.containerLayer.frame.midX,
                 y: waterfallLayer.frame.minY
                     + waterfallContentLayer.frame.minY
                     + column.containerLayer.frame.minY
+                    + column.cardsClipLayer.frame.minY
                     + card.containerLayer.frame.minY
                     + card.closeButton.containerLayer.frame.midY
             )
@@ -5123,7 +5175,7 @@
         )
         let contentHeight = CGFloat(column.cards.count) * WaterfallMetrics.cardHeight
             + CGFloat(max(0, column.cards.count - 1)) * WaterfallMetrics.cardGap
-        return max(0, contentHeight - visibleHeight)
+        return max(0, contentHeight - visibleHeight + WaterfallMetrics.selectedCardScaleOverflow * 2)
     }
 
     private func clampedWaterfallColumnOffset(_ offset: CGFloat, appGroupIndex: Int) -> CGFloat {
@@ -5546,10 +5598,13 @@
             let appGroupIndex = column.column.appGroupIndex
             let isHovered = appGroupIndex == effectiveHoveredAppGroupIndex
             let firstCardFrame = column.cards.first?.containerLayer.frame
-            let headerToFirstCardGap = firstCardFrame.map {
+            let visibleFirstCardFrame = firstCardFrame.map {
+                $0.offsetBy(dx: column.cardsClipLayer.frame.minX, dy: column.cardsClipLayer.frame.minY)
+            }
+            let headerToFirstCardGap = visibleFirstCardFrame.map {
                 column.headerLayer.frame.minY - $0.maxY
             }
-            let topToFirstCardGap = firstCardFrame.map {
+            let topToFirstCardGap = visibleFirstCardFrame.map {
                 column.containerLayer.bounds.height - $0.maxY
             }
 
@@ -5567,6 +5622,8 @@
                 "visibleFrame": dictionary(from: visibleFrame),
                 "headerFrame": dictionary(from: column.headerLayer.frame),
                 "headerBackgroundColor": colorDictionary(from: column.headerLayer.backgroundColor),
+                "cardsClipFrame": dictionary(from: column.cardsClipLayer.frame),
+                "cardsClipMasksToBounds": column.cardsClipLayer.masksToBounds,
                 "headerToFirstCardGap": headerToFirstCardGap.map(Double.init) ?? NSNull(),
                 "topToFirstCardGap": topToFirstCardGap.map(Double.init) ?? NSNull(),
                 "appNameFrame": dictionary(from: column.appNameLayer.frame),
@@ -5631,8 +5688,8 @@
     private func waterfallCardReports(for column: WaterfallColumnLayers) -> [[String: Any]] {
         column.cards.map { card in
             let visibleFrame = card.containerLayer.frame.offsetBy(
-                dx: column.containerLayer.frame.minX + waterfallContentLayer.frame.minX,
-                dy: column.containerLayer.frame.minY
+                dx: column.containerLayer.frame.minX + column.cardsClipLayer.frame.minX + waterfallContentLayer.frame.minX,
+                dy: column.containerLayer.frame.minY + column.cardsClipLayer.frame.minY
             )
             let screenshotSource = screenshotSourcesByWindowID[card.item.window.id]
 
@@ -5650,6 +5707,7 @@
                 "isFullscreen": card.item.window.isFullscreen,
                 "identifierSource": "\(card.item.window.identifierSource)",
                 "isSelected": card.item.window.id == effectiveSelection?.windowID,
+                "isKeyboardFocused": card.item.window.id == keyboardFocusedWindowID,
                 "visualStates": waterfallCardVisualStates(for: card),
                 "thumbnailStrategy": thumbnailStrategyString(for: card.item.window),
                 "screenshotSource": screenshotSourceString(screenshotSource, for: card.item.window),
@@ -5768,12 +5826,16 @@
         var states: [String] = []
         let isSelected = card.item.window.id == effectiveSelection?.windowID
         let isHovered = card.item.window.id == hoveredWindowID
+        let isKeyboardFocused = card.item.window.id == keyboardFocusedWindowID
         let isAppLinked = isWaterfallCardAppLinked(card)
-        if shouldShowSelectedVisual(isSelected: isSelected, isHovered: isHovered) {
+        if shouldShowSelectedVisual(isSelected: isSelected, isHovered: isHovered) || isKeyboardFocused {
             states.append("selected")
         } else if isSelected {
             states.append("selectedVisualSuppressed")
         }
+        if isKeyboardFocused {
+            states.append("keyboardFocused")
+        }
         if isHovered {
             states.append("hover")
         }

--
Gitblit v1.9.3