Ariver
2026-06-18 4e9fef02a574f16bb6fe3f7cef4f967e009d2a5d
Add App Shelf index markers
4 files modified
184 ■■■■■ changed files
C1.source/Resources/Aligner-Info.plist 4 ●●●● patch | view | raw | blame | history
C1.source/Sources/Aligner/QuickSwitchRootView.swift 144 ●●●●● patch | view | raw | blame | history
C1.source/Sources/Aligner/QuickSwitchSessionController.swift 22 ●●●●● patch | view | raw | blame | history
C3.tools/round1-app-shelf-fixture-qa.sh 14 ●●●●● patch | view | raw | blame | history
C1.source/Resources/Aligner-Info.plist
@@ -17,9 +17,9 @@
    <key>CFBundlePackageType</key>
    <string>APPL</string>
    <key>CFBundleShortVersionString</key>
    <string>0.0.69</string>
    <string>0.0.70</string>
    <key>CFBundleVersion</key>
    <string>20260614.2031</string>
    <string>20260618.2044</string>
    <key>LSMinimumSystemVersion</key>
    <string>26.0</string>
    <key>NSHighResolutionCapable</key>
C1.source/Sources/Aligner/QuickSwitchRootView.swift
@@ -241,6 +241,7 @@
        let item: QuickSwitchAppShelfItemViewModel
        let containerLayer: CALayer
        let backgroundLayer: CALayer
        let indexLayer: CATextLayer
        let iconLayer: CALayer
        let labelLayer: CATextLayer
        let badgeLayer: CATextLayer
