C1.source/Sources/Aligner/QuickSwitchRootView.swift
@@ -1,5 +1,6 @@
import AppKit
import QuartzCore
import Darwin
import AlignerCore
enum QuickSwitchCommitSource: String {
@@ -20,6 +21,7 @@
    private let appShelfContentLayer = CALayer()
    private let waterfallLayer = CALayer()
    private let waterfallContentLayer = CALayer()
    private lazy var closeConfirmationLayers = makeCloseConfirmationLayers()
    private let appIconProvider: any AppIconProviderProtocol = WorkspaceAppIconProvider()
    private let skeletonThumbnailProvider = NativeSkeletonThumbnailProvider(size: NSSize(width: 160, height: 96))
    private var screenshotSourcesByWindowID: [UInt32: ScreenshotResolutionSource] = [:]
@@ -36,6 +38,11 @@
    private var hoveredAppGroupIndex: Int?
    private var hoveredWindowID: UInt32?
    private var hoveredSpaceID: UInt64?
    private var hoveredCloseTarget: CloseTarget?
    private var pendingCloseTarget: CloseTarget?
    private var pendingCloseOptOut = false
    private var closeConfirmationArrowEdge: CloseConfirmationArrowEdge?
    private var closeConfirmationRequired = true
    private var hoveredSpaceLaneID: UInt64?
    private var focusedSpaceID: UInt64?
    private var debugHoveredAppGroupIndex: Int?
@@ -65,6 +72,46 @@
    private var hoverResponseLatencyMilliseconds: [Double] = []
    var onEscape: (() -> Void)?
    var onCommitSelection: ((QuickSwitchSelection, QuickSwitchCommitSource) -> Void)?
    var onRequestClose: ((QuickSwitchCloseRequest) -> Void)?
    var onCloseConfirmationDisabled: (() -> Void)?
    private enum CloseTarget: Equatable {
        case app(appGroupIndex: Int)
        case window(appGroupIndex: Int, windowIndex: Int, windowID: UInt32)
        var kindDescription: String {
            switch self {
            case .app:
                return "app"
            case .window:
                return "window"
            }
        }
    }
    private struct CloseButtonLayers {
        let containerLayer: CALayer
        let glyphLayer: CAShapeLayer
    }
    private struct CloseConfirmationLayers {
        let containerLayer: CALayer
        let backgroundLayer: CAShapeLayer
        let titleLayer: CATextLayer
        let messageLayer: CATextLayer
        let checkboxLayer: CALayer
        let checkboxMarkLayer: CATextLayer
        let checkboxLabelLayer: CATextLayer
        let confirmButtonLayer: CALayer
        let confirmButtonTextLayer: CATextLayer
        let cancelButtonLayer: CALayer
        let cancelButtonTextLayer: CATextLayer
    }
    private enum CloseConfirmationArrowEdge: String {
        case left
        case right
    }
    private struct SpaceLaneSegmentLayers {
        let space: QuickSwitchSpaceViewModel
@@ -84,6 +131,7 @@
        let labelLayer: CATextLayer
        let badgeLayer: CATextLayer
        let selectedIndicatorLayer: CALayer
        let closeButton: CloseButtonLayers
    }
    private struct AppShelfLayout {
@@ -103,12 +151,14 @@
    private struct WaterfallCardLayers {
        let item: QuickSwitchWindowCardViewModel
        let containerLayer: CALayer
        let titleBarLayer: CALayer
        let titleLayer: CATextLayer
        let metaLayer: CATextLayer
        let thumbnailLayer: CALayer
        let appIconLayer: CALayer
        let stateLayer: CATextLayer
        let shineLayer: CAGradientLayer
        let closeButton: CloseButtonLayers
    }
    private enum AppShelfMetrics {
@@ -136,7 +186,7 @@
        static let columnMaxWidth: CGFloat = 292
        static let columnPadding: CGFloat = 12
        static let headerHeight: CGFloat = 34
        static let cardHeight: CGFloat = 118
        static let cardHeight: CGFloat = 138
        static let cardGap: CGFloat = 12
        static let revealPadding: CGFloat = 24
    }
@@ -263,6 +313,12 @@
    override func mouseMoved(with event: NSEvent) {
        guard debugHoveredAppGroupIndex == nil else { return }
        let closeTarget = closeButtonTarget(at: event.locationInWindow)
        if hoveredCloseTarget != closeTarget {
            hoveredCloseTarget = closeTarget
            needsLayout = true
        }
        if let segment = spaceLaneSegment(at: event.locationInWindow) {
            hoverSpaceLaneSegment(segment)
            return
@@ -290,16 +346,27 @@
        }
        hoveredSpaceLaneID = nil
        hoveredCloseTarget = nil
        updateHoverState(appGroupIndex: nil, windowID: nil, spaceID: nil)
    }
    override func mouseExited(with event: NSEvent) {
        guard debugHoveredAppGroupIndex == nil else { return }
        hoveredSpaceLaneID = nil
        hoveredCloseTarget = nil
        updateHoverState(appGroupIndex: nil, windowID: nil, spaceID: nil)
    }
    override func mouseDown(with event: NSEvent) {
        if handleCloseConfirmationMouseDown(at: event.locationInWindow) {
            return
        }
        if let closeTarget = closeButtonTarget(at: event.locationInWindow) {
            beginCloseConfirmation(for: closeTarget)
            return
        }
        if let card = waterfallCard(at: event.locationInWindow) {
            clickWindowCard(card)
            return
@@ -359,6 +426,9 @@
            if isHoverCommand(command) {
                hoverResponseLatencyMilliseconds.append(elapsed)
            }
            if lastCommitSource == .mouse {
                break
            }
        }
    }
@@ -381,6 +451,17 @@
        keyboardResponseLatencyMilliseconds = []
        mouseResponseLatencyMilliseconds = []
        hoverResponseLatencyMilliseconds = []
    }
    func setCloseConfirmationRequired(_ required: Bool) {
        closeConfirmationRequired = required
        if !required {
            pendingCloseTarget = nil
            pendingCloseOptOut = false
            closeConfirmationLayers.containerLayer.isHidden = true
            closeConfirmationArrowEdge = nil
        }
        needsLayout = true
    }
    func applyScreenshot(_ resolution: ScreenshotResolution, for windowID: UInt32) {
@@ -428,6 +509,11 @@
        lastAppShelfScrollSyncedAppGroupIndex = nil
        hoveredWindowID = nil
        hoveredSpaceID = nil
        hoveredCloseTarget = nil
        pendingCloseTarget = nil
        pendingCloseOptOut = false
        closeConfirmationLayers.containerLayer.isHidden = true
        closeConfirmationArrowEdge = nil
        hoveredSpaceLaneID = nil
        focusedSpaceID = nil
        if debugHoveredAppGroupIndex == nil {
@@ -534,6 +620,13 @@
            "hoveredAppGroupIndex": effectiveHoveredAppGroupIndex ?? NSNull(),
            "hoveredWindowID": hoveredWindowID ?? NSNull(),
            "hoveredSpaceID": hoveredSpaceID ?? NSNull(),
            "hoveredCloseTargetKind": hoveredCloseTarget?.kindDescription ?? NSNull(),
            "pendingCloseTargetKind": pendingCloseTarget?.kindDescription ?? NSNull(),
            "pendingCloseOptOut": pendingCloseOptOut,
            "closeConfirmationRequired": closeConfirmationRequired,
            "closeConfirmationVisible": !closeConfirmationLayers.containerLayer.isHidden,
            "closeConfirmationFrame": dictionary(from: closeConfirmationLayers.containerLayer.frame),
            "closeConfirmationArrowEdge": closeConfirmationArrowEdge?.rawValue ?? NSNull(),
            "mouseCommandsApplied": mouseCommandsApplied,
            "lastMouseCommand": lastMouseCommand ?? NSNull(),
            "lastClickedAppGroupIndex": lastClickedAppGroupIndex ?? NSNull(),
@@ -635,6 +728,20 @@
            }
            recordMouseCommand("click-app:\(appGroupIndex)")
            clickAppShelfItem(appGroupIndex: appGroupIndex)
        case "hover-app-close" where parts.count == 2:
            guard let appGroupIndex = Int(parts[1]) else {
                recordMouseCommand("invalid:\(command)")
                return
            }
            recordMouseCommand("hover-app-close:\(appGroupIndex)")
            hoverAppCloseButton(appGroupIndex)
        case "click-app-close" where parts.count == 2:
            guard let appGroupIndex = Int(parts[1]) else {
                recordMouseCommand("invalid:\(command)")
                return
            }
            recordMouseCommand("click-app-close:\(appGroupIndex)")
            clickAppCloseButton(appGroupIndex)
        case "scroll-app-shelf" where parts.count == 2:
            guard let targetOffset = Double(parts[1]) else {
                recordMouseCommand("invalid:\(command)")
@@ -678,6 +785,29 @@
            if let card = waterfallCard(appGroupIndex: appGroupIndex, windowIndex: windowIndex) {
                clickWindowCard(card)
            }
        case "hover-card-close" where parts.count == 3:
            guard let appGroupIndex = Int(parts[1]), let windowIndex = Int(parts[2]) else {
                recordMouseCommand("invalid:\(command)")
                return
            }
            recordMouseCommand("hover-card-close:\(appGroupIndex):\(windowIndex)")
            hoverCardCloseButton(appGroupIndex: appGroupIndex, windowIndex: windowIndex)
        case "click-card-close" where parts.count == 3:
            guard let appGroupIndex = Int(parts[1]), let windowIndex = Int(parts[2]) else {
                recordMouseCommand("invalid:\(command)")
                return
            }
            recordMouseCommand("click-card-close:\(appGroupIndex):\(windowIndex)")
            clickCardCloseButton(appGroupIndex: appGroupIndex, windowIndex: windowIndex)
        case "toggle-close-optout":
            recordMouseCommand("toggle-close-optout")
            toggleCloseConfirmationOptOut()
        case "confirm-close":
            recordMouseCommand("confirm-close")
            confirmPendingClose()
        case "cancel-close":
            recordMouseCommand("cancel-close")
            cancelPendingClose()
        default:
            recordMouseCommand("unknown:\(command)")
        }
@@ -697,6 +827,7 @@
    private func hoverAppGroup(_ appGroupIndex: Int) {
        hoveredSpaceLaneID = nil
        hoveredCloseTarget = nil
        updateHoverState(
            appGroupIndex: appGroupIndex,
            windowID: nil,
@@ -705,12 +836,47 @@
        )
    }
    private func hoverAppCloseButton(_ appGroupIndex: Int) {
        hoverAppGroup(appGroupIndex)
        hoveredCloseTarget = .app(appGroupIndex: appGroupIndex)
        needsLayout = true
    }
    private func clickAppCloseButton(_ appGroupIndex: Int) {
        hoverAppCloseButton(appGroupIndex)
        beginCloseConfirmation(for: .app(appGroupIndex: appGroupIndex))
    }
    private func hoverWindowCard(_ card: WaterfallCardLayers) {
        hoveredSpaceLaneID = nil
        hoveredCloseTarget = nil
        updateHoverState(
            appGroupIndex: card.item.appGroupIndex,
            windowID: card.item.window.id,
            spaceID: card.item.primarySpaceID
        )
    }
    private func hoverCardCloseButton(appGroupIndex: Int, windowIndex: Int) {
        guard let card = waterfallCard(appGroupIndex: appGroupIndex, windowIndex: windowIndex) else { return }
        hoverWindowCard(card)
        hoveredCloseTarget = .window(
            appGroupIndex: appGroupIndex,
            windowIndex: windowIndex,
            windowID: card.item.window.id
        )
        needsLayout = true
    }
    private func clickCardCloseButton(appGroupIndex: Int, windowIndex: Int) {
        guard let card = waterfallCard(appGroupIndex: appGroupIndex, windowIndex: windowIndex) else { return }
        hoverCardCloseButton(appGroupIndex: appGroupIndex, windowIndex: windowIndex)
        beginCloseConfirmation(
            for: .window(
                appGroupIndex: appGroupIndex,
                windowIndex: windowIndex,
                windowID: card.item.window.id
            )
        )
    }
@@ -723,7 +889,17 @@
            "spaceID": segment.space.id,
            "label": segment.space.label
        ])
        focusSpace(segment.space.id, persistent: true)
        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")
    }
    private func focusSpace(_ spaceID: UInt64, persistent: Bool) {
@@ -774,12 +950,14 @@
            columnSelectionHistory[nextSelection.appGroupIndex] = nextSelection.windowIndex
            lastAppShelfClickChangedSelection = previousSelection != nextSelection
            ensureWaterfallSelectionVisible(nextSelection, horizontalIntent: .alignWithAppShelf(animated: true))
            updateHoverState(appGroupIndex: appGroupIndex, windowID: nil, spaceID: nil)
            commitWindowCard(firstCard, trigger: "appShelf")
        } else {
            lastAppShelfClickChangedSelection = false
            ensureWaterfallColumnVisible(column)
            updateHoverState(appGroupIndex: appGroupIndex, windowID: nil, spaceID: nil)
            needsLayout = true
        }
        updateHoverState(appGroupIndex: appGroupIndex, windowID: nil, spaceID: nil)
        needsLayout = true
    }
    private func scrollAppShelf(to targetOffset: CGFloat) {
@@ -821,12 +999,17 @@
    private func clickWindowCard(_ card: WaterfallCardLayers) {
        hoverWindowCard(card)
        commitWindowCard(card, trigger: "windowCard")
    }
    private func commitWindowCard(_ card: WaterfallCardLayers, trigger: String) {
        let selection = QuickSwitchSelection(
            appGroupIndex: card.item.appGroupIndex,
            windowIndex: card.item.windowIndex,
            windowID: card.item.window.id
        )
        DevelopmentDiagnostics.log("quickSwitch.view.clickWindowCard", [
        DevelopmentDiagnostics.log("quickSwitch.view.commitMouseWindow", [
            "trigger": trigger,
            "appGroupIndex": selection.appGroupIndex,
            "windowIndex": selection.windowIndex,
            "windowID": selection.windowID,
@@ -1231,7 +1414,7 @@
        glassLayer.addSublayer(laneLayer)
        laneLayer.addSublayer(spaceLaneContentLayer)
        shelfLayer.backgroundColor = NSColor.controlBackgroundColor.withAlphaComponent(0.60).cgColor
        shelfLayer.backgroundColor = NSColor.clear.cgColor
        shelfLayer.cornerRadius = 16
        shelfLayer.masksToBounds = true
        glassLayer.addSublayer(shelfLayer)
@@ -1241,6 +1424,9 @@
        waterfallLayer.masksToBounds = true
        glassLayer.addSublayer(waterfallLayer)
        waterfallLayer.addSublayer(waterfallContentLayer)
        closeConfirmationLayers.containerLayer.isHidden = true
        glassLayer.addSublayer(closeConfirmationLayers.containerLayer)
    }
    private func rebuildAppShelfItems() {
@@ -1293,11 +1479,13 @@
            let selectedIndicator = CALayer()
            selectedIndicator.isHidden = true
            let closeButton = makeCloseButtonLayers()
            container.addSublayer(background)
            container.addSublayer(icon)
            container.addSublayer(label)
            container.addSublayer(badge)
            container.addSublayer(closeButton.containerLayer)
            appShelfContentLayer.addSublayer(container)
            return AppShelfItemLayers(
@@ -1307,7 +1495,8 @@
                iconLayer: icon,
                labelLayer: label,
                badgeLayer: badge,
                selectedIndicatorLayer: selectedIndicator
                selectedIndicatorLayer: selectedIndicator,
                closeButton: closeButton
            )
        }
    }
@@ -1361,6 +1550,9 @@
                thumbnail.contents = skeletonThumbnailProvider
                    .withOverlayTitle(window.window.title, for: window.window.app)
                let titleBar = CALayer()
                titleBar.borderWidth = 0
                let title = makeTextLayer(
                    string: window.window.title.isEmpty ? "Untitled Window" : window.window.title,
                    fontSize: 12,
@@ -1397,24 +1589,29 @@
                shine.startPoint = CGPoint(x: 0, y: 1)
                shine.endPoint = CGPoint(x: 1, y: 0)
                shine.opacity = 0
                let closeButton = makeCloseButtonLayers()
                card.addSublayer(thumbnail)
                card.addSublayer(titleBar)
                card.addSublayer(appIcon)
                card.addSublayer(title)
                card.addSublayer(meta)
                card.addSublayer(state)
                card.addSublayer(shine)
                card.addSublayer(closeButton.containerLayer)
                container.addSublayer(card)
                return WaterfallCardLayers(
                    item: window,
                    containerLayer: card,
                    titleBarLayer: titleBar,
                    titleLayer: title,
                    metaLayer: meta,
                    thumbnailLayer: thumbnail,
                    appIconLayer: appIcon,
                    stateLayer: state,
                    shineLayer: shine
                    shineLayer: shine,
                    closeButton: closeButton
                )
            }
@@ -1463,12 +1660,15 @@
                alignment: .center
            )
            let appLayer = makeTextLayer(
                string: appIdentifierText(for: space.appNames),
                string: appIdentifierText(for: space),
                fontSize: 12,
                weight: .regular,
                color: .secondaryLabelColor
                weight: space.type == .fullscreen ? .medium : .regular,
                color: space.type == .fullscreen ? .labelColor : .secondaryLabelColor,
                alignment: space.type == .fullscreen ? .center : .left
            )
            let windowBlocks = makeWindowBlocks(count: min(space.windowCount, 5), space: space)
            let windowBlocks = space.type == .fullscreen
                ? []
                : makeWindowBlocks(count: min(space.windowCount, 5), space: space)
            let fullscreenMarkerLayer = makeFullscreenMarkerLayer(for: space)
            container.addSublayer(labelLayer)
@@ -1539,6 +1739,7 @@
        layoutSpaceLaneSegments()
        layoutAppShelfItems()
        layoutWaterfallColumns()
        layoutCloseConfirmation()
        CATransaction.commit()
    }
@@ -1664,20 +1865,24 @@
        let isCurrent = segment.space.isCurrent
        let isActive = isHovered || isFocused
        segment.containerLayer.borderWidth = isCurrent ? 1.5 : 1
        segment.containerLayer.borderColor = (isCurrent
            ? NSColor.labelColor.withAlphaComponent(0.34)
            : isActive
                ? NSColor.labelColor.withAlphaComponent(0.24)
        segment.containerLayer.borderWidth = (isCurrent || isActive) ? 1.5 : 1
        segment.containerLayer.borderColor = (isActive
            ? NSColor.controlAccentColor.withAlphaComponent(0.54)
            : isCurrent
                ? NSColor.labelColor.withAlphaComponent(0.34)
                : NSColor.separatorColor.withAlphaComponent(0.34)
        ).cgColor
        segment.containerLayer.backgroundColor = (isActive
            ? NSColor.windowBackgroundColor.withAlphaComponent(0.76)
            ? accentTintedWindowBackground(fraction: 0.10, alpha: 0.94)
            : spaceColor(for: segment.space)
        ).cgColor
        segment.containerLayer.shadowOpacity = isCurrent ? 0.12 : isActive ? 0.08 : 0
        segment.containerLayer.shadowRadius = isCurrent ? 10 : isActive ? 7 : 0
        segment.containerLayer.shadowOffset = CGSize(width: 0, height: isCurrent ? -2 : -1)
        segment.containerLayer.shadowOpacity = isActive ? 0.26 : isCurrent ? 0.14 : 0
        segment.containerLayer.shadowRadius = isActive ? 24 : isCurrent ? 12 : 0
        segment.containerLayer.shadowOffset = CGSize(width: 0, height: isActive ? -6 : isCurrent ? -2.5 : 0)
        segment.containerLayer.zPosition = isActive ? 30 : isCurrent ? 10 : 0
        segment.containerLayer.transform = isActive
            ? CATransform3DMakeScale(1.03, 1.03, 1)
            : CATransform3DIdentity
        segment.containerLayer.shadowPath = CGPath(
            roundedRect: segment.containerLayer.bounds,
            cornerWidth: segment.containerLayer.cornerRadius,
@@ -1724,6 +1929,16 @@
    private func spaceLaneSegment(at windowLocation: CGPoint) -> SpaceLaneSegmentLayers? {
        let locationInView = convert(windowLocation, from: nil)
        // 严格 band 检查:只有鼠标在 lane 的垂直 band 内才考虑是 lane hover。
        // 防止“完全空的地方”(y 不在 lane 高度范围内)误 hit 到 segment frame,
        // 导致 hoveredSpaceLaneID 粘住、activeSpaceFocusID 非 nil、窗口 spaceFocused 高亮。
        let laneMinYInView = glassLayer.frame.minY + laneLayer.frame.minY
        let laneMaxYInView = laneMinYInView + laneLayer.frame.height
        if locationInView.y < laneMinYInView || locationInView.y > laneMaxYInView {
            return nil
        }
        let locationInSpaceLaneContent = CGPoint(
            x: locationInView.x - glassLayer.frame.minX - laneLayer.frame.minX - spaceLaneContentLayer.frame.minX,
            y: locationInView.y - glassLayer.frame.minY - laneLayer.frame.minY - spaceLaneContentLayer.frame.minY
@@ -1781,8 +1996,20 @@
        let bounds = segment.containerLayer.bounds
        segment.labelLayer.frame = CGRect(x: 10, y: bounds.height - 29, width: max(1, bounds.width - 48), height: 20)
        segment.countLayer.frame = CGRect(x: bounds.width - 34, y: bounds.height - 29, width: 24, height: 20)
        segment.appLayer.frame = CGRect(x: 10, y: 9, width: max(1, bounds.width - 20), height: 17)
        segment.fullscreenMarkerLayer?.frame = CGRect(x: 10, y: bounds.height - 7, width: max(1, bounds.width - 20), height: 3)
        if segment.space.type == .fullscreen {
            let markerFrame = fullscreenMarkerFrame(in: bounds)
            segment.appLayer.frame = CGRect(
                x: 8,
                y: markerFrame.midY - 8.5,
                width: max(1, bounds.width - 16),
                height: 17
            )
        } else {
            segment.appLayer.frame = CGRect(x: 10, y: 9, width: max(1, bounds.width - 20), height: 17)
        }
        if let fullscreenMarkerLayer = segment.fullscreenMarkerLayer {
            layoutFullscreenMarkerLayer(fullscreenMarkerLayer, in: bounds)
        }
        let blockGap: CGFloat = 5
        let blockWidth = max(12, min(22, (bounds.width - 20 - CGFloat(max(0, segment.windowBlocks.count - 1)) * blockGap) / CGFloat(max(1, segment.windowBlocks.count))))
@@ -1842,38 +2069,60 @@
    private func layoutAppShelfItemContents(_ item: AppShelfItemLayers, iconSize: CGFloat) {
        let bounds = item.containerLayer.bounds
        let isSelected = item.item.appGroupIndex == effectiveSelection?.appGroupIndex
        let showsSelectedVisual = shouldShowSelectedVisual(isSelected)
        let isHovered = item.item.appGroupIndex == effectiveHoveredAppGroupIndex
        let isSpaceFocused = spaceFocusWindowCount(for: item.item.appGroupIndex) > 0
        let visualIconSize = appShelfVisualIconSize(forBaseIconSize: iconSize, selected: isSelected, hovered: isHovered)
        let backgroundPadding: CGFloat = (isSelected || isHovered) ? 4 : 2
        let visualIconSize = appShelfVisualIconSize(
            forBaseIconSize: iconSize,
            selected: showsSelectedVisual,
            hovered: isHovered,
            spaceFocused: isSpaceFocused
        )
        let backgroundPadding: CGFloat = (showsSelectedVisual || isHovered)
            ? 4
            : isSpaceFocused
                ? 6
                : 2
        let iconX = (bounds.width - visualIconSize) / 2
        let iconY = bounds.height - visualIconSize - 2
        let badgeText = appShelfBadgeText(for: item)
        let badgeWidth = max(18, CGFloat(badgeText.count) * 7 + 8)
        item.containerLayer.zPosition = appShelfZPosition(selected: isSelected, hovered: isHovered)
        item.containerLayer.zPosition = appShelfZPosition(
            selected: showsSelectedVisual,
            hovered: isHovered,
            spaceFocused: isSpaceFocused
        )
        item.backgroundLayer.backgroundColor = appShelfBackgroundColor(
            selected: isSelected,
            selected: showsSelectedVisual,
            hovered: isHovered,
            spaceFocused: isSpaceFocused
        ).cgColor
        item.backgroundLayer.borderColor = (isSelected
            ? NSColor.separatorColor.withAlphaComponent(0.38)
        item.backgroundLayer.borderColor = (showsSelectedVisual
            ? NSColor.controlAccentColor.withAlphaComponent(0.40)
            : isHovered
                ? NSColor.separatorColor.withAlphaComponent(0.28)
                ? NSColor.controlAccentColor.withAlphaComponent(0.36)
                : NSColor.clear
        ).cgColor
        item.backgroundLayer.borderWidth = (isSelected || isHovered) ? 1 : 0
        item.backgroundLayer.borderWidth = (showsSelectedVisual || isHovered) ? 1 : 0
        item.backgroundLayer.shadowColor = NSColor.black.cgColor
        item.backgroundLayer.shadowOpacity = isSelected ? 0.11 : isHovered ? 0.06 : 0
        item.backgroundLayer.shadowRadius = isSelected ? 10 : isHovered ? 7 : 0
        item.backgroundLayer.shadowOffset = CGSize(width: 0, height: -2)
        item.backgroundLayer.shadowOpacity = showsSelectedVisual ? 0.11 : isHovered ? 0.07 : isSpaceFocused ? 0.18 : 0
        item.backgroundLayer.shadowRadius = showsSelectedVisual ? 10 : isHovered ? 8 : isSpaceFocused ? 18 : 0
        item.backgroundLayer.shadowOffset = CGSize(width: 0, height: isSpaceFocused ? -4 : -2)
        item.backgroundLayer.frame = CGRect(
            x: iconX - backgroundPadding,
            y: iconY - backgroundPadding,
            width: visualIconSize + backgroundPadding * 2,
            height: visualIconSize + backgroundPadding * 2
        )
        item.backgroundLayer.shadowPath = (showsSelectedVisual || isHovered || isSpaceFocused)
            ? CGPath(
                roundedRect: item.backgroundLayer.bounds,
                cornerWidth: item.backgroundLayer.cornerRadius,
                cornerHeight: item.backgroundLayer.cornerRadius,
                transform: nil
            )
            : nil
        item.iconLayer.frame = CGRect(x: iconX, y: iconY, width: visualIconSize, height: visualIconSize)
        item.labelLayer.contentsScale = backingScaleFactor
        item.labelLayer.frame = CGRect(x: 0, y: 0, width: bounds.width, height: 14)
@@ -1888,6 +2137,15 @@
            y: iconY - 2,
            width: badgeWidth,
            height: 16
        )
        layoutCloseButton(
            item.closeButton,
            target: .app(appGroupIndex: item.item.appGroupIndex),
            visible: isHovered && canClose(app: item.item.app),
            anchorTopRight: CGPoint(
                x: min(iconX + visualIconSize - 6, bounds.width - 12),
                y: min(iconY + visualIconSize - 6, bounds.height - 12)
            )
        )
        item.selectedIndicatorLayer.frame = .zero
        item.selectedIndicatorLayer.backgroundColor = NSColor.clear.cgColor
@@ -1916,7 +2174,15 @@
    }
    private var activeSpaceFocusID: UInt64? {
        focusedSpaceID ?? hoveredSpaceLaneID
        hoveredSpaceLaneID
    }
    private var isSpaceAssociationActive: Bool {
        activeSpaceFocusID != nil
    }
    private func shouldShowSelectedVisual(_ isSelected: Bool) -> Bool {
        isSelected && !isSpaceAssociationActive
    }
    private func spaceFocusedAppGroupIndexes() -> [Int] {
@@ -1932,6 +2198,18 @@
            }
            .map(\.column.appGroupIndex)
            .sorted()
    }
    private func firstWaterfallCard(inSpaceID spaceID: UInt64) -> WaterfallCardLayers? {
        for column in waterfallColumns.sorted(by: { $0.column.appGroupIndex < $1.column.appGroupIndex }) {
            if let card = column.cards
                .sorted(by: { $0.item.windowIndex < $1.item.windowIndex })
                .first(where: { $0.item.primarySpaceID == spaceID }) {
                return card
            }
        }
        return nil
    }
    private func spaceFocusWindowCount(for appGroupIndex: Int) -> Int {
@@ -1962,33 +2240,45 @@
        return "visible"
    }
    private func appShelfVisualIconSize(forBaseIconSize iconSize: CGFloat, selected: Bool, hovered: Bool) -> CGFloat {
        guard selected || hovered else { return iconSize }
        return AppShelfMetrics.selectedIconSize
    private func appShelfVisualIconSize(
        forBaseIconSize iconSize: CGFloat,
        selected: Bool,
        hovered: Bool,
        spaceFocused: Bool
    ) -> CGFloat {
        if selected || hovered {
            return AppShelfMetrics.selectedIconSize
        }
        if spaceFocused {
            return min(AppShelfMetrics.selectedIconSize, iconSize + 8)
        }
        return iconSize
    }
    private func appShelfZPosition(selected: Bool, hovered: Bool) -> CGFloat {
        switch (selected, hovered) {
        case (true, true):
    private func appShelfZPosition(selected: Bool, hovered: Bool, spaceFocused: Bool) -> CGFloat {
        switch (selected, hovered, spaceFocused) {
        case (true, true, _):
            return 30
        case (true, false):
            return 20
        case (false, true):
            return 10
        case (false, false):
        case (true, false, _):
            return 24
        case (false, true, _):
            return 18
        case (false, false, true):
            return 12
        case (false, false, false):
            return 0
        }
    }
    private func appShelfBackgroundColor(selected: Bool, hovered: Bool, spaceFocused: Bool) -> NSColor {
        if selected {
            return NSColor.controlAccentColor.withAlphaComponent(0.12)
            return NSColor.controlAccentColor.withAlphaComponent(0.14)
        }
        if hovered {
            return NSColor.systemFill.withAlphaComponent(0.12)
            return NSColor.controlAccentColor.withAlphaComponent(0.13)
        }
        if spaceFocused {
            return NSColor.keyboardFocusIndicatorColor.withAlphaComponent(0.10)
            return NSColor.controlAccentColor.withAlphaComponent(0.14)
        }
        return .clear
    }
@@ -2117,33 +2407,59 @@
        let bounds = card.containerLayer.bounds
        let isSelected = card.item.window.id == effectiveSelection?.windowID
        let isHovered = card.item.window.id == hoveredWindowID
        applyWaterfallCardVisualState(card, selected: isSelected, hovered: isHovered)
        applyWaterfallCardVisualState(card, selected: shouldShowSelectedVisual(isSelected), hovered: isHovered)
        let thumbnailWidth = max(90, min(116, bounds.width * 0.44))
        card.thumbnailLayer.frame = CGRect(
            x: 10,
            y: 12,
            width: thumbnailWidth,
            height: bounds.height - 24
        let outerInset: CGFloat = 8
        let titleBarHeight: CGFloat = 26
        let titleGap: CGFloat = 6
        let titleBarFrame = CGRect(
            x: outerInset,
            y: bounds.height - outerInset - titleBarHeight,
            width: max(1, bounds.width - outerInset * 2),
            height: titleBarHeight
        )
        card.appIconLayer.frame = CGRect(x: 18, y: bounds.height - 38, width: 22, height: 22)
        let thumbnailFrame = CGRect(
            x: outerInset,
            y: outerInset,
            width: max(1, bounds.width - outerInset * 2),
            height: max(1, titleBarFrame.minY - outerInset - titleGap)
        )
        card.thumbnailLayer.frame = CGRect(
            x: thumbnailFrame.minX,
            y: thumbnailFrame.minY,
            width: thumbnailFrame.width,
            height: thumbnailFrame.height
        )
        card.titleBarLayer.frame = titleBarFrame
        card.appIconLayer.frame = CGRect(x: titleBarFrame.minX + 7, y: titleBarFrame.minY + 5, width: 16, height: 16)
        card.titleLayer.frame = CGRect(
            x: thumbnailWidth + 22,
            y: bounds.height - 31,
            width: max(1, bounds.width - thumbnailWidth - 34),
            x: titleBarFrame.minX + 29,
            y: titleBarFrame.minY + 6,
            width: max(1, titleBarFrame.width - 68),
            height: 16
        )
        card.metaLayer.frame = CGRect(
            x: thumbnailWidth + 22,
            y: bounds.height - 51,
            width: max(1, bounds.width - thumbnailWidth - 34),
            x: titleBarFrame.maxX - 38,
            y: titleBarFrame.minY + 6,
            width: 30,
            height: 14
        )
        card.stateLayer.frame = CGRect(
            x: thumbnailWidth + 22,
            y: 12,
            width: max(1, bounds.width - thumbnailWidth - 34),
            x: thumbnailFrame.maxX - 86,
            y: thumbnailFrame.minY + 6,
            width: 78,
            height: 14
        )
        layoutCloseButton(
            card.closeButton,
            target: .window(
                appGroupIndex: card.item.appGroupIndex,
                windowIndex: card.item.windowIndex,
                windowID: card.item.window.id
            ),
            visible: isHovered,
            anchorTopRight: CGPoint(x: bounds.maxX - 12, y: bounds.maxY - 12)
        )
        card.shineLayer.frame = CGRect(
            x: -bounds.width * 0.18,
@@ -2158,30 +2474,471 @@
        card.containerLayer.backgroundColor = (selected
            ? NSColor.windowBackgroundColor.withAlphaComponent(0.94)
            : hovered
                ? NSColor.systemFill.withAlphaComponent(0.13)
                ? accentTintedWindowBackground(fraction: 0.09, alpha: 0.95)
                : isSpaceFocused
                    ? NSColor.windowBackgroundColor.withAlphaComponent(0.88)
                    ? accentTintedWindowBackground(fraction: 0.10, alpha: 0.94)
                : NSColor.windowBackgroundColor.withAlphaComponent(0.82)
        ).cgColor
        card.containerLayer.borderColor = (selected
            ? NSColor.controlAccentColor.withAlphaComponent(0.52)
            : hovered
                ? NSColor.separatorColor.withAlphaComponent(0.46)
                ? NSColor.controlAccentColor.withAlphaComponent(0.44)
                : isSpaceFocused
                    ? NSColor.separatorColor.withAlphaComponent(0.38)
                    ? NSColor.controlAccentColor.withAlphaComponent(0.42)
                : NSColor.separatorColor.withAlphaComponent(0.28)
        ).cgColor
        card.containerLayer.borderWidth = selected ? 1.5 : 1
        card.containerLayer.borderWidth = (selected || hovered || isSpaceFocused) ? 1.5 : 1
        card.containerLayer.shadowColor = NSColor.black.cgColor
        card.containerLayer.shadowOpacity = selected ? 0.20 : hovered ? 0.08 : 0
        card.containerLayer.shadowRadius = selected ? 18 : hovered ? 10 : 0
        card.containerLayer.shadowOpacity = selected ? 0.24 : hovered ? 0.14 : isSpaceFocused ? 0.20 : 0
        card.containerLayer.shadowRadius = selected ? 22 : hovered ? 17 : isSpaceFocused ? 24 : 0
        card.containerLayer.shadowOffset = selected
            ? CGSize(width: 0, height: -5)
            ? CGSize(width: 0, height: -6)
            : hovered
                ? CGSize(width: 0, height: -2)
                : .zero
        card.containerLayer.zPosition = selected ? 20 : hovered ? 10 : 0
                ? CGSize(width: 0, height: -2.5)
                : isSpaceFocused
                    ? CGSize(width: 0, height: -5)
                    : .zero
        card.containerLayer.zPosition = selected ? 20 : hovered ? 10 : isSpaceFocused ? 8 : 0
        let focusedScale: CGFloat = selected ? 1.015 : isSpaceFocused ? 1.012 : 1.008
        card.containerLayer.transform = (selected || hovered || isSpaceFocused)
            ? CATransform3DMakeScale(focusedScale, focusedScale, 1)
            : CATransform3DIdentity
        card.containerLayer.shadowPath = (selected || hovered || isSpaceFocused)
            ? CGPath(
                roundedRect: card.containerLayer.bounds,
                cornerWidth: card.containerLayer.cornerRadius,
                cornerHeight: card.containerLayer.cornerRadius,
                transform: nil
            )
            : nil
        card.titleBarLayer.backgroundColor = NSColor.clear.cgColor
        card.titleBarLayer.borderColor = NSColor.clear.cgColor
        card.titleBarLayer.borderWidth = 0
        card.shineLayer.opacity = selected ? 1 : 0
    }
    private func layoutCloseButton(
        _ button: CloseButtonLayers,
        target: CloseTarget,
        visible: Bool,
        anchorTopRight: CGPoint
    ) {
        let isButtonHovered = hoveredCloseTarget == target
        let size: CGFloat = isButtonHovered ? 24 : 18
        let buttonFill = isButtonHovered
            ? NSColor.systemRed.withAlphaComponent(0.94)
            : NSColor.labelColor.withAlphaComponent(0.24)
        let buttonStroke = isButtonHovered
            ? NSColor.white.withAlphaComponent(0.76)
            : NSColor.white.withAlphaComponent(0.52)
        let glyphStroke = isButtonHovered
            ? NSColor.white
            : NSColor.secondaryLabelColor.withAlphaComponent(0.92)
        button.containerLayer.frame = CGRect(
            x: anchorTopRight.x - size / 2,
            y: anchorTopRight.y - size / 2,
            width: size,
            height: size
        )
        button.containerLayer.cornerRadius = size / 2
        button.containerLayer.backgroundColor = buttonFill.cgColor
        button.containerLayer.borderColor = buttonStroke.cgColor
        button.containerLayer.opacity = visible ? 1 : 0
        button.containerLayer.zPosition = isButtonHovered ? 80 : 60
        button.containerLayer.shadowOpacity = visible ? (isButtonHovered ? 0.22 : 0.07) : 0
        button.containerLayer.shadowRadius = isButtonHovered ? 9 : 5
        button.glyphLayer.frame = CGRect(x: 0, y: 0, width: size, height: size)
        button.glyphLayer.lineWidth = isButtonHovered ? 2.1 : 1.7
        button.glyphLayer.strokeColor = glyphStroke.cgColor
        button.glyphLayer.path = closeGlyphPath(size: size, hovered: isButtonHovered)
    }
    private func closeGlyphPath(size: CGFloat, hovered: Bool) -> CGPath {
        let inset: CGFloat = hovered ? 7.2 : 5.6
        let path = CGMutablePath()
        path.move(to: CGPoint(x: inset, y: inset))
        path.addLine(to: CGPoint(x: size - inset, y: size - inset))
        path.move(to: CGPoint(x: size - inset, y: inset))
        path.addLine(to: CGPoint(x: inset, y: size - inset))
        return path
    }
    private func canClose(app: AlignerApp) -> Bool {
        guard let processIdentifier = app.processIdentifier else { return false }
        return processIdentifier != getpid()
    }
    private func layoutCloseConfirmation() {
        guard let pendingCloseTarget,
              let request = closeRequest(for: pendingCloseTarget),
              let anchor = closeConfirmationAnchor(for: pendingCloseTarget)
        else {
            closeConfirmationLayers.containerLayer.isHidden = true
            closeConfirmationArrowEdge = nil
            return
        }
        let contentWidth: CGFloat = 374
        let height: CGFloat = 184
        let arrowWidth: CGFloat = 14
        let edgeMargin: CGFloat = 16
        let anchorGap: CGFloat = 12
        let width = contentWidth + arrowWidth
        let availableRight = glassLayer.bounds.maxX - anchor.x - edgeMargin
        let availableLeft = anchor.x - glassLayer.bounds.minX - edgeMargin
        let arrowEdge: CloseConfirmationArrowEdge = availableRight >= width || availableRight >= availableLeft
            ? .left
            : .right
        let preferredX = arrowEdge == .left
            ? anchor.x + anchorGap
            : anchor.x - width - anchorGap
        let x = min(
            max(edgeMargin, preferredX),
            max(edgeMargin, glassLayer.bounds.width - width - edgeMargin)
        )
        let y = min(
            max(edgeMargin, anchor.y - height + 64),
            max(edgeMargin, glassLayer.bounds.height - height - edgeMargin)
        )
        let localArrowY = min(max(anchor.y - y, 38), height - 38)
        closeConfirmationArrowEdge = arrowEdge
        let layers = closeConfirmationLayers
        layers.containerLayer.isHidden = false
        layers.containerLayer.frame = CGRect(x: x, y: y, width: width, height: height)
        let contentX = arrowEdge == .left ? arrowWidth : 0
        let contentFrame = CGRect(x: contentX, y: 0, width: contentWidth, height: height)
        let bubblePath = closeConfirmationBubblePath(
            bounds: layers.containerLayer.bounds,
            contentFrame: contentFrame,
            arrowEdge: arrowEdge,
            arrowY: localArrowY
        )
        layers.backgroundLayer.frame = layers.containerLayer.bounds
        layers.backgroundLayer.path = bubblePath
        layers.containerLayer.shadowPath = bubblePath
        layers.titleLayer.string = confirmationTitle(for: request)
        layers.messageLayer.string = confirmationMessage(for: request)
        layers.titleLayer.frame = CGRect(x: contentFrame.minX + 22, y: height - 48, width: contentFrame.width - 44, height: 22)
        layers.messageLayer.frame = CGRect(x: contentFrame.minX + 22, y: height - 78, width: contentFrame.width - 44, height: 21)
        layers.checkboxLayer.frame = CGRect(x: contentFrame.minX + 22, y: 80, width: 18, height: 18)
        layers.checkboxLayer.backgroundColor = (pendingCloseOptOut
            ? NSColor.controlAccentColor.withAlphaComponent(0.92)
            : NSColor.clear
        ).cgColor
        layers.checkboxLayer.borderColor = (pendingCloseOptOut
            ? NSColor.controlAccentColor.withAlphaComponent(0.95)
            : NSColor.separatorColor.withAlphaComponent(0.48)
        ).cgColor
        layers.checkboxMarkLayer.string = pendingCloseOptOut ? "✓" : ""
        layers.checkboxMarkLayer.frame = CGRect(x: 0, y: 1, width: 18, height: 16)
        layers.checkboxLabelLayer.frame = CGRect(x: contentFrame.minX + 48, y: 80, width: contentFrame.width - 70, height: 19)
        layers.cancelButtonLayer.frame = CGRect(x: contentFrame.maxX - 164, y: 22, width: 74, height: 32)
        layers.cancelButtonTextLayer.frame = layers.cancelButtonLayer.bounds.insetBy(dx: 0, dy: 7)
        layers.confirmButtonLayer.frame = CGRect(x: contentFrame.maxX - 78, y: 22, width: 56, height: 32)
        layers.confirmButtonTextLayer.frame = layers.confirmButtonLayer.bounds.insetBy(dx: 0, dy: 7)
    }
    private func closeConfirmationBubblePath(
        bounds: CGRect,
        contentFrame: CGRect,
        arrowEdge: CloseConfirmationArrowEdge,
        arrowY: CGFloat
    ) -> CGPath {
        let cornerRadius: CGFloat = 18
        let arrowHalfHeight: CGFloat = 10
        let path = CGMutablePath()
        let minX = contentFrame.minX
        let maxX = contentFrame.maxX
        let minY = contentFrame.minY
        let maxY = contentFrame.maxY
        let arrowY = min(max(arrowY, minY + cornerRadius + arrowHalfHeight + 4), maxY - cornerRadius - arrowHalfHeight - 4)
        path.move(to: CGPoint(x: minX + cornerRadius, y: minY))
        path.addLine(to: CGPoint(x: maxX - cornerRadius, y: minY))
        path.addQuadCurve(to: CGPoint(x: maxX, y: minY + cornerRadius), control: CGPoint(x: maxX, y: minY))
        if arrowEdge == .right {
            path.addLine(to: CGPoint(x: maxX, y: arrowY - arrowHalfHeight))
            path.addLine(to: CGPoint(x: bounds.maxX, y: arrowY))
            path.addLine(to: CGPoint(x: maxX, y: arrowY + arrowHalfHeight))
        }
        path.addLine(to: CGPoint(x: maxX, y: maxY - cornerRadius))
        path.addQuadCurve(to: CGPoint(x: maxX - cornerRadius, y: maxY), control: CGPoint(x: maxX, y: maxY))
        path.addLine(to: CGPoint(x: minX + cornerRadius, y: maxY))
        path.addQuadCurve(to: CGPoint(x: minX, y: maxY - cornerRadius), control: CGPoint(x: minX, y: maxY))
        if arrowEdge == .left {
            path.addLine(to: CGPoint(x: minX, y: arrowY + arrowHalfHeight))
            path.addLine(to: CGPoint(x: bounds.minX, y: arrowY))
            path.addLine(to: CGPoint(x: minX, y: arrowY - arrowHalfHeight))
        }
        path.addLine(to: CGPoint(x: minX, y: minY + cornerRadius))
        path.addQuadCurve(to: CGPoint(x: minX + cornerRadius, y: minY), control: CGPoint(x: minX, y: minY))
        path.closeSubpath()
        return path
    }
    private func confirmationTitle(for request: QuickSwitchCloseRequest) -> String {
        switch request {
        case .app:
            return "关闭 \(request.appName)?"
        case .window:
            return "关闭这个窗口?"
        }
    }
    private func confirmationMessage(for request: QuickSwitchCloseRequest) -> String {
        switch request {
        case .app:
            return "将退出该应用,并一次性关闭它的所有窗口。"
        case .window:
            return "将关闭此窗口,未保存内容可能需要应用确认。"
        }
    }
    private func closeRequest(for target: CloseTarget) -> QuickSwitchCloseRequest? {
        switch target {
        case .app(let appGroupIndex):
            return appShelfItems
                .first { $0.item.appGroupIndex == appGroupIndex }
                .map { .app($0.item) }
        case .window(let appGroupIndex, let windowIndex, let windowID):
            guard let card = waterfallCard(appGroupIndex: appGroupIndex, windowIndex: windowIndex),
                  card.item.window.id == windowID
            else {
                return nil
            }
            return .window(card.item)
        }
    }
    private func requestClose(for target: CloseTarget) {
        guard let request = closeRequest(for: target) else {
            DevelopmentDiagnostics.log("quickSwitch.view.closeRequest.missingTarget", [
                "targetKind": target.kindDescription
            ])
            return
        }
        pendingCloseTarget = nil
        pendingCloseOptOut = false
        closeConfirmationLayers.containerLayer.isHidden = true
        closeConfirmationArrowEdge = nil
        if pendingCloseOptOut {
            onCloseConfirmationDisabled?()
        }
        onRequestClose?(request)
    }
    private func beginCloseConfirmation(for target: CloseTarget) {
        if closeConfirmationRequired {
            pendingCloseTarget = target
            pendingCloseOptOut = false
            DevelopmentDiagnostics.log("quickSwitch.view.closeConfirmation.show", [
                "targetKind": target.kindDescription
            ])
            needsLayout = true
            return
        }
        requestClose(for: target)
    }
    private func confirmPendingClose() {
        guard let target = pendingCloseTarget else { return }
        let shouldDisableConfirmation = pendingCloseOptOut
        pendingCloseTarget = nil
        pendingCloseOptOut = false
        closeConfirmationLayers.containerLayer.isHidden = true
        closeConfirmationArrowEdge = nil
        if shouldDisableConfirmation {
            onCloseConfirmationDisabled?()
        }
        if let request = closeRequest(for: target) {
            onRequestClose?(request)
        }
    }
    private func cancelPendingClose() {
        DevelopmentDiagnostics.log("quickSwitch.view.closeConfirmation.cancel")
        pendingCloseTarget = nil
        pendingCloseOptOut = false
        closeConfirmationLayers.containerLayer.isHidden = true
        closeConfirmationArrowEdge = nil
        needsLayout = true
    }
    private func toggleCloseConfirmationOptOut() {
        pendingCloseOptOut.toggle()
        needsLayout = true
    }
    private enum CloseConfirmationHit {
        case confirm
        case cancel
        case checkbox
        case background
        case outside
    }
    private func closeConfirmationHit(at windowLocation: NSPoint) -> CloseConfirmationHit {
        let location = convert(windowLocation, from: nil)
        let locationInGlass = CGPoint(
            x: location.x - glassLayer.frame.minX,
            y: location.y - glassLayer.frame.minY
        )
        let layers = closeConfirmationLayers
        guard !layers.containerLayer.isHidden else { return .outside }
        guard layers.containerLayer.frame.contains(locationInGlass) else { return .outside }
        let local = CGPoint(
            x: locationInGlass.x - layers.containerLayer.frame.minX,
            y: locationInGlass.y - layers.containerLayer.frame.minY
        )
        if layers.confirmButtonLayer.frame.contains(local) {
            return .confirm
        }
        if layers.cancelButtonLayer.frame.contains(local) {
            return .cancel
        }
        let checkboxHitFrame = layers.checkboxLayer.frame.union(layers.checkboxLabelLayer.frame).insetBy(dx: -6, dy: -4)
        if checkboxHitFrame.contains(local) {
            return .checkbox
        }
        return .background
    }
    private func handleCloseConfirmationMouseDown(at windowLocation: NSPoint) -> Bool {
        switch closeConfirmationHit(at: windowLocation) {
        case .confirm:
            confirmPendingClose()
            return true
        case .cancel:
            cancelPendingClose()
            return true
        case .checkbox:
            toggleCloseConfirmationOptOut()
            return true
        case .background:
            return true
        case .outside:
            if pendingCloseTarget != nil {
                cancelPendingClose()
                return true
            }
            return false
        }
    }
    private func closeButtonTarget(at windowLocation: NSPoint) -> CloseTarget? {
        if let target = appShelfCloseButtonTarget(at: windowLocation) {
            return target
        }
        return waterfallCloseButtonTarget(at: windowLocation)
    }
    private func appShelfCloseButtonTarget(at windowLocation: NSPoint) -> CloseTarget? {
        let locationInView = convert(windowLocation, from: nil)
        let locationInAppShelfContent = CGPoint(
            x: locationInView.x - glassLayer.frame.minX - shelfLayer.frame.minX - appShelfContentLayer.frame.minX,
            y: locationInView.y - glassLayer.frame.minY - shelfLayer.frame.minY - appShelfContentLayer.frame.minY
        )
        for item in appShelfItems {
            let target = CloseTarget.app(appGroupIndex: item.item.appGroupIndex)
            guard item.closeButton.containerLayer.opacity > 0,
                  canClose(app: item.item.app)
            else { continue }
            let locationInItem = CGPoint(
                x: locationInAppShelfContent.x - item.containerLayer.frame.minX,
                y: locationInAppShelfContent.y - item.containerLayer.frame.minY
            )
            if item.closeButton.containerLayer.frame.insetBy(dx: -8, dy: -8).contains(locationInItem) {
                return target
            }
        }
        return nil
    }
    private func waterfallCloseButtonTarget(at windowLocation: NSPoint) -> CloseTarget? {
        let locationInView = convert(windowLocation, from: nil)
        let locationInWaterfallContent = CGPoint(
            x: locationInView.x - glassLayer.frame.minX - waterfallLayer.frame.minX - waterfallContentLayer.frame.minX,
            y: locationInView.y - glassLayer.frame.minY - waterfallLayer.frame.minY - waterfallContentLayer.frame.minY
        )
        for column in waterfallColumns where column.containerLayer.frame.contains(locationInWaterfallContent) {
            let locationInColumn = CGPoint(
                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 card.closeButton.containerLayer.opacity > 0 else { continue }
                let locationInCard = CGPoint(
                    x: locationInColumn.x - card.containerLayer.frame.minX,
                    y: locationInColumn.y - card.containerLayer.frame.minY
                )
                if card.closeButton.containerLayer.frame.insetBy(dx: -8, dy: -8).contains(locationInCard) {
                    return .window(
                        appGroupIndex: card.item.appGroupIndex,
                        windowIndex: card.item.windowIndex,
                        windowID: card.item.window.id
                    )
                }
            }
        }
        return nil
    }
    private func closeConfirmationAnchor(for target: CloseTarget) -> CGPoint? {
        switch target {
        case .app(let appGroupIndex):
            guard let item = appShelfItems.first(where: { $0.item.appGroupIndex == appGroupIndex }) else {
                return nil
            }
            return CGPoint(
                x: shelfLayer.frame.minX
                    + appShelfContentLayer.frame.minX
                    + item.containerLayer.frame.minX
                    + item.closeButton.containerLayer.frame.midX,
                y: shelfLayer.frame.minY
                    + appShelfContentLayer.frame.minY
                    + item.containerLayer.frame.minY
                    + item.closeButton.containerLayer.frame.midY
            )
        case .window(let appGroupIndex, let windowIndex, let windowID):
            guard let column = waterfallColumns.first(where: { $0.column.appGroupIndex == appGroupIndex }),
                  let card = column.cards.first(where: {
                      $0.item.windowIndex == windowIndex && $0.item.window.id == windowID
                  })
            else {
                return nil
            }
            return CGPoint(
                x: waterfallLayer.frame.minX
                    + waterfallContentLayer.frame.minX
                    + column.containerLayer.frame.minX
                    + card.containerLayer.frame.minX
                    + card.closeButton.containerLayer.frame.midX,
                y: waterfallLayer.frame.minY
                    + waterfallContentLayer.frame.minY
                    + column.containerLayer.frame.minY
                    + card.containerLayer.frame.minY
                    + card.closeButton.containerLayer.frame.midY
            )
        }
    }
    private func accentTintedWindowBackground(fraction: CGFloat, alpha: CGFloat) -> NSColor {
        let color = NSColor.windowBackgroundColor.blended(withFraction: fraction, of: NSColor.controlAccentColor)
            ?? NSColor.windowBackgroundColor
        return color.withAlphaComponent(alpha)
    }
    private func waterfallColumnWidth() -> CGFloat {
@@ -2262,10 +3019,13 @@
    private func makeFullscreenMarkerLayer(for space: QuickSwitchSpaceViewModel) -> CALayer? {
        guard space.type == .fullscreen else { return nil }
        let layer = CALayer()
        layer.cornerRadius = 1.5
        layer.backgroundColor = NSColor.secondaryLabelColor.withAlphaComponent(space.isCurrent ? 0.46 : 0.32).cgColor
        return layer
        let shapeLayer = CAShapeLayer()
        shapeLayer.fillColor = NSColor.clear.cgColor
        shapeLayer.strokeColor = NSColor.secondaryLabelColor.withAlphaComponent(space.isCurrent ? 0.42 : 0.28).cgColor
        shapeLayer.lineWidth = 1.2
        shapeLayer.lineCap = .round
        shapeLayer.lineJoin = .round
        return shapeLayer
    }
    private func makeTextLayer(
@@ -2286,6 +3046,133 @@
        return textLayer
    }
    private func makeCloseButtonLayers() -> CloseButtonLayers {
        let container = CALayer()
        container.cornerRadius = 8
        container.masksToBounds = false
        container.backgroundColor = NSColor.systemRed.withAlphaComponent(0.92).cgColor
        container.borderColor = NSColor.white.withAlphaComponent(0.72).cgColor
        container.borderWidth = 0.8
        container.shadowColor = NSColor.black.cgColor
        container.shadowOpacity = 0.14
        container.shadowRadius = 5
        container.shadowOffset = CGSize(width: 0, height: -1)
        container.opacity = 0
        let glyph = CAShapeLayer()
        glyph.contentsScale = backingScaleFactor
        glyph.strokeColor = NSColor.white.cgColor
        glyph.fillColor = NSColor.clear.cgColor
        glyph.lineCap = .round
        glyph.lineJoin = .round
        container.addSublayer(glyph)
        return CloseButtonLayers(containerLayer: container, glyphLayer: glyph)
    }
    private func makeCloseConfirmationLayers() -> CloseConfirmationLayers {
        let container = CALayer()
        container.masksToBounds = false
        container.backgroundColor = NSColor.clear.cgColor
        container.shadowColor = NSColor.black.cgColor
        container.shadowOpacity = 0.20
        container.shadowRadius = 20
        container.shadowOffset = CGSize(width: 0, height: -8)
        container.zPosition = 200
        let background = CAShapeLayer()
        background.contentsScale = backingScaleFactor
        background.fillColor = NSColor.windowBackgroundColor.withAlphaComponent(0.985).cgColor
        background.strokeColor = NSColor.separatorColor.withAlphaComponent(0.36).cgColor
        background.lineWidth = 1
        let title = makeTextLayer(
            string: "",
            fontSize: 15,
            weight: .semibold,
            color: .labelColor
        )
        let message = makeTextLayer(
            string: "",
            fontSize: 13,
            weight: .regular,
            color: .secondaryLabelColor
        )
        message.truncationMode = .end
        let checkbox = CALayer()
        checkbox.cornerRadius = 4
        checkbox.borderWidth = 1
        checkbox.borderColor = NSColor.separatorColor.withAlphaComponent(0.48).cgColor
        checkbox.backgroundColor = NSColor.clear.cgColor
        let checkmark = makeTextLayer(
            string: "",
            fontSize: 11,
            weight: .bold,
            color: .white,
            alignment: .center
        )
        let checkboxLabel = makeTextLayer(
            string: "以后不再提醒",
            fontSize: 13,
            weight: .regular,
            color: .secondaryLabelColor
        )
        let confirmButton = CALayer()
        confirmButton.cornerRadius = 7
        confirmButton.backgroundColor = NSColor.systemRed.withAlphaComponent(0.94).cgColor
        let confirmText = makeTextLayer(
            string: "关闭",
            fontSize: 13,
            weight: .semibold,
            color: .white,
            alignment: .center
        )
        let cancelButton = CALayer()
        cancelButton.cornerRadius = 7
        cancelButton.backgroundColor = NSColor.systemFill.withAlphaComponent(0.16).cgColor
        cancelButton.borderColor = NSColor.separatorColor.withAlphaComponent(0.22).cgColor
        cancelButton.borderWidth = 1
        let cancelText = makeTextLayer(
            string: "取消",
            fontSize: 13,
            weight: .medium,
            color: .labelColor,
            alignment: .center
        )
        container.addSublayer(background)
        container.addSublayer(title)
        container.addSublayer(message)
        container.addSublayer(checkbox)
        checkbox.addSublayer(checkmark)
        container.addSublayer(checkboxLabel)
        container.addSublayer(confirmButton)
        confirmButton.addSublayer(confirmText)
        container.addSublayer(cancelButton)
        cancelButton.addSublayer(cancelText)
        return CloseConfirmationLayers(
            containerLayer: container,
            backgroundLayer: background,
            titleLayer: title,
            messageLayer: message,
            checkboxLayer: checkbox,
            checkboxMarkLayer: checkmark,
            checkboxLabelLayer: checkboxLabel,
            confirmButtonLayer: confirmButton,
            confirmButtonTextLayer: confirmText,
            cancelButtonLayer: cancelButton,
            cancelButtonTextLayer: cancelText
        )
    }
    private func makeWindowBlocks(count: Int, space: QuickSwitchSpaceViewModel) -> [CALayer] {
        guard count > 0 else { return [] }
@@ -2300,17 +3187,61 @@
        }
    }
    private func appIdentifierText(for appNames: [String]) -> String {
        var identifiers = appNames.prefix(3).compactMap { name -> String? in
    private func appIdentifierText(for space: QuickSwitchSpaceViewModel) -> String {
        if space.type == .fullscreen,
           let appName = space.appNames.first?.trimmingCharacters(in: .whitespacesAndNewlines),
           !appName.isEmpty {
            return appName
        }
        var identifiers = space.appNames.prefix(3).compactMap { name -> String? in
            let trimmed = name.trimmingCharacters(in: .whitespacesAndNewlines)
            return trimmed.first.map { String($0) }
        }
        if appNames.count > 3 {
            identifiers.append("+\(appNames.count - 3)")
        if space.appNames.count > 3 {
            identifiers.append("+\(space.appNames.count - 3)")
        }
        return identifiers.joined(separator: " ")
    }
    private func layoutFullscreenMarkerLayer(_ layer: CALayer, in segmentBounds: CGRect) {
        let markerFrame = fullscreenMarkerFrame(in: segmentBounds)
        layer.frame = markerFrame
        guard let shapeLayer = layer as? CAShapeLayer else { return }
        let bounds = shapeLayer.bounds
        let cornerLength = min(12, max(7, min(bounds.width, bounds.height) * 0.28))
        let path = CGMutablePath()
        path.move(to: CGPoint(x: 0, y: cornerLength))
        path.addLine(to: CGPoint(x: 0, y: 0))
        path.addLine(to: CGPoint(x: cornerLength, y: 0))
        path.move(to: CGPoint(x: bounds.maxX - cornerLength, y: 0))
        path.addLine(to: CGPoint(x: bounds.maxX, y: 0))
        path.addLine(to: CGPoint(x: bounds.maxX, y: cornerLength))
        path.move(to: CGPoint(x: bounds.maxX, y: bounds.maxY - cornerLength))
        path.addLine(to: CGPoint(x: bounds.maxX, y: bounds.maxY))
        path.addLine(to: CGPoint(x: bounds.maxX - cornerLength, y: bounds.maxY))
        path.move(to: CGPoint(x: cornerLength, y: bounds.maxY))
        path.addLine(to: CGPoint(x: 0, y: bounds.maxY))
        path.addLine(to: CGPoint(x: 0, y: bounds.maxY - cornerLength))
        shapeLayer.path = path
    }
    private func fullscreenMarkerFrame(in segmentBounds: CGRect) -> CGRect {
        CGRect(
            x: 10,
            y: 30,
            width: max(1, segmentBounds.width - 20),
            height: max(1, segmentBounds.height - 58)
        )
    }
    private func columnSelectionHistoryReports() -> [[String: Int]] {
@@ -2347,15 +3278,22 @@
                "labelFrame": dictionary(from: item.labelLayer.frame),
                "labelTruncationMode": "middle",
                "badgeVisible": !item.badgeLayer.isHidden,
                "badgeText": item.badgeLayer.string ?? ""
                "badgeText": item.badgeLayer.string ?? "",
                "closeButtonVisible": item.closeButton.containerLayer.opacity > 0,
                "closeButtonFrame": dictionary(from: item.closeButton.containerLayer.frame),
                "closeButtonSize": Double(item.closeButton.containerLayer.bounds.width),
                "closeButtonFillColor": colorDictionary(from: item.closeButton.containerLayer.backgroundColor)
            ] as [String: Any]
        }
    }
    private func appShelfVisualStates(for item: AppShelfItemLayers) -> [String] {
        var states: [String] = []
        if item.item.appGroupIndex == effectiveSelection?.appGroupIndex {
        let isSelected = item.item.appGroupIndex == effectiveSelection?.appGroupIndex
        if shouldShowSelectedVisual(isSelected) {
            states.append("selected")
        } else if isSelected {
            states.append("selectedVisualSuppressed")
        }
        if item.item.appGroupIndex == effectiveHoveredAppGroupIndex {
            states.append("hover")
@@ -2466,8 +3404,20 @@
                "screenshotSource": screenshotSourceString(screenshotSource, for: card.item.window),
                "screenshotFallbackReason": screenshotFallbackReasonString(screenshotSource),
                "screenshotNotRequestedReason": screenshotSkippedReasonsByWindowID[card.item.window.id] ?? NSNull(),
                "backgroundColor": colorDictionary(from: card.containerLayer.backgroundColor),
                "shadowOpacity": Double(card.containerLayer.shadowOpacity),
                "shadowRadius": Double(card.containerLayer.shadowRadius),
                "thumbnailFrame": dictionary(from: card.thumbnailLayer.frame),
                "titleBarFrame": dictionary(from: card.titleBarLayer.frame),
                "titleBarBorderWidth": Double(card.titleBarLayer.borderWidth),
                "titleFrame": dictionary(from: card.titleLayer.frame),
                "metaFrame": dictionary(from: card.metaLayer.frame),
                "appIconFrame": dictionary(from: card.appIconLayer.frame),
                "stateFrame": dictionary(from: card.stateLayer.frame),
                "closeButtonVisible": card.closeButton.containerLayer.opacity > 0,
                "closeButtonFrame": dictionary(from: card.closeButton.containerLayer.frame),
                "closeButtonSize": Double(card.closeButton.containerLayer.bounds.width),
                "closeButtonFillColor": colorDictionary(from: card.closeButton.containerLayer.backgroundColor),
                "shineVisible": card.shineLayer.opacity > 0,
                "zPosition": Double(card.containerLayer.zPosition),
                "frame": dictionary(from: card.containerLayer.frame),
@@ -2562,8 +3512,11 @@
    private func waterfallCardVisualStates(for card: WaterfallCardLayers) -> [String] {
        var states: [String] = []
        if card.item.window.id == effectiveSelection?.windowID {
        let isSelected = card.item.window.id == effectiveSelection?.windowID
        if shouldShowSelectedVisual(isSelected) {
            states.append("selected")
        } else if isSelected {
            states.append("selectedVisualSuppressed")
        }
        if card.item.window.id == hoveredWindowID {
            states.append("hover")
@@ -2598,6 +3551,13 @@
                    "type": spaceTypeName(space.type),
                    "visualStates": visualStates(for: space),
                    "fullscreenMarkerVisible": segment.fullscreenMarkerLayer != nil,
                    "fullscreenMarkerKind": segment.fullscreenMarkerLayer == nil ? "none" : "cornerBracket",
                    "fullscreenMarkerFrame": segment.fullscreenMarkerLayer.map { dictionary(from: $0.frame) } ?? NSNull(),
                    "windowBlockCount": segment.windowBlocks.count,
                    "backgroundColor": colorDictionary(from: segment.containerLayer.backgroundColor),
                    "shadowOpacity": Double(segment.containerLayer.shadowOpacity),
                    "shadowRadius": Double(segment.containerLayer.shadowRadius),
                    "appFrame": dictionary(from: segment.appLayer.frame),
                    "frame": dictionary(from: segment.containerLayer.frame),
                    "visibleFrame": dictionary(from: visibleFrame),
                    "isCurrent": space.isCurrent,
@@ -2649,4 +3609,24 @@
            "height": Double(rect.height)
        ]
    }
    private func colorDictionary(from color: CGColor?) -> [String: Double] {
        guard let color,
              let nsColor = NSColor(cgColor: color)?.usingColorSpace(.deviceRGB)
        else {
            return [
                "red": 0,
                "green": 0,
                "blue": 0,
                "alpha": 0
            ]
        }
        return [
            "red": Double(nsColor.redComponent),
            "green": Double(nsColor.greenComponent),
            "blue": Double(nsColor.blueComponent),
            "alpha": Double(nsColor.alphaComponent)
        ]
    }
}