mirror of
https://github.com/yattee/yattee.git
synced 2026-07-20 14:22:02 +00:00
Resize macOS player sheet to match video aspect ratio
In sheet (inline) mode the expanded player kept a fixed size and never reacted to the video's aspect ratio, unlike window/floating mode. Drive the sheet's backing NSWindow size from the video aspect ratio, reusing the same sizing math as the standalone window. - Extract shared sizing into ExpandedPlayerWindowManager.fittedPlayerSize and add fittedSheetSize for the sheet path. - Add SheetWindowResizer (NSViewRepresentable) + .sheetWindowSize modifier that resizes the hosting sheet window, since .presentationSizing(.fitted) only fits once at presentation. - Let the sheet content fill the window and paint the window background black so animated resizes stay gap-free with no white bars.
This commit is contained in:
@@ -93,9 +93,18 @@ struct ContentView: View {
|
||||
},
|
||||
set: { appEnvironment.navigationCoordinator.isPlayerExpanded = $0 }
|
||||
)) {
|
||||
let size = expandedSheetSize(appEnvironment: appEnvironment)
|
||||
ExpandedPlayerSheet()
|
||||
.frame(minWidth: 640, minHeight: 480)
|
||||
// Only a floor — the content fills the sheet window so it tracks
|
||||
// the window's animated resize with no gap. Do NOT pin an exact
|
||||
// size here: a fixed frame snaps instantly while the window
|
||||
// animates, exposing the window background as bars.
|
||||
.frame(minWidth: 640, minHeight: 360)
|
||||
.presentationSizing(.fitted)
|
||||
// `.presentationSizing(.fitted)` only fits the sheet once, at
|
||||
// presentation. Resize the backing window directly when the
|
||||
// aspect-ratio-derived size changes so the sheet tracks the video.
|
||||
.sheetWindowSize(size)
|
||||
}
|
||||
#elseif os(tvOS)
|
||||
.fullScreenCover(isPresented: Binding(
|
||||
@@ -111,6 +120,22 @@ struct ContentView: View {
|
||||
private func presentExpandedPlayerWindow(appEnvironment: AppEnvironment) {
|
||||
ExpandedPlayerWindowManager.shared.show(with: appEnvironment, animated: true)
|
||||
}
|
||||
|
||||
/// Size for the expanded-player sheet, derived from the current video aspect
|
||||
/// ratio (when auto-resize is enabled) so the sheet re-fits like window mode.
|
||||
/// Reading `videoAspectRatio` / `playerSheetAutoResize` here registers the
|
||||
/// @Observable dependency that drives the re-fit.
|
||||
private func expandedSheetSize(appEnvironment: AppEnvironment) -> CGSize {
|
||||
let settings = appEnvironment.settingsManager
|
||||
let aspect: Double
|
||||
if settings.playerSheetAutoResize,
|
||||
let ratio = appEnvironment.playerService.state.videoAspectRatio, ratio > 0 {
|
||||
aspect = ratio
|
||||
} else {
|
||||
aspect = 16.0 / 9.0 // fixed default when auto-resize is off / not yet known
|
||||
}
|
||||
return ExpandedPlayerWindowManager.fittedSheetSize(for: aspect)
|
||||
}
|
||||
#endif
|
||||
|
||||
#if os(macOS)
|
||||
|
||||
@@ -20,11 +20,11 @@ final class ExpandedPlayerWindowManager: NSObject {
|
||||
private weak var appEnvironment: AppEnvironment?
|
||||
|
||||
// Configuration
|
||||
private let minWidth: CGFloat = 640
|
||||
private let minHeight: CGFloat = 360
|
||||
private let maxScreenRatio: CGFloat = 0.7
|
||||
private let targetVideoHeight: CGFloat = 720
|
||||
private let defaultAspectRatio: Double = 16.0 / 9.0
|
||||
private static let minWidth: CGFloat = 640
|
||||
private static let minHeight: CGFloat = 360
|
||||
private static let maxScreenRatio: CGFloat = 0.7
|
||||
private static let targetVideoHeight: CGFloat = 720
|
||||
private static let defaultAspectRatio: Double = 16.0 / 9.0
|
||||
|
||||
var isPresented: Bool {
|
||||
playerWindow != nil
|
||||
@@ -106,7 +106,7 @@ final class ExpandedPlayerWindowManager: NSObject {
|
||||
|
||||
// Lock manual resize to the video aspect ratio. Seeded with 16:9 here;
|
||||
// updated as soon as the real video aspect ratio is known.
|
||||
applyAspectRatioConstraint(defaultAspectRatio, to: window)
|
||||
applyAspectRatioConstraint(Self.defaultAspectRatio, to: window)
|
||||
|
||||
// Force the intended size. Assigning `contentViewController` above resizes
|
||||
// the window to the hosting controller's fitting size — and the lightweight
|
||||
@@ -327,8 +327,8 @@ final class ExpandedPlayerWindowManager: NSObject {
|
||||
// Derive a minimum size that lies on the same ratio so the lower bound
|
||||
// doesn't force the window off-ratio (which would re-introduce bars).
|
||||
// Anchor on minHeight and scale width by aspect.
|
||||
let derivedMinWidth = max(minHeight * CGFloat(aspectRatio), 320)
|
||||
window.minSize = NSSize(width: derivedMinWidth, height: minHeight)
|
||||
let derivedMinWidth = max(Self.minHeight * CGFloat(aspectRatio), 320)
|
||||
window.minSize = NSSize(width: derivedMinWidth, height: Self.minHeight)
|
||||
}
|
||||
|
||||
private func configureWindowLevel(_ window: NSWindow, floating: Bool) {
|
||||
@@ -347,12 +347,12 @@ final class ExpandedPlayerWindowManager: NSObject {
|
||||
}
|
||||
|
||||
let screenFrame = screen.visibleFrame
|
||||
let maxWidth = screenFrame.width * maxScreenRatio
|
||||
let maxHeight = screenFrame.height * maxScreenRatio
|
||||
let maxWidth = screenFrame.width * Self.maxScreenRatio
|
||||
let maxHeight = screenFrame.height * Self.maxScreenRatio
|
||||
|
||||
// Start with 16:9 aspect ratio at target height
|
||||
var width: CGFloat = targetVideoHeight * 16 / 9
|
||||
var height: CGFloat = targetVideoHeight
|
||||
var width: CGFloat = Self.targetVideoHeight * 16 / 9
|
||||
var height: CGFloat = Self.targetVideoHeight
|
||||
|
||||
// Scale down if needed
|
||||
if width > maxWidth {
|
||||
@@ -364,10 +364,17 @@ final class ExpandedPlayerWindowManager: NSObject {
|
||||
width = height * 16 / 9
|
||||
}
|
||||
|
||||
return NSSize(width: max(width, minWidth), height: max(height, minHeight))
|
||||
return NSSize(width: max(width, Self.minWidth), height: max(height, Self.minHeight))
|
||||
}
|
||||
|
||||
private func calculateWindowSize(for aspectRatio: Double, screenFrame: NSRect) -> NSSize {
|
||||
Self.fittedPlayerSize(for: aspectRatio, screenFrame: screenFrame)
|
||||
}
|
||||
|
||||
/// Shared sizing math for both the standalone-window path and the sheet path.
|
||||
/// Anchors on `targetVideoHeight`, derives width from the aspect ratio, then
|
||||
/// clamps to `maxScreenRatio` of the screen and the minimum size.
|
||||
static func fittedPlayerSize(for aspectRatio: Double, screenFrame: NSRect) -> NSSize {
|
||||
let maxWidth = screenFrame.width * maxScreenRatio
|
||||
let maxHeight = screenFrame.height * maxScreenRatio
|
||||
|
||||
@@ -397,6 +404,15 @@ final class ExpandedPlayerWindowManager: NSObject {
|
||||
return NSSize(width: width, height: height)
|
||||
}
|
||||
|
||||
/// Fitted sheet content size for a given aspect ratio, resolving the screen
|
||||
/// internally so callers (e.g. `ContentView`) don't need AppKit. Returns a
|
||||
/// plain `CGSize` (`NSSize == CGSize` on macOS).
|
||||
static func fittedSheetSize(for aspectRatio: Double) -> CGSize {
|
||||
let screenFrame = (NSScreen.main ?? NSScreen.screens.first)?.visibleFrame
|
||||
?? NSRect(x: 0, y: 0, width: 1280, height: 800)
|
||||
return fittedPlayerSize(for: aspectRatio, screenFrame: screenFrame)
|
||||
}
|
||||
|
||||
private func constrainToScreen(_ frame: NSRect, screen: NSScreen) -> NSRect {
|
||||
let screenFrame = screen.visibleFrame
|
||||
var adjustedFrame = frame
|
||||
@@ -487,4 +503,73 @@ private struct ExpandedPlayerWindowRoot: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Sheet Window Resizer
|
||||
|
||||
/// Resizes the hosting sheet's `NSWindow` to `targetSize` whenever it changes.
|
||||
///
|
||||
/// `.presentationSizing(.fitted)` only fits the sheet to its content once, at
|
||||
/// presentation time — it does not re-fit when the content's ideal size changes
|
||||
/// later (e.g. when the video aspect ratio is decoded). This reaches the sheet's
|
||||
/// backing window directly and resizes it, mirroring how the standalone window
|
||||
/// path drives `setFrame` in `resizeToFitAspectRatio`. It keeps the window
|
||||
/// horizontally centered and top-anchored so the sheet grows/shrinks the way an
|
||||
/// attached sheet naturally sits.
|
||||
struct SheetWindowResizer: NSViewRepresentable {
|
||||
let targetSize: CGSize
|
||||
|
||||
func makeCoordinator() -> Coordinator { Coordinator() }
|
||||
|
||||
final class Coordinator {
|
||||
var hasApplied = false
|
||||
}
|
||||
|
||||
func makeNSView(context _: Context) -> NSView {
|
||||
NSView(frame: .zero)
|
||||
}
|
||||
|
||||
func updateNSView(_ nsView: NSView, context: Context) {
|
||||
let target = targetSize
|
||||
let coordinator = context.coordinator
|
||||
// The window isn't attached during the same runloop tick as the SwiftUI
|
||||
// update, so defer the resize until the view is in a window.
|
||||
DispatchQueue.main.async {
|
||||
guard target.width > 0, target.height > 0 else { return }
|
||||
guard let window = nsView.window else { return }
|
||||
|
||||
// Paint the window/content black so that if the content ever lags the
|
||||
// window frame mid-resize, the exposed area is black (matching the
|
||||
// player background) rather than the system's light window color.
|
||||
if window.backgroundColor != .black {
|
||||
window.backgroundColor = .black
|
||||
window.isOpaque = true
|
||||
}
|
||||
|
||||
let current = window.frame
|
||||
guard abs(current.width - target.width) > 1
|
||||
|| abs(current.height - target.height) > 1 else { return }
|
||||
|
||||
// Snap to the correct size the first time (no animation) so the sheet
|
||||
// opens at the right aspect; animate later aspect-ratio changes.
|
||||
let shouldAnimate = coordinator.hasApplied
|
||||
coordinator.hasApplied = true
|
||||
|
||||
// Keep horizontally centered; anchor the top edge so the sheet grows
|
||||
// downward (AppKit y-origin is bottom-left, so hold `maxY`).
|
||||
let newOrigin = NSPoint(
|
||||
x: current.midX - target.width / 2,
|
||||
y: current.maxY - target.height
|
||||
)
|
||||
let newFrame = NSRect(origin: newOrigin, size: target)
|
||||
window.setFrame(newFrame, display: true, animate: shouldAnimate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
/// Resizes the hosting sheet window to `size` when it changes (macOS sheets).
|
||||
func sheetWindowSize(_ size: CGSize) -> some View {
|
||||
background(SheetWindowResizer(targetSize: size))
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
Reference in New Issue
Block a user