diff --git a/Checks/app-startup.py b/Checks/app-startup.py index 3df602d..8e7a94b 100644 --- a/Checks/app-startup.py +++ b/Checks/app-startup.py @@ -298,6 +298,7 @@ def check_cli_instances(root, app, fixtures): "queue preserves video size and mouse target", action="queue", visible=True) window = opened[first_pid]["window"] if (not window["resizable"] or window["aspectWidth"] != 0 or window["aspectHeight"] != 0 + or window["resizeIncrementWidth"] <= 0 or window["resizeIncrementHeight"] <= 0 or window["minWidth"] > 660 or window["minHeight"] > 300 or window["maxWidth"] <= window["width"] + 80 or window["maxHeight"] <= window["height"] + 80 diff --git a/Checks/app-startup.swift b/Checks/app-startup.swift index 7a1c799..aab390e 100644 --- a/Checks/app-startup.swift +++ b/Checks/app-startup.swift @@ -111,6 +111,8 @@ private enum CLIInstanceCheck { "contentMinWidth": window.contentMinSize.width, "contentMinHeight": window.contentMinSize.height, "contentMaxWidth": window.contentMaxSize.width, "contentMaxHeight": window.contentMaxSize.height, "aspectWidth": window.aspectRatio.width, "aspectHeight": window.aspectRatio.height, + "resizeIncrementWidth": window.resizeIncrements.width, + "resizeIncrementHeight": window.resizeIncrements.height, "resizable": window.styleMask.contains(.resizable), ] if let surface = surface(in: content) { diff --git a/README.md b/README.md index fbf8e6c..1f87a61 100644 --- a/README.md +++ b/README.md @@ -157,6 +157,8 @@ The full suite requires a graphical macOS session and a working audio device; it The startup checks verify that playing video remains attached, unhidden and sized to its native drag surface, including with the queue open; playback-state checks alone do not establish renderer visibility. +Queue-open resize checks also require positive native resize increments: clearing AppKit's aspect ratio alone leaves zero increments after aspect-locked video. Physical edge-drag resizing still needs manual verification. + `make benchmark-playback` measures cold source startup, cached/uncached seeks, and sustained native/Cast playback with repeatable loopback fixtures. It requires the same macOS session/audio device and fixture-generation FFmpeg; results exclude live extraction, WAN/TLS, and physical-receiver latency. The [benchmark runner](Checks/playback-benchmark.py) accepts `--compare-native ` for alternating native-only comparisons; add `--seek-work` to isolate cache-miss seeks when both binaries support that mode. Preserve the baseline executable before rebuilding. diff --git a/Sources/App/CompactWindow.swift b/Sources/App/CompactWindow.swift index 01234c8..001d7db 100644 --- a/Sources/App/CompactWindow.swift +++ b/Sources/App/CompactWindow.swift @@ -20,6 +20,7 @@ final class CompactWindowController: NSObject { let minSize: NSSize let contentMinSize: NSSize let aspectRatio: NSSize + let resizeIncrements: NSSize let styleMask: NSWindow.StyleMask let titleVisibility: NSWindow.TitleVisibility let titlebarAppearsTransparent: Bool @@ -34,6 +35,7 @@ final class CompactWindowController: NSObject { minSize = window.minSize contentMinSize = window.contentMinSize aspectRatio = window.aspectRatio + resizeIncrements = window.resizeIncrements styleMask = window.styleMask titleVisibility = window.titleVisibility titlebarAppearsTransparent = window.titlebarAppearsTransparent @@ -310,7 +312,11 @@ final class CompactWindowController: NSObject { window.minSize = snapshot.minSize window.contentMinSize = snapshot.contentMinSize window.setFrame(snapshot.frame, display: true) - window.aspectRatio = snapshot.aspectRatio + if snapshot.aspectRatio == .zero { + window.resizeIncrements = snapshot.resizeIncrements + } else { + window.aspectRatio = snapshot.aspectRatio + } self.snapshot = nil } @@ -380,7 +386,7 @@ final class CompactWindowController: NSObject { frame.origin.x = min(max(window.frame.minX, usable.minX), usable.maxX - frame.width) frame.origin.y = min(max(window.frame.minY, usable.minY), usable.maxY - frame.height) } - window.aspectRatio = .zero + window.resizeIncrements = NSSize(width: 1, height: 1) animateFrame(frame, of: window, animated: animated) { [weak self, weak window] in guard let self, let window else { return } if Self.canFitAspect(aspect, available: usable.size) { diff --git a/Sources/App/TerebiApp.swift b/Sources/App/TerebiApp.swift index 2cb7ef4..cb1a158 100644 --- a/Sources/App/TerebiApp.swift +++ b/Sources/App/TerebiApp.swift @@ -36,7 +36,8 @@ final class PlayerModel: ObservableObject { didSet { guard showsQueue != oldValue, let window, !isFullScreen, !isCompact, !compactWindow.isTransitioning else { return } - window.aspectRatio = .zero + // Setting aspectRatio to zero leaves AppKit's resize increments at zero. + window.resizeIncrements = NSSize(width: 1, height: 1) var frame = window.frame frame.size.width += showsQueue ? 260 : -260 if let screen = window.screen?.visibleFrame { @@ -706,7 +707,7 @@ final class PlayerModel: ObservableObject { youtubeHeight = nil youtubeQuality = nil needsVideoFit = false - window?.aspectRatio = .zero + window?.resizeIncrements = NSSize(width: 1, height: 1) commandError = nil isSeeking = false window?.representedURL = nil @@ -979,7 +980,7 @@ final class PlayerModel: ObservableObject { } needsVideoFit = false guard state.has_video, aspect.isFinite, aspect > 0 else { - window.aspectRatio = .zero + window.resizeIncrements = NSSize(width: 1, height: 1) return } let screen = (window.screen ?? NSScreen.main)?.visibleFrame ?? window.frame @@ -988,10 +989,14 @@ final class PlayerModel: ObservableObject { let size = NSSize(width: width + sidebarWidth, height: width / aspect) // ponytail: extreme ratios retain letterboxing; compact controls if exact fitting is needed. guard size.width >= window.minSize.width, size.height >= window.minSize.height else { - window.aspectRatio = .zero + window.resizeIncrements = NSSize(width: 1, height: 1) return } - window.aspectRatio = showsQueue ? .zero : NSSize(width: aspect, height: 1) + if showsQueue { + window.resizeIncrements = NSSize(width: 1, height: 1) + } else { + window.aspectRatio = NSSize(width: aspect, height: 1) + } let frame = NSRect( x: min(max(window.frame.midX - size.width / 2, screen.minX), screen.maxX - size.width), y: min(max(window.frame.midY - size.height / 2, screen.minY), screen.maxY - size.height),