fix: stabilize aspect locked queue window resizing
This commit is contained in:
@@ -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 {
|
static func main() throws {
|
||||||
try geometry()
|
try geometry()
|
||||||
try windowBehavior()
|
try windowBehavior()
|
||||||
try reducedMotion()
|
try reducedMotion()
|
||||||
try rememberedSize()
|
try rememberedSize()
|
||||||
|
try continuousQueueResize()
|
||||||
print("Compact window checks passed")
|
print("Compact window checks passed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
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.
|
`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 <baseline-binary>` 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.
|
The [benchmark runner](Checks/playback-benchmark.py) accepts `--compare-native <baseline-binary>` 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.
|
||||||
|
|||||||
@@ -197,6 +197,8 @@ final class CompactWindowController: NSObject {
|
|||||||
// NSObject forwarding returns untyped Any; accesses remain on AppKit's main thread.
|
// NSObject forwarding returns untyped Any; accesses remain on AppKit's main thread.
|
||||||
nonisolated(unsafe) weak var original: (any NSWindowDelegate)?
|
nonisolated(unsafe) weak var original: (any NSWindowDelegate)?
|
||||||
private weak var owner: CompactWindowController?
|
private weak var owner: CompactWindowController?
|
||||||
|
private var resizeStartSize: NSSize?
|
||||||
|
private var resizeWidthDriven: Bool?
|
||||||
|
|
||||||
init(owner: CompactWindowController, original: (any NSWindowDelegate)?) {
|
init(owner: CompactWindowController, original: (any NSWindowDelegate)?) {
|
||||||
self.owner = owner
|
self.owner = owner
|
||||||
@@ -212,6 +214,19 @@ final class CompactWindowController: NSObject {
|
|||||||
let responds = MainActor.assumeIsolated { original?.responds(to: selector) ?? false }
|
let responds = MainActor.assumeIsolated { original?.responds(to: selector) ?? false }
|
||||||
return responds ? original : nil
|
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 {
|
func windowWillResize(_ sender: NSWindow, to proposedSize: NSSize) -> NSSize {
|
||||||
let proposed = original?.windowWillResize?(sender, to: proposedSize) ?? proposedSize
|
let proposed = original?.windowWillResize?(sender, to: proposedSize) ?? proposedSize
|
||||||
guard let owner, !owner.isCompact, !owner.isTransitioning,
|
guard let owner, !owner.isCompact, !owner.isTransitioning,
|
||||||
@@ -220,8 +235,16 @@ final class CompactWindowController: NSObject {
|
|||||||
layout.aspect.isFinite, layout.aspect > 0 else { return proposed }
|
layout.aspect.isFinite, layout.aspect > 0 else { return proposed }
|
||||||
// The sidebar stays fixed-width; only the video area is aspect-locked.
|
// The sidebar stays fixed-width; only the video area is aspect-locked.
|
||||||
let chrome = sender.frame.height - sender.contentRect(forFrameRect: sender.frame).height
|
let chrome = sender.frame.height - sender.contentRect(forFrameRect: sender.frame).height
|
||||||
let widthDriven = abs(proposed.width - sender.frame.width) >=
|
let start = resizeStartSize ?? sender.frame.size
|
||||||
abs(proposed.height - sender.frame.height) * layout.aspect
|
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,
|
let minimumHeight = max(sender.minSize.height - chrome,
|
||||||
(sender.minSize.width - layout.sidebarWidth) / layout.aspect)
|
(sender.minSize.width - layout.sidebarWidth) / layout.aspect)
|
||||||
let height = max(minimumHeight, widthDriven
|
let height = max(minimumHeight, widthDriven
|
||||||
|
|||||||
Reference in New Issue
Block a user