|
| 1 | +import AppKit |
| 2 | + |
| 3 | +// MARK: - MainWindowLayout |
| 4 | + |
| 5 | +/// Shared sizing contract for Kaset's primary app window. |
| 6 | +/// |
| 7 | +/// SwiftUI's `.frame(minWidth:minHeight:)` documents the layout floor for the |
| 8 | +/// view hierarchy, while this helper applies the same floor to the underlying |
| 9 | +/// `NSWindow` so live resizing and restored autosaved frames cannot shrink the |
| 10 | +/// window below the point where the sidebar/player controls remain usable. |
| 11 | +enum MainWindowLayout { |
| 12 | + static let autosaveName = "KasetMainWindow" |
| 13 | + static let windowTitle = "Kaset" |
| 14 | + static let minimumWidth: CGFloat = 980 |
| 15 | + static let minimumHeight: CGFloat = 600 |
| 16 | + static let defaultWidth: CGFloat = 1100 |
| 17 | + static let defaultHeight: CGFloat = 760 |
| 18 | + |
| 19 | + static var minimumContentSize: NSSize { |
| 20 | + NSSize(width: minimumWidth, height: minimumHeight) |
| 21 | + } |
| 22 | + |
| 23 | + /// Returns true for windows that are known to be the primary app window. |
| 24 | + static func isPrimaryWindowIdentity(title: String, frameAutosaveName: String) -> Bool { |
| 25 | + frameAutosaveName == self.autosaveName || title == self.windowTitle |
| 26 | + } |
| 27 | + |
| 28 | + @MainActor |
| 29 | + static func isPrimaryWindow(_ window: NSWindow) -> Bool { |
| 30 | + self.isPrimaryWindowIdentity(title: window.title, frameAutosaveName: window.frameAutosaveName) |
| 31 | + } |
| 32 | + |
| 33 | + /// Applies the primary-window sizing contract to an AppKit window. |
| 34 | + @MainActor |
| 35 | + static func configure(_ window: NSWindow) { |
| 36 | + guard self.isPrimaryWindow(window) else { return } |
| 37 | + |
| 38 | + if window.frameAutosaveName.isEmpty { |
| 39 | + window.setFrameAutosaveName(self.autosaveName) |
| 40 | + } |
| 41 | + |
| 42 | + window.contentMinSize = self.minimumContentSize |
| 43 | + self.expandIfNeeded(window) |
| 44 | + } |
| 45 | + |
| 46 | + /// Pure clamp used by both AppKit configuration and tests. |
| 47 | + static func clampedContentSize(_ contentSize: NSSize) -> NSSize { |
| 48 | + NSSize( |
| 49 | + width: max(contentSize.width, self.minimumWidth), |
| 50 | + height: max(contentSize.height, self.minimumHeight) |
| 51 | + ) |
| 52 | + } |
| 53 | + |
| 54 | + @MainActor |
| 55 | + private static func expandIfNeeded(_ window: NSWindow) { |
| 56 | + let currentFrame = window.frame |
| 57 | + let currentContentSize = window.contentRect(forFrameRect: currentFrame).size |
| 58 | + let clampedContentSize = Self.clampedContentSize(currentContentSize) |
| 59 | + |
| 60 | + guard clampedContentSize.width > currentContentSize.width |
| 61 | + || clampedContentSize.height > currentContentSize.height |
| 62 | + else { return } |
| 63 | + |
| 64 | + let clampedFrameSize = window.frameRect( |
| 65 | + forContentRect: NSRect(origin: .zero, size: clampedContentSize) |
| 66 | + ).size |
| 67 | + var clampedFrame = currentFrame |
| 68 | + clampedFrame.size = clampedFrameSize |
| 69 | + // Keep the titlebar/top edge anchored when expanding a stale restored |
| 70 | + // frame, so the window does not jump downward on launch/reopen. |
| 71 | + clampedFrame.origin.y = currentFrame.maxY - clampedFrameSize.height |
| 72 | + |
| 73 | + let constrainedFrame = window.constrainFrameRect(clampedFrame, to: window.screen) |
| 74 | + window.setFrame(constrainedFrame, display: true) |
| 75 | + } |
| 76 | +} |
0 commit comments