Ariver
2026-06-19 b1368b6a51379714958af82cb1e3dc24183b0583
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?
@@ -229,6 +230,32 @@
        case keyboard
    }
    private enum HorizontalKeyboardFocusKind: String {
        case app
        case window
    }
    private struct HorizontalKeyboardFocusNode {
        let kind: HorizontalKeyboardFocusKind
        let appGroupIndex: Int
        let windowIndex: Int?
        let windowID: UInt32?
        let spaceID: UInt64?
        let frame: CGRect
        let order: Int
        var center: CGPoint {
            CGPoint(x: frame.midX, y: frame.midY)
        }
        func matches(_ other: HorizontalKeyboardFocusNode) -> Bool {
            kind == other.kind
                && appGroupIndex == other.appGroupIndex
                && windowIndex == other.windowIndex
                && windowID == other.windowID
        }
    }
    private struct SpaceLaneSegmentLayers {
        let space: QuickSwitchSpaceViewModel
        let containerLayer: CALayer
@@ -266,6 +293,7 @@
    private struct WaterfallColumnLayers {
        let column: QuickSwitchWaterfallColumnViewModel
        let containerLayer: CALayer
        let cardsClipLayer: CALayer
        let headerLayer: CALayer
        let appNameLayer: CATextLayer
        let countLayer: CATextLayer
@@ -355,6 +383,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 +769,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 +846,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 +867,7 @@
            currentSelection = viewModel?.initialSelection
            columnSelectionHistory = [:]
            keyboardFocusedAppGroupIndex = nil
            keyboardFocusedWindowID = nil
            keyboardCommandsApplied = []
            lastKeyboardCommand = nil
            lastKeyboardAppIndexCommand = nil
@@ -1256,6 +1294,8 @@
            "selectedWindowIndex": effectiveSelection?.windowIndex ?? NSNull(),
            "selectedWindowID": effectiveSelection?.windowID ?? NSNull(),
            "keyboardFocusedAppGroupIndex": keyboardFocusedAppGroupIndex ?? NSNull(),
            "keyboardFocusedWindowID": keyboardFocusedWindowID ?? NSNull(),
            "horizontalAppDefaultWindowID": horizontalAppDefaultWindowID ?? NSNull(),
            "keyboardCommandsApplied": keyboardCommandsApplied,
            "lastKeyboardCommand": lastKeyboardCommand ?? NSNull(),
            "lastKeyboardAppIndexCommand": lastKeyboardAppIndexCommand ?? NSNull(),
@@ -1380,7 +1420,7 @@
        case "tab":
            if isHorizontalMasonryMode {
                recordKeyboardCommand("tab")
                moveSelection(.right)
                moveHorizontalMasonryTabSelection()
            } else {
                tabIgnoredCount += 1
                recordKeyboardCommand("tabIgnored")
@@ -1450,11 +1490,15 @@
        }
        keyboardFocusedAppGroupIndex = appGroupIndex
        keyboardFocusedWindowID = nil
        lastKeyboardAppIndexCommand = appIndexCommand
        selectionChangedByLastCommand = false
        clearKeyboardBoundaryFeedback()
        ensureAppShelfItemVisible(appGroupIndex)
        layoutSubtreeIfNeeded()
        if isHorizontalMasonryMode {
            _ = selectHorizontalMasonryDefaultWindowForApp(appGroupIndex)
        }
        hoverAppGroup(appGroupIndex, source: .keyboard)
        needsLayout = true
        return true
@@ -1482,6 +1526,53 @@
    private var appHoverTargetAppGroupIndex: Int? {
        guard case .app(let appGroupIndex) = hoverTarget else { return nil }
        return appGroupIndex
    }
    private var horizontalAppDefaultWindowID: UInt32? {
        guard isHorizontalMasonryMode,
              case .app(let appGroupIndex) = hoverTarget
        else {
            return nil
        }
        return firstHorizontalMasonryCard(for: appGroupIndex)?.item.window.id
    }
    private func firstHorizontalMasonryCard(for appGroupIndex: Int) -> WaterfallCardLayers? {
        waterfallColumns
            .first { $0.column.appGroupIndex == appGroupIndex }?
            .cards
            .sorted { $0.item.windowIndex < $1.item.windowIndex }
            .first
    }
    private func isHorizontalAppDefaultCard(_ card: WaterfallCardLayers) -> Bool {
        guard isHorizontalMasonryMode,
              card.item.window.id == horizontalAppDefaultWindowID
        else {
            return false
        }
        return true
    }
    @discardableResult
    private func selectHorizontalMasonryDefaultWindowForApp(
        _ appGroupIndex: Int,
        recordsSelectionChange: Bool = false
    ) -> QuickSwitchSelection? {
        guard isHorizontalMasonryMode,
              let card = firstHorizontalMasonryCard(for: appGroupIndex)
        else {
            return nil
        }
        let previousSelection = effectiveSelection
        let selection = selectionForCard(card)
        currentSelection = selection
        columnSelectionHistory[selection.appGroupIndex] = selection.windowIndex
        if recordsSelectionChange {
            selectionChangedByLastCommand = previousSelection != selection
        }
        return selection
    }
    private func moveVerticalSelectionWithinFocusedColumn(_ direction: QuickSwitchKeyboardDirection) {
@@ -1533,6 +1624,7 @@
        keyboardFocusedAppGroupIndex = targetAppGroupIndex
        currentSelection = nextSelection
        keyboardFocusedWindowID = nextSelection.windowID
        columnSelectionHistory[nextSelection.appGroupIndex] = nextSelection.windowIndex
        selectionChangedByLastCommand = previousSelection != nextSelection
        clearKeyboardBoundaryFeedback()
@@ -2025,6 +2117,11 @@
        hoveredSpaceID = target.spaceID
        hoveredSpaceLaneID = target.spaceLaneID
        if case .app(let appGroupIndex) = target, isHorizontalMasonryMode {
            keyboardFocusedWindowID = nil
            _ = selectHorizontalMasonryDefaultWindowForApp(appGroupIndex)
        }
        if alignsWaterfallWithAppShelf, let appGroupIndex = target.appGroupIndex {
            alignWaterfallColumnWithAppShelfIcon(appGroupIndex: appGroupIndex, animated: true)
        }
@@ -2074,29 +2171,360 @@
    }
    private func moveHorizontalMasonrySelection(_ direction: QuickSwitchKeyboardDirection) {
        guard let current = currentSelection ?? currentViewModel?.initialSelection,
              let next = nextHorizontalMasonrySelection(from: current, direction: direction)
        layoutSubtreeIfNeeded()
        let nodes = horizontalKeyboardFocusNodes()
        guard !nodes.isEmpty,
              let current = currentHorizontalKeyboardFocusNode(in: nodes),
              let next = nextHorizontalKeyboardFocusNode(from: current, direction: direction, in: nodes)
        else {
            DevelopmentDiagnostics.log("quickSwitch.view.moveHorizontalMasonrySelection.blocked", [
                "direction": String(describing: direction),
                "hasNodes": !nodes.isEmpty,
                "hasSelection": (currentSelection ?? currentViewModel?.initialSelection) != nil
            ])
            selectionChangedByLastCommand = false
            return
        }
        focusHorizontalKeyboardNode(next, direction: direction)
        DevelopmentDiagnostics.log("quickSwitch.view.moveHorizontalMasonrySelection", [
            "direction": String(describing: direction),
            "kind": next.kind.rawValue,
            "appGroupIndex": next.appGroupIndex,
            "windowIndex": next.windowIndex ?? -1,
            "windowID": next.windowID ?? 0
        ])
    }
    private func moveHorizontalMasonryTabSelection() {
        guard let current = currentSelection ?? currentViewModel?.initialSelection,
              let next = nextHorizontalMasonrySelection(from: current, direction: .right)
        else {
            DevelopmentDiagnostics.log("quickSwitch.view.moveHorizontalMasonryTabSelection.blocked", [
                "hasSelection": (currentSelection ?? currentViewModel?.initialSelection) != nil
            ])
            selectionChangedByLastCommand = false
            return
        }
        columnSelectionHistory[current.appGroupIndex] = current.windowIndex
        let previousSelection = effectiveSelection
        currentSelection = next
        keyboardFocusedAppGroupIndex = next.appGroupIndex
        keyboardFocusedWindowID = next.windowID
        columnSelectionHistory[next.appGroupIndex] = next.windowIndex
        selectionChangedByLastCommand = previousSelection != next
        clearKeyboardBoundaryFeedback()
        setHoverTarget(
            .window(appGroupIndex: next.appGroupIndex, windowID: next.windowID, spaceID: nil),
            source: .keyboard
        )
        ensureCurrentSelectionVisible(horizontalIntent: .alignWithAppShelf(animated: true))
        DevelopmentDiagnostics.log("quickSwitch.view.moveHorizontalMasonrySelection", [
            "direction": String(describing: direction),
        DevelopmentDiagnostics.log("quickSwitch.view.moveHorizontalMasonryTabSelection", [
            "appGroupIndex": next.appGroupIndex,
            "windowIndex": next.windowIndex,
            "windowID": next.windowID
        ])
        needsLayout = true
    }
    private func horizontalKeyboardFocusNodes() -> [HorizontalKeyboardFocusNode] {
        var nodes: [HorizontalKeyboardFocusNode] = []
        nodes.reserveCapacity(appShelfItems.count + waterfallColumns.reduce(0) { $0 + $1.cards.count })
        for (index, item) in appShelfItems.enumerated() {
            let frame = item.iconLayer.frame.offsetBy(
                dx: shelfLayer.frame.minX
                    + appShelfContentLayer.frame.minX
                    + item.containerLayer.frame.minX,
                dy: shelfLayer.frame.minY
                    + appShelfContentLayer.frame.minY
                    + item.containerLayer.frame.minY
            )
            nodes.append(HorizontalKeyboardFocusNode(
                kind: .app,
                appGroupIndex: item.item.appGroupIndex,
                windowIndex: nil,
                windowID: nil,
                spaceID: nil,
                frame: frame,
                order: index
            ))
        }
        var order = appShelfItems.count
        for column in waterfallColumns.sorted(by: { $0.column.appGroupIndex < $1.column.appGroupIndex }) {
            for card in column.cards.sorted(by: { $0.item.windowIndex < $1.item.windowIndex }) {
                let frame = card.containerLayer.frame.offsetBy(
                    dx: waterfallLayer.frame.minX
                        + waterfallContentLayer.frame.minX
                        + column.containerLayer.frame.minX
                        + column.cardsClipLayer.frame.minX,
                    dy: waterfallLayer.frame.minY
                        + waterfallContentLayer.frame.minY
                        + column.containerLayer.frame.minY
                        + column.cardsClipLayer.frame.minY
                )
                nodes.append(HorizontalKeyboardFocusNode(
                    kind: .window,
                    appGroupIndex: card.item.appGroupIndex,
                    windowIndex: card.item.windowIndex,
                    windowID: card.item.window.id,
                    spaceID: card.item.primarySpaceID,
                    frame: frame,
                    order: order
                ))
                order += 1
            }
        }
        return nodes
    }
    private func currentHorizontalKeyboardFocusNode(
        in nodes: [HorizontalKeyboardFocusNode]
    ) -> HorizontalKeyboardFocusNode? {
        if case .app(let appGroupIndex) = hoverTarget,
           let node = nodes.first(where: { $0.kind == .app && $0.appGroupIndex == appGroupIndex }) {
            return node
        }
        if let keyboardFocusedWindowID,
           let node = nodes.first(where: { $0.kind == .window && $0.windowID == keyboardFocusedWindowID }) {
            return node
        }
        if case .window(_, let windowID, _) = hoverTarget,
           let node = nodes.first(where: { $0.kind == .window && $0.windowID == windowID }) {
            return node
        }
        if let selection = effectiveSelection,
           let node = nodes.first(where: { $0.kind == .window && $0.windowID == selection.windowID }) {
            return node
        }
        return nodes.min { $0.order < $1.order }
    }
    private func nextHorizontalKeyboardFocusNode(
        from current: HorizontalKeyboardFocusNode,
        direction: QuickSwitchKeyboardDirection,
        in nodes: [HorizontalKeyboardFocusNode]
    ) -> HorizontalKeyboardFocusNode? {
        let candidates = nodes.filter { !$0.matches(current) }
        guard !candidates.isEmpty else { return current }
        if current.kind == .app,
           direction == .down,
           let firstWindow = firstHorizontalWindowNode(for: current.appGroupIndex, in: nodes) {
            return firstWindow
        }
        if current.kind == .window,
           direction == .up,
           current.windowID == firstHorizontalMasonryCard(for: current.appGroupIndex)?.item.window.id,
           let appNode = nodes.first(where: { $0.kind == .app && $0.appGroupIndex == current.appGroupIndex }) {
            return appNode
        }
        switch direction {
        case .left, .right:
            let sameKindCandidates = candidates.filter { $0.kind == current.kind }
            return nextHorizontalFocusByX(
                from: current,
                direction: direction,
                candidates: sameKindCandidates.isEmpty ? candidates : sameKindCandidates
            )
        case .up, .down:
            return nextHorizontalFocusByY(from: current, direction: direction, candidates: candidates)
        }
    }
    private func firstHorizontalWindowNode(
        for appGroupIndex: Int,
        in nodes: [HorizontalKeyboardFocusNode]
    ) -> HorizontalKeyboardFocusNode? {
        nodes
            .filter { $0.kind == .window && $0.appGroupIndex == appGroupIndex }
            .min { lhs, rhs in
                (lhs.windowIndex ?? .max, lhs.order) < (rhs.windowIndex ?? .max, rhs.order)
            }
    }
    private func nextHorizontalFocusByX(
        from current: HorizontalKeyboardFocusNode,
        direction: QuickSwitchKeyboardDirection,
        candidates: [HorizontalKeyboardFocusNode]
    ) -> HorizontalKeyboardFocusNode? {
        let epsilon: CGFloat = 0.5
        let forward = direction == .right
        let directional = candidates.filter { node in
            forward
                ? node.center.x > current.center.x + epsilon
                : node.center.x < current.center.x - epsilon
        }
        let sameBand = directional.filter { isHorizontalFocusSameBand($0, current) }
        let pool = sameBand.isEmpty ? directional : sameBand
        if let next = pool.sorted(by: horizontalFocusXSort(from: current, forward: forward)).first {
            return next
        }
        let wrapSameBand = candidates.filter { isHorizontalFocusSameBand($0, current) }
        let wrapPool = wrapSameBand.isEmpty ? candidates : wrapSameBand
        return wrapPool.sorted(by: horizontalFocusXWrapSort(from: current, forward: forward)).first
    }
    private func nextHorizontalFocusByY(
        from current: HorizontalKeyboardFocusNode,
        direction: QuickSwitchKeyboardDirection,
        candidates: [HorizontalKeyboardFocusNode]
    ) -> HorizontalKeyboardFocusNode? {
        let epsilon: CGFloat = 0.5
        let upward = direction == .up
        let directional = candidates.filter { node in
            upward
                ? node.center.y > current.center.y + epsilon
                : node.center.y < current.center.y - epsilon
        }
        if let next = directional.sorted(by: horizontalFocusYSort(from: current, upward: upward)).first {
            return next
        }
        return candidates.sorted(by: horizontalFocusYWrapSort(from: current, upward: upward)).first
    }
    private func isHorizontalFocusSameBand(
        _ lhs: HorizontalKeyboardFocusNode,
        _ rhs: HorizontalKeyboardFocusNode
    ) -> Bool {
        let tolerance = max(lhs.frame.height, rhs.frame.height) * 0.75
        return abs(lhs.center.y - rhs.center.y) <= tolerance
    }
    private func horizontalFocusXSort(
        from current: HorizontalKeyboardFocusNode,
        forward: Bool
    ) -> (HorizontalKeyboardFocusNode, HorizontalKeyboardFocusNode) -> Bool {
        { lhs, rhs in
            let lhsPrimary = abs(lhs.center.x - current.center.x)
            let rhsPrimary = abs(rhs.center.x - current.center.x)
            if abs(lhsPrimary - rhsPrimary) > 0.5 {
                return lhsPrimary < rhsPrimary
            }
            let lhsSecondary = abs(lhs.center.y - current.center.y)
            let rhsSecondary = abs(rhs.center.y - current.center.y)
            if abs(lhsSecondary - rhsSecondary) > 0.5 {
                return lhsSecondary < rhsSecondary
            }
            return forward ? lhs.order < rhs.order : lhs.order > rhs.order
        }
    }
    private func horizontalFocusXWrapSort(
        from current: HorizontalKeyboardFocusNode,
        forward: Bool
    ) -> (HorizontalKeyboardFocusNode, HorizontalKeyboardFocusNode) -> Bool {
        { lhs, rhs in
            if abs(lhs.center.x - rhs.center.x) > 0.5 {
                return forward ? lhs.center.x < rhs.center.x : lhs.center.x > rhs.center.x
            }
            let lhsSecondary = abs(lhs.center.y - current.center.y)
            let rhsSecondary = abs(rhs.center.y - current.center.y)
            if abs(lhsSecondary - rhsSecondary) > 0.5 {
                return lhsSecondary < rhsSecondary
            }
            return forward ? lhs.order < rhs.order : lhs.order > rhs.order
        }
    }
    private func horizontalFocusYSort(
        from current: HorizontalKeyboardFocusNode,
        upward: Bool
    ) -> (HorizontalKeyboardFocusNode, HorizontalKeyboardFocusNode) -> Bool {
        { lhs, rhs in
            let lhsPrimary = abs(lhs.center.y - current.center.y)
            let rhsPrimary = abs(rhs.center.y - current.center.y)
            if abs(lhsPrimary - rhsPrimary) > 0.5 {
                return lhsPrimary < rhsPrimary
            }
            let lhsSecondary = abs(lhs.center.x - current.center.x)
            let rhsSecondary = abs(rhs.center.x - current.center.x)
            if abs(lhsSecondary - rhsSecondary) > 0.5 {
                return lhsSecondary < rhsSecondary
            }
            return upward ? lhs.order < rhs.order : lhs.order > rhs.order
        }
    }
    private func horizontalFocusYWrapSort(
        from current: HorizontalKeyboardFocusNode,
        upward: Bool
    ) -> (HorizontalKeyboardFocusNode, HorizontalKeyboardFocusNode) -> Bool {
        { lhs, rhs in
            if abs(lhs.center.y - rhs.center.y) > 0.5 {
                return upward ? lhs.center.y < rhs.center.y : lhs.center.y > rhs.center.y
            }
            let lhsSecondary = abs(lhs.center.x - current.center.x)
            let rhsSecondary = abs(rhs.center.x - current.center.x)
            if abs(lhsSecondary - rhsSecondary) > 0.5 {
                return lhsSecondary < rhsSecondary
            }
            return upward ? lhs.order < rhs.order : lhs.order > rhs.order
        }
    }
    private func focusHorizontalKeyboardNode(
        _ node: HorizontalKeyboardFocusNode,
        direction _: QuickSwitchKeyboardDirection
    ) {
        switch node.kind {
        case .app:
            keyboardFocusedAppGroupIndex = node.appGroupIndex
            keyboardFocusedWindowID = nil
            clearKeyboardBoundaryFeedback()
            ensureAppShelfItemVisible(node.appGroupIndex)
            _ = selectHorizontalMasonryDefaultWindowForApp(
                node.appGroupIndex,
                recordsSelectionChange: true
            )
            hoverAppGroup(node.appGroupIndex, source: .keyboard)
        case .window:
            guard let windowIndex = node.windowIndex,
                  let windowID = node.windowID
            else {
                selectionChangedByLastCommand = false
                return
            }
            let previousSelection = effectiveSelection
            let next = QuickSwitchSelection(
                appGroupIndex: node.appGroupIndex,
                windowIndex: windowIndex,
                windowID: windowID
            )
            keyboardFocusedAppGroupIndex = node.appGroupIndex
            keyboardFocusedWindowID = windowID
            currentSelection = next
            columnSelectionHistory[next.appGroupIndex] = next.windowIndex
            selectionChangedByLastCommand = previousSelection != next
            clearKeyboardBoundaryFeedback()
            ensureAppShelfItemVisible(node.appGroupIndex)
            setHoverTarget(
                .window(appGroupIndex: node.appGroupIndex, windowID: windowID, spaceID: node.spaceID),
                source: .keyboard
            )
            ensureCurrentSelectionVisible(horizontalIntent: .alignWithAppShelf(animated: true))
        }
        needsLayout = true
    }
@@ -2767,8 +3195,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 +3218,10 @@
                alignment: .right
            )
            appName.zPosition = 42
            count.zPosition = 42
            container.addSublayer(cardsClip)
            container.addSublayer(header)
            container.addSublayer(appName)
            container.addSublayer(count)
@@ -2851,7 +3288,7 @@
                card.addSublayer(state)
                card.addSublayer(shine)
                card.addSublayer(closeButton.containerLayer)
                container.addSublayer(card)
                cardsClip.addSublayer(card)
                return WaterfallCardLayers(
                    item: window,
@@ -2872,6 +3309,7 @@
            return WaterfallColumnLayers(
                column: column,
                containerLayer: container,
                cardsClipLayer: cardsClip,
                headerLayer: header,
                appNameLayer: appName,
                countLayer: count,
@@ -3591,7 +4029,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 +4599,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 +4740,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 +4790,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 +4815,14 @@
        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 isHorizontalAppDefaultFocused = isHorizontalAppDefaultCard(card)
        let isAppLinked = isWaterfallCardAppLinked(card)
        applyWaterfallCardVisualState(
            card,
            selected: shouldShowSelectedVisual(isSelected: isSelected, isHovered: isHovered),
            selected: shouldShowSelectedVisual(isSelected: isSelected, isHovered: isHovered)
                || isKeyboardFocused
                || isHorizontalAppDefaultFocused,
            hovered: isHovered,
            appLinked: isAppLinked
        )
@@ -4469,7 +4928,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
@@ -4481,9 +4940,12 @@
                transform: nil
            )
            : nil
        card.titleBarLayer.backgroundColor = appLinked
            ? spaceLaneOccupiedColor.withAlphaComponent(0.18).cgColor
            : NSColor.clear.cgColor
        card.titleBarLayer.backgroundColor = (selected
            ? spaceLaneOccupiedColor.withAlphaComponent(0.22)
            : appLinked
                ? spaceLaneOccupiedColor.withAlphaComponent(0.18)
                : NSColor.clear
        ).cgColor
        card.titleBarLayer.borderColor = NSColor.clear.cgColor
        card.titleBarLayer.borderWidth = 0
        card.shineLayer.opacity = selected ? 1 : 0
@@ -4945,11 +5407,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 +5460,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
            )
@@ -5018,6 +5489,10 @@
        if isHorizontalMasonryMode {
            return true
        }
        if keyboardFocusedWindowID != nil {
            return false
        }
        return waterfallColumns
@@ -5123,7 +5598,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 +6021,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 +6045,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 +6111,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 +6130,8 @@
                "isFullscreen": card.item.window.isFullscreen,
                "identifierSource": "\(card.item.window.identifierSource)",
                "isSelected": card.item.window.id == effectiveSelection?.windowID,
                "isKeyboardFocused": card.item.window.id == keyboardFocusedWindowID,
                "isHorizontalAppDefault": isHorizontalAppDefaultCard(card),
                "visualStates": waterfallCardVisualStates(for: card),
                "thumbnailStrategy": thumbnailStrategyString(for: card.item.window),
                "screenshotSource": screenshotSourceString(screenshotSource, for: card.item.window),
@@ -5674,6 +6156,7 @@
                "closeButtonGlyphLineWidth": Double(card.closeButton.glyphLayer.lineWidth),
                "shineVisible": card.shineLayer.opacity > 0,
                "zPosition": Double(card.containerLayer.zPosition),
                "bounds": dictionary(from: card.containerLayer.bounds),
                "frame": dictionary(from: card.containerLayer.frame),
                "visibleFrame": dictionary(from: visibleFrame),
                "titleTruncationMode": "middle"
@@ -5768,12 +6251,22 @@
        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 isHorizontalAppDefaultFocused = isHorizontalAppDefaultCard(card)
        let isAppLinked = isWaterfallCardAppLinked(card)
        if shouldShowSelectedVisual(isSelected: isSelected, isHovered: isHovered) {
        if shouldShowSelectedVisual(isSelected: isSelected, isHovered: isHovered)
            || isKeyboardFocused
            || isHorizontalAppDefaultFocused {
            states.append("selected")
        } else if isSelected {
            states.append("selectedVisualSuppressed")
        }
        if isKeyboardFocused {
            states.append("keyboardFocused")
        }
        if isHorizontalAppDefaultFocused {
            states.append("appDefault")
        }
        if isHovered {
            states.append("hover")
        }