fix quick switch shelf badges and space labels
| | |
| | | <key>CFBundlePackageType</key> |
| | | <string>APPL</string> |
| | | <key>CFBundleShortVersionString</key> |
| | | <string>0.0.21</string> |
| | | <string>0.0.22</string> |
| | | <key>CFBundleVersion</key> |
| | | <string>20260604.0030</string> |
| | | <string>20260604.1515</string> |
| | | <key>LSMinimumSystemVersion</key> |
| | | <string>26.0</string> |
| | | <key>NSHighResolutionCapable</key> |
| | |
| | | badge.masksToBounds = true |
| | | |
| | | let selectedIndicator = CALayer() |
| | | selectedIndicator.cornerRadius = 2 |
| | | selectedIndicator.backgroundColor = (item.appGroupIndex == selectedAppGroupIndex |
| | | ? NSColor.controlAccentColor |
| | | : NSColor.clear |
| | | ).cgColor |
| | | selectedIndicator.isHidden = true |
| | | |
| | | container.addSublayer(background) |
| | | container.addSublayer(icon) |
| | | container.addSublayer(label) |
| | | container.addSublayer(badge) |
| | | container.addSublayer(selectedIndicator) |
| | | appShelfContentLayer.addSublayer(container) |
| | | |
| | | return AppShelfItemLayers( |
| | |
| | | : NSColor.controlAccentColor.withAlphaComponent(0.90) |
| | | ).cgColor |
| | | item.badgeLayer.frame = CGRect( |
| | | x: iconX + visualIconSize - badgeWidth + 3, |
| | | y: iconY + visualIconSize - 14, |
| | | x: iconX - 3, |
| | | y: iconY - 2, |
| | | width: badgeWidth, |
| | | height: 16 |
| | | ) |
| | | item.selectedIndicatorLayer.frame = CGRect( |
| | | x: bounds.midX - 8, |
| | | y: 0, |
| | | width: 16, |
| | | height: 4 |
| | | ) |
| | | item.selectedIndicatorLayer.backgroundColor = (isSelected |
| | | ? NSColor.controlAccentColor |
| | | : NSColor.clear |
| | | ).cgColor |
| | | item.selectedIndicatorLayer.frame = .zero |
| | | item.selectedIndicatorLayer.backgroundColor = NSColor.clear.cgColor |
| | | } |
| | | |
| | | private func appShelfRows() -> [[AppShelfItemLayers]] { |
| | |
| | | appCountsBySpace: [UInt64: Int], |
| | | appNamesBySpace: [UInt64: [String]] |
| | | ) -> [QuickSwitchDisplayViewModel] { |
| | | displays |
| | | let sortedDisplays = displays |
| | | .sorted { $0.uuid.localizedStandardCompare($1.uuid) == .orderedAscending } |
| | | let displayCount = sortedDisplays.count |
| | | |
| | | return sortedDisplays |
| | | .enumerated() |
| | | .map { displayIndex, display in |
| | | let displayLabel = displayLabel(for: displayIndex) |
| | |
| | | id: space.id, |
| | | displayUUID: space.displayUUID, |
| | | index: space.index, |
| | | label: "\(displayLabel)\(space.index)", |
| | | label: spaceLabel(for: space, displayIndex: displayIndex, displayCount: displayCount), |
| | | type: space.type, |
| | | isCurrent: currentSpaceIDs.contains(space.id), |
| | | windowCount: windowCountsBySpace[space.id] ?? 0, |
| | |
| | | |
| | | private static func labelsBySpaceID(from displays: [AlignerDisplay]) -> [UInt64: String] { |
| | | var labels: [UInt64: String] = [:] |
| | | |
| | | for (displayIndex, display) in displays |
| | | let sortedDisplays = displays |
| | | .sorted(by: { $0.uuid.localizedStandardCompare($1.uuid) == .orderedAscending }) |
| | | .enumerated() |
| | | { |
| | | let displayLabel = displayLabel(for: displayIndex) |
| | | let displayCount = sortedDisplays.count |
| | | |
| | | for (displayIndex, display) in sortedDisplays.enumerated() { |
| | | for space in display.spaces { |
| | | labels[space.id] = "\(displayLabel)\(space.index)" |
| | | labels[space.id] = spaceLabel(for: space, displayIndex: displayIndex, displayCount: displayCount) |
| | | } |
| | | } |
| | | |
| | |
| | | |
| | | return String(String.UnicodeScalarView(scalars)) |
| | | } |
| | | |
| | | private static func spaceLabel(for space: AlignerSpace, displayIndex: Int, displayCount: Int) -> String { |
| | | if displayCount <= 1 { |
| | | return String(format: "%02d", space.index) |
| | | } |
| | | |
| | | return "\(displayLabel(for: displayIndex))\(space.index)" |
| | | } |
| | | } |
| | |
| | | XCTAssertEqual(viewModel.initialSelection, QuickSwitchSelection(appGroupIndex: 0, windowIndex: 0, windowID: 1)) |
| | | } |
| | | |
| | | func testQuickSwitchViewModelBuilderUsesNumericSpaceLabelsForSingleDisplay() { |
| | | let app = AlignerApp(bundleIdentifier: "com.example.Alpha", name: "Alpha", category: .generic) |
| | | let snapshot = QuickSwitchSnapshotBuilder.snapshot( |
| | | displays: [makeDisplay(spaceIDs: [10, 11, 12])], |
| | | windows: [ |
| | | AlignerWindow(id: 1, app: app, title: "A1", spaceIDs: [10]), |
| | | AlignerWindow(id: 2, app: app, title: "A2", spaceIDs: [11]) |
| | | ] |
| | | ) |
| | | |
| | | let viewModel = QuickSwitchViewModelBuilder.viewModel(from: snapshot, currentSpaceIDs: [11]) |
| | | |
| | | XCTAssertEqual(viewModel.displays.flatMap { $0.spaces.map(\.label) }, ["01", "02", "03"]) |
| | | XCTAssertEqual(viewModel.displays.flatMap { $0.spaces.filter(\.isCurrent).map(\.label) }, ["02"]) |
| | | XCTAssertEqual(viewModel.waterfallColumns.flatMap { $0.windows.map(\.primarySpaceLabel) }, ["01", "02"]) |
| | | } |
| | | |
| | | func testQuickSwitchFocusPolicySelectsFirstStableWindowInitially() { |
| | | let app = makeApp() |
| | | let snapshot = QuickSwitchSnapshotBuilder.snapshot( |