diff --git a/Checks/compact-window.swift b/Checks/compact-window.swift index 2292ac2..b854c0f 100644 --- a/Checks/compact-window.swift +++ b/Checks/compact-window.swift @@ -472,11 +472,46 @@ struct CompactWindowChecks { } } + static func continuousQueueResize() throws { + let aspect = 16.0 / 9.0 + let sidebar: CGFloat = 260 + let window = NSWindow(contentRect: NSRect(x: -10_000, y: -10_000, width: 1_160, height: 900 / aspect), + styleMask: [.titled, .resizable, .fullSizeContentView], + backing: .buffered, defer: false) + window.isReleasedWhenClosed = false + defer { window.close() } + let controller = CompactWindowController() + controller.normalVideoLayout = { (aspect, sidebar) } + controller.attach(window) + for horizontal in [true, false] { + let start = window.frame + let chrome = start.height - window.contentRect(forFrameRect: start).height + window.delegate?.windowWillStartLiveResize?(Notification(name: NSWindow.willStartLiveResizeNotification, object: window)) + // AppKit proposes sizes from the pointer's drag origin. The delegate's + // correction of the other dimension must not feed back into axis selection. + for offset in [CGFloat(20), 21, 22, 23, 24, 23, 22, 21, 20, 0, -1, -2] { + let proposal = NSSize(width: start.width + (horizontal ? offset : 0), + height: start.height + (horizontal ? 0 : offset)) + let size = window.delegate!.windowWillResize!(window, to: proposal) + try require(close(horizontal ? size.width : size.height, + horizontal ? proposal.width : proposal.height), + "continuous queue resize follows the dragged edge without snapping back") + try require(close((size.width - sidebar) / (size.height - chrome), aspect), + "continuous queue resize keeps the video aspect") + var frame = start + frame.size = size + window.setFrame(frame, display: false) + } + window.delegate?.windowDidEndLiveResize?(Notification(name: NSWindow.didEndLiveResizeNotification, object: window)) + } + } + static func main() throws { try geometry() try windowBehavior() try reducedMotion() try rememberedSize() + try continuousQueueResize() print("Compact window checks passed") } } diff --git a/README.md b/README.md index ae33d72..06c4bc6 100644 --- a/README.md +++ b/README.md @@ -159,7 +159,7 @@ 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 require positive native resize increments and exercise horizontal/vertical resize-delegate proposals to preserve video aspect independently of the fixed-width sidebar. Physical edge-drag resizing still needs manual verification. +Queue-open resize checks require positive native resize increments and exercise continuous horizontal/vertical resize-delegate sequences, including reversals, to preserve video aspect without snap-back. The driving axis is fixed for each live resize gesture so aspect corrections do not feed back into axis selection. 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 1f6b91a..b5b018c 100644 --- a/Sources/App/CompactWindow.swift +++ b/Sources/App/CompactWindow.swift @@ -197,6 +197,8 @@ final class CompactWindowController: NSObject { // NSObject forwarding returns untyped Any; accesses remain on AppKit's main thread. nonisolated(unsafe) weak var original: (any NSWindowDelegate)? private weak var owner: CompactWindowController? + private var resizeStartSize: NSSize? + private var resizeWidthDriven: Bool? init(owner: CompactWindowController, original: (any NSWindowDelegate)?) { self.owner = owner @@ -212,6 +214,19 @@ final class CompactWindowController: NSObject { let responds = MainActor.assumeIsolated { original?.responds(to: selector) ?? false } return responds ? original : nil } + + func windowWillStartLiveResize(_ notification: Notification) { + resizeStartSize = (notification.object as? NSWindow)?.frame.size + resizeWidthDriven = nil + original?.windowWillStartLiveResize?(notification) + } + + func windowDidEndLiveResize(_ notification: Notification) { + resizeStartSize = nil + resizeWidthDriven = nil + original?.windowDidEndLiveResize?(notification) + } + func windowWillResize(_ sender: NSWindow, to proposedSize: NSSize) -> NSSize { let proposed = original?.windowWillResize?(sender, to: proposedSize) ?? proposedSize guard let owner, !owner.isCompact, !owner.isTransitioning, @@ -220,8 +235,16 @@ final class CompactWindowController: NSObject { layout.aspect.isFinite, layout.aspect > 0 else { return proposed } // The sidebar stays fixed-width; only the video area is aspect-locked. let chrome = sender.frame.height - sender.contentRect(forFrameRect: sender.frame).height - let widthDriven = abs(proposed.width - sender.frame.width) >= - abs(proposed.height - sender.frame.height) * layout.aspect + let start = resizeStartSize ?? sender.frame.size + let widthChange = abs(proposed.width - start.width) + let heightChange = abs(proposed.height - start.height) * layout.aspect + // Latch the user's axis once. Comparing against each corrected frame + // makes our own aspect adjustment look like a new drag on the other axis. + if resizeStartSize != nil, resizeWidthDriven == nil, + widthChange > 0 || heightChange > 0 { + resizeWidthDriven = widthChange >= heightChange + } + let widthDriven = resizeWidthDriven ?? (widthChange >= heightChange) let minimumHeight = max(sender.minSize.height - chrome, (sender.minSize.width - layout.sidebarWidth) / layout.aspect) let height = max(minimumHeight, widthDriven