mirror of
https://github.com/yattee/yattee.git
synced 2026-11-03 10:27:19 +00:00
Fix inline macOS player aspect resizing
This commit is contained in:
@@ -30,6 +30,17 @@ final class ExpandedPlayerWindowManager: NSObject {
|
|||||||
private var overlayFullScreenObservers: [NSObjectProtocol] = []
|
private var overlayFullScreenObservers: [NSObjectProtocol] = []
|
||||||
private var overlayToolbarWasVisible: Bool?
|
private var overlayToolbarWasVisible: Bool?
|
||||||
private var overlayWasFullScreenAtBegin = false
|
private var overlayWasFullScreenAtBegin = false
|
||||||
|
private var isOverlayParentEnteringFullScreen = false
|
||||||
|
|
||||||
|
/// The main window belongs to the app rather than the player. Preserve its
|
||||||
|
/// normal sizing rules while the inline overlay temporarily owns resizing.
|
||||||
|
private struct WindowSizingConstraints {
|
||||||
|
let contentAspectRatio: NSSize
|
||||||
|
let contentResizeIncrements: NSSize
|
||||||
|
let contentMinSize: NSSize
|
||||||
|
let minSize: NSSize
|
||||||
|
}
|
||||||
|
private var overlaySizingConstraints: WindowSizingConstraints?
|
||||||
|
|
||||||
/// Window hosting ContentView, registered by `MainContentWindowReader`.
|
/// Window hosting ContentView, registered by `MainContentWindowReader`.
|
||||||
/// `beginInlineOverlay` must not guess via mainWindow/keyWindow: when the
|
/// `beginInlineOverlay` must not guess via mainWindow/keyWindow: when the
|
||||||
@@ -67,8 +78,15 @@ final class ExpandedPlayerWindowManager: NSObject {
|
|||||||
/// alone fills the screen; restored when fullscreen ends.
|
/// alone fills the screen; restored when fullscreen ends.
|
||||||
private var cornerPanelsHiddenForFullScreen = false
|
private var cornerPanelsHiddenForFullScreen = false
|
||||||
|
|
||||||
private var isPlayerWindowFullScreen: Bool {
|
private var activePlayerWindow: NSWindow? {
|
||||||
isPlayerWindowEnteringFullScreen || (playerWindow?.styleMask.contains(.fullScreen) ?? false)
|
playerWindow ?? overlayParent
|
||||||
|
}
|
||||||
|
|
||||||
|
private var isActivePlayerWindowFullScreen: Bool {
|
||||||
|
if let playerWindow {
|
||||||
|
return isPlayerWindowEnteringFullScreen || playerWindow.styleMask.contains(.fullScreen)
|
||||||
|
}
|
||||||
|
return isOverlayParentEnteringFullScreen || (overlayParent?.styleMask.contains(.fullScreen) ?? false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configuration
|
// Configuration
|
||||||
@@ -87,9 +105,9 @@ final class ExpandedPlayerWindowManager: NSObject {
|
|||||||
private enum PendingAspectLock { case lock(Double), unlock }
|
private enum PendingAspectLock { case lock(Double), unlock }
|
||||||
private var pendingLiveResizeAspectLock: PendingAspectLock?
|
private var pendingLiveResizeAspectLock: PendingAspectLock?
|
||||||
|
|
||||||
/// Whether the user is currently dragging the player window's edge/corner.
|
/// Whether the user is currently dragging the active player host window.
|
||||||
var isLiveResizing: Bool {
|
var isLiveResizing: Bool {
|
||||||
playerWindow?.inLiveResize == true
|
activePlayerWindow?.inLiveResize == true
|
||||||
}
|
}
|
||||||
|
|
||||||
var isPresented: Bool {
|
var isPresented: Bool {
|
||||||
@@ -402,7 +420,11 @@ final class ExpandedPlayerWindowManager: NSObject {
|
|||||||
// entering flag is raised now so the aspect re-lock the panel hide
|
// entering flag is raised now so the aspect re-lock the panel hide
|
||||||
// triggers parks instead of auto-fitting the window mid-way.
|
// triggers parks instead of auto-fitting the window mid-way.
|
||||||
if !target.styleMask.contains(.fullScreen) {
|
if !target.styleMask.contains(.fullScreen) {
|
||||||
if playerWindow != nil { isPlayerWindowEnteringFullScreen = true }
|
if playerWindow != nil {
|
||||||
|
isPlayerWindowEnteringFullScreen = true
|
||||||
|
} else {
|
||||||
|
isOverlayParentEnteringFullScreen = true
|
||||||
|
}
|
||||||
if hideCornerPanelsForFullScreen() {
|
if hideCornerPanelsForFullScreen() {
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + Self.cornerPanelHideDuration) { [weak self] in
|
DispatchQueue.main.asyncAfter(deadline: .now() + Self.cornerPanelHideDuration) { [weak self] in
|
||||||
self?.performNativeFullScreenToggle()
|
self?.performNativeFullScreenToggle()
|
||||||
@@ -452,14 +474,31 @@ final class ExpandedPlayerWindowManager: NSObject {
|
|||||||
|
|
||||||
LoggingService.shared.debug("ExpandedPlayerWindowManager: beginning inline overlay", category: .player)
|
LoggingService.shared.debug("ExpandedPlayerWindowManager: beginning inline overlay", category: .player)
|
||||||
overlayParent = parent
|
overlayParent = parent
|
||||||
|
overlaySizingConstraints = WindowSizingConstraints(
|
||||||
|
contentAspectRatio: parent.contentAspectRatio,
|
||||||
|
contentResizeIncrements: parent.contentResizeIncrements,
|
||||||
|
contentMinSize: parent.contentMinSize,
|
||||||
|
minSize: parent.minSize
|
||||||
|
)
|
||||||
overlayToolbarWasVisible = parent.toolbar?.isVisible
|
overlayToolbarWasVisible = parent.toolbar?.isVisible
|
||||||
parent.toolbar?.isVisible = false
|
parent.toolbar?.isVisible = false
|
||||||
overlayWasFullScreenAtBegin = parent.styleMask.contains(.fullScreen)
|
overlayWasFullScreenAtBegin = parent.styleMask.contains(.fullScreen)
|
||||||
|
isOverlayParentEnteringFullScreen = false
|
||||||
|
hasCompletedInitialSizing = false
|
||||||
|
pendingFullScreenAspectRatio = nil
|
||||||
|
pendingLiveResizeAspectLock = nil
|
||||||
|
|
||||||
let coordinator = AppEnvironment.shared.navigationCoordinator
|
let coordinator = AppEnvironment.shared.navigationCoordinator
|
||||||
coordinator.isMacInlinePlayerFullScreen = overlayWasFullScreenAtBegin
|
coordinator.isMacInlinePlayerFullScreen = overlayWasFullScreenAtBegin
|
||||||
observeOverlayFullScreenTransitions(of: parent)
|
observeOverlayFullScreenTransitions(of: parent)
|
||||||
|
|
||||||
|
// ExpandedPlayerSheet can appear before ContentView's onChange registers
|
||||||
|
// the parent window. Re-run the same sizing decision on the next turn so
|
||||||
|
// the inline presentation cannot miss its initial aspect setup.
|
||||||
|
DispatchQueue.main.async { [weak self] in
|
||||||
|
self?.synchronizeActiveWindowAspectRatio()
|
||||||
|
}
|
||||||
|
|
||||||
// The overlay's render container may mount while the mini capsule is
|
// The overlay's render container may mount while the mini capsule is
|
||||||
// still tearing down, leaving the shared render view parked with no
|
// still tearing down, leaving the shared render view parked with no
|
||||||
// later lifecycle event to re-trigger adoption — retry until it lands.
|
// later lifecycle event to re-trigger adoption — retry until it lands.
|
||||||
@@ -490,11 +529,16 @@ final class ExpandedPlayerWindowManager: NSObject {
|
|||||||
if let wasVisible = overlayToolbarWasVisible {
|
if let wasVisible = overlayToolbarWasVisible {
|
||||||
parent.toolbar?.isVisible = wasVisible
|
parent.toolbar?.isVisible = wasVisible
|
||||||
}
|
}
|
||||||
|
restoreOverlaySizingConstraints(on: parent)
|
||||||
|
|
||||||
AppEnvironment.shared.navigationCoordinator.isMacInlinePlayerFullScreen = false
|
AppEnvironment.shared.navigationCoordinator.isMacInlinePlayerFullScreen = false
|
||||||
overlayToolbarWasVisible = nil
|
overlayToolbarWasVisible = nil
|
||||||
overlayWasFullScreenAtBegin = false
|
overlayWasFullScreenAtBegin = false
|
||||||
|
isOverlayParentEnteringFullScreen = false
|
||||||
|
pendingFullScreenAspectRatio = nil
|
||||||
|
pendingLiveResizeAspectLock = nil
|
||||||
overlayParent = nil
|
overlayParent = nil
|
||||||
|
overlaySizingConstraints = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Mirrors the parent window's fullscreen transitions into the coordinator
|
/// Mirrors the parent window's fullscreen transitions into the coordinator
|
||||||
@@ -513,7 +557,9 @@ final class ExpandedPlayerWindowManager: NSObject {
|
|||||||
queue: .main
|
queue: .main
|
||||||
) { _ in
|
) { _ in
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
ExpandedPlayerWindowManager.shared.hideCornerPanelsForFullScreen()
|
let manager = ExpandedPlayerWindowManager.shared
|
||||||
|
manager.isOverlayParentEnteringFullScreen = true
|
||||||
|
manager.hideCornerPanelsForFullScreen()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
center.addObserver(
|
center.addObserver(
|
||||||
@@ -522,6 +568,7 @@ final class ExpandedPlayerWindowManager: NSObject {
|
|||||||
queue: .main
|
queue: .main
|
||||||
) { _ in
|
) { _ in
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
|
ExpandedPlayerWindowManager.shared.isOverlayParentEnteringFullScreen = false
|
||||||
AppEnvironment.shared.navigationCoordinator.isMacInlinePlayerFullScreen = true
|
AppEnvironment.shared.navigationCoordinator.isMacInlinePlayerFullScreen = true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -531,10 +578,24 @@ final class ExpandedPlayerWindowManager: NSObject {
|
|||||||
queue: .main
|
queue: .main
|
||||||
) { _ in
|
) { _ in
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
|
let manager = ExpandedPlayerWindowManager.shared
|
||||||
|
manager.isOverlayParentEnteringFullScreen = false
|
||||||
AppEnvironment.shared.navigationCoordinator.isMacInlinePlayerFullScreen = false
|
AppEnvironment.shared.navigationCoordinator.isMacInlinePlayerFullScreen = false
|
||||||
// Mirror of the enter order: the window is back at its
|
// Mirror of the enter order: the window is back at its
|
||||||
// size first, then the panels slide in.
|
// size first, then the panels slide in.
|
||||||
ExpandedPlayerWindowManager.shared.restoreCornerPanelsAfterFullScreen()
|
manager.restoreCornerPanelsAfterFullScreen()
|
||||||
|
if let window = manager.overlayParent {
|
||||||
|
manager.replayPendingFullScreenAspectRatio(on: window)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
center.addObserver(
|
||||||
|
forName: NSWindow.didEndLiveResizeNotification,
|
||||||
|
object: parent,
|
||||||
|
queue: .main
|
||||||
|
) { _ in
|
||||||
|
Task { @MainActor in
|
||||||
|
ExpandedPlayerWindowManager.shared.applyPendingLiveResizeAspectLock()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@@ -623,13 +684,13 @@ final class ExpandedPlayerWindowManager: NSObject {
|
|||||||
/// - aspectRatio: Video width / height ratio
|
/// - aspectRatio: Video width / height ratio
|
||||||
/// - animated: Whether to animate the resize
|
/// - animated: Whether to animate the resize
|
||||||
func resizeToFitAspectRatio(_ aspectRatio: Double, animated: Bool = true) {
|
func resizeToFitAspectRatio(_ aspectRatio: Double, animated: Bool = true) {
|
||||||
guard let window = playerWindow else { return }
|
guard let window = activePlayerWindow else { return }
|
||||||
guard aspectRatio > 0 else { return }
|
guard aspectRatio > 0 else { return }
|
||||||
|
|
||||||
// In native fullscreen the video must keep the whole screen; constraining
|
// In native fullscreen the video must keep the whole screen; constraining
|
||||||
// or reframing the window here would letterbox the content inside the
|
// or reframing the window here would letterbox the content inside the
|
||||||
// fullscreen space. Park the ratio and replay it on fullscreen exit.
|
// fullscreen space. Park the ratio and replay it on fullscreen exit.
|
||||||
if isPlayerWindowFullScreen {
|
if isActivePlayerWindowFullScreen {
|
||||||
pendingFullScreenAspectRatio = aspectRatio
|
pendingFullScreenAspectRatio = aspectRatio
|
||||||
LoggingService.shared.debug(
|
LoggingService.shared.debug(
|
||||||
"resizeToFitAspectRatio deferred while fullscreen aspect=\(aspectRatio)",
|
"resizeToFitAspectRatio deferred while fullscreen aspect=\(aspectRatio)",
|
||||||
@@ -690,9 +751,9 @@ final class ExpandedPlayerWindowManager: NSObject {
|
|||||||
/// changing the current frame. Use this when the auto-resize setting is
|
/// changing the current frame. Use this when the auto-resize setting is
|
||||||
/// disabled but we still want manual resizing to be ratio-locked.
|
/// disabled but we still want manual resizing to be ratio-locked.
|
||||||
func lockAspectRatio(_ aspectRatio: Double) {
|
func lockAspectRatio(_ aspectRatio: Double) {
|
||||||
guard let window = playerWindow else { return }
|
guard let window = activePlayerWindow else { return }
|
||||||
guard aspectRatio > 0 else { return }
|
guard aspectRatio > 0 else { return }
|
||||||
if isPlayerWindowFullScreen {
|
if isActivePlayerWindowFullScreen {
|
||||||
pendingFullScreenAspectRatio = aspectRatio
|
pendingFullScreenAspectRatio = aspectRatio
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -708,7 +769,7 @@ final class ExpandedPlayerWindowManager: NSObject {
|
|||||||
/// video-plus-panels arrangement rather than the video alone. The size
|
/// video-plus-panels arrangement rather than the video alone. The size
|
||||||
/// floor stays enforced by `windowWillResize(_:to:)`.
|
/// floor stays enforced by `windowWillResize(_:to:)`.
|
||||||
func unlockAspectRatio() {
|
func unlockAspectRatio() {
|
||||||
guard let window = playerWindow else { return }
|
guard let window = activePlayerWindow else { return }
|
||||||
pendingFullScreenAspectRatio = nil
|
pendingFullScreenAspectRatio = nil
|
||||||
if window.inLiveResize {
|
if window.inLiveResize {
|
||||||
pendingLiveResizeAspectLock = .unlock
|
pendingLiveResizeAspectLock = .unlock
|
||||||
@@ -720,7 +781,11 @@ final class ExpandedPlayerWindowManager: NSObject {
|
|||||||
// ratio, which makes interactive resize a no-op (the window can't be
|
// ratio, which makes interactive resize a no-op (the window can't be
|
||||||
// resized at all). Resetting the increments to 1×1 is the supported way
|
// resized at all). Resetting the increments to 1×1 is the supported way
|
||||||
// to return to free resizing.
|
// to return to free resizing.
|
||||||
window.contentResizeIncrements = NSSize(width: 1, height: 1)
|
if window === overlayParent {
|
||||||
|
restoreOverlaySizingConstraints(on: window)
|
||||||
|
} else {
|
||||||
|
window.contentResizeIncrements = NSSize(width: 1, height: 1)
|
||||||
|
}
|
||||||
LoggingService.shared.debug(
|
LoggingService.shared.debug(
|
||||||
"unlockAspectRatio (corner layout) aspect=\(window.contentAspectRatio) increments=\(window.contentResizeIncrements)",
|
"unlockAspectRatio (corner layout) aspect=\(window.contentAspectRatio) increments=\(window.contentResizeIncrements)",
|
||||||
category: .player
|
category: .player
|
||||||
@@ -729,6 +794,55 @@ final class ExpandedPlayerWindowManager: NSObject {
|
|||||||
|
|
||||||
// MARK: - Private Helpers
|
// MARK: - Private Helpers
|
||||||
|
|
||||||
|
private func synchronizeActiveWindowAspectRatio() {
|
||||||
|
let appEnvironment = AppEnvironment.shared
|
||||||
|
if appEnvironment.isCornerPanelLayoutActive {
|
||||||
|
unlockAspectRatio()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let aspectRatio = appEnvironment.playerService.state.videoAspectRatio,
|
||||||
|
aspectRatio > 0 else { return }
|
||||||
|
lockAspectRatio(aspectRatio)
|
||||||
|
if appEnvironment.settingsManager.playerSheetAutoResize {
|
||||||
|
resizeToFitAspectRatio(aspectRatio, animated: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func restoreOverlaySizingConstraints(on window: NSWindow) {
|
||||||
|
guard let constraints = overlaySizingConstraints else { return }
|
||||||
|
window.contentMinSize = constraints.contentMinSize
|
||||||
|
window.minSize = constraints.minSize
|
||||||
|
if constraints.contentAspectRatio.width > 0,
|
||||||
|
constraints.contentAspectRatio.height > 0 {
|
||||||
|
window.contentAspectRatio = constraints.contentAspectRatio
|
||||||
|
} else {
|
||||||
|
// Setting resize increments is AppKit's supported way to clear an
|
||||||
|
// aspect constraint; assigning a zero contentAspectRatio is not.
|
||||||
|
window.contentResizeIncrements = constraints.contentResizeIncrements
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func applyPendingLiveResizeAspectLock() {
|
||||||
|
guard let pending = pendingLiveResizeAspectLock else { return }
|
||||||
|
pendingLiveResizeAspectLock = nil
|
||||||
|
switch pending {
|
||||||
|
case let .lock(ratio):
|
||||||
|
lockAspectRatio(ratio)
|
||||||
|
case .unlock:
|
||||||
|
unlockAspectRatio()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func replayPendingFullScreenAspectRatio(on window: NSWindow) {
|
||||||
|
guard let ratio = pendingFullScreenAspectRatio else { return }
|
||||||
|
pendingFullScreenAspectRatio = nil
|
||||||
|
guard !AppEnvironment.shared.isCornerPanelLayoutActive else { return }
|
||||||
|
Self.applyAspectRatioConstraint(ratio, to: window)
|
||||||
|
if AppEnvironment.shared.settingsManager.playerSheetAutoResize {
|
||||||
|
resizeToFitAspectRatio(ratio, animated: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Sets `contentAspectRatio` and a ratio-consistent minimum content size on
|
/// Sets `contentAspectRatio` and a ratio-consistent minimum content size on
|
||||||
/// the window so that interactive resize couples width and height
|
/// the window so that interactive resize couples width and height
|
||||||
/// proportionally and can't shrink below a usable minimum.
|
/// proportionally and can't shrink below a usable minimum.
|
||||||
@@ -868,7 +982,7 @@ extension ExpandedPlayerWindowManager: NSWindowDelegate {
|
|||||||
/// So the floor is computed here from constants and applied to the proposed
|
/// So the floor is computed here from constants and applied to the proposed
|
||||||
/// (already aspect-correct) frame, growing any under-sized proposal back up
|
/// (already aspect-correct) frame, growing any under-sized proposal back up
|
||||||
/// while preserving its ratio.
|
/// while preserving its ratio.
|
||||||
nonisolated func windowWillResize(_: NSWindow, to frameSize: NSSize) -> NSSize {
|
nonisolated func windowWillResize(_ window: NSWindow, to frameSize: NSSize) -> NSSize {
|
||||||
MainActor.assumeIsolated {
|
MainActor.assumeIsolated {
|
||||||
// Enforce the floor from constants here rather than from
|
// Enforce the floor from constants here rather than from
|
||||||
// `window.minSize`/`contentMinSize`: NSHostingController resets those
|
// `window.minSize`/`contentMinSize`: NSHostingController resets those
|
||||||
@@ -877,7 +991,7 @@ extension ExpandedPlayerWindowManager: NSWindowDelegate {
|
|||||||
// `contentAspectRatio` to `frameSize`, so we only need to grow an
|
// `contentAspectRatio` to `frameSize`, so we only need to grow an
|
||||||
// under-sized proposal back up to the minimum while preserving its
|
// under-sized proposal back up to the minimum while preserving its
|
||||||
// (already correct) aspect ratio.
|
// (already correct) aspect ratio.
|
||||||
let isAspectLocked = (playerWindow?.contentAspectRatio.width ?? 0) > 0
|
let isAspectLocked = window.contentAspectRatio.width > 0
|
||||||
|
|
||||||
var width: CGFloat
|
var width: CGFloat
|
||||||
var height: CGFloat
|
var height: CGFloat
|
||||||
@@ -911,14 +1025,7 @@ extension ExpandedPlayerWindowManager: NSWindowDelegate {
|
|||||||
|
|
||||||
nonisolated func windowDidEndLiveResize(_: Notification) {
|
nonisolated func windowDidEndLiveResize(_: Notification) {
|
||||||
MainActor.assumeIsolated {
|
MainActor.assumeIsolated {
|
||||||
guard let pending = pendingLiveResizeAspectLock else { return }
|
applyPendingLiveResizeAspectLock()
|
||||||
pendingLiveResizeAspectLock = nil
|
|
||||||
switch pending {
|
|
||||||
case let .lock(ratio):
|
|
||||||
lockAspectRatio(ratio)
|
|
||||||
case .unlock:
|
|
||||||
unlockAspectRatio()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -994,15 +1101,7 @@ extension ExpandedPlayerWindowManager: NSWindowDelegate {
|
|||||||
// advanced to a different-aspect video): restore the manual-resize
|
// advanced to a different-aspect video): restore the manual-resize
|
||||||
// ratio lock, and auto-fit the window if that setting is on —
|
// ratio lock, and auto-fit the window if that setting is on —
|
||||||
// exactly what the aspect-change handler would have done windowed.
|
// exactly what the aspect-change handler would have done windowed.
|
||||||
if let ratio = pendingFullScreenAspectRatio {
|
replayPendingFullScreenAspectRatio(on: window)
|
||||||
pendingFullScreenAspectRatio = nil
|
|
||||||
// Corner layout with visible panels keeps the window unconstrained.
|
|
||||||
guard appEnvironment?.isCornerPanelLayoutActive != true else { return }
|
|
||||||
Self.applyAspectRatioConstraint(ratio, to: window)
|
|
||||||
if appEnvironment?.settingsManager.playerSheetAutoResize == true {
|
|
||||||
resizeToFitAspectRatio(ratio, animated: false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user