Ariver
2026-06-11 0045898e8cd7b2d8972bb24c8ac27e514bdd8c70
C1.source/Sources/Aligner/QuickSwitchRootView.swift
@@ -78,8 +78,13 @@
    private var keyboardResponseLatencyMilliseconds: [Double] = []
    private var mouseResponseLatencyMilliseconds: [Double] = []
    private var hoverResponseLatencyMilliseconds: [Double] = []
    private var lastProjectionTransitionMovedLayerCount = 0
    private var lastProjectionTransitionFadeInLayerCount = 0
    private var lastProjectionTransitionFadeOutLayerCount = 0
    private var lastProjectionTransitionDurationMilliseconds: Double = 0
    var onEscape: (() -> Void)?
    var onCommitSelection: ((QuickSwitchSelection, QuickSwitchCommitSource) -> Void)?
    var onSpaceLaneClick: ((UInt64) -> Void)?
    var onRequestClose: ((QuickSwitchCloseRequest) -> Void)?
    var onCloseConfirmationDisabled: (() -> Void)?
@@ -256,6 +261,22 @@
        let stateLayer: CATextLayer
        let shineLayer: CAGradientLayer
        let closeButton: CloseButtonLayers
    }
    private struct ProjectionLayerSnapshot {
        let layer: CALayer
        let frame: CGRect
    }
    private struct ProjectionColumnSnapshot {
        let layer: CALayer
        let frame: CGRect
        let cardsByWindowID: [UInt32: ProjectionLayerSnapshot]
    }
    private struct ProjectionTransitionSnapshot {
        let appShelfItemsByAppGroupIndex: [Int: ProjectionLayerSnapshot]
        let waterfallColumnsByAppGroupIndex: [Int: ProjectionColumnSnapshot]
    }
    private enum AppShelfMetrics {
@@ -608,8 +629,44 @@
        }
    }
    func effectiveSelectionForProjection() -> QuickSwitchSelection? {
        effectiveSelection
    }
    func apply(viewModel: QuickSwitchViewModel?) {
        apply(viewModel: viewModel, preservingInteractionState: false, animatedProjection: false)
    }
    func applyProjected(viewModel: QuickSwitchViewModel?) {
        apply(viewModel: viewModel, preservingInteractionState: true, animatedProjection: true)
    }
    private func apply(
        viewModel: QuickSwitchViewModel?,
        preservingInteractionState: Bool,
        animatedProjection: Bool
    ) {
        let transitionSnapshot = animatedProjection ? captureProjectionTransitionSnapshot() : nil
        if !animatedProjection {
            resetProjectionTransitionReport()
        }
        currentViewModel = viewModel
        if preservingInteractionState {
            currentSelection = validSelection(currentSelection, in: viewModel) ?? viewModel?.initialSelection
            columnSelectionHistory = columnSelectionHistory.filter { appGroupIndex, _ in
                viewModel?.waterfallColumns.contains { $0.appGroupIndex == appGroupIndex } == true
            }
            if let currentSelection {
                columnSelectionHistory[currentSelection.appGroupIndex] = currentSelection.windowIndex
            }
            hoveredCloseTarget = nil
            pendingCloseTarget = nil
            pendingCloseRequest = nil
            pendingCloseOptOut = false
            closeConfirmationLayers.containerLayer.isHidden = true
            closeConfirmationArrowEdge = nil
        } else {
        screenshotSourcesByWindowID = [:]
        screenshotSkippedReasonsByWindowID = [:]
        currentSelection = viewModel?.initialSelection
@@ -637,6 +694,8 @@
        clearCloseFeedback()
        setHoverTarget(.none)
        hoveredAppGroupIndex = debugHoveredAppGroupIndex
        }
        if let currentSelection {
            columnSelectionHistory[currentSelection.appGroupIndex] = currentSelection.windowIndex
        }
@@ -646,6 +705,250 @@
        needsLayout = true
        ensureCurrentSelectionVisible(horizontalIntent: .alignWithAppShelf(animated: false))
        needsLayout = true
        if animatedProjection, let transitionSnapshot {
            layoutSubtreeIfNeeded()
            applyProjectionTransitionAnimations(from: transitionSnapshot)
        }
    }
    private func validSelection(
        _ selection: QuickSwitchSelection?,
        in viewModel: QuickSwitchViewModel?
    ) -> QuickSwitchSelection? {
        guard let selection, let viewModel else { return nil }
        for column in viewModel.waterfallColumns {
            if let card = column.windows.first(where: { $0.window.id == selection.windowID }) {
                return QuickSwitchSelection(
                    appGroupIndex: card.appGroupIndex,
                    windowIndex: card.windowIndex,
                    windowID: card.window.id
                )
            }
        }
        return nil
    }
    private func captureProjectionTransitionSnapshot() -> ProjectionTransitionSnapshot {
        ProjectionTransitionSnapshot(
            appShelfItemsByAppGroupIndex: Dictionary(
                uniqueKeysWithValues: appShelfItems.map { item in
                    (
                        item.item.appGroupIndex,
                        ProjectionLayerSnapshot(
                            layer: item.containerLayer,
                            frame: item.containerLayer.frame
                        )
                    )
                }
            ),
            waterfallColumnsByAppGroupIndex: Dictionary(
                uniqueKeysWithValues: waterfallColumns.map { column in
                    (
                        column.column.appGroupIndex,
                        ProjectionColumnSnapshot(
                            layer: column.containerLayer,
                            frame: column.containerLayer.frame,
                            cardsByWindowID: Dictionary(
                                uniqueKeysWithValues: column.cards.map { card in
                                    (
                                        card.item.window.id,
                                        ProjectionLayerSnapshot(
                                            layer: card.containerLayer,
                                            frame: card.containerLayer.frame
                                        )
                                    )
                                }
                            )
                        )
                    )
                }
            )
        )
    }
    private func applyProjectionTransitionAnimations(from snapshot: ProjectionTransitionSnapshot) {
        let reduceMotion = NSWorkspace.shared.accessibilityDisplayShouldReduceMotion
        let duration: CFTimeInterval = reduceMotion ? 0.12 : 0.21
        let timing = CAMediaTimingFunction(name: reduceMotion ? .linear : .easeOut)
        var movedLayerCount = 0
        var fadeInLayerCount = 0
        var fadeOutLayerCount = 0
        for item in appShelfItems {
            guard let oldSnapshot = snapshot.appShelfItemsByAppGroupIndex[item.item.appGroupIndex] else {
                if addFadeInAnimation(to: item.containerLayer, duration: duration, timing: timing) {
                    fadeInLayerCount += 1
                }
                continue
            }
            if addPositionAnimation(to: item.containerLayer, from: oldSnapshot.frame, duration: duration, timing: timing) {
                movedLayerCount += 1
            }
        }
        let visibleAppGroupIndexes = Set(appShelfItems.map(\.item.appGroupIndex))
        for (appGroupIndex, oldSnapshot) in snapshot.appShelfItemsByAppGroupIndex
            where !visibleAppGroupIndexes.contains(appGroupIndex) {
            if addFadeOutSnapshot(
                oldSnapshot,
                to: appShelfContentLayer,
                frame: oldSnapshot.frame,
                duration: duration,
                timing: timing
            ) {
                fadeOutLayerCount += 1
            }
        }
        for column in waterfallColumns {
            guard let oldSnapshot = snapshot.waterfallColumnsByAppGroupIndex[column.column.appGroupIndex] else {
                if addFadeInAnimation(to: column.containerLayer, duration: duration, timing: timing) {
                    fadeInLayerCount += 1
                }
                continue
            }
            if addPositionAnimation(to: column.containerLayer, from: oldSnapshot.frame, duration: duration, timing: timing) {
                movedLayerCount += 1
            }
            for card in column.cards {
                guard let oldCardSnapshot = oldSnapshot.cardsByWindowID[card.item.window.id] else {
                    if addFadeInAnimation(to: card.containerLayer, duration: duration, timing: timing) {
                        fadeInLayerCount += 1
                    }
                    continue
                }
                if addPositionAnimation(to: card.containerLayer, from: oldCardSnapshot.frame, duration: duration, timing: timing) {
                    movedLayerCount += 1
                }
            }
        }
        let visibleColumnsByAppGroupIndex = Dictionary(
            uniqueKeysWithValues: waterfallColumns.map { ($0.column.appGroupIndex, $0) }
        )
        for (appGroupIndex, oldSnapshot) in snapshot.waterfallColumnsByAppGroupIndex {
            guard let visibleColumn = visibleColumnsByAppGroupIndex[appGroupIndex] else {
                if addFadeOutSnapshot(
                    ProjectionLayerSnapshot(layer: oldSnapshot.layer, frame: oldSnapshot.frame),
                    to: waterfallContentLayer,
                    frame: oldSnapshot.frame,
                    duration: duration,
                    timing: timing
                ) {
                    fadeOutLayerCount += 1
                }
                continue
            }
            let visibleWindowIDs = Set(visibleColumn.cards.map(\.item.window.id))
            for (windowID, oldCardSnapshot) in oldSnapshot.cardsByWindowID
                where !visibleWindowIDs.contains(windowID) {
                let absoluteFrame = oldCardSnapshot.frame.offsetBy(
                    dx: oldSnapshot.frame.minX,
                    dy: oldSnapshot.frame.minY
                )
                if addFadeOutSnapshot(
                    oldCardSnapshot,
                    to: waterfallContentLayer,
                    frame: absoluteFrame,
                    duration: duration,
                    timing: timing
                ) {
                    fadeOutLayerCount += 1
                }
            }
        }
        lastProjectionTransitionMovedLayerCount = movedLayerCount
        lastProjectionTransitionFadeInLayerCount = fadeInLayerCount
        lastProjectionTransitionFadeOutLayerCount = fadeOutLayerCount
        lastProjectionTransitionDurationMilliseconds = duration * 1_000
    }
    private func resetProjectionTransitionReport() {
        lastProjectionTransitionMovedLayerCount = 0
        lastProjectionTransitionFadeInLayerCount = 0
        lastProjectionTransitionFadeOutLayerCount = 0
        lastProjectionTransitionDurationMilliseconds = 0
    }
    @discardableResult
    private func addPositionAnimation(
        to layer: CALayer,
        from oldFrame: CGRect,
        duration: CFTimeInterval,
        timing: CAMediaTimingFunction
    ) -> Bool {
        let oldPosition = CGPoint(x: oldFrame.midX, y: oldFrame.midY)
        let newPosition = layer.position
        guard abs(oldPosition.x - newPosition.x) > 0.5 || abs(oldPosition.y - newPosition.y) > 0.5 else {
            return false
        }
        let animation = CABasicAnimation(keyPath: "position")
        animation.fromValue = oldPosition
        animation.toValue = newPosition
        animation.duration = duration
        animation.timingFunction = timing
        animation.isRemovedOnCompletion = true
        layer.add(animation, forKey: "quickSwitchProjectionPosition")
        return true
    }
    @discardableResult
    private func addFadeInAnimation(
        to layer: CALayer,
        duration: CFTimeInterval,
        timing: CAMediaTimingFunction
    ) -> Bool {
        let animation = CABasicAnimation(keyPath: "opacity")
        animation.fromValue = 0
        animation.toValue = layer.opacity
        animation.duration = duration
        animation.timingFunction = timing
        animation.isRemovedOnCompletion = true
        layer.add(animation, forKey: "quickSwitchProjectionFadeIn")
        return true
    }
    @discardableResult
    private func addFadeOutSnapshot(
        _ snapshot: ProjectionLayerSnapshot,
        to parentLayer: CALayer,
        frame: CGRect,
        duration: CFTimeInterval,
        timing: CAMediaTimingFunction
    ) -> Bool {
        let layer = snapshot.layer
        layer.removeAllAnimations()
        layer.removeFromSuperlayer()
        CATransaction.begin()
        CATransaction.setDisableActions(true)
        layer.frame = frame
        layer.opacity = max(layer.opacity, 0.01)
        layer.zPosition = max(layer.zPosition, 240)
        parentLayer.addSublayer(layer)
        CATransaction.commit()
        let animation = CABasicAnimation(keyPath: "opacity")
        animation.fromValue = layer.opacity
        animation.toValue = 0
        animation.duration = duration
        animation.timingFunction = timing
        animation.isRemovedOnCompletion = true
        CATransaction.begin()
        CATransaction.setDisableActions(true)
        layer.opacity = 0
        CATransaction.commit()
        layer.add(animation, forKey: "quickSwitchProjectionFadeOut")
        DispatchQueue.main.asyncAfter(deadline: .now() + duration + 0.05) { [weak layer] in
            layer?.removeFromSuperlayer()
        }
        return true
    }
    func reportDictionary() -> [String: Any] {
@@ -685,6 +988,8 @@
            "spaceLaneHoveredSpaceID": hoveredSpaceLaneID ?? NSNull(),
            "spaceLaneFocusedSpaceID": focusedSpaceID ?? NSNull(),
            "activeSpaceFocusID": activeSpaceFocusID ?? NSNull(),
            "spaceFilterLockedSpaceID": currentViewModel?.lockedSpaceID ?? NSNull(),
            "spaceFilterActive": currentViewModel?.lockedSpaceID != nil,
            "spaceFocusAppGroupIndexes": spaceFocusedAppGroupIndexes(),
            "appHoverAssociatedSpaceIDs": appHoverAssociatedSpaceIDs.sorted(),
            "appHoverAssociatedSpaceLabels": appHoverAssociatedSpaceLabels,
@@ -732,6 +1037,10 @@
            "waterfallAlignmentTolerance": Double(WaterfallAlignmentMetrics.tolerance),
            "waterfallAlignmentAnimationDurationMilliseconds": Double(WaterfallAlignmentMetrics.animationDuration * 1_000),
            "waterfallAlignmentAnimationTimingName": "easeOut",
            "projectionTransitionMovedLayerCount": lastProjectionTransitionMovedLayerCount,
            "projectionTransitionFadeInLayerCount": lastProjectionTransitionFadeInLayerCount,
            "projectionTransitionFadeOutLayerCount": lastProjectionTransitionFadeOutLayerCount,
            "projectionTransitionDurationMilliseconds": lastProjectionTransitionDurationMilliseconds,
            "reduceMotionEffective": NSWorkspace.shared.accessibilityDisplayShouldReduceMotion,
            "selectedAppGroupIndex": effectiveSelection?.appGroupIndex ?? NSNull(),
            "selectedWindowIndex": effectiveSelection?.windowIndex ?? NSNull(),
@@ -1021,16 +1330,8 @@
            "label": segment.space.label
        ])
        lastSpaceLaneClickSpaceID = segment.space.id
        guard let firstCard = firstWaterfallCard(inSpaceID: segment.space.id) else {
            DevelopmentDiagnostics.log("quickSwitch.view.clickSpaceLane.emptySpaceUnsupported", [
                "spaceID": segment.space.id,
                "label": segment.space.label
            ])
            return
        }
        commitWindowCard(firstCard, trigger: "spaceLane")
        onSpaceLaneClick?(segment.space.id)
        lastSpaceLaneClickSpaceID = segment.space.id
    }
    private func focusSpace(_ spaceID: UInt64, persistent: Bool, source: HoverTargetSource = .event) {
@@ -2118,6 +2419,7 @@
            segment.space.id == activeSpaceFocusID
                || appHoverAssociatedSpaceIDs.contains(segment.space.id)
                || segment.space.id == windowHoverAssociatedSpaceID
                || segment.space.id == currentViewModel?.lockedSpaceID
        }
        let hasCurrentSegment = group.display.spaces.contains { $0.isCurrent }
        group.containerLayer.backgroundColor = NSColor.windowBackgroundColor.cgColor
