Lock macOS inline player sheet resize to the video aspect ratio

In floating/window mode, interactive resize is aspect-locked via the
window's contentAspectRatio, so no black bars appear. The inline sheet
had no such lock, allowing free resize with pan bars on the sides.

Make applyAspectRatioConstraint static so the sheet path can reuse the
exact same lock the standalone window uses, and apply it to the sheet's
backing NSWindow from SheetWindowResizer whenever the video ratio is
known. Like the standalone window, the resize lock always follows the
real video ratio (independent of the playerSheetAutoResize size setting).
This commit is contained in:
Arkadiusz Fal
2026-05-27 23:21:07 +02:00
parent 6bc58f10b2
commit ef213307dc
2 changed files with 37 additions and 9 deletions

View File

@@ -94,6 +94,7 @@ struct ContentView: View {
set: { appEnvironment.navigationCoordinator.isPlayerExpanded = $0 }
)) {
let size = expandedSheetSize(appEnvironment: appEnvironment)
let lockAspect = sheetLockAspectRatio(appEnvironment: appEnvironment)
ExpandedPlayerSheet()
// Floor keeps the content flexible so it tracks the window's
// animated resize with no gap do NOT pin an exact (max) size:
@@ -109,8 +110,9 @@ struct ContentView: View {
.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)
// aspect-ratio-derived size changes so the sheet tracks the video,
// and lock interactive resize to the video ratio (no black bars).
.sheetWindowSize(size, aspectRatio: lockAspect)
}
#elseif os(tvOS)
.fullScreenCover(isPresented: Binding(
@@ -142,6 +144,16 @@ struct ContentView: View {
}
return ExpandedPlayerWindowManager.fittedSheetSize(for: aspect)
}
/// Real video aspect ratio to lock the sheet's interactive resize to, or `0`
/// when unknown. Unlike the sheet *size* (which honors `playerSheetAutoResize`),
/// the resize lock always follows the actual video ratio mirroring the
/// standalone window, which locks unconditionally. Reading `videoAspectRatio`
/// here registers the @Observable dependency that keeps the lock in sync.
private func sheetLockAspectRatio(appEnvironment: AppEnvironment) -> Double {
let ratio = appEnvironment.playerService.state.videoAspectRatio ?? 0
return ratio > 0 ? ratio : 0
}
#endif
#if os(macOS)

View File

@@ -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(Self.defaultAspectRatio, to: window)
Self.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
@@ -281,7 +281,7 @@ final class ExpandedPlayerWindowManager: NSObject {
guard aspectRatio > 0 else { return }
// Always update the aspect-ratio lock, even if we end up not resizing here.
applyAspectRatioConstraint(aspectRatio, to: window)
Self.applyAspectRatioConstraint(aspectRatio, to: window)
// Get screen bounds
guard let screen = window.screen ?? NSScreen.main else { return }
@@ -311,14 +311,16 @@ final class ExpandedPlayerWindowManager: NSObject {
func lockAspectRatio(_ aspectRatio: Double) {
guard let window = playerWindow else { return }
guard aspectRatio > 0 else { return }
applyAspectRatioConstraint(aspectRatio, to: window)
Self.applyAspectRatioConstraint(aspectRatio, to: window)
}
// MARK: - Private Helpers
/// Sets `contentAspectRatio` and a ratio-consistent `minSize` on the window
/// so that interactive resize couples width and height proportionally.
private func applyAspectRatioConstraint(_ aspectRatio: Double, to window: NSWindow) {
/// Static so the inline-sheet path (`SheetWindowResizer`) can lock the sheet's
/// backing window to the same ratio the standalone window uses.
static func applyAspectRatioConstraint(_ aspectRatio: Double, to window: NSWindow) {
guard aspectRatio > 0 else { return }
// contentAspectRatio is expressed as a ratio; using (aspectRatio, 1) keeps it exact.
@@ -517,6 +519,10 @@ private struct ExpandedPlayerWindowRoot: View {
/// attached sheet naturally sits.
struct SheetWindowResizer: NSViewRepresentable {
let targetSize: CGSize
/// Real video aspect ratio (width / height) to lock interactive resize to, or
/// `0` when unknown. Mirrors the standalone window's `contentAspectRatio` lock
/// so dragging the sheet edge keeps the video's ratio instead of adding bars.
let aspectRatio: Double
func makeCoordinator() -> Coordinator { Coordinator() }
@@ -530,6 +536,7 @@ struct SheetWindowResizer: NSViewRepresentable {
func updateNSView(_ nsView: NSView, context: Context) {
let target = targetSize
let aspect = aspectRatio
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.
@@ -545,6 +552,13 @@ struct SheetWindowResizer: NSViewRepresentable {
window.isOpaque = true
}
// Lock interactive resize to the video ratio so dragging the sheet's
// edge keeps the aspect (no black bars), matching the standalone
// window. Applied every pass so it tracks aspect-ratio changes.
if aspect > 0 {
ExpandedPlayerWindowManager.applyAspectRatioConstraint(aspect, to: window)
}
let current = window.frame
guard abs(current.width - target.width) > 1
|| abs(current.height - target.height) > 1 else { return }
@@ -567,9 +581,11 @@ struct SheetWindowResizer: NSViewRepresentable {
}
extension View {
/// Resizes the hosting sheet window to `size` when it changes (macOS sheets).
func sheetWindowSize(_ size: CGSize) -> some View {
background(SheetWindowResizer(targetSize: size))
/// Resizes the hosting sheet window to `size` when it changes and locks its
/// interactive resize to `aspectRatio` (macOS sheets). Pass `aspectRatio == 0`
/// to leave resize unconstrained.
func sheetWindowSize(_ size: CGSize, aspectRatio: Double) -> some View {
background(SheetWindowResizer(targetSize: size, aspectRatio: aspectRatio))
}
}
#endif