Ariver
2026-06-12 0754f2706a79128e87b65867e1d33096d53687fa
C1.source/Sources/Aligner/QuickSwitchRootView.swift
@@ -24,6 +24,9 @@
    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 appIconProvider: any AppIconProviderProtocol = WorkspaceAppIconProvider()
    private let skeletonThumbnailProvider = NativeSkeletonThumbnailProvider(size: NSSize(width: 160, height: 96))
    private var screenshotSourcesByWindowID: [UInt32: ScreenshotResolutionSource] = [:]
@@ -49,6 +52,7 @@
    private var closeConfirmationRequired = true
    private var closeFeedback: CloseFeedback?
    private var closeFeedbackHideTask: Task<Void, Never>?
    private var isSettingsButtonHovered = false
    private var hoverTarget: HoverTarget = .none
    private var hoverTargetSource: HoverTargetSource = .event
    private var hoveredSpaceLaneID: UInt64?
@@ -91,6 +95,7 @@
    var onCommitSelection: ((QuickSwitchSelection, QuickSwitchCommitSource) -> Void)?
    var onSpaceLaneClick: ((UInt64, Int) -> Void)?
    var onBackgroundClick: (() -> Bool)?
    var onOpenSettings: (() -> Void)?
    var onRequestClose: ((QuickSwitchCloseRequest) -> Void)?
    var onCloseConfirmationDisabled: (() -> Void)?
