import AppKit @MainActor final class CompactWindowController: NSObject { var onChange: (() -> Void)? private(set) var isCompact = false private(set) var isTransitioning = false var snappingEnabled: Bool { didSet { guard snappingEnabled != oldValue else { return } defaults.set(snappingEnabled, forKey: "compactSnappingEnabled") onChange?() } } private struct Snapshot { let frame: NSRect let minSize: NSSize let contentMinSize: NSSize let aspectRatio: NSSize let styleMask: NSWindow.StyleMask let titleVisibility: NSWindow.TitleVisibility let titlebarAppearsTransparent: Bool let collectionBehavior: NSWindow.CollectionBehavior let level: NSWindow.Level let hidesOnDeactivate: Bool let isMovableByWindowBackground: Bool @MainActor init(_ window: NSWindow) { frame = window.frame minSize = window.minSize contentMinSize = window.contentMinSize aspectRatio = window.aspectRatio styleMask = window.styleMask titleVisibility = window.titleVisibility titlebarAppearsTransparent = window.titlebarAppearsTransparent collectionBehavior = window.collectionBehavior level = window.level hidesOnDeactivate = window.hidesOnDeactivate isMovableByWindowBackground = window.isMovableByWindowBackground } } private enum FullScreenTransition { case entering, exiting } private let defaults: UserDefaults private weak var window: NSWindow? private var snapshot: Snapshot? private var fullScreenTransition: FullScreenTransition? private var delegateProxy: FullScreenDelegate? private var sessionActive = false private var wantsCompact = false private var wantsFullScreen = false private var originatedFullScreen = false private var videoAspect = 16.0 / 9.0 private var fittedAspect: Double? init(defaults: UserDefaults = .standard) { self.defaults = defaults defaults.register(defaults: ["compactSnappingEnabled": true]) snappingEnabled = defaults.bool(forKey: "compactSnappingEnabled") super.init() } deinit { NotificationCenter.default.removeObserver(self) } func attach(_ window: NSWindow) { guard window !== self.window else { return } if let oldWindow = self.window, fullScreenTransition == nil, !oldWindow.styleMask.contains(.fullScreen) { restoreSnapshot(to: oldWindow) } restoreDelegate() NotificationCenter.default.removeObserver(self) self.window = window snapshot = nil fullScreenTransition = nil sessionActive = false wantsCompact = false wantsFullScreen = false originatedFullScreen = false setState(compact: false, transitioning: false) let proxy = FullScreenDelegate(owner: self, original: window.delegate) delegateProxy = proxy window.delegate = proxy for name in [NSWindow.willEnterFullScreenNotification, NSWindow.didEnterFullScreenNotification, NSWindow.willExitFullScreenNotification, NSWindow.didExitFullScreenNotification, NSWindow.willCloseNotification] { NotificationCenter.default.addObserver(self, selector: #selector(windowNotification(_:)), name: name, object: window) } } func toggle(aspect: Double) { if sessionActive && wantsCompact { restore() return } guard let window, aspect.isFinite, aspect > 0 else { return } if !sessionActive { sessionActive = true originatedFullScreen = window.styleMask.contains(.fullScreen) || fullScreenTransition != nil } videoAspect = aspect wantsCompact = true wantsFullScreen = false reconcile() } func restore(resumeFullScreen: Bool = true) { guard sessionActive else { return } wantsCompact = false wantsFullScreen = resumeFullScreen && originatedFullScreen reconcile() } func updateAspect(_ aspect: Double) { guard aspect.isFinite, aspect > 0, aspect != videoAspect else { return } videoAspect = aspect guard isCompact, !isTransitioning, let window else { return } fitCompactWindow(window, initial: false) } func snapAfterDrag() { guard isCompact, !isTransitioning, snappingEnabled, let window else { return } let frame = Self.snappedFrame(window.frame, visibleFrame: visibleFrame(for: window)) if frame.origin != window.frame.origin { window.setFrameOrigin(frame.origin) } } @objc private func windowNotification(_ notification: Notification) { switch notification.name { case NSWindow.willEnterFullScreenNotification: fullScreenTransition = .entering setState(compact: isCompact, transitioning: true) case NSWindow.willExitFullScreenNotification: fullScreenTransition = .exiting setState(compact: isCompact, transitioning: true) case NSWindow.didEnterFullScreenNotification, NSWindow.didExitFullScreenNotification: fullScreenTransition = nil reconcile() case NSWindow.willCloseNotification: restoreDelegate() NotificationCenter.default.removeObserver(self) window = nil snapshot = nil fullScreenTransition = nil sessionActive = false wantsCompact = false wantsFullScreen = false originatedFullScreen = false setState(compact: false, transitioning: false) default: break } } private func restoreDelegate() { if let window, let delegateProxy, window.delegate === delegateProxy { window.delegate = delegateProxy.original } delegateProxy = nil } private func fullScreenFailed() { fullScreenTransition = nil wantsFullScreen = false if isCompact { wantsCompact = true setState(compact: true, transitioning: false) } else { if let window, !window.styleMask.contains(.fullScreen) { restoreSnapshot(to: window) } finishSession() } } @MainActor private final class FullScreenDelegate: NSObject, NSWindowDelegate { // NSObject forwarding returns untyped Any; accesses remain on AppKit's main thread. nonisolated(unsafe) weak var original: (any NSWindowDelegate)? private weak var owner: CompactWindowController? init(owner: CompactWindowController, original: (any NSWindowDelegate)?) { self.owner = owner self.original = original } nonisolated override func responds(to selector: Selector!) -> Bool { if super.responds(to: selector) { return true } return MainActor.assumeIsolated { original?.responds(to: selector) ?? false } } nonisolated override func forwardingTarget(for selector: Selector!) -> Any? { let responds = MainActor.assumeIsolated { original?.responds(to: selector) ?? false } return responds ? original : nil } func windowDidFailToEnterFullScreen(_ window: NSWindow) { owner?.fullScreenFailed() original?.windowDidFailToEnterFullScreen?(window) } func windowDidFailToExitFullScreen(_ window: NSWindow) { owner?.fullScreenFailed() original?.windowDidFailToExitFullScreen?(window) } } // A toggle changes the destination, never reverses an in-flight native animation. private func reconcile() { guard let window, fullScreenTransition == nil else { return } guard sessionActive else { setState(compact: isCompact, transitioning: false) return } if wantsCompact { if window.styleMask.contains(.fullScreen) { toggleNativeFullScreen(window, transition: .exiting) } else if !isCompact { enterCompact(window) } else { if fittedAspect != videoAspect { fitCompactWindow(window, initial: false) } setState(compact: true, transitioning: false) } return } if window.styleMask.contains(.fullScreen) { if wantsFullScreen && snapshot == nil { finishSession() } else { toggleNativeFullScreen(window, transition: .exiting) } return } if snapshot != nil { setState(compact: isCompact, transitioning: true) restoreSnapshot(to: window) setState(compact: false, transitioning: wantsFullScreen) } if wantsFullScreen { toggleNativeFullScreen(window, transition: .entering) } else { finishSession() } } private func toggleNativeFullScreen(_ window: NSWindow, transition: FullScreenTransition) { fullScreenTransition = transition setState(compact: isCompact, transitioning: true) window.toggleFullScreen(nil) } private func enterCompact(_ window: NSWindow) { setState(compact: false, transitioning: true) // After didExitFullScreen, this is the normal frame, not the fullscreen frame. snapshot = Snapshot(window) window.aspectRatio = .zero window.minSize = .zero window.contentMinSize = .zero // A borderless NSWindow cannot become key; retain native keyboard and resize behavior. window.styleMask.formUnion([.titled, .fullSizeContentView, .resizable]) window.titleVisibility = .hidden window.titlebarAppearsTransparent = true window.minSize = NSSize(width: 160, height: 90) window.contentMinSize = NSSize(width: 160, height: 90) var behavior = window.collectionBehavior behavior.subtract([.primary, .auxiliary, .fullScreenPrimary, .fullScreenNone, .moveToActiveSpace]) behavior.formUnion([.canJoinAllSpaces, .canJoinAllApplications, .fullScreenAuxiliary]) window.collectionBehavior = behavior window.level = .floating window.hidesOnDeactivate = false window.isMovableByWindowBackground = true fitCompactWindow(window, initial: true) setState(compact: true, transitioning: false) } private func restoreSnapshot(to window: NSWindow) { guard let snapshot else { return } window.aspectRatio = .zero window.minSize = .zero window.contentMinSize = .zero window.styleMask = snapshot.styleMask window.titleVisibility = snapshot.titleVisibility window.titlebarAppearsTransparent = snapshot.titlebarAppearsTransparent window.collectionBehavior = snapshot.collectionBehavior window.level = snapshot.level window.hidesOnDeactivate = snapshot.hidesOnDeactivate window.isMovableByWindowBackground = snapshot.isMovableByWindowBackground window.minSize = snapshot.minSize window.contentMinSize = snapshot.contentMinSize window.setFrame(snapshot.frame, display: true) window.aspectRatio = snapshot.aspectRatio self.snapshot = nil } private func finishSession() { sessionActive = false originatedFullScreen = false wantsCompact = false wantsFullScreen = false setState(compact: false, transitioning: false) } private func setState(compact: Bool, transitioning: Bool) { guard isCompact != compact || isTransitioning != transitioning else { return } isCompact = compact isTransitioning = transitioning onChange?() } private func visibleFrame(for window: NSWindow) -> NSRect { (window.screen ?? NSScreen.main)?.visibleFrame ?? window.frame } private func fitCompactWindow(_ window: NSWindow, initial: Bool) { let visibleFrame = visibleFrame(for: window) let usable = Self.usableFrame(visibleFrame) var frame = Self.initialFrame(aspect: videoAspect, visibleFrame: visibleFrame) if !initial { frame.size = Self.compactSize(aspect: videoAspect, width: window.frame.width, available: usable.size) frame.origin.x = min(max(window.frame.minX, usable.minX), usable.maxX - frame.width) frame.origin.y = min(max(window.frame.minY, usable.minY), usable.maxY - frame.height) } window.aspectRatio = .zero window.setFrame(frame, display: true) if Self.canFitAspect(videoAspect, available: usable.size) { window.aspectRatio = NSSize(width: videoAspect, height: 1) } fittedAspect = videoAspect } nonisolated static func initialFrame(aspect: Double, visibleFrame: NSRect) -> NSRect { let usable = usableFrame(visibleFrame) let size = compactSize(aspect: aspect, width: 320, available: usable.size) return NSRect(x: usable.maxX - size.width, y: usable.maxY - size.height, width: size.width, height: size.height) } nonisolated static func snappedFrame(_ frame: NSRect, visibleFrame: NSRect) -> NSRect { guard frame.origin.x.isFinite, frame.origin.y.isFinite, frame.width.isFinite, frame.height.isFinite, frame.width >= 0, frame.height >= 0, validVisibleFrame(visibleFrame) else { return frame } let usable = usableFrame(visibleFrame) var result = frame result.origin.x = snappedOrigin(frame.minX, lower: usable.minX, upper: usable.maxX - frame.width) result.origin.y = snappedOrigin(frame.minY, lower: usable.minY, upper: usable.maxY - frame.height) return result } nonisolated private static func snappedOrigin(_ origin: CGFloat, lower: CGFloat, upper: CGFloat) -> CGFloat { let nearest = abs(origin - lower) <= abs(origin - upper) ? lower : upper return abs(origin - nearest) <= 24 ? nearest : origin } nonisolated private static func validVisibleFrame(_ frame: NSRect) -> Bool { frame.origin.x.isFinite && frame.origin.y.isFinite && frame.width.isFinite && frame.height.isFinite && frame.width > 0 && frame.height > 0 && frame.maxX.isFinite && frame.maxY.isFinite } nonisolated private static func usableFrame(_ visibleFrame: NSRect) -> NSRect { guard validVisibleFrame(visibleFrame) else { return .zero } return visibleFrame.insetBy(dx: min(16, visibleFrame.width / 2), dy: min(16, visibleFrame.height / 2)) } nonisolated private static func canFitAspect(_ aspect: Double, available: NSSize) -> Bool { guard aspect.isFinite, aspect > 0, available.width > 0, available.height > 0 else { return false } return aspect >= min(160, available.width) / available.height && aspect <= available.width / min(90, available.height) } nonisolated private static func compactSize(aspect: Double, width: CGFloat, available: NSSize) -> NSSize { guard available.width > 0, available.height > 0 else { return .zero } let aspect = aspect.isFinite && aspect > 0 ? aspect : 16.0 / 9.0 var width = min(max(width, min(160, available.width)), available.width) if canFitAspect(aspect, available: available) { width = min(max(width, min(90, available.height) * aspect), available.height * aspect) return NSSize(width: width, height: width / aspect) } // Extreme video ratios letterbox instead of violating the compact minimum or screen bounds. return NSSize(width: width, height: min(max(width / (16.0 / 9.0), min(90, available.height)), available.height)) } }