diff --git a/Loop/Core/LoopManager.swift b/Loop/Core/LoopManager.swift index c73a485e..9909c45c 100644 --- a/Loop/Core/LoopManager.swift +++ b/Loop/Core/LoopManager.swift @@ -319,7 +319,7 @@ extension LoopManager { let adjustedBounds = PaddingConfiguration .getConfiguredPadding(for: currentScreen) - .applyToBounds(currentScreen.cgSafeScreenFrame) + .applyToBounds(currentScreen.cgSafeScreenFrame, screen: currentScreen) let proportionalSize = CGRect( x: (currentFrame.minX - adjustedBounds.minX) / adjustedBounds.width, diff --git a/Loop/Extensions/Defaults+Extensions.swift b/Loop/Extensions/Defaults+Extensions.swift index ed9aa172..35c73489 100644 --- a/Loop/Extensions/Defaults+Extensions.swift +++ b/Loop/Extensions/Defaults+Extensions.swift @@ -106,6 +106,12 @@ extension Defaults.Keys { /// Reset with `defaults delete com.MrKai77.Loop paddingMinimumScreenSize` static let paddingMinimumScreenSize = Key("paddingMinimumScreenSize", default: 0, iCloud: true) + /// Ignore the notch height when calculating top padding, so the effective + /// distance from the screen top matches non-notch displays. + /// Adjust with `defaults write com.MrKai77.Loop ignoreNotch -bool true` + /// Reset with `defaults delete com.MrKai77.Loop ignoreNotch` + static let ignoreNotch = Key("ignoreNotch", default: false, iCloud: true) + /// Snap threshold for window snapping, defined in points. /// Adjust with `defaults write com.MrKai77.Loop snapThreshold -float x` /// Reset with `defaults delete com.MrKai77.Loop snapThreshold` diff --git a/Loop/Window Management/Window Manipulation/PaddingConfiguration.swift b/Loop/Window Management/Window Manipulation/PaddingConfiguration.swift index a1583d77..68175c8d 100644 --- a/Loop/Window Management/Window Manipulation/PaddingConfiguration.swift +++ b/Loop/Window Management/Window Manipulation/PaddingConfiguration.swift @@ -37,13 +37,21 @@ struct PaddingConfiguration: Codable, Defaults.Serializable, Hashable { } func applyToBounds( - _ bounds: CGRect + _ bounds: CGRect, + screen: NSScreen? = nil ) -> CGRect { - bounds + let notchOffset: CGFloat = if Defaults[.ignoreNotch], let screen { + screen.menubarHeight + } else { + 0 + } + let effectiveTopPadding = max(0, totalTopPadding - notchOffset) + + return bounds .padding(.leading, left) .padding(.trailing, right) .padding(.bottom, bottom) - .padding(.top, totalTopPadding) + .padding(.top, effectiveTopPadding) } /// Applies padding to a frame that was calculated using non-padded bounds. diff --git a/Loop/Window Management/Window Manipulation/ResizeContext.swift b/Loop/Window Management/Window Manipulation/ResizeContext.swift index b5d2a10e..2a5aa9a4 100644 --- a/Loop/Window Management/Window Manipulation/ResizeContext.swift +++ b/Loop/Window Management/Window Manipulation/ResizeContext.swift @@ -54,7 +54,7 @@ final class ResizeContext { self.screen = screen self.bounds = bounds self.padding = padding - self.paddedBounds = padding.applyToBounds(bounds) + self.paddedBounds = padding.applyToBounds(bounds, screen: screen) self.action = action self.parentAction = parentAction self.initialMousePosition = initialMousePosition @@ -65,7 +65,7 @@ final class ResizeContext { self.screen = screen bounds = screen?.cgSafeScreenFrame ?? .zero padding = PaddingConfiguration.getConfiguredPadding(for: screen) - paddedBounds = padding.applyToBounds(bounds) + paddedBounds = padding.applyToBounds(bounds, screen: screen) needsRecompute = true } diff --git a/Loop/Window Management/Window Manipulation/WindowFrameResolver.swift b/Loop/Window Management/Window Manipulation/WindowFrameResolver.swift index 6b3c43d7..09d82a3e 100644 --- a/Loop/Window Management/Window Manipulation/WindowFrameResolver.swift +++ b/Loop/Window Management/Window Manipulation/WindowFrameResolver.swift @@ -36,7 +36,6 @@ enum WindowFrameResolver { /// - Returns: a tuple containing the computed frame and the sides to adjust for grow/shrink actions. static func getFrame(resizeContext: ResizeContext) -> FrameResult { let action = resizeContext.action - let window = resizeContext.window let bounds = resizeContext.paddedBounds let direction = action.direction @@ -52,11 +51,8 @@ enum WindowFrameResolver { } var result: CGRect = calculateTargetFrame( - for: action, - window: window, - bounds: bounds, sidesToAdjust: &sidesToAdjust, - resizeContext: resizeContext + context: resizeContext ) if result.size.width < 0 || result.size.height < 0 || !result.isFinite { @@ -72,19 +68,17 @@ enum WindowFrameResolver { extension WindowFrameResolver { /// Calculates the target frame for the specified window action based on the direction, window, bounds, and whether it is a preview. /// - Parameters: - /// - action: the window action to calculate the frame for. - /// - window: the window to be manipulated. - /// - bounds: the bounds within which the window should be manipulated. /// - sidesToAdjust: inout parameter for tracking which edges to adjust during grow/shrink actions. - /// - resizeContext: the context tracking frame and edge adjustment state. + /// - context: the context tracking frame and edge adjustment state. /// - Returns: the calculated target frame for the specified window action. private static func calculateTargetFrame( - for action: WindowAction, - window: Window?, - bounds: CGRect, sidesToAdjust: inout Edge.Set?, - resizeContext: ResizeContext + context: ResizeContext ) -> CGRect { + let bounds = context.paddedBounds + let action = context.action + let window = context.window + let direction = action.direction var result: CGRect = .zero @@ -97,7 +91,7 @@ extension WindowFrameResolver { return window.frame } - let frameToResizeFrom = resizeContext.cachedTargetFrame.raw + let frameToResizeFrom = context.cachedTargetFrame.raw // Compute which edges to adjust based on edges touching bounds let edgesTouchingBounds = frameToResizeFrom.getEdgesTouchingBounds(bounds) @@ -119,7 +113,7 @@ extension WindowFrameResolver { } // This allows for control over each side - let frameToResizeFrom = resizeContext.cachedTargetFrame.raw + let frameToResizeFrom = context.cachedTargetFrame.raw // Compute which edges to adjust based on direction switch direction { @@ -145,7 +139,7 @@ extension WindowFrameResolver { ) } else if direction.willMove { - let frameToResizeFrom = resizeContext.getTargetFrame().raw + let frameToResizeFrom = context.getTargetFrame().raw result = calculatePositionAdjustment(for: action, frameToResizeFrom: frameToResizeFrom) @@ -165,10 +159,10 @@ extension WindowFrameResolver { result = getInitialFrame(window: window) } else if direction == .maximizeHeight, let window { - result = getMaximizeHeightFrame(window: window, bounds: bounds) + result = getMaximizeHeightFrame(window: window, bounds: bounds, padding: context.padding) } else if direction == .maximizeWidth, let window { - result = getMaximizeWidthFrame(window: window, bounds: bounds) + result = getMaximizeWidthFrame(window: window, bounds: bounds, padding: context.padding) } else if direction == .unstash, let window { result = getInitialFrame(window: window) @@ -395,12 +389,17 @@ extension WindowFrameResolver { /// - Parameters: /// - window: the window whose current frame is used as a reference. /// - bounds: the area within which the window should be resized. + /// - padding: the padding that the user has configured to apply to windows. /// - Returns: a CGRect representing a frame that maximizes the window's height. - private static func getMaximizeHeightFrame(window: Window, bounds: CGRect) -> CGRect { + private static func getMaximizeHeightFrame( + window: Window, + bounds: CGRect, + padding: PaddingConfiguration + ) -> CGRect { CGRect( - x: window.frame.minX, + x: window.frame.minX - padding.window / 2, y: bounds.minY, - width: window.frame.width, + width: window.frame.width + padding.window, height: bounds.height ) } @@ -409,13 +408,18 @@ extension WindowFrameResolver { /// - Parameters: /// - window: the window whose current frame is used as a reference. /// - bounds: the area within which the window should be resized. + /// - padding: the padding that the user has configured to apply to windows. /// - Returns: a CGRect representing a frame that maximizes the window's width. - private static func getMaximizeWidthFrame(window: Window, bounds: CGRect) -> CGRect { + private static func getMaximizeWidthFrame( + window: Window, + bounds: CGRect, + padding: PaddingConfiguration + ) -> CGRect { CGRect( x: bounds.minX, - y: window.frame.minY, + y: window.frame.minY - padding.window / 2, width: bounds.width, - height: window.frame.height + height: window.frame.height + padding.window ) } @@ -434,9 +438,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 diff --git a/Loop/Window Management/Window Manipulation/WindowRecords.swift b/Loop/Window Management/Window Manipulation/WindowRecords.swift index 3a0bbae5..9e0c26fe 100644 --- a/Loop/Window Management/Window Manipulation/WindowRecords.swift +++ b/Loop/Window Management/Window Manipulation/WindowRecords.swift @@ -68,7 +68,7 @@ enum WindowRecords { let windowFrame = window.frame let adjustedBounds = PaddingConfiguration .getConfiguredPadding(for: screen) - .applyToBounds(screen.cgSafeScreenFrame) + .applyToBounds(screen.cgSafeScreenFrame, screen: screen) let proportionalSize = CGRect( x: (windowFrame.minX - adjustedBounds.minX) / adjustedBounds.width,