From 7b14edfb1667ca6078786e11d81c9a7ed0a30ea1 Mon Sep 17 00:00:00 2001 From: Kami Date: Mon, 2 Feb 2026 17:26:55 +1000 Subject: [PATCH 1/3] =?UTF-8?q?=E2=9C=A8=20Misc=20updates?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Loop/App/AppDelegate.swift | 3 +- Loop/Extensions/Defaults+Extensions.swift | 1 + Loop/Extensions/NSScreen+Extensions.swift | 59 ++++++++++++++++++ Loop/Localizable.xcstrings | 10 +++ .../Behavior/BehaviorConfiguration.swift | 3 + .../StashActionConfigurationView.swift | 6 +- Loop/Stashing/StashDirection.swift | 26 +++++++- Loop/Stashing/StashManager.swift | 62 ++++++++++++++----- Loop/Stashing/StashedWindowInfo.swift | 26 ++++++-- .../WindowFrameResolver.swift | 6 +- 10 files changed, 172 insertions(+), 30 deletions(-) diff --git a/Loop/App/AppDelegate.swift b/Loop/App/AppDelegate.swift index b8002f43..a31d68da 100644 --- a/Loop/App/AppDelegate.swift +++ b/Loop/App/AppDelegate.swift @@ -31,7 +31,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate { await Defaults.iCloud.waitForSyncCompletion() } - if !launchedAsLoginItem { + // Show settings window only if not launched as login item AND startHidden is disabled + if !launchedAsLoginItem, !Defaults[.startHidden] { SettingsWindowManager.shared.show() } else { // Closing also hides the dock icon if needed. diff --git a/Loop/Extensions/Defaults+Extensions.swift b/Loop/Extensions/Defaults+Extensions.swift index 6311028d..ed9aa172 100644 --- a/Loop/Extensions/Defaults+Extensions.swift +++ b/Loop/Extensions/Defaults+Extensions.swift @@ -41,6 +41,7 @@ extension Defaults.Keys { // Behavior static let launchAtLogin = Key("launchAtLogin", default: false, iCloud: true) + static let startHidden = Key("startHidden", default: false, iCloud: true) static let hideMenuBarIcon = Key("hideMenuBarIcon", default: false, iCloud: false) static let animationConfiguration = Key("animationConfiguration", default: .snappy, iCloud: true) static let windowSnapping = Key("windowSnapping", default: false, iCloud: true) diff --git a/Loop/Extensions/NSScreen+Extensions.swift b/Loop/Extensions/NSScreen+Extensions.swift index 91ad9d17..f9b47f85 100644 --- a/Loop/Extensions/NSScreen+Extensions.swift +++ b/Loop/Extensions/NSScreen+Extensions.swift @@ -130,10 +130,23 @@ extension NSScreen { return max(0, bottom - top) } + private func horizontalOverlap(with other: NSScreen) -> CGFloat { + let a = frame + let b = other.frame + + let left = max(a.minX, b.minX) + let right = min(a.maxX, b.maxX) + return max(0, right - left) + } + private func screensInSameRow(screens: [NSScreen], overlapThreshold: CGFloat = 10.0) -> [NSScreen] { screens.filter { verticalOverlap(with: $0) >= overlapThreshold } } + private func screensInSameColumn(screens: [NSScreen], overlapThreshold: CGFloat = 10.0) -> [NSScreen] { + screens.filter { horizontalOverlap(with: $0) >= overlapThreshold } + } + func leftmostScreenInSameRow(overlapThreshold: CGFloat = 10.0) -> NSScreen { let sameRowScreens = screensInSameRow(screens: NSScreen.screens, overlapThreshold: overlapThreshold) @@ -179,4 +192,50 @@ extension NSScreen { return bestScreen ?? self } + + func topmostScreenInSameColumn(overlapThreshold: CGFloat = 10.0) -> NSScreen { + let sameColumnScreens = screensInSameColumn(screens: NSScreen.screens, overlapThreshold: overlapThreshold) + + let topCandidates = sameColumnScreens.filter { $0.frame.maxY <= self.frame.minY } + + guard !topCandidates.isEmpty else { + return self + } + + var bestScreen: NSScreen? = nil + var bestOverlap: CGFloat = -1 + + for screen in topCandidates { + let overlap = horizontalOverlap(with: screen) + if overlap > bestOverlap || (overlap == bestOverlap && screen.frame.minY < bestScreen?.frame.minY ?? .infinity) { + bestScreen = screen + bestOverlap = overlap + } + } + + return bestScreen ?? self + } + + func bottommostScreenInSameColumn(overlapThreshold: CGFloat = 10.0) -> NSScreen { + let sameColumnScreens = screensInSameColumn(screens: NSScreen.screens, overlapThreshold: overlapThreshold) + + let bottomCandidates = sameColumnScreens.filter { $0.frame.minY >= self.frame.maxY } + + guard !bottomCandidates.isEmpty else { + return self + } + + var bestScreen: NSScreen? = nil + var bestOverlap: CGFloat = -1 + + for screen in bottomCandidates { + let overlap = horizontalOverlap(with: screen) + if overlap > bestOverlap || (overlap == bestOverlap && screen.frame.maxY > bestScreen?.frame.maxY ?? -.infinity) { + bestScreen = screen + bestOverlap = overlap + } + } + + return bestScreen ?? self + } } diff --git a/Loop/Localizable.xcstrings b/Loop/Localizable.xcstrings index 50ba116a..3fb6ea5b 100644 --- a/Loop/Localizable.xcstrings +++ b/Loop/Localizable.xcstrings @@ -11830,6 +11830,16 @@ } } }, + "Start hidden" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Start hidden" + } + } + } + }, "Left" : { "comment" : "Label for a slider in Loop’s padding settings\nSide of a trigger key", "localizations" : { diff --git a/Loop/Settings Window/Settings/Behavior/BehaviorConfiguration.swift b/Loop/Settings Window/Settings/Behavior/BehaviorConfiguration.swift index 9fbcf858..c08dcf3d 100644 --- a/Loop/Settings Window/Settings/Behavior/BehaviorConfiguration.swift +++ b/Loop/Settings Window/Settings/Behavior/BehaviorConfiguration.swift @@ -13,6 +13,7 @@ struct BehaviorConfigurationView: View { @Environment(\.luminareAnimation) private var luminareAnimation @Default(.launchAtLogin) var launchAtLogin + @Default(.startHidden) var startHidden @Default(.hideMenuBarIcon) var hideMenuBarIcon @Default(.animationConfiguration) var animationConfiguration @Default(.windowSnapping) var windowSnapping @@ -55,6 +56,8 @@ struct BehaviorConfigurationView: View { LuminareSection(String(localized: "General", comment: "Section header shown in settings")) { LuminareToggle("Launch at login", isOn: $launchAtLogin) + LuminareToggle("Start hidden", isOn: $startHidden) + LuminareToggle("Hide menu bar icon", isOn: $hideMenuBarIcon) LuminareSliderPicker( diff --git a/Loop/Settings Window/Settings/Keybinds/Modal Views/StashActionConfigurationView.swift b/Loop/Settings Window/Settings/Keybinds/Modal Views/StashActionConfigurationView.swift index 4ca5a456..5564a086 100644 --- a/Loop/Settings Window/Settings/Keybinds/Modal Views/StashActionConfigurationView.swift +++ b/Loop/Settings Window/Settings/Keybinds/Modal Views/StashActionConfigurationView.swift @@ -36,7 +36,9 @@ struct StashActionConfigurationView: View { private let defaultAnchor: CustomWindowActionAnchor = .topLeft private var anchors: [CustomWindowActionAnchor] { - [.topLeft, .topRight, .left, .right, .bottomLeft, .bottomRight] + [.topLeft, .top, .topRight, + .left, .center, .right, + .bottomLeft, .bottom, .bottomRight] } private var sizeModes: [CustomWindowActionSizeMode] { @@ -192,7 +194,7 @@ struct StashActionConfigurationView: View { } } ), - columns: 2 + columns: 3 ) { anchor in IconView(action: anchor.iconAction) } diff --git a/Loop/Stashing/StashDirection.swift b/Loop/Stashing/StashDirection.swift index b5e55670..bd51a07f 100644 --- a/Loop/Stashing/StashDirection.swift +++ b/Loop/Stashing/StashDirection.swift @@ -11,10 +11,20 @@ import Foundation enum StashEdge: String, CustomDebugStringConvertible { case left case right + case top + case bottom var debugDescription: String { rawValue } + + var isHorizontal: Bool { + self == .left || self == .right + } + + var isVertical: Bool { + self == .top || self == .bottom + } } // MARK: - Helpers @@ -22,9 +32,21 @@ enum StashEdge: String, CustomDebugStringConvertible { extension WindowAction { var stashEdge: StashEdge? { switch direction { - case .stash where [.left, .topLeft, .bottomLeft].contains(anchor): + case .stash where anchor == .left: + .left + case .stash where anchor == .right: + .right + case .stash where anchor == .top: + .top + case .stash where anchor == .bottom: + .bottom + case .stash where anchor == .topLeft: + .left + case .stash where anchor == .topRight: + .right + case .stash where anchor == .bottomLeft: .left - case .stash where [.right, .topRight, .bottomRight].contains(anchor): + case .stash where anchor == .bottomRight: .right default: nil diff --git a/Loop/Stashing/StashManager.swift b/Loop/Stashing/StashManager.swift index eb97eb31..d08681b0 100644 --- a/Loop/Stashing/StashManager.swift +++ b/Loop/Stashing/StashManager.swift @@ -62,7 +62,8 @@ final class StashManager { /// Two windows can be stacked along the same edge of the screen as long as there is enough non-overlapping space /// to allow the user to easily position the cursor over either window. - private let minimumVisibleHeightToKeepWindowStacked: CGFloat = 100 + /// This applies to vertical space for horizontal edges (left/right) and horizontal space for vertical edges (top/bottom). + private let minimumVisibleSizeToKeepWindowStacked: CGFloat = 100 private lazy var store: StashedWindowsStore = { let store = StashedWindowsStore() @@ -373,12 +374,16 @@ private extension StashManager { .mouseMoved, // Normal mouse movement .leftMouseDragged // Dragging items to stashed windows ], - callback: handleMouseMoved + callback: { [weak self] cgEvent in + self?.handleMouseMoved(cgEvent: cgEvent) + } ) monitor.start() mouseMonitor = monitor - frontmostAppMonitor = Task { @MainActor in + frontmostAppMonitor = Task { @MainActor [weak self] in + guard let self else { return } + let notifications = NSWorkspace.shared.notificationCenter.notifications( named: NSWorkspace.didActivateApplicationNotification ) @@ -395,10 +400,21 @@ private extension StashManager { log.info("Stopping listening for reveal triggers…") - mouseMonitor?.stop() - mouseMonitor = nil + // Cancel tasks first frontmostAppMonitor?.cancel() frontmostAppMonitor = nil + + // Stop and release the monitor + // The monitor's deinit will handle cleanup of the event tap + mouseMonitor?.stop() + + // Delay the release to allow the run loop to process the stop + let monitor = mouseMonitor + mouseMonitor = nil + + DispatchQueue.main.async { + _ = monitor // Keep alive until run loop processes the removal + } } /// Handles mouse movement events with a debounce to avoid excessive processing. @@ -523,9 +539,9 @@ private extension StashManager { unstash(stashedWindow, resetFrame: true, resetFrameAnimated: animate) } else { let currentFrame = stashedWindow.computeStashedFrame(peekSize: stashedWindowVisiblePadding) - let tolerance = minimumVisibleHeightToKeepWindowStacked + let tolerance = minimumVisibleSizeToKeepWindowStacked - if !isThereEnoughNonOverlappingSpace(between: newFrame, and: currentFrame, tolerance: tolerance) { + if !isThereEnoughNonOverlappingSpace(between: newFrame, and: currentFrame, edge: windowToStash.action.stashEdge, tolerance: tolerance) { log.info("Trying to stash a window overlapping another one. Replacing…") unstash(stashedWindow, resetFrame: true, resetFrameAnimated: animate) } @@ -535,21 +551,31 @@ private extension StashManager { /// Determines whether two rectangles have enough non-overlapping space between them. /// - /// This function compares the vertical ranges (y-axis) of two rectangles, `rect1` and `rect2`, - /// and checks if they are either non-overlapping or sufficiently offset vertically by at least - /// a given `tolerance` value. This ensures that if windows are stashed along the same edge of the screen, - /// they do not overlap each other and leave enough visible space (as defined by `tolerance`). + /// This function checks if windows stashed along the same edge have sufficient separation: + /// - For horizontal edges (left/right): compares vertical ranges (y-axis) + /// - For vertical edges (top/bottom): compares horizontal ranges (x-axis) /// /// - Parameters: /// - rect1: The first rectangle representing a stashed window's frame. /// - rect2: The second rectangle representing another window's frame. - /// - tolerance: The minimum number of pixels that must separate the two windows (in the vertical direction). + /// - edge: The edge where windows are stashed (determines which axis to check). + /// - tolerance: The minimum number of pixels that must separate the two windows. /// /// - Returns: `true` if the two rectangles do not overlap or are separated by at least `tolerance` pixels; /// `false` otherwise. - func isThereEnoughNonOverlappingSpace(between rect1: CGRect, and rect2: CGRect, tolerance: CGFloat) -> Bool { - let range1 = rect1.minY...rect1.maxY - let range2 = rect2.minY...rect2.maxY + func isThereEnoughNonOverlappingSpace(between rect1: CGRect, and rect2: CGRect, edge: StashEdge?, tolerance: CGFloat) -> Bool { + let range1: ClosedRange + let range2: ClosedRange + + // For horizontal edges (left/right), check vertical overlap + // For vertical edges (top/bottom), check horizontal overlap + if edge?.isHorizontal == true { + range1 = rect1.minY...rect1.maxY + range2 = rect2.minY...rect2.maxY + } else { + range1 = rect1.minX...rect1.maxX + range2 = rect2.minX...rect2.maxX + } return areRangesNonOverlappingByAtLeast(tolerance, range1, range2) } @@ -614,7 +640,7 @@ private extension StashManager { } func getScreenForEdge(currentScreen: NSScreen, edge: StashEdge) -> NSScreen? { - // Two screens are considered in the same "row" if they overlap vertically by at least `threshold` points + // Two screens are considered in the same "row" or "column" if they overlap by at least `threshold` points let threshold: CGFloat = 100 return switch edge { @@ -622,6 +648,10 @@ private extension StashManager { currentScreen.leftmostScreenInSameRow(overlapThreshold: threshold) case .right: currentScreen.rightmostScreenInSameRow(overlapThreshold: threshold) + case .top: + currentScreen.topmostScreenInSameColumn(overlapThreshold: threshold) + case .bottom: + currentScreen.bottommostScreenInSameColumn(overlapThreshold: threshold) } } } diff --git a/Loop/Stashing/StashedWindowInfo.swift b/Loop/Stashing/StashedWindowInfo.swift index 8f05cbed..ca2cc874 100644 --- a/Loop/Stashing/StashedWindowInfo.swift +++ b/Loop/Stashing/StashedWindowInfo.swift @@ -23,14 +23,28 @@ struct StashedWindowInfo: Equatable { var frame = WindowFrameResolver.getFrame(for: action, window: window, bounds: bounds) let minPeekSize: CGFloat = 1 - let maxPeekSize = frame.width * maxPeekPercent - let clampedPeekSize = max(minPeekSize, min(peekSize, maxPeekSize)) switch action.stashEdge { - case .left: - frame.origin.x = bounds.minX - frame.width + clampedPeekSize - case .right: - frame.origin.x = bounds.maxX - clampedPeekSize + case .left, .right: + let maxPeekSize = frame.width * maxPeekPercent + let clampedPeekSize = max(minPeekSize, min(peekSize, maxPeekSize)) + + if action.stashEdge == .left { + frame.origin.x = bounds.minX - frame.width + clampedPeekSize + } else { + frame.origin.x = bounds.maxX - clampedPeekSize + } + + case .top, .bottom: + let maxPeekSize = frame.height * maxPeekPercent + let clampedPeekSize = max(minPeekSize, min(peekSize, maxPeekSize)) + + if action.stashEdge == .top { + frame.origin.y = bounds.minY - frame.height + clampedPeekSize + } else { + frame.origin.y = bounds.maxY - clampedPeekSize + } + case .none: log.warn("Trying to compute the stash frame for a non-stash related action.") } diff --git a/Loop/Window Management/Window Manipulation/WindowFrameResolver.swift b/Loop/Window Management/Window Manipulation/WindowFrameResolver.swift index ca12d06a..e495945e 100644 --- a/Loop/Window Management/Window Manipulation/WindowFrameResolver.swift +++ b/Loop/Window Management/Window Manipulation/WindowFrameResolver.swift @@ -433,9 +433,9 @@ extension WindowFrameResolver { .filter { !$0.intersects(currentFrame) } // Ensure it doesn't intersect with the current window .map { $0.intersection(screenFrame) } // Crop it to the screen frame - // Computes the closest window obstacle in each of the four cardinal directions - // (left, right, top, bottom) relative to the current window, and returns the boundaries - // formed by these obstacles, constrained to the screen frame. + /// Computes the closest window obstacle in each of the four cardinal directions + /// (left, right, top, bottom) relative to the current window, and returns the boundaries + /// formed by these obstacles, constrained to the screen frame. func computeBoundaries() -> (minX: CGFloat, minY: CGFloat, maxX: CGFloat, maxY: CGFloat) { var minX = screenFrame.minX var minY = screenFrame.minY From 0042129ff7d7183eb8403d36beb05732cbee71bc Mon Sep 17 00:00:00 2001 From: Kai Azim Date: Tue, 3 Feb 2026 23:52:07 -0700 Subject: [PATCH 2/3] =?UTF-8?q?=F0=9F=8E=A8=20Fix=20swiftformat=20errors,?= =?UTF-8?q?=20gray=20out=20center=20stash=20option?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Modal Views/CustomActionConfigurationView.swift | 4 +++- .../Modal Views/StashActionConfigurationView.swift | 5 ++++- .../CustomWindowActionAnchor.swift | 13 ++++++++++--- .../Window Manipulation/WindowFrameResolver.swift | 6 +++--- 4 files changed, 20 insertions(+), 8 deletions(-) diff --git a/Loop/Settings Window/Settings/Keybinds/Modal Views/CustomActionConfigurationView.swift b/Loop/Settings Window/Settings/Keybinds/Modal Views/CustomActionConfigurationView.swift index 06a77328..9732cd77 100644 --- a/Loop/Settings Window/Settings/Keybinds/Modal Views/CustomActionConfigurationView.swift +++ b/Loop/Settings Window/Settings/Keybinds/Modal Views/CustomActionConfigurationView.swift @@ -229,7 +229,9 @@ struct CustomActionConfigurationView: View { ), columns: 3 ) { anchor in - IconView(action: anchor.iconAction) + if let action = anchor.iconAction { + IconView(action: action) + } } .luminareRoundingBehavior(bottom: !showMacOSCenterToggle) diff --git a/Loop/Settings Window/Settings/Keybinds/Modal Views/StashActionConfigurationView.swift b/Loop/Settings Window/Settings/Keybinds/Modal Views/StashActionConfigurationView.swift index 5564a086..ad54b893 100644 --- a/Loop/Settings Window/Settings/Keybinds/Modal Views/StashActionConfigurationView.swift +++ b/Loop/Settings Window/Settings/Keybinds/Modal Views/StashActionConfigurationView.swift @@ -38,6 +38,7 @@ struct StashActionConfigurationView: View { private var anchors: [CustomWindowActionAnchor] { [.topLeft, .top, .topRight, .left, .center, .right, + .left, .none, .right, .bottomLeft, .bottom, .bottomRight] } @@ -196,7 +197,9 @@ struct StashActionConfigurationView: View { ), columns: 3 ) { anchor in - IconView(action: anchor.iconAction) + if let action = anchor.iconAction { + IconView(action: action) + } } .luminareRoundingBehavior(top: true, bottom: true) } else { diff --git a/Loop/Window Management/Window Action/Custom Window Sizes/CustomWindowActionAnchor.swift b/Loop/Window Management/Window Action/Custom Window Sizes/CustomWindowActionAnchor.swift index 45e6fd79..0a063f62 100644 --- a/Loop/Window Management/Window Action/Custom Window Sizes/CustomWindowActionAnchor.swift +++ b/Loop/Window Management/Window Action/Custom Window Sizes/CustomWindowActionAnchor.swift @@ -5,11 +5,13 @@ // Created by Kai Azim on 2024-01-01. // +import Luminare import SwiftUI -enum CustomWindowActionAnchor: Int, Codable, CaseIterable, Identifiable { +enum CustomWindowActionAnchor: Int, Codable, Identifiable, LuminareSelectionData { var id: Self { self } + case none = -1 case topLeft = 0 case top = 1 case topRight = 2 @@ -20,18 +22,23 @@ enum CustomWindowActionAnchor: Int, Codable, CaseIterable, Identifiable { case left = 7 case center = 8 case macOSCenter = 9 + + var isSelectable: Bool { + self != .none + } } extension CustomWindowActionAnchor { private static var iconActionCache: [CustomWindowActionAnchor: WindowAction] = [:] - var iconAction: WindowAction { + var iconAction: WindowAction? { // Prevents re-initializing the same action multiple times if let cachedAction = CustomWindowActionAnchor.iconActionCache[self] { return cachedAction } - let newAction: WindowAction = switch self { + let newAction: WindowAction? = switch self { + case .none: nil case .topLeft: .init(.topLeftQuarter) case .top: .init(.topHalf) case .topRight: .init(.topRightQuarter) diff --git a/Loop/Window Management/Window Manipulation/WindowFrameResolver.swift b/Loop/Window Management/Window Manipulation/WindowFrameResolver.swift index e495945e..ca12d06a 100644 --- a/Loop/Window Management/Window Manipulation/WindowFrameResolver.swift +++ b/Loop/Window Management/Window Manipulation/WindowFrameResolver.swift @@ -433,9 +433,9 @@ extension WindowFrameResolver { .filter { !$0.intersects(currentFrame) } // Ensure it doesn't intersect with the current window .map { $0.intersection(screenFrame) } // Crop it to the screen frame - /// Computes the closest window obstacle in each of the four cardinal directions - /// (left, right, top, bottom) relative to the current window, and returns the boundaries - /// formed by these obstacles, constrained to the screen frame. + // Computes the closest window obstacle in each of the four cardinal directions + // (left, right, top, bottom) relative to the current window, and returns the boundaries + // formed by these obstacles, constrained to the screen frame. func computeBoundaries() -> (minX: CGFloat, minY: CGFloat, maxX: CGFloat, maxY: CGFloat) { var minX = screenFrame.minX var minY = screenFrame.minY From ade32373a5273023ec2fbab6202f7fcd65809ec5 Mon Sep 17 00:00:00 2001 From: Kai Azim Date: Tue, 3 Feb 2026 23:57:57 -0700 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=90=9E=20Remove=20top=20stash=20actio?= =?UTF-8?q?n=20due=20to=20unreliable=20behavior?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It appears that the Accessibility API requires a window’s title bar to remain visible during manipulation. As a result, stashing windows on top caused unpredictable behavior: the window would sometimes disappear entirely or remain stationary in a revealed state instead of moving into the expected peeking state. However, it would never animate between the hidden/shown state even when enabled. --- Loop/Extensions/NSScreen+Extensions.swift | 23 ------------------- .../StashActionConfigurationView.swift | 3 +-- Loop/Stashing/StashDirection.swift | 5 +--- Loop/Stashing/StashManager.swift | 8 +++---- Loop/Stashing/StashedWindowInfo.swift | 9 ++------ 5 files changed, 7 insertions(+), 41 deletions(-) diff --git a/Loop/Extensions/NSScreen+Extensions.swift b/Loop/Extensions/NSScreen+Extensions.swift index f9b47f85..02abe313 100644 --- a/Loop/Extensions/NSScreen+Extensions.swift +++ b/Loop/Extensions/NSScreen+Extensions.swift @@ -193,29 +193,6 @@ extension NSScreen { return bestScreen ?? self } - func topmostScreenInSameColumn(overlapThreshold: CGFloat = 10.0) -> NSScreen { - let sameColumnScreens = screensInSameColumn(screens: NSScreen.screens, overlapThreshold: overlapThreshold) - - let topCandidates = sameColumnScreens.filter { $0.frame.maxY <= self.frame.minY } - - guard !topCandidates.isEmpty else { - return self - } - - var bestScreen: NSScreen? = nil - var bestOverlap: CGFloat = -1 - - for screen in topCandidates { - let overlap = horizontalOverlap(with: screen) - if overlap > bestOverlap || (overlap == bestOverlap && screen.frame.minY < bestScreen?.frame.minY ?? .infinity) { - bestScreen = screen - bestOverlap = overlap - } - } - - return bestScreen ?? self - } - func bottommostScreenInSameColumn(overlapThreshold: CGFloat = 10.0) -> NSScreen { let sameColumnScreens = screensInSameColumn(screens: NSScreen.screens, overlapThreshold: overlapThreshold) diff --git a/Loop/Settings Window/Settings/Keybinds/Modal Views/StashActionConfigurationView.swift b/Loop/Settings Window/Settings/Keybinds/Modal Views/StashActionConfigurationView.swift index ad54b893..a022847c 100644 --- a/Loop/Settings Window/Settings/Keybinds/Modal Views/StashActionConfigurationView.swift +++ b/Loop/Settings Window/Settings/Keybinds/Modal Views/StashActionConfigurationView.swift @@ -36,8 +36,7 @@ struct StashActionConfigurationView: View { private let defaultAnchor: CustomWindowActionAnchor = .topLeft private var anchors: [CustomWindowActionAnchor] { - [.topLeft, .top, .topRight, - .left, .center, .right, + [.topLeft, .none, .topRight, .left, .none, .right, .bottomLeft, .bottom, .bottomRight] } diff --git a/Loop/Stashing/StashDirection.swift b/Loop/Stashing/StashDirection.swift index bd51a07f..f5515a53 100644 --- a/Loop/Stashing/StashDirection.swift +++ b/Loop/Stashing/StashDirection.swift @@ -11,7 +11,6 @@ import Foundation enum StashEdge: String, CustomDebugStringConvertible { case left case right - case top case bottom var debugDescription: String { @@ -23,7 +22,7 @@ enum StashEdge: String, CustomDebugStringConvertible { } var isVertical: Bool { - self == .top || self == .bottom + self == .bottom } } @@ -36,8 +35,6 @@ extension WindowAction { .left case .stash where anchor == .right: .right - case .stash where anchor == .top: - .top case .stash where anchor == .bottom: .bottom case .stash where anchor == .topLeft: diff --git a/Loop/Stashing/StashManager.swift b/Loop/Stashing/StashManager.swift index d08681b0..f9826b8b 100644 --- a/Loop/Stashing/StashManager.swift +++ b/Loop/Stashing/StashManager.swift @@ -62,7 +62,7 @@ final class StashManager { /// Two windows can be stacked along the same edge of the screen as long as there is enough non-overlapping space /// to allow the user to easily position the cursor over either window. - /// This applies to vertical space for horizontal edges (left/right) and horizontal space for vertical edges (top/bottom). + /// This applies to vertical space for horizontal edges (left/right) and horizontal space for vertical edges (bottom). private let minimumVisibleSizeToKeepWindowStacked: CGFloat = 100 private lazy var store: StashedWindowsStore = { @@ -553,7 +553,7 @@ private extension StashManager { /// /// This function checks if windows stashed along the same edge have sufficient separation: /// - For horizontal edges (left/right): compares vertical ranges (y-axis) - /// - For vertical edges (top/bottom): compares horizontal ranges (x-axis) + /// - For vertical edges (bottom): compares horizontal ranges (x-axis) /// /// - Parameters: /// - rect1: The first rectangle representing a stashed window's frame. @@ -568,7 +568,7 @@ private extension StashManager { let range2: ClosedRange // For horizontal edges (left/right), check vertical overlap - // For vertical edges (top/bottom), check horizontal overlap + // For vertical edges (bottom), check horizontal overlap if edge?.isHorizontal == true { range1 = rect1.minY...rect1.maxY range2 = rect2.minY...rect2.maxY @@ -648,8 +648,6 @@ private extension StashManager { currentScreen.leftmostScreenInSameRow(overlapThreshold: threshold) case .right: currentScreen.rightmostScreenInSameRow(overlapThreshold: threshold) - case .top: - currentScreen.topmostScreenInSameColumn(overlapThreshold: threshold) case .bottom: currentScreen.bottommostScreenInSameColumn(overlapThreshold: threshold) } diff --git a/Loop/Stashing/StashedWindowInfo.swift b/Loop/Stashing/StashedWindowInfo.swift index ca2cc874..1ae1c713 100644 --- a/Loop/Stashing/StashedWindowInfo.swift +++ b/Loop/Stashing/StashedWindowInfo.swift @@ -35,15 +35,10 @@ struct StashedWindowInfo: Equatable { frame.origin.x = bounds.maxX - clampedPeekSize } - case .top, .bottom: + case .bottom: let maxPeekSize = frame.height * maxPeekPercent let clampedPeekSize = max(minPeekSize, min(peekSize, maxPeekSize)) - - if action.stashEdge == .top { - frame.origin.y = bounds.minY - frame.height + clampedPeekSize - } else { - frame.origin.y = bounds.maxY - clampedPeekSize - } + frame.origin.y = bounds.maxY - clampedPeekSize case .none: log.warn("Trying to compute the stash frame for a non-stash related action.")