From 3d8d37306e39db5c8eb41fafd95a13112d573f62 Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Thu, 11 Jun 2026 22:24:39 +0800
Subject: [PATCH] Center filtered Waterfall columns

---
 C1.source/Sources/AlignerCore/Windows/QuickSwitchViewModel.swift |  179 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
 1 files changed, 169 insertions(+), 10 deletions(-)

diff --git a/C1.source/Sources/AlignerCore/Windows/QuickSwitchViewModel.swift b/C1.source/Sources/AlignerCore/Windows/QuickSwitchViewModel.swift
index 49adc7b..1bec9df 100644
--- a/C1.source/Sources/AlignerCore/Windows/QuickSwitchViewModel.swift
+++ b/C1.source/Sources/AlignerCore/Windows/QuickSwitchViewModel.swift
@@ -5,17 +5,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 {
@@ -197,7 +200,8 @@
             displays: displays,
             appShelf: appShelf,
             waterfallColumns: columns,
-            initialSelection: QuickSwitchFocusPolicy.initialSelection(in: snapshot)
+            initialSelection: QuickSwitchFocusPolicy.initialSelection(in: snapshot),
+            lockedSpaceID: nil
         )
     }
 
@@ -208,8 +212,11 @@
         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)
@@ -220,7 +227,7 @@
                             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,
@@ -256,14 +263,13 @@
 
     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)
             }
         }
 
@@ -328,4 +334,157 @@
 
         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)"
+    }
+}
+
+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