From 658a974fa94ad84321feef1f9d320948fcd0a141 Mon Sep 17 00:00:00 2001 From: Cipher Shad0w Date: Tue, 28 Oct 2025 19:32:12 +0100 Subject: [PATCH 1/4] =?UTF-8?q?=E2=9C=A8=20Add=20proportional=20frame=20ca?= =?UTF-8?q?lculations=20for=20window=20resizing=20across=20screens?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Window Action/WindowAction.swift | 30 ++++++-- .../Window Manipulation/WindowEngine.swift | 70 ++++++++++++++++++- 2 files changed, 95 insertions(+), 5 deletions(-) diff --git a/Loop/Window Management/Window Action/WindowAction.swift b/Loop/Window Management/Window Action/WindowAction.swift index da72fe47..13b46592 100644 --- a/Loop/Window Management/Window Action/WindowAction.swift +++ b/Loop/Window Management/Window Action/WindowAction.swift @@ -222,8 +222,9 @@ struct WindowAction: Codable, Identifiable, Hashable, Equatable, Defaults.Serial /// - disablePadding: whether to disable padding. `true` when calculating non-AX-usage frames, such as for angle calculations in radial menu or in config UI. /// - screen: the screen on which the bounds are located. Only used to determine if padding should be applied (see `getBounds()`). /// - isPreview: ensures that when manipulating the preview window, the last target frame does not affect the actual resizing of the window. + /// - proportionalFrame: optional proportional frame when moving between screens. Values should be between 0.0 and 1.0. /// - Returns: the calculated frame for the specified window action. - func getFrame(window: Window?, bounds: CGRect, disablePadding: Bool = false, screen: NSScreen? = nil, isPreview: Bool = false) -> CGRect { + func getFrame(window: Window?, bounds: CGRect, disablePadding: Bool = false, screen: NSScreen? = nil, isPreview: Bool = false, proportionalFrame: CGRect? = nil) -> CGRect { let noFrameActions: [WindowDirection] = [.noAction, .cycle, .minimize, .hide] guard !noFrameActions.contains(direction) else { return NSRect(origin: bounds.center, size: .zero) @@ -234,7 +235,7 @@ struct WindowAction: Codable, Identifiable, Hashable, Equatable, Defaults.Serial } var bounds: CGRect = getBounds(from: bounds, disablePadding: disablePadding, screen: screen) - var result: CGRect = calculateTargetFrame(direction, window, bounds, isPreview) + var result: CGRect = calculateTargetFrame(direction, window, bounds, isPreview, proportionalFrame: proportionalFrame) if !disablePadding { if !willManipulateExistingWindowFrame { @@ -293,12 +294,18 @@ extension WindowAction { /// - window: the window to be manipulated. /// - bounds: the bounds within which the window should be manipulated. /// - isPreview: whether the action is being performed on a preview window. + /// - proportionalFrame: optional proportional frame when moving between screens. /// - Returns: the calculated target frame for the specified window action. - private func calculateTargetFrame(_ direction: WindowDirection, _ window: Window?, _ bounds: CGRect, _ isPreview: Bool) -> CGRect { + private func calculateTargetFrame(_ direction: WindowDirection, _ window: Window?, _ bounds: CGRect, _ isPreview: Bool, proportionalFrame: CGRect? = nil) -> CGRect { var result: CGRect = .zero if direction.frameMultiplyValues != nil { - result = applyFrameMultiplyValues(bounds) + // When moving between screens with a proportional frame, use proportional sizing + if let proportionalFrame { + result = applyProportionalFrame(proportionalFrame, bounds) + } else { + result = applyFrameMultiplyValues(bounds) + } } else if direction.willAdjustSize { // Can't grow or shrink a window that is not resizable @@ -409,6 +416,21 @@ extension WindowAction { ) } + /// Applies a proportional frame (from a previous screen) to new bounds. + /// This is used when moving windows between screens to maintain their relative size. + /// - Parameters: + /// - proportionalFrame: The proportional frame with values between 0.0 and 1.0 + /// - bounds: The new screen bounds to apply the proportions to + /// - Returns: A new `CGRect` with the proportional frame applied to the new bounds + private func applyProportionalFrame(_ proportionalFrame: CGRect, _ bounds: CGRect) -> CGRect { + CGRect( + x: bounds.origin.x + (bounds.width * proportionalFrame.minX), + y: bounds.origin.y + (bounds.height * proportionalFrame.minY), + width: bounds.width * proportionalFrame.width, + height: bounds.height * proportionalFrame.height + ) + } + /// Calculates the user-specified custom frame relative to the provided bounds. /// - Parameters: /// - window: the window to be manipulated. diff --git a/Loop/Window Management/Window Manipulation/WindowEngine.swift b/Loop/Window Management/Window Manipulation/WindowEngine.swift index b541a4f5..095fc59f 100644 --- a/Loop/Window Management/Window Manipulation/WindowEngine.swift +++ b/Loop/Window Management/Window Manipulation/WindowEngine.swift @@ -37,6 +37,13 @@ enum WindowEngine { WindowRecords.record(window, action) } + // Calculate proportional frame when moving between screens + // This ensures windows maintain their relative size across different screen resolutions + var proportionalFrame: CGRect? = nil + if willChangeScreens, let currentScreen, action.direction.frameMultiplyValues != nil { + proportionalFrame = calculateProportionalFrame(window: window, fromScreen: currentScreen, action: action) + } + // If the action is to hide, minimize or fullscreen perform the action then return if action.direction == .hide { window.toggleHidden() @@ -86,7 +93,8 @@ enum WindowEngine { let targetFrame: CGRect = action.getFrame( window: window, bounds: screen.safeScreenFrame, - screen: screen + screen: screen, + proportionalFrame: proportionalFrame ) logger.info("Target window frame: \(targetFrame.debugDescription)") @@ -282,4 +290,64 @@ enum WindowEngine { window.minimized = true } } + + /// Calculates the proportional frame of a window relative to its current screen. + /// This is used when moving windows between screens to maintain their relative size. + /// - Parameters: + /// - window: The window to calculate proportions for + /// - fromScreen: The screen the window is currently on + /// - action: The window action being performed + /// - Returns: A CGRect representing the proportional frame (values between 0.0 and 1.0) or nil if not applicable + private static func calculateProportionalFrame(window: Window, fromScreen: NSScreen, action: WindowAction) -> CGRect? { + // Only calculate proportions for actions with frame multiply values (halves, quarters, thirds, etc.) + guard action.direction.frameMultiplyValues != nil else { + return nil + } + + let currentFrame = window.frame + let currentBounds = fromScreen.safeScreenFrame + + let usePadding = PaddingSettings.enablePadding && + (Defaults[.paddingMinimumScreenSize] == 0 || fromScreen.diagonalSize > Defaults[.paddingMinimumScreenSize]) + + let adjustedBounds = if usePadding { + PaddingSettings.padding.apply(on: currentBounds) + } else { + currentBounds + } + + guard currentFrame.intersects(adjustedBounds) else { + print("Window frame doesn't intersect with source screen bounds - skipping proportional calculation") + return nil + } + + // Calculate proportional position and size relative to the adjusted bounds + let proportionalX = (currentFrame.minX - adjustedBounds.minX) / adjustedBounds.width + let proportionalY = (currentFrame.minY - adjustedBounds.minY) / adjustedBounds.height + let proportionalWidth = currentFrame.width / adjustedBounds.width + let proportionalHeight = currentFrame.height / adjustedBounds.height + + // Validate proportional values are reasonable + let tolerance: CGFloat = 0.1 + guard proportionalX > -tolerance, + proportionalY > -tolerance, + proportionalWidth > 0, + proportionalWidth <= 1.0 + tolerance, + proportionalHeight > 0, + proportionalHeight <= 1.0 + tolerance else { + print("Invalid proportional frame calculated (x=\(proportionalX), y=\(proportionalY), w=\(proportionalWidth), h=\(proportionalHeight)) - skipping") + return nil + } + + let proportions = CGRect( + x: proportionalX, + y: proportionalY, + width: proportionalWidth, + height: proportionalHeight + ) + + print("Calculated proportional frame: x=\(proportionalX), y=\(proportionalY), w=\(proportionalWidth), h=\(proportionalHeight)") + + return proportions + } } From e92a50ac117c530bb71de32ff2c7acb590c44179 Mon Sep 17 00:00:00 2001 From: Cipher Shad0w Date: Tue, 28 Oct 2025 19:32:32 +0100 Subject: [PATCH 2/4] =?UTF-8?q?=F0=9F=90=9E=20Fix=20window=20animation=20l?= =?UTF-8?q?ag=20when=20moving=20between=20monitors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Window Manipulation/WindowTransformAnimation.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Loop/Window Management/Window Manipulation/WindowTransformAnimation.swift b/Loop/Window Management/Window Manipulation/WindowTransformAnimation.swift index 2d871292..d06ba7ea 100644 --- a/Loop/Window Management/Window Manipulation/WindowTransformAnimation.swift +++ b/Loop/Window Management/Window Manipulation/WindowTransformAnimation.swift @@ -86,7 +86,7 @@ final class WindowTransformAnimation: NSAnimation { window.size = newFrame.size } - lastWindowFrame = window.frame + lastWindowFrame = newFrame if currentProgress >= 1.0 { WindowTransformAnimation.currentAnimations[window.cgWindowID] = nil From f786dfd1ab2a77397bb351e8ecd451555efd692c Mon Sep 17 00:00:00 2001 From: Cipher Shad0w Date: Tue, 28 Oct 2025 19:49:08 +0100 Subject: [PATCH 3/4] =?UTF-8?q?=F0=9F=90=9E=20Fix=20undefined=20currentScr?= =?UTF-8?q?een=20variable=20in=20WindowEngine?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Loop/Window Management/Window Manipulation/WindowEngine.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Loop/Window Management/Window Manipulation/WindowEngine.swift b/Loop/Window Management/Window Manipulation/WindowEngine.swift index 095fc59f..de34a66c 100644 --- a/Loop/Window Management/Window Manipulation/WindowEngine.swift +++ b/Loop/Window Management/Window Manipulation/WindowEngine.swift @@ -26,7 +26,8 @@ enum WindowEngine { shouldRecord: Bool = true ) { guard action.direction != .noAction else { return } - let willChangeScreens = ScreenUtility.screenContaining(window) != screen + let currentScreen = ScreenUtility.screenContaining(window) + let willChangeScreens = currentScreen != screen let windowTitle = window.nsRunningApplication?.localizedName ?? window.title ?? "" logger.info("Resizing \(windowTitle) to \(action.direction.debugDescription) on \(screen.localizedName)") From d4feb5fff6e293f1aba673c207f52ee11d2abfa3 Mon Sep 17 00:00:00 2001 From: Cipher Shad0w Date: Thu, 30 Oct 2025 12:24:38 +0100 Subject: [PATCH 4/4] =?UTF-8?q?=F0=9F=90=9E=20Replace=20print=20statements?= =?UTF-8?q?=20with=20logger?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Loop/Window Management/Window Action/WindowAction.swift | 2 +- .../Window Manipulation/WindowEngine.swift | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Loop/Window Management/Window Action/WindowAction.swift b/Loop/Window Management/Window Action/WindowAction.swift index 13b46592..07e86f85 100644 --- a/Loop/Window Management/Window Action/WindowAction.swift +++ b/Loop/Window Management/Window Action/WindowAction.swift @@ -423,7 +423,7 @@ extension WindowAction { /// - bounds: The new screen bounds to apply the proportions to /// - Returns: A new `CGRect` with the proportional frame applied to the new bounds private func applyProportionalFrame(_ proportionalFrame: CGRect, _ bounds: CGRect) -> CGRect { - CGRect( + return CGRect( x: bounds.origin.x + (bounds.width * proportionalFrame.minX), y: bounds.origin.y + (bounds.height * proportionalFrame.minY), width: bounds.width * proportionalFrame.width, diff --git a/Loop/Window Management/Window Manipulation/WindowEngine.swift b/Loop/Window Management/Window Manipulation/WindowEngine.swift index de34a66c..510bd4ab 100644 --- a/Loop/Window Management/Window Manipulation/WindowEngine.swift +++ b/Loop/Window Management/Window Manipulation/WindowEngine.swift @@ -318,7 +318,7 @@ enum WindowEngine { } guard currentFrame.intersects(adjustedBounds) else { - print("Window frame doesn't intersect with source screen bounds - skipping proportional calculation") + logger.debug("Window frame doesn't intersect with source screen bounds - skipping proportional calculation") return nil } @@ -336,7 +336,7 @@ enum WindowEngine { proportionalWidth <= 1.0 + tolerance, proportionalHeight > 0, proportionalHeight <= 1.0 + tolerance else { - print("Invalid proportional frame calculated (x=\(proportionalX), y=\(proportionalY), w=\(proportionalWidth), h=\(proportionalHeight)) - skipping") + logger.debug("Invalid proportional frame calculated (x=\(proportionalX), y=\(proportionalY), w=\(proportionalWidth), h=\(proportionalHeight)) - skipping") return nil } @@ -347,7 +347,7 @@ enum WindowEngine { height: proportionalHeight ) - print("Calculated proportional frame: x=\(proportionalX), y=\(proportionalY), w=\(proportionalWidth), h=\(proportionalHeight)") + logger.debug("Calculated proportional frame: x=\(proportionalX), y=\(proportionalY), w=\(proportionalWidth), h=\(proportionalHeight)") return proportions }