From 6836a94c95b00a383c5a1e0cc0fb53d119be3122 Mon Sep 17 00:00:00 2001 From: Pascal Date: Wed, 17 Jun 2026 01:28:58 -0400 Subject: [PATCH] Add multi-ruler manager and lifecycle --- Free Ruler/AppDelegate.swift | 163 ++++++++++++++++-- Free Ruler/GroupedRulerWindow.swift | 254 ++++++++++++++++++++++++++-- FreeRulerTests/RulerCoreTests.swift | 72 ++++++++ 3 files changed, 459 insertions(+), 30 deletions(-) diff --git a/Free Ruler/AppDelegate.swift b/Free Ruler/AppDelegate.swift index dd81390..204472a 100644 --- a/Free Ruler/AppDelegate.swift +++ b/Free Ruler/AppDelegate.swift @@ -62,6 +62,13 @@ class AppDelegate: NSObject, NSApplicationDelegate { var rulers: [RulerController] = [] var groupedRulerController: GroupedRulerController? + lazy var rulerManager: RulerManager = { + let manager = RulerManager() + manager.onActiveControllerChanged = { [weak self] controller in + self?.groupedRulerController = controller + } + return manager + }() var timer: Timer? private var timerInterval: TimeInterval? @@ -269,7 +276,10 @@ class AppDelegate: NSObject, NSApplicationDelegate { }, prefs.observe(\Prefs.floatRulers, options: .new) { prefs, changed in self.updateFloatRulersMenuItem() - self.groupedRulerController?.updateIsFloatingPanel() + for controller in self.rulerManager.controllers { + controller.updateIsFloatingPanel() + } + self.legacyGroupedRulerController?.updateIsFloatingPanel() self.uiTestSupport?.writePreferencesState() }, prefs.observe(\Prefs.groupRulers, options: .new) { prefs, changed in @@ -279,7 +289,10 @@ class AppDelegate: NSObject, NSApplicationDelegate { }, prefs.observe(\Prefs.rulerShadow, options: .new) { prefs, changed in self.updateRulerShadowMenuItem() - self.groupedRulerController?.updateHasShadow() + for controller in self.rulerManager.controllers { + controller.updateHasShadow() + } + self.legacyGroupedRulerController?.updateHasShadow() self.uiTestSupport?.writePreferencesState() }, prefs.observe(\Prefs.rulerColor, options: .new) { prefs, changed in @@ -308,7 +321,10 @@ class AppDelegate: NSObject, NSApplicationDelegate { for ruler in rulers { ruler.rulerWindow.rule.redrawForPreferenceChange() } - groupedRulerController?.redrawForPreferenceChange() + for controller in rulerManager.controllers { + controller.redrawForPreferenceChange() + } + legacyGroupedRulerController?.redrawForPreferenceChange() } func updateFloatRulersMenuItem() { @@ -324,6 +340,12 @@ class AppDelegate: NSObject, NSApplicationDelegate { } func createRulersIfNeeded() { + guard !rulerManager.hasRulers else { return } + + rulerManager.createRuler() + } + + private func createLegacyRulersIfNeeded() { guard rulers.isEmpty else { return } rulers = [ @@ -346,11 +368,18 @@ class AppDelegate: NSObject, NSApplicationDelegate { func showRulers() { createRulersIfNeeded() - rulerVisibility.showAll() - applyRulerWindowMode(showRulersIfNeeded: true) + rulerManager.showAll() + updateMouseTickTimer() } func toggleRuler(orientation: Orientation) { + if rulerManager.hasRulers { + let controller = rulerManager.activeController ?? rulerManager.createRuler() + controller.toggleWing(orientation) + updateMouseTickTimer() + return + } + guard canToggleRulerVisibility else { return } guard rulerController(orientation: orientation) != nil else { return } @@ -370,7 +399,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { } private func rulerController(orientation: Orientation) -> RulerController? { - createRulersIfNeeded() + createLegacyRulersIfNeeded() return existingRulerController(orientation: orientation) } @@ -400,7 +429,13 @@ class AppDelegate: NSObject, NSApplicationDelegate { } private func applyRulerWindowMode(showRulersIfNeeded: Bool = false) { - createRulersIfNeeded() + if rulerManager.hasRulers { + rulerManager.showAll() + updateMouseTickTimer() + return + } + + createLegacyRulersIfNeeded() detachRulerWindows() switch rulerWindowMode { @@ -494,12 +529,25 @@ class AppDelegate: NSObject, NSApplicationDelegate { return groupedRulerController?.isVisible == true } + private var legacyGroupedRulerController: GroupedRulerController? { + guard let groupedRulerController = groupedRulerController, + !rulerManager.controllers.contains(where: { $0 === groupedRulerController }) else { + return nil + } + + return groupedRulerController + } + private func isRulerVisible(_ ruler: RulerController?) -> Bool { guard let ruler = ruler else { return false } return rulerVisibility.isVisible(ruler.ruler.orientation) } private var isRulerFrontmost: Bool { + if rulerManager.controllers.contains(where: { $0.groupedWindow.isKeyWindow }) { + return true + } + if groupedRulerController?.groupedWindow.isKeyWindow == true { return true } @@ -508,7 +556,9 @@ class AppDelegate: NSObject, NSApplicationDelegate { } private var hasVisibleRuler: Bool { - return isGroupedRulerVisible || rulers.contains { $0.rulerWindow.isVisible } + return rulerManager.hasVisibleRulers + || isGroupedRulerVisible + || rulers.contains { $0.rulerWindow.isVisible } } private var canToggleRulerVisibility: Bool { @@ -528,7 +578,10 @@ class AppDelegate: NSObject, NSApplicationDelegate { for ruler in rulers { ruler.foreground() } - groupedRulerController?.foreground() + for controller in rulerManager.controllers { + controller.foreground() + } + legacyGroupedRulerController?.foreground() mouseTickTimerPolicy.applicationDidBecomeActive() updateMouseTickTimer() @@ -540,7 +593,10 @@ class AppDelegate: NSObject, NSApplicationDelegate { for ruler in rulers { ruler.background() } - groupedRulerController?.background() + for controller in rulerManager.controllers { + controller.background() + } + legacyGroupedRulerController?.background() mouseTickTimerPolicy.applicationDidResignActive() updateMouseTickTimer() @@ -600,7 +656,26 @@ class AppDelegate: NSObject, NSApplicationDelegate { } } + @IBAction func newRuler(_ sender: Any) { + let controller = rulerManager.createRuler() + controller.show() + updateMouseTickTimer() + } + @IBAction func closeKeyWindow(_ sender: Any) { + if let controller = rulerManager.controller(containing: NSApp.keyWindow) { + rulerManager.close(controller) + updateMouseTickTimer() + return + } + + if rulerManager.hasRulers, + NSApp.keyWindow == nil, + rulerManager.closeActiveRuler() { + updateMouseTickTimer() + return + } + if let groupedRulerController = groupedRulerController, groupedRulerController.groupedWindow.isKeyWindow { syncGroupedRulerFramesToRulerWindows(persistAutosave: true) @@ -623,6 +698,11 @@ class AppDelegate: NSObject, NSApplicationDelegate { mouseLoc.x = mouseLoc.x.rounded() mouseLoc.y = mouseLoc.y.rounded() + if let controller = rulerManager.activeController { + controller.align(at: mouseLoc) + return + } + if prefs.groupRulers, let groupedRulerController = groupedRulerController, groupedRulerController.isVisible { @@ -637,7 +717,21 @@ class AppDelegate: NSObject, NSApplicationDelegate { } @IBAction func resetRulerPositions(_ sender: Any) { - createRulersIfNeeded() + if rulerManager.hasRulers { + prefs.zeroCorner = Prefs.defaultZeroCorner + for controller in rulerManager.controllers { + controller.state.settings.zeroCorner = Prefs.defaultZeroCorner + controller.state.layout = RulerLayoutState.defaults( + zeroCorner: Prefs.defaultZeroCorner + ) + controller.state.visibility = RulerWingVisibility() + controller.show() + } + updateMouseTickTimer() + return + } + + createLegacyRulersIfNeeded() prefs.zeroCorner = Prefs.defaultZeroCorner @@ -676,7 +770,16 @@ class AppDelegate: NSObject, NSApplicationDelegate { } func flipRulers(along orientation: Orientation) { - createRulersIfNeeded() + if let controller = rulerManager.activeController { + let flippedCorner = prefs.zeroCorner.flipped(along: orientation) + controller.prepareForZeroCornerChange(to: flippedCorner) + controller.state.settings.zeroCorner = flippedCorner + prefs.zeroCorner = flippedCorner + controller.redrawForPreferenceChange() + return + } + + createLegacyRulersIfNeeded() let oldGeometry = ZeroCornerGeometry(zeroCorner: prefs.zeroCorner) let flippedCorner = prefs.zeroCorner.flipped(along: orientation) @@ -757,6 +860,10 @@ class AppDelegate: NSObject, NSApplicationDelegate { modifierFlags: NSEvent.ModifierFlags, sender: Any ) -> Bool { + if let controller = sender as? GroupedRulerController { + rulerManager.markActive(controller) + } + let keyboardModifiers = modifierFlags .intersection(.deviceIndependentFlagsMask) .subtracting(.capsLock) @@ -827,8 +934,8 @@ class AppDelegate: NSObject, NSApplicationDelegate { return groupedRulerController.groupedWindow.screen } - if groupedRulerController?.groupedWindow.isKeyWindow == true { - return groupedRulerController?.groupedWindow.screen + if let activeController = rulerManager.activeController { + return activeController.groupedWindow.screen } return rulers.first { $0.rulerWindow.isKeyWindow }?.rulerWindow.screen @@ -863,15 +970,31 @@ extension AppDelegate: NSMenuItemValidation { func validateMenuItem(_ menuItem: NSMenuItem) -> Bool { switch menuItem.action { + case #selector(newRuler(_:)): + return true case #selector(closeKeyWindow(_:)): return NSApp.keyWindow?.isVisible == true case #selector(toggleHorizontalRuler(_:)): + if let controller = rulerManager.activeController { + menuItem.title = controller.state.isWingVisible(.horizontal) + ? NSLocalizedString("Hide Horizontal Ruler", comment: "Menu item title to hide the horizontal ruler") + : NSLocalizedString("Show Horizontal Ruler", comment: "Menu item title to show the horizontal ruler") + return true + } + let ruler = existingRulerController(orientation: .horizontal) menuItem.title = isRulerVisible(ruler) ? NSLocalizedString("Hide Horizontal Ruler", comment: "Menu item title to hide the horizontal ruler") : NSLocalizedString("Show Horizontal Ruler", comment: "Menu item title to show the horizontal ruler") return canToggleRulerVisibility case #selector(toggleVerticalRuler(_:)): + if let controller = rulerManager.activeController { + menuItem.title = controller.state.isWingVisible(.vertical) + ? NSLocalizedString("Hide Vertical Ruler", comment: "Menu item title to hide the vertical ruler") + : NSLocalizedString("Show Vertical Ruler", comment: "Menu item title to show the vertical ruler") + return true + } + let ruler = existingRulerController(orientation: .vertical) menuItem.title = isRulerVisible(ruler) ? NSLocalizedString("Hide Vertical Ruler", comment: "Menu item title to hide the vertical ruler") @@ -930,7 +1053,10 @@ extension AppDelegate { ruler.rulerWindow.rule.showMouseTick = isEnabled } - groupedRulerController?.setMouseTickDrawingEnabled(isEnabled) + for controller in rulerManager.controllers { + controller.setMouseTickDrawingEnabled(isEnabled) + } + legacyGroupedRulerController?.setMouseTickDrawingEnabled(isEnabled) } private func updateMouseTickTimer() { @@ -974,6 +1100,13 @@ extension AppDelegate { mouseLoc.x = mouseLoc.x.rounded() mouseLoc.y = mouseLoc.y.rounded() + if rulerManager.hasRulers { + for controller in rulerManager.controllers where controller.isVisible { + controller.drawMouseTick(at: mouseLoc) + } + return + } + if let groupedRulerController = groupedRulerController, groupedRulerController.isVisible { groupedRulerController.drawMouseTick(at: mouseLoc) diff --git a/Free Ruler/GroupedRulerWindow.swift b/Free Ruler/GroupedRulerWindow.swift index 80026ef..0fca3e8 100644 --- a/Free Ruler/GroupedRulerWindow.swift +++ b/Free Ruler/GroupedRulerWindow.swift @@ -1069,6 +1069,9 @@ final class GroupedRulerController: NSWindowController, NSWindowDelegate, Notifi var notificationObservers: [NSObjectProtocol] = [] let groupedWindow: GroupedRulerWindow + var state: RulerInstanceState + var onBecameActive: ((GroupedRulerController) -> Void)? + var onStateChanged: ((GroupedRulerController) -> Void)? private var keyListener: Any? private var mouseInteraction: RulerMouseInteractionState! @@ -1093,8 +1096,29 @@ final class GroupedRulerController: NSWindowController, NSWindowDelegate, Notifi } } - init(frame: NSRect) { - groupedWindow = GroupedRulerWindow(frame: frame) + convenience init(frame: NSRect) { + let layout = GroupedRulerLayout.layout(groupFrame: frame, zeroCorner: prefs.zeroCorner) + let state = RulerInstanceState( + settings: RulerSettings(defaults: prefs), + layout: RulerLayoutState( + horizontalFrame: layout.horizontalFrame, + verticalFrame: layout.verticalFrame, + zeroCorner: prefs.zeroCorner + ) + ) + + self.init(state: state) + } + + init(state: RulerInstanceState) { + self.state = state + let layout = state.layout.layout(zeroCorner: state.settings.zeroCorner) + groupedWindow = GroupedRulerWindow( + frame: layout.visibleFrame( + showsHorizontalRule: state.visibility.showsHorizontal, + showsVerticalRule: state.visibility.showsVertical + ) + ) super.init(window: groupedWindow) createObservers() @@ -1105,6 +1129,7 @@ final class GroupedRulerController: NSWindowController, NSWindowDelegate, Notifi mouseInteraction = RulerMouseInteractionState(owner: self) { [weak self] event in return self?.mouseIsInsideRuler(with: event) ?? false } + applyStateToWindow(display: false) } required init?(coder: NSCoder) { @@ -1121,38 +1146,53 @@ final class GroupedRulerController: NSWindowController, NSWindowDelegate, Notifi return groupedWindow.isVisible } + func show() { + applyStateToWindow(display: false) + showWindow(self) + groupedWindow.orderFrontRegardless() + } + func show( horizontalFrame: NSRect, verticalFrame: NSRect, showsHorizontalRule: Bool, showsVerticalRule: Bool ) { - let layout = GroupedRulerLayout.joined( + state.layout = RulerLayoutState( horizontalFrame: horizontalFrame, verticalFrame: verticalFrame, zeroCorner: prefs.zeroCorner ) - groupedWindow.setVisibleRules( + state.settings.zeroCorner = prefs.zeroCorner + state.visibility = RulerWingVisibility( horizontal: showsHorizontalRule, vertical: showsVerticalRule ) - updateMouseTickDrawingVisibility() - groupedWindow.setFrame( - layout.visibleFrame( - showsHorizontalRule: showsHorizontalRule, - showsVerticalRule: showsVerticalRule - ), - display: false - ) - groupedWindow.updateLayoutForCurrentZeroCorner() - showWindow(self) - groupedWindow.orderFrontRegardless() + show() } func hide() { groupedWindow.orderOut(self) } + @discardableResult + func toggleWing(_ orientation: Orientation) -> Bool { + guard state.toggleWing(orientation) else { return false } + + applyStateToWindow(display: true) + notifyStateChanged() + return true + } + + @discardableResult + func setWing(_ orientation: Orientation, isVisible: Bool) -> Bool { + guard state.setWing(orientation, isVisible: isVisible) else { return false } + + applyStateToWindow(display: true) + notifyStateChanged() + return true + } + func syncFrames( to horizontalWindow: RulerWindow, and verticalWindow: RulerWindow, @@ -1181,6 +1221,7 @@ final class GroupedRulerController: NSWindowController, NSWindowDelegate, Notifi groupedWindow.setFrame(groupedWindow.visibleFrame(in: layout), display: true) groupedWindow.updateLayoutForCurrentZeroCorner() + captureStateFromWindow() } func prepareForZeroCornerChange(to zeroCorner: ZeroCorner) { @@ -1195,6 +1236,8 @@ final class GroupedRulerController: NSWindowController, NSWindowDelegate, Notifi ) groupedWindow.setFrame(groupedWindow.visibleFrame(in: layout), display: true) + state.settings.zeroCorner = zeroCorner + captureStateFromWindow() } func foreground() { @@ -1238,6 +1281,51 @@ final class GroupedRulerController: NSWindowController, NSWindowDelegate, Notifi && groupedWindow.isRuleVisible(.vertical) } + private func applyStateToWindow(display: Bool) { + let zeroCorner = state.settings.zeroCorner + let layout = state.layout.layout(zeroCorner: zeroCorner) + groupedWindow.setVisibleRules( + horizontal: state.visibility.showsHorizontal, + vertical: state.visibility.showsVertical + ) + updateMouseTickDrawingVisibility() + groupedWindow.setFrame( + layout.visibleFrame( + showsHorizontalRule: state.visibility.showsHorizontal, + showsVerticalRule: state.visibility.showsVertical + ), + display: display + ) + groupedWindow.updateLayoutForCurrentZeroCorner() + } + + private func captureStateFromWindow() { + var horizontalLength = state.layout.horizontalLength + var verticalLength = state.layout.verticalLength + + if groupedWindow.isRuleVisible(.horizontal) { + horizontalLength = groupedWindow.screenFrame(for: .horizontal).width + } + if groupedWindow.isRuleVisible(.vertical) { + verticalLength = groupedWindow.screenFrame(for: .vertical).height + } + + state.layout = RulerLayoutState( + zeroPoint: groupedWindow.zeroPoint(), + horizontalLength: horizontalLength, + verticalLength: verticalLength + ) + state.visibility = RulerWingVisibility( + horizontal: groupedWindow.isRuleVisible(.horizontal), + vertical: groupedWindow.isRuleVisible(.vertical) + ) + notifyStateChanged() + } + + private func notifyStateChanged() { + onStateChanged?(self) + } + private func syncedRulerFrames( horizontalWindow: RulerWindow, verticalWindow: RulerWindow @@ -1312,6 +1400,7 @@ final class GroupedRulerController: NSWindowController, NSWindowDelegate, Notifi func windowDidEndLiveResize(_ notification: Notification) { syncRulerWindowFrames(persistAutosave: true) + captureStateFromWindow() mouseInteraction.windowDidEndLiveResize() } @@ -1326,10 +1415,12 @@ final class GroupedRulerController: NSWindowController, NSWindowDelegate, Notifi isLeftMouseButtonPressed: isLeftMouseButtonPressed() ) ) + captureStateFromWindow() mouseInteraction.windowDidMove(isLeftMouseButtonPressed: isLeftMouseButtonPressed()) } func windowDidBecomeKey(_ notification: Notification) { + onBecameActive?(self) startKeyListener() } @@ -1356,6 +1447,7 @@ final class GroupedRulerController: NSWindowController, NSWindowDelegate, Notifi func finishMouseDrag(with event: NSEvent) { if mouseInteraction.finishMouseDrag(with: event) { syncRulerWindowFrames(persistAutosave: true) + captureStateFromWindow() } } @@ -1471,6 +1563,138 @@ extension GroupedRulerController { } } } + +final class RulerManager { + typealias ControllerFactory = (RulerInstanceState) -> GroupedRulerController + + private let controllerFactory: ControllerFactory + private(set) var controllers: [GroupedRulerController] = [] + private(set) var activeRulerID: UUID? + var onActiveControllerChanged: ((GroupedRulerController?) -> Void)? + + init( + initialStates: [RulerInstanceState] = [], + controllerFactory: @escaping ControllerFactory = { GroupedRulerController(state: $0) } + ) { + self.controllerFactory = controllerFactory + restore(initialStates) + } + + var hasRulers: Bool { + return !controllers.isEmpty + } + + var hasVisibleRulers: Bool { + return controllers.contains { $0.isVisible } + } + + var activeController: GroupedRulerController? { + if let activeRulerID = activeRulerID, + let controller = controllers.first(where: { $0.state.id == activeRulerID }) { + return controller + } + + if let keyController = controllers.first(where: { $0.groupedWindow.isKeyWindow }) { + return keyController + } + + return controllers.last + } + + var states: [RulerInstanceState] { + return controllers.map { $0.state } + } + + @discardableResult + func createRuler( + defaults: RulerSettings = RulerSettings(defaults: prefs), + screenFrame: NSRect = defaultRulerScreenFrame() + ) -> GroupedRulerController { + let state = RulerInstanceState.createFromDefaults( + defaults: defaults, + screenFrame: screenFrame + ) + + return addRuler(state: state) + } + + @discardableResult + func addRuler(state: RulerInstanceState) -> GroupedRulerController { + let controller = controllerFactory(state) + configure(controller) + controllers.append(controller) + markActive(controller) + return controller + } + + func restore(_ states: [RulerInstanceState]) { + for controller in controllers { + controller.hide() + } + + controllers = [] + activeRulerID = nil + onActiveControllerChanged?(nil) + + for state in states where state.hasVisibleWing { + addRuler(state: state) + } + } + + func showAll() { + for controller in controllers { + controller.show() + } + + if let activeController = activeController { + activeController.groupedWindow.makeKey() + } + } + + @discardableResult + func closeActiveRuler() -> Bool { + guard let activeController = activeController else { return false } + + close(activeController) + return true + } + + func close(_ controller: GroupedRulerController) { + controller.hide() + controllers.removeAll { $0 === controller } + + if activeRulerID == controller.state.id { + activeRulerID = controllers.last?.state.id + onActiveControllerChanged?(activeController) + } + } + + func markActive(_ controller: GroupedRulerController) { + guard controllers.contains(where: { $0 === controller }) else { return } + + activeRulerID = controller.state.id + onActiveControllerChanged?(controller) + } + + func controller(containing window: NSWindow?) -> GroupedRulerController? { + guard let window = window else { return nil } + + return controllers.first { $0.groupedWindow === window } + } + + private func configure(_ controller: GroupedRulerController) { + controller.onBecameActive = { [weak self, weak controller] _ in + guard let controller = controller else { return } + self?.markActive(controller) + } + controller.onStateChanged = { [weak self, weak controller] _ in + guard let controller = controller, + self?.activeRulerID == controller.state.id else { return } + + self?.activeRulerID = controller.state.id + } + } +} #endif private func groupedRulerLShapedPath( diff --git a/FreeRulerTests/RulerCoreTests.swift b/FreeRulerTests/RulerCoreTests.swift index b6b24ee..12d23e5 100644 --- a/FreeRulerTests/RulerCoreTests.swift +++ b/FreeRulerTests/RulerCoreTests.swift @@ -138,6 +138,78 @@ final class RulerCoreTests: XCTestCase { ) } + func testRulerManagerCreatesTracksActivatesAndClosesRulers() { + let manager = RulerManager() + defer { + for controller in manager.controllers { + controller.hide() + } + } + + let first = manager.createRuler( + defaults: RulerSettings(unit: .pixels), + screenFrame: NSRect(x: 0, y: 0, width: 1000, height: 800) + ) + let second = manager.createRuler( + defaults: RulerSettings(unit: .inches), + screenFrame: NSRect(x: 0, y: 0, width: 1000, height: 800) + ) + + XCTAssertEqual(manager.controllers.count, 2) + XCTAssertTrue(manager.activeController === second) + + manager.markActive(first) + XCTAssertTrue(manager.activeController === first) + + XCTAssertTrue(manager.closeActiveRuler()) + XCTAssertEqual(manager.controllers.count, 1) + XCTAssertTrue(manager.activeController === second) + XCTAssertEqual(manager.states.map(\.settings.unit), [.inches]) + } + + func testRulerManagerRestoresStatesAndShowsAllControllers() { + let firstID = UUID(uuidString: "F775A858-ED72-4242-B84B-E08B27EE1C9F")! + let secondID = UUID(uuidString: "D922071D-D02B-4DF7-8762-3497D9FD90B4")! + let manager = RulerManager(initialStates: [ + RulerInstanceState( + id: firstID, + settings: RulerSettings(unit: .pixels), + visibility: RulerWingVisibility(horizontal: true, vertical: false), + layout: RulerLayoutState( + zeroPoint: NSPoint(x: 200, y: 300), + horizontalLength: 320, + verticalLength: 180 + ) + ), + RulerInstanceState( + id: secondID, + settings: RulerSettings(unit: .millimeters), + visibility: RulerWingVisibility(horizontal: false, vertical: true), + layout: RulerLayoutState( + zeroPoint: NSPoint(x: 400, y: 500), + horizontalLength: 220, + verticalLength: 280 + ) + ), + ]) + defer { + for controller in manager.controllers { + controller.hide() + } + } + + XCTAssertEqual(manager.controllers.map(\.state.id), [firstID, secondID]) + XCTAssertTrue(manager.activeController === manager.controllers.last) + + manager.showAll() + + XCTAssertTrue(manager.hasVisibleRulers) + XCTAssertTrue(manager.controllers[0].groupedWindow.isRuleVisible(.horizontal)) + XCTAssertFalse(manager.controllers[0].groupedWindow.isRuleVisible(.vertical)) + XCTAssertFalse(manager.controllers[1].groupedWindow.isRuleVisible(.horizontal)) + XCTAssertTrue(manager.controllers[1].groupedWindow.isRuleVisible(.vertical)) + } + func testZeroCornerRawValuesPreservePersistedOrder() { XCTAssertEqual(ZeroCorner.topLeft.rawValue, 0) XCTAssertEqual(ZeroCorner.topRight.rawValue, 1)