diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index b242f5c1..41a57562 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -3066,6 +3066,15 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent minHeight: CGFloat ) -> CGRect { if targetDisplay.visibleFrame.intersects(frame) { + // Preserve the user's exact frame when enough of the top of the window + // remains reachable on-screen; only clamp when the saved frame would + // reopen with an inaccessible titlebar/top strip. + if shouldPreserveAccessibleFrame( + frame: frame, + targetDisplay: targetDisplay + ) { + return frame + } return clampFrame( frame, within: targetDisplay.visibleFrame, @@ -3092,6 +3101,38 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent ) } + private nonisolated static func shouldPreserveAccessibleFrame( + frame: CGRect, + targetDisplay: SessionDisplayGeometry, + minimumVisibleTopStripWidth: CGFloat = 120, + topStripHeight: CGFloat = 64, + minimumVisibleTopStripHeight: CGFloat = 24 + ) -> Bool { + let standardizedFrame = frame.standardized + guard standardizedFrame.width.isFinite, + standardizedFrame.height.isFinite, + standardizedFrame.width > 0, + standardizedFrame.height > 0, + standardizedFrame.intersects(targetDisplay.frame) else { + return false + } + + let stripHeight = min(topStripHeight, standardizedFrame.height) + let topStrip = CGRect( + x: standardizedFrame.minX, + y: standardizedFrame.maxY - stripHeight, + width: standardizedFrame.width, + height: stripHeight + ) + let visibleTopStrip = topStrip.intersection(targetDisplay.visibleFrame) + guard !visibleTopStrip.isNull else { return false } + + let requiredWidth = min(minimumVisibleTopStripWidth, standardizedFrame.width) + let requiredHeight = min(minimumVisibleTopStripHeight, stripHeight) + return visibleTopStrip.width >= requiredWidth + && visibleTopStrip.height >= requiredHeight + } + private nonisolated static func display( for snapshot: SessionDisplaySnapshot?, in displays: [SessionDisplayGeometry] diff --git a/cmuxTests/SessionPersistenceTests.swift b/cmuxTests/SessionPersistenceTests.swift index 8c00c0c1..0c71c59f 100644 --- a/cmuxTests/SessionPersistenceTests.swift +++ b/cmuxTests/SessionPersistenceTests.swift @@ -693,6 +693,34 @@ final class SessionPersistenceTests: XCTestCase { XCTAssertEqual(restored.height, 1_410, accuracy: 0.001) } + func testResolvedWindowFramePreservesExactGeometryWhenDisplayChangesButWindowRemainsAccessible() { + let savedFrame = SessionRectSnapshot(x: 1_100, y: -20, width: 1_280, height: 1_000) + let savedDisplay = SessionDisplaySnapshot( + displayID: 2, + frame: SessionRectSnapshot(x: 0, y: 0, width: 2_560, height: 1_440), + visibleFrame: SessionRectSnapshot(x: 0, y: 0, width: 2_560, height: 1_410) + ) + let adjustedDisplay = AppDelegate.SessionDisplayGeometry( + displayID: 2, + frame: CGRect(x: 0, y: 0, width: 2_560, height: 1_440), + visibleFrame: CGRect(x: 0, y: 40, width: 2_560, height: 1_360) + ) + + let restored = AppDelegate.resolvedWindowFrame( + from: savedFrame, + display: savedDisplay, + availableDisplays: [adjustedDisplay], + fallbackDisplay: adjustedDisplay + ) + + XCTAssertNotNil(restored) + guard let restored else { return } + XCTAssertEqual(restored.minX, 1_100, accuracy: 0.001) + XCTAssertEqual(restored.minY, -20, accuracy: 0.001) + XCTAssertEqual(restored.width, 1_280, accuracy: 0.001) + XCTAssertEqual(restored.height, 1_000, accuracy: 0.001) + } + func testResolvedWindowFrameClampsWhenDisplayGeometryChangesEvenWithSameDisplayID() { let savedFrame = SessionRectSnapshot(x: 1_303, y: -90, width: 1_280, height: 1_410) let savedDisplay = SessionDisplaySnapshot(