Lock macOS player window resize to video aspect ratio

This commit is contained in:
Arkadiusz Fal
2026-05-08 18:32:43 +02:00
parent b7b7c5ac62
commit 9b85ae2b13
2 changed files with 44 additions and 5 deletions

View File

@@ -953,6 +953,11 @@ private struct PlayerMacOSEventHandlersModifier: ViewModifier {
private func handleAspectRatioChange(oldValue: Double?, newValue: Double?) { private func handleAspectRatioChange(oldValue: Double?, newValue: Double?) {
guard let newValue, newValue > 0 else { return } guard let newValue, newValue > 0 else { return }
// Always keep the manual-resize aspect lock in sync, regardless of the
// auto-resize setting the user always wants resize to be ratio-locked.
ExpandedPlayerWindowManager.shared.lockAspectRatio(newValue)
guard appEnvironment?.settingsManager.playerSheetAutoResize == true else { return } guard appEnvironment?.settingsManager.playerSheetAutoResize == true else { return }
let shouldAnimate = oldValue != nil let shouldAnimate = oldValue != nil
@@ -982,9 +987,12 @@ private struct PlayerMacOSEventHandlersModifier: ViewModifier {
} }
private func handleMacOSAppear() { private func handleMacOSAppear() {
if appEnvironment?.settingsManager.playerSheetAutoResize == true, guard let aspectRatio = playerState?.videoAspectRatio, aspectRatio > 0 else { return }
let aspectRatio = playerState?.videoAspectRatio,
aspectRatio > 0 { // Lock aspect ratio for manual resize unconditionally.
ExpandedPlayerWindowManager.shared.lockAspectRatio(aspectRatio)
if appEnvironment?.settingsManager.playerSheetAutoResize == true {
ExpandedPlayerWindowManager.shared.resizeToFitAspectRatio(aspectRatio, animated: false) ExpandedPlayerWindowManager.shared.resizeToFitAspectRatio(aspectRatio, animated: false)
} }
} }

View File

@@ -21,9 +21,10 @@ final class ExpandedPlayerWindowManager: NSObject {
// Configuration // Configuration
private let minWidth: CGFloat = 640 private let minWidth: CGFloat = 640
private let minHeight: CGFloat = 480 private let minHeight: CGFloat = 360
private let maxScreenRatio: CGFloat = 0.7 private let maxScreenRatio: CGFloat = 0.7
private let targetVideoHeight: CGFloat = 720 private let targetVideoHeight: CGFloat = 720
private let defaultAspectRatio: Double = 16.0 / 9.0
var isPresented: Bool { var isPresented: Bool {
playerWindow != nil playerWindow != nil
@@ -89,9 +90,12 @@ final class ExpandedPlayerWindowManager: NSObject {
window.titleVisibility = .hidden window.titleVisibility = .hidden
window.isMovableByWindowBackground = true window.isMovableByWindowBackground = true
window.backgroundColor = NSColor.windowBackgroundColor window.backgroundColor = NSColor.windowBackgroundColor
window.minSize = NSSize(width: minWidth, height: minHeight)
window.contentViewController = hostingController window.contentViewController = hostingController
// 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)
// Set up window delegate for close handling // Set up window delegate for close handling
// Make ExpandedPlayerWindowManager itself the delegate to avoid lifecycle issues // Make ExpandedPlayerWindowManager itself the delegate to avoid lifecycle issues
window.delegate = self window.delegate = self
@@ -256,6 +260,9 @@ final class ExpandedPlayerWindowManager: NSObject {
guard let window = playerWindow else { return } guard let window = playerWindow else { return }
guard aspectRatio > 0 else { return } guard aspectRatio > 0 else { return }
// Always update the aspect-ratio lock, even if we end up not resizing here.
applyAspectRatioConstraint(aspectRatio, to: window)
// Get screen bounds // Get screen bounds
guard let screen = window.screen ?? NSScreen.main else { return } guard let screen = window.screen ?? NSScreen.main else { return }
let screenFrame = screen.visibleFrame let screenFrame = screen.visibleFrame
@@ -278,8 +285,32 @@ final class ExpandedPlayerWindowManager: NSObject {
window.setFrame(adjustedFrame, display: true, animate: animated) window.setFrame(adjustedFrame, display: true, animate: animated)
} }
/// Locks the window's resize behavior to the given aspect ratio without
/// changing the current frame. Use this when the auto-resize setting is
/// disabled but we still want manual resizing to be ratio-locked.
func lockAspectRatio(_ aspectRatio: Double) {
guard let window = playerWindow else { return }
guard aspectRatio > 0 else { return }
applyAspectRatioConstraint(aspectRatio, to: window)
}
// MARK: - Private Helpers // 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) {
guard aspectRatio > 0 else { return }
// contentAspectRatio is expressed as a ratio; using (aspectRatio, 1) keeps it exact.
window.contentAspectRatio = NSSize(width: aspectRatio, height: 1)
// 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)
}
private func configureWindowLevel(_ window: NSWindow, floating: Bool) { private func configureWindowLevel(_ window: NSWindow, floating: Bool) {
if floating { if floating {
window.level = .floating window.level = .floating