fix: scope control fades to preserve responsive window animation

This commit is contained in:
2026-10-18 18:43:05 +02:00
parent 29a70678a4
commit c2261ad2f0
5 changed files with 87 additions and 30 deletions
+5 -3
View File
@@ -46,10 +46,12 @@ def check_window_layout(root, app, fixtures):
)
log = diagnostics.read_text(errors="replace") if diagnostics.exists() else ""
report = json.loads(result_path.read_text()) if result_path.exists() else {}
if result.returncode != 0 or not report.get("passed") or report.get("steps") != 32:
expected_animations = 0 if report.get("reduceMotion") else 16
if (result.returncode != 0 or not report.get("passed") or report.get("steps") != 32
or report.get("animatedTransitions") != expected_animations):
raise RuntimeError(f"native window layout failed: {report}; {result.stdout}{result.stderr}{log}")
print("PASS native SwiftUI window layout: repeated compact/normal transitions with queue open/closed, "
"rapid intent changes, original window and exact normal geometry restored")
print(f"PASS native SwiftUI window layout: {report['animatedTransitions']} observed animations, "
"responsive mid-animation requests, queue open/closed, minimum sizes and exact normal geometry restored")
def check_cli_instances(root, app, fixtures):
+46 -2
View File
@@ -23,6 +23,11 @@ private enum WindowLayoutCheck {
static var step = 0
static var windowIdentity: ObjectIdentifier?
static var normalFrame = NSRect.zero
static var animationStart: NSRect?
static var animationTarget = NSRect.zero
static var sawIntermediateFrame = false
static var animatedTransitions = 0
static var expectsAnimation: Bool { !NSWorkspace.shared.accessibilityDisplayShouldReduceMotion }
// Infrastructure timeout only; assertions depend on native transition completion.
static let deadline = ContinuousClock.now.advanced(by: .seconds(30))
@@ -47,17 +52,46 @@ private enum WindowLayoutCheck {
static func finish(_ error: String? = nil) {
timer?.invalidate()
let result: [String: Any] = ["passed": error == nil, "error": error ?? "", "steps": step]
let result: [String: Any] = ["passed": error == nil, "error": error ?? "", "steps": step,
"animatedTransitions": animatedTransitions, "reduceMotion": !expectsAnimation]
try! JSONSerialization.data(withJSONObject: result).write(to: resultURL, options: .atomic)
PlayerModel.shared.shutdown()
NSApp.terminate(nil)
}
static func sameFrame(_ lhs: NSRect, _ rhs: NSRect) -> Bool {
abs(lhs.minX - rhs.minX) < 1 && abs(lhs.minY - rhs.minY) < 1 &&
abs(lhs.width - rhs.width) < 1 && abs(lhs.height - rhs.height) < 1
}
static func observeAnimation(from window: NSWindow, to target: NSRect) {
animationStart = window.frame
animationTarget = target
sawIntermediateFrame = false
}
static func advance() {
let model = PlayerModel.shared
guard ContinuousClock.now < deadline else { finish("Window layout did not settle at step \(step)"); return }
guard model.canTogglePlayback, model.state.has_video, !model.compactWindow.isTransitioning,
guard model.canTogglePlayback, model.state.has_video,
let window = NSApp.windows.first(where: { $0.canBecomeMain && !($0 is NSPanel) }) else { return }
if model.compactWindow.isTransitioning {
if expectsAnimation, let start = animationStart, !sawIntermediateFrame,
!sameFrame(window.frame, start), !sameFrame(window.frame, animationTarget) {
sawIntermediateFrame = true
// These run-loop-delivered requests must remain responsive mid-animation.
model.toggleCompact()
model.toggleCompact()
}
return
}
if animationStart != nil {
if expectsAnimation {
guard sawIntermediateFrame else { finish("No nonblocking animation frames at step \(step)"); return }
animatedTransitions += 1
}
animationStart = nil
}
if windowIdentity == nil { windowIdentity = ObjectIdentifier(window) }
guard ObjectIdentifier(window) == windowIdentity else { finish("Transition replaced the playback window"); return }
window.layoutIfNeeded()
@@ -74,8 +108,14 @@ private enum WindowLayoutCheck {
case 1, 5:
window.setFrame(NSRect(x: 100, y: 200, width: 763, height: 368), display: true)
normalFrame = window.frame
guard window.minSize.width >= (model.showsQueue ? 660 : 400), window.minSize.height >= 240 else {
finish("Normal minimum size was lost: \(window.minSize)"); return
}
case 2, 6:
// Repeated intent changes must keep the original restoration frame.
observeAnimation(from: window, to: CompactWindowController.initialFrame(
aspect: model.state.video_aspect_ratio,
visibleFrame: (window.screen ?? NSScreen.main!).visibleFrame, width: 320))
model.toggleCompact()
model.toggleCompact()
model.toggleCompact()
@@ -84,6 +124,10 @@ private enum WindowLayoutCheck {
abs(window.frame.width / window.frame.height - model.state.video_aspect_ratio) < 0.01 else {
finish("Compact geometry did not settle at step \(step): \(window.frame)"); return
}
guard window.minSize.width >= 160, window.minSize.height >= 90 else {
finish("Compact minimum size was lost: \(window.minSize)"); return
}
observeAnimation(from: window, to: normalFrame)
model.toggleCompact()
}
step += 1