Ariver
2026-06-14 4d4294f8c375a06ad87e12a2c73a98a19eb7ed7e
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? {
@@ -4448,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)
@@ -4565,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(