@@ -2133,11 +2435,12 @@
    private func applySpaceLaneVisualState(_ segment: SpaceLaneSegmentLayers) {
        let isHovered = segment.space.id == activeSpaceFocusID
        let isFocused = segment.space.id == activeSpaceFocusID
        let isLocked = segment.space.id == currentViewModel?.lockedSpaceID
        let isCurrent = segment.space.isCurrent
        let hasWindows = segment.space.windowCount > 0
        let isAppAssociated = appHoverAssociatedSpaceIDs.contains(segment.space.id)
        let isWindowAssociated = segment.space.id == windowHoverAssociatedSpaceID
        let isActive = isHovered || isFocused
        let isActive = isHovered || isFocused || isLocked
        let isAssociated = (isAppAssociated || isWindowAssociated) && !isActive
        segment.containerLayer.borderWidth = (hasWindows || isCurrent || isActive || isAssociated) ? 1.5 : 1
@@ -2169,15 +2472,15 @@
                colors: spaceLaneIdleOccupiedGradientColors()
            )
        }
        segment.containerLayer.shadowOpacity = hasWindows
        segment.containerLayer.shadowOpacity = (hasWindows || isLocked)
            ? (isActive ? 0.26 : isAssociated ? 0.18 : isCurrent ? 0.14 : 0)
            : 0
        segment.containerLayer.shadowRadius = hasWindows
        segment.containerLayer.shadowRadius = (hasWindows || isLocked)
            ? (isActive ? 24 : isAssociated ? 18 : isCurrent ? 12 : 0)
            : 0
        segment.containerLayer.shadowOffset = CGSize(
            width: 0,
            height: hasWindows ? (isActive ? -6 : isAssociated ? -4 : isCurrent ? -2.5 : 0) : 0
            height: (hasWindows || isLocked) ? (isActive ? -6 : isAssociated ? -4 : isCurrent ? -2.5 : 0) : 0
        )
        segment.containerLayer.zPosition = isActive ? 30 : isAssociated ? 18 : isCurrent ? 10 : 0
        segment.containerLayer.transform = (isActive || isAssociated)
@@ -4164,6 +4467,9 @@
        if space.id == activeSpaceFocusID {
            states.append("focused")
        }
        if space.id == currentViewModel?.lockedSpaceID {
            states.append("locked")
        }
        if space.type == .fullscreen {
            states.append("fullscreen")
        }