Files
terebi/Checks/compact-window.swift
T

437 lines
23 KiB
Swift

import AppKit
import CoreFoundation
import Foundation
@main
@MainActor
struct CompactWindowChecks {
struct Failure: Error, CustomStringConvertible {
let description: String
}
static func require(_ condition: @autoclosure () -> Bool, _ message: String) throws {
if !condition() { throw Failure(description: "FAIL: \(message)") }
}
static func close(_ actual: CGFloat, _ expected: CGFloat) -> Bool {
abs(actual - expected) < 0.001
}
static func sameFrame(_ actual: NSRect, _ expected: NSRect) -> Bool {
close(actual.minX, expected.minX) && close(actual.minY, expected.minY)
&& close(actual.width, expected.width) && close(actual.height, expected.height)
}
static func geometry() throws {
let screen = NSRect(x: 100, y: 200, width: 1_000, height: 700)
func snap(_ x: CGFloat, _ y: CGFloat, to expectedX: CGFloat, _ expectedY: CGFloat) throws {
let frame = NSRect(x: x, y: y, width: 320, height: 180)
let expected = NSRect(x: expectedX, y: expectedY, width: 320, height: 180)
try require(sameFrame(CompactWindowController.snappedFrame(frame, visibleFrame: screen), expected),
"snap (\(x), \(y)) to (\(expectedX), \(expectedY)), preserving size")
}
// Inset edges: x = 116 / 764, y = 216 / 704. Each axis acts independently.
try snap(130, 460, to: 116, 460)
try snap(750, 460, to: 764, 460)
try snap(440, 230, to: 440, 216)
try snap(440, 690, to: 440, 704)
try snap(130, 230, to: 116, 216)
try snap(750, 230, to: 764, 216)
try snap(130, 690, to: 116, 704)
try snap(750, 690, to: 764, 704)
try snap(140, 460, to: 116, 460)
try snap(740, 460, to: 764, 460)
try snap(440, 240, to: 440, 216)
try snap(440, 680, to: 440, 704)
try snap(92, 460, to: 116, 460)
try snap(788, 460, to: 764, 460)
try snap(141, 460, to: 141, 460)
try snap(739, 460, to: 739, 460)
try snap(440, 241, to: 440, 241)
try snap(440, 679, to: 440, 679)
try snap(440, 192, to: 440, 216)
try snap(440, 728, to: 440, 704)
try snap(91, 460, to: 91, 460)
try snap(789, 460, to: 789, 460)
try snap(440, 191, to: 440, 191)
try snap(440, 729, to: 440, 729)
try snap(440, 460, to: 440, 460)
let negativeScreen = NSRect(x: -1_440, y: -900, width: 1_440, height: 900)
let negativeFrame = NSRect(x: -1_410, y: -870, width: 320, height: 180)
try require(sameFrame(CompactWindowController.snappedFrame(negativeFrame, visibleFrame: negativeScreen),
NSRect(x: -1_424, y: -884, width: 320, height: 180)),
"negative-origin screen uses its own inset edges")
let narrowScreen = NSRect(x: 0, y: 0, width: 100, height: 200)
try require(sameFrame(CompactWindowController.snappedFrame(
NSRect(x: 35, y: 80, width: 40, height: 20), visibleFrame: narrowScreen),
NSRect(x: 44, y: 80, width: 40, height: 20)),
"overlapping magnetic zones choose the closest edge, not first edge")
let usableScreen = NSRect(x: -1_200, y: 50, width: 1_200, height: 800)
let landscape = CompactWindowController.initialFrame(aspect: 16.0 / 9.0, visibleFrame: usableScreen)
try require(sameFrame(landscape, NSRect(x: -336, y: 654, width: 320, height: 180)),
"16:9 enters at top right with 320pt width and 16pt inset")
let portrait = CompactWindowController.initialFrame(aspect: 9.0 / 16.0, visibleFrame: usableScreen)
try require(sameFrame(portrait, NSRect(x: -336, y: 834 - 5_120.0 / 9.0, width: 320, height: 5_120.0 / 9.0)),
"portrait preserves exact aspect when it fits")
for aspect in [16.0 / 9.0, 9.0 / 16.0, 100.0, 0.01] {
let frame = CompactWindowController.initialFrame(aspect: aspect, visibleFrame: usableScreen)
try require(frame.origin.x.isFinite && frame.origin.y.isFinite
&& frame.width.isFinite && frame.height.isFinite,
"initial geometry is finite for aspect \(aspect)")
try require(frame.width >= 160 && frame.height >= 90,
"extreme aspect \(aspect) never defeats minimum size")
try require(frame.minX >= usableScreen.minX + 16 && frame.minY >= usableScreen.minY + 16
&& frame.maxX <= usableScreen.maxX - 16 && frame.maxY <= usableScreen.maxY - 16,
"aspect \(aspect) stays inside inset usable screen")
try require(close(frame.maxX, usableScreen.maxX - 16) && close(frame.maxY, usableScreen.maxY - 16),
"aspect \(aspect) stays anchored top right")
}
let smallScreen = NSRect(x: 0, y: 0, width: 300, height: 400)
let bounded = CompactWindowController.initialFrame(aspect: 16.0 / 9.0, visibleFrame: smallScreen)
try require(sameFrame(bounded, NSRect(x: 16, y: 233.25, width: 268, height: 150.75)),
"target width shrinks to usable bounds without distorting ordinary aspect")
}
// Run the real AppKit completion callbacks. The monotonic deadline is only a hang watchdog.
static func settled(_ controller: CompactWindowController, compact: Bool,
checking: () throws -> Void = {}) throws {
let deadline = ContinuousClock.now.advanced(by: .seconds(5))
while controller.isTransitioning {
try checking()
try require(ContinuousClock.now < deadline, "native compact transition completes")
CFRunLoopRunInMode(.defaultMode, 0.01, false)
}
try checking()
try require(controller.isCompact == compact, "settled mode matches the latest destination")
}
@MainActor
struct WindowState {
let frame: NSRect
let minSize: NSSize
let contentMinSize: NSSize
let aspect: NSSize
let style: NSWindow.StyleMask
let titleVisibility: NSWindow.TitleVisibility
let transparentTitlebar: Bool
let collection: NSWindow.CollectionBehavior
let level: NSWindow.Level
let hides: Bool
let drag: Bool
init(_ window: NSWindow) {
frame = window.frame
minSize = window.minSize
contentMinSize = window.contentMinSize
aspect = window.aspectRatio
style = window.styleMask
titleVisibility = window.titleVisibility
transparentTitlebar = window.titlebarAppearsTransparent
collection = window.collectionBehavior
level = window.level
hides = window.hidesOnDeactivate
drag = window.isMovableByWindowBackground
}
func requireMatches(_ window: NSWindow) throws {
try CompactWindowChecks.require(CompactWindowChecks.sameFrame(window.frame, frame),
"restore recovers original normal geometry")
try CompactWindowChecks.require(window.minSize == minSize && window.contentMinSize == contentMinSize
&& window.aspectRatio == aspect, "restore recovers aspect and both minima")
try CompactWindowChecks.require(window.styleMask == style && window.collectionBehavior == collection
&& window.level == level, "restore recovers style, Spaces policy, and level")
try CompactWindowChecks.require(window.titleVisibility == titleVisibility
&& window.titlebarAppearsTransparent == transparentTitlebar,
"restore recovers original native chrome")
try CompactWindowChecks.require(window.hidesOnDeactivate == hides
&& window.isMovableByWindowBackground == drag,
"restore recovers deactivate and background drag behavior")
}
}
@MainActor
final class WindowFixture {
let window: NSWindow
let content: NSView
let sentinel: NSView
let sentinelFrame: NSRect
let original: WindowState
let identity: ObjectIdentifier
init() throws {
// Never orderFront, makeKey, or create an actual native fullscreen Space.
window = NSWindow(contentRect: NSRect(x: -10_000, y: -10_000, width: 640, height: 360),
styleMask: [.titled, .closable, .miniaturizable, .resizable],
backing: .buffered, defer: false)
window.isReleasedWhenClosed = false
guard let content = window.contentView else {
throw Failure(description: "FAIL: missing content view")
}
self.content = content
sentinel = NSView(frame: NSRect(x: 5, y: 7, width: 12, height: 14))
content.addSubview(sentinel)
sentinelFrame = sentinel.frame
window.minSize = NSSize(width: 300, height: 180)
window.contentMinSize = NSSize(width: 220, height: 120)
window.aspectRatio = NSSize(width: 4, height: 3)
window.collectionBehavior = [.managed, .fullScreenPrimary]
window.level = .normal
window.hidesOnDeactivate = true
window.isMovableByWindowBackground = false
original = WindowState(window)
identity = ObjectIdentifier(window)
}
func preservedContent() throws {
try CompactWindowChecks.require(ObjectIdentifier(window) == identity && sentinel.window === window,
"compact mode keeps the actual rendering window")
try CompactWindowChecks.require(window.contentView === content && sentinel.superview === content
&& content.subviews.count == 1
&& CompactWindowChecks.sameFrame(sentinel.frame, sentinelFrame),
"compact mode preserves content and attached sentinel without reparenting")
try CompactWindowChecks.require(!window.isVisible && !window.isKeyWindow,
"checks never display or key a window")
}
func restored() throws {
try original.requireMatches(window)
try preservedContent()
}
func compact(aspect: Double) throws {
try CompactWindowChecks.require(window.level == .floating && !window.hidesOnDeactivate,
"compact floats and remains visible on deactivate")
try CompactWindowChecks.require(window.collectionBehavior.contains(
[.canJoinAllSpaces, .canJoinAllApplications, .fullScreenAuxiliary]),
"compact can join Spaces, other-app fullscreen, and Stage Manager")
try CompactWindowChecks.require(window.collectionBehavior.intersection(
[.primary, .auxiliary, .fullScreenPrimary, .fullScreenNone]).isEmpty,
"compact removes incompatible primary and auxiliary policies")
try CompactWindowChecks.require(window.contentMinSize == NSSize(width: 160, height: 90),
"compact uses 160x90 content minimum")
try CompactWindowChecks.require(window.isMovableByWindowBackground,
"compact allows background dragging")
try CompactWindowChecks.require(window.canBecomeKey, "compact accepts keyboard playback controls")
try CompactWindowChecks.require(window.titleVisibility == .hidden
&& window.titlebarAppearsTransparent,
"compact hides native title chrome")
let visible = (window.screen ?? NSScreen.main!).visibleFrame
try CompactWindowChecks.require(CompactWindowChecks.close(window.frame.maxX, visible.maxX - 16)
&& CompactWindowChecks.close(window.frame.maxY, visible.maxY - 16)
&& CompactWindowChecks.close(window.frame.width / window.frame.height,
CGFloat(aspect)),
"compact enters at inset top right with playback aspect")
try preservedContent()
}
}
// A later real native round-trip gives canceled animations a completion opportunity without
// guessing elapsed animation time. Watch the interrupted window throughout and after it.
static func nativeCompletionOpportunity(checking: () throws -> Void) throws {
let fixture = try WindowFixture()
let controller = CompactWindowController(reduceMotion: { false })
defer {
controller.restore(resumeFullScreen: false, animated: false)
fixture.window.close()
}
controller.attach(fixture.window)
controller.toggle(aspect: 16.0 / 9.0)
try require(controller.isTransitioning, "native completion barrier is an actual animated entry")
try settled(controller, compact: true, checking: checking)
try fixture.compact(aspect: 16.0 / 9.0)
controller.restore(resumeFullScreen: false)
try settled(controller, compact: false, checking: checking)
try fixture.restored()
}
static func windowBehavior() throws {
_ = NSApplication.shared
guard NSScreen.main != nil else {
throw Failure(description: "FAIL: native window checks require a macOS GUI session with a screen")
}
let fixture = try WindowFixture()
let window = fixture.window
let controller = CompactWindowController(reduceMotion: { false })
defer {
controller.restore(resumeFullScreen: false, animated: false)
window.close()
}
controller.attach(window)
try require(!controller.isCompact && !controller.isTransitioning, "attached window starts ordinary")
try require(controller.snappingEnabled, "isolated controller defaults snapping on")
controller.toggle(aspect: 16.0 / 9.0)
try require(controller.isCompact && controller.isTransitioning,
"entry presents the compact destination while native geometry settles")
try fixture.preservedContent()
try settled(controller, compact: true)
try fixture.compact(aspect: 16.0 / 9.0)
let visible = (window.screen ?? NSScreen.main!).visibleFrame
let nearEdge = NSRect(x: visible.minX + 26, y: visible.minY + 27,
width: window.frame.width, height: window.frame.height)
window.setFrame(nearEdge, display: false)
try require(sameFrame(window.frame, nearEdge), "native move installs unsnapped drag position")
controller.snapAfterDrag()
try require(close(window.frame.minX, visible.minX + 16) && close(window.frame.minY, visible.minY + 16)
&& window.frame.size == nearEdge.size, "enabled preference snaps the actual moved window")
controller.snappingEnabled = false
let second = CompactWindowController(snappingEnabled: false, reduceMotion: { false })
try require(!second.snappingEnabled, "configuration can disable snapping for a new controller")
window.setFrame(nearEdge, display: false)
controller.snapAfterDrag()
try require(sameFrame(window.frame, nearEdge), "disabled preference leaves an actual moved window untouched")
second.snappingEnabled = true
let third = CompactWindowController(snappingEnabled: false, reduceMotion: { false })
try require(!third.snappingEnabled, "a session override does not change the next controller's settings")
controller.snappingEnabled = true
let resized = NSRect(x: nearEdge.minX, y: nearEdge.minY, width: 240, height: 135)
window.setFrame(resized, display: false)
controller.updateAspect(16.0 / 9.0)
try require(sameFrame(window.frame, resized),
"repeated aspect updates preserve a user's compact resize and placement")
controller.updateAspect(4.0 / 3.0)
try require(controller.isCompact && !controller.isTransitioning, "aspect update retains compact mode")
try require(close(window.frame.width / window.frame.height, 4.0 / 3.0),
"actual compact window follows changed playback aspect")
try fixture.preservedContent()
controller.toggle(aspect: 4.0 / 3.0)
try settled(controller, compact: false)
try fixture.restored()
// Enter -> exit before entry completes: the original normal snapshot must survive.
controller.toggle(aspect: 16.0 / 9.0)
try require(controller.isTransitioning, "entry is in flight before its destination changes")
controller.toggle(aspect: 16.0 / 9.0)
try settled(controller, compact: false)
try fixture.restored()
// Exit -> enter keeps the newest aspect and must not save an intermediate restore frame.
controller.toggle(aspect: 16.0 / 9.0)
try settled(controller, compact: true)
controller.toggle(aspect: 16.0 / 9.0)
try require(!controller.isCompact && controller.isTransitioning,
"exit presents the ordinary destination while native geometry settles")
controller.toggle(aspect: 4.0 / 3.0)
try settled(controller, compact: true)
try fixture.compact(aspect: 4.0 / 3.0)
controller.restore(resumeFullScreen: false)
try settled(controller, compact: false)
try fixture.restored()
// Alternating requests within one native animation settle only at the latest destination.
for compactDestination in [false, true] {
controller.toggle(aspect: 16.0 / 9.0)
try require(controller.isTransitioning, "alternating requests interrupt native entry")
for _ in 0..<(compactDestination ? 6 : 5) {
controller.toggle(aspect: 16.0 / 9.0)
}
try settled(controller, compact: compactDestination)
if compactDestination {
try fixture.compact(aspect: 16.0 / 9.0)
controller.restore(resumeFullScreen: false)
try settled(controller, compact: false)
}
try fixture.restored()
}
controller.toggle(aspect: 16.0 / 9.0)
controller.restore(resumeFullScreen: false)
try settled(controller, compact: false)
try fixture.restored()
controller.restore(resumeFullScreen: false)
try settled(controller, compact: false)
try fixture.restored()
// Urgent teardown is immediate; old native completion cannot touch its restored snapshot.
controller.toggle(aspect: 16.0 / 9.0)
try require(controller.isTransitioning, "urgent restore interrupts native entry")
controller.restore(resumeFullScreen: false, animated: false)
try require(!controller.isCompact && !controller.isTransitioning, "urgent restore settles immediately")
try fixture.restored()
try nativeCompletionOpportunity {
try require(!controller.isCompact && !controller.isTransitioning,
"stale completion cannot revive an urgently restored session")
try fixture.restored()
}
let replacement = try WindowFixture()
defer { replacement.window.close() }
controller.toggle(aspect: 16.0 / 9.0)
try require(controller.isTransitioning, "attachment interrupts native entry")
controller.attach(replacement.window)
try require(!controller.isCompact && !controller.isTransitioning, "replacement starts ordinary")
try fixture.restored()
controller.toggle(aspect: 4.0 / 3.0)
try settled(controller, compact: true) { try fixture.restored() }
try replacement.compact(aspect: 4.0 / 3.0)
controller.restore(resumeFullScreen: false)
try settled(controller, compact: false) { try fixture.restored() }
try replacement.restored()
controller.attach(window)
// Replay AppKit's failure lifecycle without creating a visible fullscreen Space.
NotificationCenter.default.post(name: NSWindow.willEnterFullScreenNotification, object: window)
controller.toggle(aspect: 16.0 / 9.0)
try require(controller.isTransitioning && !controller.isCompact,
"compact entry waits for an in-flight native fullscreen transition")
window.delegate?.windowDidFailToEnterFullScreen?(window)
try settled(controller, compact: false)
try fixture.restored()
NotificationCenter.default.post(name: NSWindow.willExitFullScreenNotification, object: window)
controller.toggle(aspect: 16.0 / 9.0)
window.delegate?.windowDidFailToExitFullScreen?(window)
try settled(controller, compact: false)
try fixture.restored()
controller.toggle(aspect: 16.0 / 9.0)
try require(controller.isTransitioning, "close interrupts native entry")
window.close()
try require(!controller.isCompact && !controller.isTransitioning, "closed window ends its session")
let closedState = WindowState(window)
controller.attach(replacement.window)
controller.toggle(aspect: 16.0 / 9.0)
func closedWindowUnchanged() throws {
try closedState.requireMatches(window)
try fixture.preservedContent()
}
try settled(controller, compact: true, checking: closedWindowUnchanged)
try replacement.compact(aspect: 16.0 / 9.0)
controller.restore(resumeFullScreen: false)
try settled(controller, compact: false, checking: closedWindowUnchanged)
try replacement.restored()
}
static func reducedMotion() throws {
let fixture = try WindowFixture()
let controller = CompactWindowController(reduceMotion: { true })
defer {
controller.restore(resumeFullScreen: false, animated: false)
fixture.window.close()
}
controller.attach(fixture.window)
controller.toggle(aspect: 16.0 / 9.0)
try require(controller.isCompact && !controller.isTransitioning,
"Reduce Motion enters with geometry and controls already settled")
try fixture.compact(aspect: 16.0 / 9.0)
controller.toggle(aspect: 16.0 / 9.0)
try require(!controller.isCompact && !controller.isTransitioning,
"Reduce Motion restores without a pending frame animation")
try fixture.restored()
try nativeCompletionOpportunity {
try require(!controller.isCompact && !controller.isTransitioning,
"Reduce Motion leaves no completion that can revive compact state")
try fixture.restored()
}
}
static func main() throws {
try geometry()
try windowBehavior()
try reducedMotion()
print("Compact window checks passed")
}
}