@@ -322,13 +327,11 @@
    private enum HorizontalMasonryMetrics {
        static let horizontalPadding: CGFloat = 12
        static let verticalPadding: CGFloat = 0
        static let cardGap: CGFloat = 16
        static let horizontalGap: CGFloat = 16
        static let verticalGap: CGFloat = WaterfallMetrics.cardGap
        static let cardMinWidth: CGFloat = 220
        static let cardMaxWidth: CGFloat = 320
        static let titleBarHeight: CGFloat = 26
        static let titleGap: CGFloat = 6
        static let minThumbnailHeight: CGFloat = 92
        static let maxThumbnailHeightFraction: CGFloat = 0.40
        static let cardHeight: CGFloat = WaterfallMetrics.cardHeight
    }
    private enum WaterfallHorizontalIntent {
@@ -481,6 +484,17 @@
    override func mouseMoved(with event: NSEvent) {
        guard debugHoveredAppGroupIndex == nil else { return }
        let settingsHovered = settingsButtonContains(windowLocation: event.locationInWindow)
        if isSettingsButtonHovered != settingsHovered {
            isSettingsButtonHovered = settingsHovered
            needsLayout = true
        }
        if settingsHovered {
            hoveredCloseTarget = nil
            setHoverTarget(.none)
            return
        }
        let closeTarget = closeButtonTarget(at: event.locationInWindow)
        if hoveredCloseTarget != closeTarget {
            hoveredCloseTarget = closeTarget
@@ -491,6 +505,7 @@
    override func mouseExited(with event: NSEvent) {
        guard debugHoveredAppGroupIndex == nil else { return }
        isSettingsButtonHovered = false
        hoveredCloseTarget = nil
        setHoverTarget(.none)
    }
@@ -506,6 +521,11 @@
        }
        if handleCloseConfirmationMouseDown(at: event.locationInWindow) {
            return
        }
        if settingsButtonContains(windowLocation: event.locationInWindow) {
            clickSettingsButton(source: "mouse")
            return
        }
@@ -1060,6 +1080,9 @@
            "activeSpaceFocusID": activeSpaceFocusID ?? NSNull(),
            "spaceFilterLockedSpaceID": currentViewModel?.lockedSpaceID ?? NSNull(),
            "spaceFilterActive": currentViewModel?.lockedSpaceID != nil,
            "settingsButtonVisible": !settingsButtonLayer.isHidden,
            "settingsButtonHovered": isSettingsButtonHovered,
            "settingsButtonFrame": dictionary(from: settingsButtonLayer.frame),
            "spaceFocusAppGroupIndexes": spaceFocusedAppGroupIndexes(),
            "appHoverAssociatedSpaceIDs": appHoverAssociatedSpaceIDs.sorted(),
            "appHoverAssociatedSpaceLabels": appHoverAssociatedSpaceLabels,
@@ -1302,6 +1325,9 @@
        case "click-background":
            recordMouseCommand("click-background")
            _ = onBackgroundClick?()
        case "click-settings":
            recordMouseCommand("click-settings")
            clickSettingsButton(source: "debug")
        case "hover-card" where parts.count == 3:
            guard let appGroupIndex = Int(parts[1]), let windowIndex = Int(parts[2]) else {
                recordMouseCommand("invalid:\(command)")
@@ -2244,6 +2270,26 @@
        closeFeedbackTextLayer.truncationMode = .end
        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)
    }
    private func rebuildAppShelfItems() {
@@ -2581,8 +2627,40 @@
        layoutWaterfallColumns()
        layoutCloseConfirmation()
        layoutCloseFeedback()
        layoutSettingsButton()
        CATransaction.commit()
    }
    private func layoutSettingsButton() {
        let size: CGFloat = 34
        let topMargin = topSafeAreaInset + RootLayoutMetrics.topContentMargin
        let x = max(
            RootLayoutMetrics.horizontalMargin,
            glassLayer.bounds.width - RootLayoutMetrics.horizontalMargin - size
        )
        let y = max(
            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)
            .cgColor
        settingsButtonLayer.borderColor = NSColor.separatorColor
            .withAlphaComponent(isSettingsButtonHovered ? 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
            : NSColor.secondaryLabelColor
        ).cgColor
    }
    private var topSafeAreaInset: CGFloat {
@@ -2896,6 +2974,26 @@
    private func spaceLaneSegment(spaceID: UInt64) -> SpaceLaneSegmentLayers? {
        spaceLaneSegments.first { $0.space.id == spaceID }
    }
    private func settingsButtonContains(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)
    }
    private func clickSettingsButton(source: String) {
        DevelopmentDiagnostics.log("quickSwitch.view.clickSettings", [
            "source": source
        ])
        lastMouseCommand = "click-settings"
        if !mouseCommandsApplied.contains("click-settings") {
            mouseCommandsApplied.append("click-settings")
        }
        onOpenSettings?()
    }
    private func appShelfItem(at windowLocation: CGPoint) -> AppShelfItemLayers? {
@@ -3406,14 +3504,14 @@
            1,
            Int(
                floor(
                    (availableWidth + HorizontalMasonryMetrics.cardGap)
                        / (HorizontalMasonryMetrics.cardMinWidth + HorizontalMasonryMetrics.cardGap)
                    (availableWidth + HorizontalMasonryMetrics.horizontalGap)
                        / (HorizontalMasonryMetrics.cardMinWidth + HorizontalMasonryMetrics.horizontalGap)
                )
            )
        )
        let laneCount = min(maximumLaneCount, max(1, cards.count))
        let fittingWidth = (
            availableWidth - CGFloat(max(0, laneCount - 1)) * HorizontalMasonryMetrics.cardGap
            availableWidth - CGFloat(max(0, laneCount - 1)) * HorizontalMasonryMetrics.horizontalGap
        ) / CGFloat(laneCount)
        let cardWidth = floor(
            min(
@@ -3422,7 +3520,7 @@
            )
        )
        let usedWidth = CGFloat(laneCount) * cardWidth
            + CGFloat(max(0, laneCount - 1)) * HorizontalMasonryMetrics.cardGap
            + CGFloat(max(0, laneCount - 1)) * HorizontalMasonryMetrics.horizontalGap
        let startX = max(
            HorizontalMasonryMetrics.horizontalPadding,
            (viewportWidth - usedWidth) / 2
@@ -3438,12 +3536,8 @@
                }
                return lhs.offset < rhs.offset
            }?.offset ?? 0
            let cardHeight = horizontalMasonryCardHeight(
                for: card,
                width: cardWidth,
                viewportHeight: waterfallLayer.bounds.height
            )
            let x = startX + CGFloat(laneIndex) * (cardWidth + HorizontalMasonryMetrics.cardGap)
            let cardHeight = HorizontalMasonryMetrics.cardHeight
            let x = startX + CGFloat(laneIndex) * (cardWidth + HorizontalMasonryMetrics.horizontalGap)
            let top = HorizontalMasonryMetrics.verticalPadding + laneHeights[laneIndex]
            plannedFrames.append((
                windowID: card.item.window.id,
@@ -3452,11 +3546,11 @@
                width: cardWidth,
                height: cardHeight
            ))
            laneHeights[laneIndex] += cardHeight + HorizontalMasonryMetrics.cardGap
            laneHeights[laneIndex] += cardHeight + HorizontalMasonryMetrics.verticalGap
        }
        let tallestLane = laneHeights.max().map {
            max(0, $0 - HorizontalMasonryMetrics.cardGap)
            max(0, $0 - HorizontalMasonryMetrics.verticalGap)
        } ?? 0
        let contentHeight = max(
            waterfallLayer.bounds.height,
@@ -3477,43 +3571,6 @@
        )
        return HorizontalMasonryLayout(contentHeight: contentHeight, cardFramesByWindowID: frames)
    }
    private func horizontalMasonryCardHeight(
        for card: WaterfallCardLayers,
        width: CGFloat,
        viewportHeight: CGFloat
    ) -> CGFloat {
        let thumbnailHeight = min(
            max(
                HorizontalMasonryMetrics.minThumbnailHeight,
                width / horizontalMasonryThumbnailAspectRatio(for: card)
            ),
            max(1, viewportHeight * HorizontalMasonryMetrics.maxThumbnailHeightFraction)
        )
        return HorizontalMasonryMetrics.titleBarHeight
            + HorizontalMasonryMetrics.titleGap
            + thumbnailHeight
    }
    private func horizontalMasonryThumbnailAspectRatio(for card: WaterfallCardLayers) -> CGFloat {
        if card.item.window.isFullscreen {
            return 16.0 / 9.0
        }
        if card.item.window.isMinimized {
            return 4.0 / 3.0
        }
        switch Int(card.item.window.id % 4) {
        case 0:
            return 16.0 / 9.0
        case 1:
            return 3.0 / 2.0
        case 2:
            return 4.0 / 3.0
        default:
            return 5.0 / 4.0
        }
    }
    private func horizontalMasonryContentFrame() -> CGRect {
@@ -4381,6 +4438,21 @@
        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(
        string: String,
        fontSize: CGFloat,