fix: stabilize native window layout transitions

This commit is contained in:
2026-10-18 18:16:16 +02:00
parent 9670645b9a
commit 29a70678a4
4 changed files with 104 additions and 12 deletions
+78
View File
@@ -5,6 +5,10 @@ import MediaPlayer
@_cdecl("terebi_app_main")
@MainActor
public func checkAppStartup() {
if let index = CommandLine.arguments.firstIndex(of: "--check-window-layout") {
WindowLayoutCheck.run(result: URL(fileURLWithPath: CommandLine.arguments[index + 1]))
return
}
if let configuration = Bundle.main.url(forResource: "cli-check", withExtension: "json") {
CLIInstanceCheck.run(configuration)
return
@@ -12,6 +16,80 @@ public func checkAppStartup() {
AppStartupCheck.run()
}
@MainActor
private enum WindowLayoutCheck {
static var timer: Timer?
static var resultURL: URL!
static var step = 0
static var windowIdentity: ObjectIdentifier?
static var normalFrame = NSRect.zero
// Infrastructure timeout only; assertions depend on native transition completion.
static let deadline = ContinuousClock.now.advanced(by: .seconds(30))
static func run(result: URL) {
resultURL = result
_ = NSApplication.shared
_ = NotificationCenter.default.addObserver(forName: NSApplication.willFinishLaunchingNotification,
object: nil, queue: .main) { _ in
MainActor.assumeIsolated { PlayerModel.shared.setVolume(0) }
}
_ = NotificationCenter.default.addObserver(forName: NSWindow.didUpdateNotification,
object: nil, queue: .main) { _ in
MainActor.assumeIsolated {
for window in NSApp.windows { window.alphaValue = 0; window.ignoresMouseEvents = true }
}
}
timer = Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true) { _ in
MainActor.assumeIsolated { advance() }
}
terebiAppMain()
}
static func finish(_ error: String? = nil) {
timer?.invalidate()
let result: [String: Any] = ["passed": error == nil, "error": error ?? "", "steps": step]
try! JSONSerialization.data(withJSONObject: result).write(to: resultURL, options: .atomic)
PlayerModel.shared.shutdown()
NSApp.terminate(nil)
}
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,
let window = NSApp.windows.first(where: { $0.canBecomeMain && !($0 is NSPanel) }) else { return }
if windowIdentity == nil { windowIdentity = ObjectIdentifier(window) }
guard ObjectIdentifier(window) == windowIdentity else { finish("Transition replaced the playback window"); return }
window.layoutIfNeeded()
switch step % 8 {
case 0, 4:
if step > 0 {
let actual = window.frame
guard abs(actual.minX - normalFrame.minX) < 1, abs(actual.minY - normalFrame.minY) < 1,
abs(actual.width - normalFrame.width) < 1, abs(actual.height - normalFrame.height) < 1,
!model.isCompact else { finish("Normal geometry was not restored at step \(step): \(actual)"); return }
}
if step == 32 { finish(); return }
model.showsQueue = step % 8 == 4
case 1, 5:
window.setFrame(NSRect(x: 100, y: 200, width: 763, height: 368), display: true)
normalFrame = window.frame
case 2, 6:
// Repeated intent changes must keep the original restoration frame.
model.toggleCompact()
model.toggleCompact()
model.toggleCompact()
default:
guard model.isCompact, window.frame.width >= 160, window.frame.height >= 90,
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
}
model.toggleCompact()
}
step += 1
}
}
@MainActor
private enum AppStartupCheck {
static var model: PlayerModel?