diff --git a/Yattee/Views/Player/CornerPlayerLayout.swift b/Yattee/Views/Player/CornerPlayerLayout.swift index 1c70e4e5..80e99a51 100644 --- a/Yattee/Views/Player/CornerPlayerLayout.swift +++ b/Yattee/Views/Player/CornerPlayerLayout.swift @@ -71,8 +71,15 @@ struct CornerPlayerLayout: View { /// matching the panel drag handle look. static var grabberWidth: CGFloat { 12 } static var maxColumnWidthCap: CGFloat { 560 } - /// Minimum width kept for the video column. - static var minVideoColumnWidth: CGFloat { 480 } + /// Minimum width kept for the video column. macOS needs room for the + /// floating control bar (up to 500pt wide plus its edge margins). + static var minVideoColumnWidth: CGFloat { + #if os(macOS) + 560 + #else + 480 + #endif + } /// Maximum fraction of the window height the video area may take when a /// below-video region needs space. static var maxVideoHeightFraction: CGFloat { 0.72 } diff --git a/Yattee/Views/Player/ExpandedPlayerSheet+Layouts.swift b/Yattee/Views/Player/ExpandedPlayerSheet+Layouts.swift index 2f806595..71961575 100644 --- a/Yattee/Views/Player/ExpandedPlayerSheet+Layouts.swift +++ b/Yattee/Views/Player/ExpandedPlayerSheet+Layouts.swift @@ -22,7 +22,8 @@ extension ExpandedPlayerSheet { /// keeps it off tall-phone landscape (iPhone Pro Max is 956×440) while /// the iPhone Duo interior (951×669) and every iPad qualify. The gate is /// size-only — the Duo interior behaves like a small iPad, and the - /// exterior display never reaches the width. + /// exterior display never reaches the width. On macOS it is not a gate + /// but the player window's resize floor while the corner layout is on. static let minHeightForCornerLayout: CGFloat = 600 /// Tabletop (foldable, hinge partially open): vertical room reserved on diff --git a/Yattee/Views/Player/ExpandedPlayerWindowManager.swift b/Yattee/Views/Player/ExpandedPlayerWindowManager.swift index 9e191c1c..81dcac13 100644 --- a/Yattee/Views/Player/ExpandedPlayerWindowManager.swift +++ b/Yattee/Views/Player/ExpandedPlayerWindowManager.swift @@ -789,6 +789,7 @@ final class ExpandedPlayerWindowManager: NSObject { restoreOverlaySizingConstraints(on: window) } else { window.contentResizeIncrements = NSSize(width: 1, height: 1) + growToCornerLayoutMinimum(window) } LoggingService.shared.debug( "unlockAspectRatio (corner layout) aspect=\(window.contentAspectRatio) increments=\(window.contentResizeIncrements)", @@ -796,6 +797,45 @@ final class ExpandedPlayerWindowManager: NSObject { ) } + /// Smallest window the corner layout fits in: its width threshold (below + /// it the layout falls back to stacked), room for the narrowest side + /// column next to a video column wide enough for the controls, and + /// enough height for the video plus the below-video panel, capped to the + /// screen. + static func cornerLayoutMinimumSize(for screen: NSScreen?) -> NSSize { + let columnsWidth = CornerPlayerLayout.minColumnWidth + + CornerPlayerLayout.grabberWidth + + CornerPlayerLayout.minVideoColumnWidth + var size = NSSize( + width: max(ExpandedPlayerSheet.minWidthForCornerLayout, columnsWidth), + height: ExpandedPlayerSheet.minHeightForCornerLayout + ) + if let visibleFrame = screen?.visibleFrame { + size.width = min(size.width, visibleFrame.width) + size.height = min(size.height, visibleFrame.height) + } + return size + } + + /// The corner layout resolves from window width only, so it can take over + /// a window shorter than it needs; grow the height (keeping the top edge) + /// so the floor `windowWillResize(_:to:)` enforces holds from the start. + private func growToCornerLayoutMinimum(_ window: NSWindow) { + guard !isActivePlayerWindowFullScreen else { return } + let screen = window.screen ?? NSScreen.main + let minimum = Self.cornerLayoutMinimumSize(for: screen) + var frame = window.frame + guard frame.width < minimum.width || frame.height < minimum.height else { return } + let top = frame.maxY + frame.size.width = max(frame.width, minimum.width) + frame.size.height = max(frame.height, minimum.height) + frame.origin.y = top - frame.height + if let screen { + frame = constrainToScreen(frame, screen: screen) + } + window.setFrame(frame, display: true, animate: true) + } + // MARK: - Private Helpers private func synchronizeActiveWindowAspectRatio() { @@ -1019,8 +1059,12 @@ extension ExpandedPlayerWindowManager: NSWindowDelegate { // Free resize (corner layout): clamp each axis on its own. // Preserving the proposal's ratio here would widen the window // while the user drags only its bottom edge past the floor. - width = max(frameSize.width, 320) - height = max(frameSize.height, Self.minHeight) + // The floor is the corner layout's own minimum, so a drag + // can't squeeze the layout or push it under its width + // threshold (which would flip it to stacked mid-drag). + let minimum = Self.cornerLayoutMinimumSize(for: window.screen ?? NSScreen.main) + width = max(frameSize.width, minimum.width) + height = max(frameSize.height, minimum.height) } return NSSize(width: width, height: height) diff --git a/Yattee/Views/Player/macOS/MacOSPlayerControlsView.swift b/Yattee/Views/Player/macOS/MacOSPlayerControlsView.swift index e9776850..bdc18e97 100644 --- a/Yattee/Views/Player/macOS/MacOSPlayerControlsView.swift +++ b/Yattee/Views/Player/macOS/MacOSPlayerControlsView.swift @@ -330,7 +330,10 @@ struct MacOSPlayerControlsView: View { // AppKit claims drags on the glass capsule as window moves // and the reposition gesture never fires. .background(WindowDragBlockingView()) - .frame(width: 650) + // Never wider than the container: a wider bar frame + // widens the whole VStack, and the top bar then + // overflows (and is clipped) on both sides. + .frame(width: min(650, geometry.size.width)) .padding(.bottom, BarDrag.bottomPadding) .offset(barOffset(in: geometry.size)) .gesture(barDragGesture(in: geometry.size))