From 6a98c5ad23e9c74475a64a9c48141b2a523d30db Mon Sep 17 00:00:00 2001 From: Focus2321 Date: Wed, 22 Jul 2026 16:35:21 -0500 Subject: [PATCH] Fix instant switching window order --- Reef/Models/CyclePanelState.swift | 17 ++++++++++ Reef/UI/CyclePanel/CyclePanelController.swift | 32 +++++++++++++++++-- ReefTests/InstantSwitchModeTests.swift | 19 +++++++++++ 3 files changed, 65 insertions(+), 3 deletions(-) diff --git a/Reef/Models/CyclePanelState.swift b/Reef/Models/CyclePanelState.swift index 349c8ca..c192b7c 100644 --- a/Reef/Models/CyclePanelState.swift +++ b/Reef/Models/CyclePanelState.swift @@ -118,6 +118,23 @@ final class CyclePanelState: ObservableObject { return (focusedIndex + 1) % windowIDs.count } + nonisolated static func reconciledWindowIDs( + previousWindowIDs: [CGWindowID], + availableWindowIDs: [CGWindowID] + ) -> [CGWindowID] { + let availableWindowIDSet = Set(availableWindowIDs) + var includedWindowIDs = Set() + + let existingWindowIDs = previousWindowIDs.filter { + availableWindowIDSet.contains($0) && includedWindowIDs.insert($0).inserted + } + let newWindowIDs = availableWindowIDs.filter { + includedWindowIDs.insert($0).inserted + } + + return existingWindowIDs + newWindowIDs + } + func removeCurrentItem() { guard items.indices.contains(selectedIndex) else { return } diff --git a/Reef/UI/CyclePanel/CyclePanelController.swift b/Reef/UI/CyclePanel/CyclePanelController.swift index 897c2e8..66869cd 100644 --- a/Reef/UI/CyclePanel/CyclePanelController.swift +++ b/Reef/UI/CyclePanel/CyclePanelController.swift @@ -17,6 +17,8 @@ final class CyclePanelController: NSObject { private var keyDownMonitor: Any? private var currentApplication: Application? private var panelAnchorCenter: CGPoint? + private var directCycleApplication: Application? + private var directCycleWindowIDs: [CGWindowID] = [] private let panelContentWidth: CGFloat = 400 private let maxPanelFrameHeightCap: CGFloat = 520 @@ -158,15 +160,39 @@ final class CyclePanelController: NSObject { return frontApplication.getFocusedWindow()?.cgWindowID }() - let windowIDs = windows.map(\.cgWindowID) + let availableWindowIDs = windows.compactMap(\.cgWindowID) + guard availableWindowIDs.count == windows.count else { + let windowIDs = windows.map(\.cgWindowID) + guard let index = CyclePanelState.nextWindowIndex( + windowIDs: windowIDs, + focusedWindowID: focusedWindowID + ) else { + return + } + + windows[index].focus() + return + } + + if directCycleApplication.map({ isSameApplication($0, application) }) != true { + directCycleApplication = application + directCycleWindowIDs = [] + } + + directCycleWindowIDs = CyclePanelState.reconciledWindowIDs( + previousWindowIDs: directCycleWindowIDs, + availableWindowIDs: availableWindowIDs + ) + guard let index = CyclePanelState.nextWindowIndex( - windowIDs: windowIDs, + windowIDs: directCycleWindowIDs.map(Optional.some), focusedWindowID: focusedWindowID ) else { return } - windows[index].focus() + let nextWindowID = directCycleWindowIDs[index] + windows.first { $0.cgWindowID == nextWindowID }?.focus() } func isShowingSwitcher(for application: Application) -> Bool { diff --git a/ReefTests/InstantSwitchModeTests.swift b/ReefTests/InstantSwitchModeTests.swift index cd40ea7..3fd9dd9 100644 --- a/ReefTests/InstantSwitchModeTests.swift +++ b/ReefTests/InstantSwitchModeTests.swift @@ -60,4 +60,23 @@ struct InstantSwitchModeTests { #expect(index == 0) } + + @Test func directSwitchKeepsStableOrderWhenFocusedWindowMovesToFront() { + let initialOrder = CyclePanelState.reconciledWindowIDs( + previousWindowIDs: [], + availableWindowIDs: [10, 20, 30] + ) + let reorderedWindows = CyclePanelState.reconciledWindowIDs( + previousWindowIDs: initialOrder, + availableWindowIDs: [20, 10, 30] + ) + let nextIndex = CyclePanelState.nextWindowIndex( + windowIDs: reorderedWindows.map(Optional.some), + focusedWindowID: 20 + ) + let nextWindowID = nextIndex.map { reorderedWindows[$0] } + + #expect(reorderedWindows == [10, 20, 30]) + #expect(nextWindowID == 30) + } }