From 3bd3e220be93991f6cac48f60dc5a0551204746f Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Sat, 20 Jun 2026 00:27:17 +0800
Subject: [PATCH] Freeze 0.1 release version
---
C1.source/Sources/AlignerCore/Windows/QuickSwitchViewModel.swift | 217 ++++++++++++++++++++++++++++++++++++++++++++++++++++-
1 files changed, 211 insertions(+), 6 deletions(-)
diff --git a/C1.source/Sources/AlignerCore/Windows/QuickSwitchViewModel.swift b/C1.source/Sources/AlignerCore/Windows/QuickSwitchViewModel.swift
index 1686294..18bc70a 100644
--- a/C1.source/Sources/AlignerCore/Windows/QuickSwitchViewModel.swift
+++ b/C1.source/Sources/AlignerCore/Windows/QuickSwitchViewModel.swift
@@ -1,3 +1,4 @@
+import CoreGraphics
import Foundation
public struct QuickSwitchViewModel: Equatable, Sendable {
@@ -5,17 +6,20 @@
public let appShelf: [QuickSwitchAppShelfItemViewModel]
public let waterfallColumns: [QuickSwitchWaterfallColumnViewModel]
public let initialSelection: QuickSwitchSelection?
+ public let lockedSpaceID: UInt64?
public init(
displays: [QuickSwitchDisplayViewModel],
appShelf: [QuickSwitchAppShelfItemViewModel],
waterfallColumns: [QuickSwitchWaterfallColumnViewModel],
- initialSelection: QuickSwitchSelection?
+ initialSelection: QuickSwitchSelection?,
+ lockedSpaceID: UInt64? = nil
) {
self.displays = displays
self.appShelf = appShelf
self.waterfallColumns = waterfallColumns
self.initialSelection = initialSelection
+ self.lockedSpaceID = lockedSpaceID
}
public var spaceCount: Int {
@@ -59,6 +63,7 @@
public let windowCount: Int
public let appCount: Int
public let appNames: [String]
+ public let splitViewAppNames: [String]
public init(
id: UInt64,
@@ -69,7 +74,8 @@
isCurrent: Bool = false,
windowCount: Int,
appCount: Int,
- appNames: [String] = []
+ appNames: [String] = [],
+ splitViewAppNames: [String] = []
) {
self.id = id
self.displayUUID = displayUUID
@@ -80,6 +86,7 @@
self.windowCount = windowCount
self.appCount = appCount
self.appNames = appNames
+ self.splitViewAppNames = splitViewAppNames
}
}
@@ -163,12 +170,14 @@
let windowCountsBySpace = countWindowsBySpace(windowItems)
let appCountsBySpace = countAppsBySpace(snapshot.appGroups)
let appNamesBySpace = appNamesBySpace(snapshot.appGroups)
+ let splitViewAppNamesBySpace = splitViewAppNamesBySpace(snapshot.appGroups)
let displays = displayViewModels(
from: snapshot.displays,
currentSpaceIDs: currentSpaceIDs,
windowCountsBySpace: windowCountsBySpace,
appCountsBySpace: appCountsBySpace,
- appNamesBySpace: appNamesBySpace
+ appNamesBySpace: appNamesBySpace,
+ splitViewAppNamesBySpace: splitViewAppNamesBySpace
)
let appShelf = appShelfViewModels(from: snapshot.appGroups)
@@ -197,7 +206,8 @@
displays: displays,
appShelf: appShelf,
waterfallColumns: columns,
- initialSelection: QuickSwitchFocusPolicy.initialSelection(in: snapshot)
+ initialSelection: QuickSwitchFocusPolicy.initialSelection(in: snapshot),
+ lockedSpaceID: nil
)
}
@@ -206,7 +216,8 @@
currentSpaceIDs: Set<UInt64>,
windowCountsBySpace: [UInt64: Int],
appCountsBySpace: [UInt64: Int],
- appNamesBySpace: [UInt64: [String]]
+ appNamesBySpace: [UInt64: [String]],
+ splitViewAppNamesBySpace: [UInt64: [String]]
) -> [QuickSwitchDisplayViewModel] {
let sortedDisplays = displays
.sorted { $0.uuid.localizedStandardCompare($1.uuid) == .orderedAscending }
@@ -228,7 +239,8 @@
isCurrent: currentSpaceIDs.contains(space.id),
windowCount: windowCountsBySpace[space.id] ?? 0,
appCount: appCountsBySpace[space.id] ?? 0,
- appNames: appNamesBySpace[space.id] ?? []
+ appNames: appNamesBySpace[space.id] ?? [],
+ splitViewAppNames: splitViewAppNamesBySpace[space.id] ?? []
)
}
@@ -305,6 +317,54 @@
return namesBySpace
}
+ private static func splitViewAppNamesBySpace(_ appGroups: [QuickSwitchAppGroup]) -> [UInt64: [String]] {
+ struct Candidate {
+ let appName: String
+ let windowID: UInt32
+ let frame: CGRect
+ }
+
+ var candidatesBySpace: [UInt64: [Candidate]] = [:]
+
+ for group in appGroups {
+ for item in group.windows {
+ guard item.window.isFullscreen,
+ let primarySpaceID = item.primarySpaceID,
+ let frame = item.window.frame,
+ frame.width > 0,
+ frame.height > 0
+ else {
+ continue
+ }
+
+ candidatesBySpace[primarySpaceID, default: []].append(
+ Candidate(
+ appName: group.app.name,
+ windowID: item.window.id,
+ frame: frame
+ )
+ )
+ }
+ }
+
+ return candidatesBySpace.compactMapValues { candidates in
+ guard candidates.count >= 2 else { return nil }
+
+ return candidates
+ .sorted { lhs, rhs in
+ if lhs.frame.midX != rhs.frame.midX {
+ return lhs.frame.midX < rhs.frame.midX
+ }
+ if lhs.frame.midY != rhs.frame.midY {
+ return lhs.frame.midY < rhs.frame.midY
+ }
+ return lhs.windowID < rhs.windowID
+ }
+ .prefix(2)
+ .map(\.appName)
+ }
+ }
+
private static func orderedUnique<T: Hashable>(_ values: [T]) -> [T] {
var seen = Set<T>()
var result: [T] = []
@@ -339,3 +399,148 @@
return "\(displayLabel(for: displayIndex))\(space.index)"
}
}
+
+public enum QuickSwitchSpaceFilterPolicy {
+ public static func projectedViewModel(
+ from viewModel: QuickSwitchViewModel,
+ lockedSpaceID: UInt64?,
+ preferredSelection: QuickSwitchSelection? = nil
+ ) -> QuickSwitchViewModel {
+ guard let lockedSpaceID else {
+ return replacingInitialSelection(
+ in: viewModel,
+ lockedSpaceID: nil,
+ preferredSelection: preferredSelection
+ )
+ }
+
+ var nextGlobalIndex = 0
+ let columns = viewModel.waterfallColumns.compactMap { column -> QuickSwitchWaterfallColumnViewModel? in
+ let windows = column.windows.filter { $0.primarySpaceID == lockedSpaceID }
+ guard !windows.isEmpty else { return nil }
+
+ let reindexedWindows = windows.enumerated().map { windowIndex, card in
+ defer { nextGlobalIndex += 1 }
+ return QuickSwitchWindowCardViewModel(
+ appGroupIndex: column.appGroupIndex,
+ windowIndex: windowIndex,
+ globalIndex: nextGlobalIndex,
+ window: card.window,
+ primarySpaceID: card.primarySpaceID,
+ primarySpaceLabel: card.primarySpaceLabel
+ )
+ }
+
+ return QuickSwitchWaterfallColumnViewModel(
+ appGroupIndex: column.appGroupIndex,
+ app: column.app,
+ windows: reindexedWindows
+ )
+ }
+
+ let columnsByAppGroupIndex = Dictionary(uniqueKeysWithValues: columns.map { ($0.appGroupIndex, $0) })
+ let appShelf = viewModel.appShelf.compactMap { item -> QuickSwitchAppShelfItemViewModel? in
+ guard let column = columnsByAppGroupIndex[item.appGroupIndex] else { return nil }
+ return QuickSwitchAppShelfItemViewModel(
+ appGroupIndex: item.appGroupIndex,
+ app: item.app,
+ windowCount: column.windows.count,
+ primarySpaceIDs: orderedPrimarySpaceIDs(in: column.windows),
+ hasMinimizedWindows: column.windows.contains { $0.window.isMinimized },
+ hasFullscreenWindows: column.windows.contains { $0.window.isFullscreen }
+ )
+ }
+
+ return QuickSwitchViewModel(
+ displays: viewModel.displays,
+ appShelf: appShelf,
+ waterfallColumns: columns,
+ initialSelection: initialSelection(in: columns, preferredSelection: preferredSelection),
+ lockedSpaceID: lockedSpaceID
+ )
+ }
+
+ public static func singleFullscreenSelection(
+ inSpaceID spaceID: UInt64,
+ viewModel: QuickSwitchViewModel
+ ) -> QuickSwitchSelection? {
+ guard viewModel.displays
+ .flatMap(\.spaces)
+ .contains(where: { $0.id == spaceID && $0.type == .fullscreen })
+ else {
+ return nil
+ }
+
+ let cards = viewModel.waterfallColumns
+ .flatMap(\.windows)
+ .filter { $0.primarySpaceID == spaceID }
+
+ guard cards.count == 1,
+ let card = cards.first,
+ card.window.isFullscreen
+ else {
+ return nil
+ }
+
+ return QuickSwitchSelection(
+ appGroupIndex: card.appGroupIndex,
+ windowIndex: card.windowIndex,
+ windowID: card.window.id
+ )
+ }
+
+ private static func replacingInitialSelection(
+ in viewModel: QuickSwitchViewModel,
+ lockedSpaceID: UInt64?,
+ preferredSelection: QuickSwitchSelection?
+ ) -> QuickSwitchViewModel {
+ QuickSwitchViewModel(
+ displays: viewModel.displays,
+ appShelf: viewModel.appShelf,
+ waterfallColumns: viewModel.waterfallColumns,
+ initialSelection: initialSelection(in: viewModel.waterfallColumns, preferredSelection: preferredSelection)
+ ?? viewModel.initialSelection,
+ lockedSpaceID: lockedSpaceID
+ )
+ }
+
+ private static func initialSelection(
+ in columns: [QuickSwitchWaterfallColumnViewModel],
+ preferredSelection: QuickSwitchSelection?
+ ) -> QuickSwitchSelection? {
+ if let preferredSelection,
+ let matchingCard = columns
+ .flatMap(\.windows)
+ .first(where: { $0.window.id == preferredSelection.windowID }) {
+ return QuickSwitchSelection(
+ appGroupIndex: matchingCard.appGroupIndex,
+ windowIndex: matchingCard.windowIndex,
+ windowID: matchingCard.window.id
+ )
+ }
+
+ guard let firstCard = columns.first?.windows.first else { return nil }
+ return QuickSwitchSelection(
+ appGroupIndex: firstCard.appGroupIndex,
+ windowIndex: firstCard.windowIndex,
+ windowID: firstCard.window.id
+ )
+ }
+
+ private static func orderedPrimarySpaceIDs(in cards: [QuickSwitchWindowCardViewModel]) -> [UInt64] {
+ var seen = Set<UInt64>()
+ var result: [UInt64] = []
+
+ for card in cards {
+ guard let spaceID = card.primarySpaceID,
+ !seen.contains(spaceID)
+ else {
+ continue
+ }
+ seen.insert(spaceID)
+ result.append(spaceID)
+ }
+
+ return result
+ }
+}
--
Gitblit v1.9.3