Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 26 additions & 4 deletions Loop/Window Management/Window Action/WindowAction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
return 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.
Expand Down
73 changes: 71 additions & 2 deletions Loop/Window Management/Window Manipulation/WindowEngine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 ?? "<unknown>"
logger.info("Resizing \(windowTitle) to \(action.direction.debugDescription) on \(screen.localizedName)")
Expand All @@ -37,6 +38,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()
Expand Down Expand Up @@ -86,7 +94,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)")

Expand Down Expand Up @@ -282,4 +291,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 {
logger.debug("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 {
logger.debug("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
)

logger.debug("Calculated proportional frame: x=\(proportionalX), y=\(proportionalY), w=\(proportionalWidth), h=\(proportionalHeight)")

return proportions
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down