@@ -299,6 +300,10 @@
        static let normalGap: CGFloat = 16
        static let minGap: CGFloat = 8
        static let maxRows = 2
        static let indexFontSize: CGFloat = 15
        static let compressedIndexFontSize: CGFloat = 13
        static let indexWidth: CGFloat = 16
        static let indexHeight: CGFloat = 18
    }
    private enum RootLayoutMetrics {
@@ -751,11 +756,38 @@
        apply(viewModel: viewModel, preservingInteractionState: true, animatedProjection: true)
    }
    func hoverNextAppGroupIndex(cycleForward: Bool = true) -> Bool {
        guard !appShelfItems.isEmpty else {
            return false
        }
        let orderedIndexes = appShelfItems.map(\.item.appGroupIndex)
        let sortedIndexes = orderedIndexes
        var currentIndexPosition: Int?
        if let currentHovered = effectiveHoveredAppGroupIndex {
            currentIndexPosition = sortedIndexes.firstIndex(of: currentHovered)
        }
        let nextPosition = currentIndexPosition.map { current in
            if cycleForward {
                (current + 1) % sortedIndexes.count
            } else {
                (current - 1 + sortedIndexes.count) % sortedIndexes.count
            }
        } ?? 0
        let nextAppGroupIndex = sortedIndexes[nextPosition]
        hoverAppGroup(nextAppGroupIndex)
        return true
    }
    private func apply(
        viewModel: QuickSwitchViewModel?,
        preservingInteractionState: Bool,
        animatedProjection: Bool
    ) {
        let preservedWindowThumbnails = preservingInteractionState ? cachedThumbnailsByWindowID() : [:]
        let transitionSnapshot = animatedProjection ? captureProjectionTransitionSnapshot() : nil
        if !animatedProjection {
            resetProjectionTransitionReport()
@@ -816,12 +848,35 @@
        rebuildSpaceLaneSegments()
        rebuildAppShelfItems()
        rebuildWaterfallColumns()
        if preservingInteractionState {
            restoreThumbnailsFromCache(preservedWindowThumbnails)
        }
        needsLayout = true
        ensureCurrentSelectionVisible(horizontalIntent: .alignWithAppShelf(animated: false))
        needsLayout = true
        if animatedProjection, let transitionSnapshot {
            layoutSubtreeIfNeeded()
            applyProjectionTransitionAnimations(from: transitionSnapshot)
        }
    }
    private func cachedThumbnailsByWindowID() -> [UInt32: Any] {
        var snapshots: [UInt32: Any] = [:]
        for card in waterfallCardsByWindowID().values {
            snapshots[card.item.window.id] = card.thumbnailLayer.contents
        }
        return snapshots
    }
    private func restoreThumbnailsFromCache(_ cachedThumbnails: [UInt32: Any]) {
        for card in waterfallCardsByWindowID().values {
            if let thumbnail = cachedThumbnails[card.item.window.id] {
                card.thumbnailLayer.contents = thumbnail
            }
        }
    }
@@ -2337,7 +2392,7 @@
            item.containerLayer.removeFromSuperlayer()
        }
        appShelfItems = (currentViewModel?.appShelf ?? []).map { item in
        appShelfItems = (currentViewModel?.appShelf ?? []).enumerated().map { ordinal, item in
            let container = CALayer()
            container.name = item.app.name
@@ -2353,6 +2408,18 @@
            icon.contents = appIconProvider.icon(for: item.app)
            icon.cornerRadius = 12
            icon.masksToBounds = false
            let indexLabel = makeTextLayer(
                string: appShelfIndexText(forOrdinal: ordinal),
                fontSize: AppShelfMetrics.indexFontSize,
                weight: .regular,
                color: appShelfIndexColor,
                alignment: .center
            )
            indexLabel.font = appShelfIndexFont(size: AppShelfMetrics.indexFontSize)
            indexLabel.fontSize = AppShelfMetrics.indexFontSize
            indexLabel.truncationMode = .end
            indexLabel.zPosition = 14
            let label = makeTextLayer(
                string: item.app.name,
@@ -2378,6 +2445,7 @@
            let closeButton = makeCloseButtonLayers()
            container.addSublayer(background)
            container.addSublayer(indexLabel)
            container.addSublayer(icon)
            container.addSublayer(label)
            container.addSublayer(badge)
@@ -2388,6 +2456,7 @@
                item: item,
                containerLayer: container,
                backgroundLayer: background,
                indexLayer: indexLabel,
                iconLayer: icon,
                labelLayer: label,
                badgeLayer: badge,
@@ -3421,6 +3490,13 @@
            )
            : nil
        item.iconLayer.frame = CGRect(x: iconX, y: iconY, width: visualIconSize, height: visualIconSize)
        layoutAppShelfIndexLayer(
            item.indexLayer,
            iconFrame: item.iconLayer.frame,
            iconSize: visualIconSize,
            itemBounds: bounds,
            highlighted: showsSelectedVisual || isHovered
        )
        item.labelLayer.contentsScale = backingScaleFactor
        item.labelLayer.frame = CGRect(x: 0, y: 0, width: bounds.width, height: 14)
        item.badgeLayer.string = badgeText
@@ -3446,6 +3522,67 @@
        )
        item.selectedIndicatorLayer.frame = .zero
        item.selectedIndicatorLayer.backgroundColor = NSColor.clear.cgColor
    }
    private var appShelfIndexColor: NSColor {
        NSColor.labelColor.withAlphaComponent(0.34)
    }
    private var appShelfIndexHighlightedColor: NSColor {
        NSColor.labelColor.withAlphaComponent(0.42)
    }
    private func appShelfIndexFont(size: CGFloat) -> NSFont {
        for fontName in ["Bradley Hand ITC", "Noteworthy-Light", "Marker Felt", "Snell Roundhand"] {
            if let font = NSFont(name: fontName, size: size) {
                return font
            }
        }
        return NSFont.monospacedSystemFont(ofSize: size, weight: .regular)
    }
    private func appShelfIndexText(forOrdinal ordinal: Int) -> String {
        let symbols = Array("1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ")
        guard ordinal >= 0 else { return "" }
        if ordinal < symbols.count {
            return String(symbols[ordinal])
        }
        let overflowIndex = ordinal - symbols.count
        let prefix = symbols[10 + (overflowIndex / 26) % 26]
        let suffix = symbols[10 + overflowIndex % 26]
        return "\(prefix)\(suffix)"
    }
    private func layoutAppShelfIndexLayer(
        _ layer: CATextLayer,
        iconFrame: CGRect,
        iconSize: CGFloat,
        itemBounds: CGRect,
        highlighted: Bool
    ) {
        let fontSize = iconSize <= AppShelfMetrics.minIconSize + 1
            ? AppShelfMetrics.compressedIndexFontSize
            : AppShelfMetrics.indexFontSize
        layer.contentsScale = backingScaleFactor
        layer.font = appShelfIndexFont(size: fontSize)
        layer.fontSize = fontSize
        layer.foregroundColor = (highlighted ? appShelfIndexHighlightedColor : appShelfIndexColor).cgColor
        layer.opacity = iconSize <= AppShelfMetrics.minIconSize + 1 ? 0.72 : 0.82
        let indexWidth = AppShelfMetrics.indexWidth
        let indexHeight = AppShelfMetrics.indexHeight
        let preferredX = iconFrame.minX - indexWidth - 1
        let clampedX = max(-6, min(itemBounds.width - indexWidth, preferredX))
        let preferredY = iconFrame.maxY - indexHeight + 4
        let clampedY = max(0, min(itemBounds.height - indexHeight, preferredY))
        layer.frame = CGRect(
            x: clampedX,
            y: clampedY,
            width: indexWidth,
            height: indexHeight
        )
    }
    private func appShelfRows() -> [[AppShelfItemLayers]] {
@@ -5040,6 +5177,11 @@
                "frame": dictionary(from: item.containerLayer.frame),
                "visibleFrame": dictionary(from: visibleFrame),
                "iconFrame": dictionary(from: item.iconLayer.frame),
                "indexText": item.indexLayer.string ?? "",
                "indexFrame": dictionary(from: item.indexLayer.frame),
                "indexVisible": !item.indexLayer.isHidden && item.indexLayer.opacity > 0,
                "indexFontSize": Double(item.indexLayer.fontSize),
                "indexColor": colorDictionary(from: item.indexLayer.foregroundColor),
                "labelFrame": dictionary(from: item.labelLayer.frame),
                "labelTruncationMode": "middle",
                "badgeVisible": !item.badgeLayer.isHidden,
C1.source/Sources/Aligner/QuickSwitchSessionController.swift
@@ -280,7 +280,27 @@
    func retriggerFromShortcut() {
        DevelopmentDiagnostics.log("quickSwitch.session.retriggerFromShortcut")
        show()
        guard isVisible else {
            show()
            return
        }
        guard currentViewModel != nil else {
            DevelopmentDiagnostics.log("quickSwitch.session.retriggerFromShortcut.noSnapshot", [
                "action": "fallbackShow"
            ])
            show()
            return
        }
        let didCycle = view.hoverNextAppGroupIndex()
        DevelopmentDiagnostics.log("quickSwitch.session.retriggerFromShortcut.hoverApp", [
            "didCycle": didCycle
        ])
        if !didCycle {
            show()
        }
    }
    func setWaterfallViewMode(_ mode: QuickSwitchWaterfallViewMode) {
C3.tools/round1-app-shelf-fixture-qa.sh
@@ -104,6 +104,13 @@
        print(json.dumps(report, indent=2, ensure_ascii=False), file=sys.stderr)
        sys.exit(1)
def expected_index_text(index):
    symbols = list("1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ")
    if index < len(symbols):
        return symbols[index]
    overflow = index - len(symbols)
    return symbols[10 + (overflow // 26) % 26] + symbols[10 + overflow % 26]
root = report.get("rootView", {})
items = root.get("appShelfItems", [])
columns = root.get("waterfallColumns", [])
@@ -171,6 +178,12 @@
require(normal_items, "fixture must include normal app items")
require(all(item.get("iconFrame", {}).get("width") == expected_icon_size for item in normal_items), "normal app icons must match expected fixture icon size")
require(all(item.get("labelTruncationMode") == "middle" for item in items), "all App Shelf labels must use middle truncation")
require(all(item.get("indexVisible") is True for item in items), "all App Shelf items must expose a visible hand-written index marker")
require(all(item.get("indexText") == expected_index_text(index) for index, item in enumerate(items)), "App Shelf index markers must follow 1234567890ABCDEFG order")
require(all(item.get("indexFontSize", 0) >= 13 for item in items), "App Shelf index markers must keep readable font size")
require(all(0 < item.get("indexColor", {}).get("alpha", 0) <= 0.5 for item in items), "App Shelf index markers must stay low-noise with light alpha")
require(all(item.get("indexFrame", {}).get("width", 0) >= 14 for item in items), "App Shelf index markers must reserve enough width")
require(all(item.get("indexFrame", {}).get("x", 99) < item.get("iconFrame", {}).get("x", -99) for item in items), "App Shelf index markers must sit beside the icon, not over the icon center")
row_y_values = {round(item.get("frame", {}).get("y", 0), 2) for item in items}
require(len(row_y_values) == expected_rows, "fixture App Shelf visible rows must match expected rows")
@@ -192,6 +205,7 @@
    "hoveredColumnTitleColor": hovered_title_color,
    "hoveredColumnCountColor": hovered_count_color,
    "blueReference": blue_reference,
    "firstIndexes": [item.get("indexText") for item in items[:min(12, len(items))]],
    "selectedZ": selected_z,
    "hoverZ": hover_z,
    "scrollable": root.get("appShelfScrollable"),