import AppKit import QuartzCore enum OverlayPanelFactory { @MainActor static func makePanel( contentRect: NSRect, level: NSWindow.Level, hasShadow: Bool, acceptsMouseMovedEvents: Bool = false) -> NSPanel { let panel = NSPanel( contentRect: contentRect, styleMask: [.nonactivatingPanel, .borderless], backing: .buffered, defer: false) panel.isOpaque = false panel.backgroundColor = .clear panel.hasShadow = hasShadow panel.level = level panel.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary, .transient] panel.hidesOnDeactivate = false panel.isMovable = false panel.isFloatingPanel = true panel.becomesKeyOnlyIfNeeded = true panel.titleVisibility = .hidden panel.titlebarAppearsTransparent = true panel.acceptsMouseMovedEvents = acceptsMouseMovedEvents return panel } @MainActor static func animatePresent(window: NSWindow, from start: NSRect, to target: NSRect, duration: TimeInterval = 0.18) { window.setFrame(start, display: true) window.alphaValue = 0 window.orderFrontRegardless() NSAnimationContext.runAnimationGroup { context in context.duration = duration context.timingFunction = CAMediaTimingFunction(name: .easeOut) window.animator().setFrame(target, display: true) window.animator().alphaValue = 1 } } @MainActor static func animateFrame(window: NSWindow, to frame: NSRect, duration: TimeInterval = 0.12) { NSAnimationContext.runAnimationGroup { context in context.duration = duration context.timingFunction = CAMediaTimingFunction(name: .easeOut) window.animator().setFrame(frame, display: true) } } @MainActor static func applyFrame(window: NSWindow?, target: NSRect, animate: Bool) { guard let window else { return } if animate { self.animateFrame(window: window, to: target) } else { window.setFrame(target, display: true) } } @MainActor static func present( window: NSWindow?, isFirstPresent: Bool, target: NSRect, startOffsetY: CGFloat = -6, onFirstPresent: (() -> Void)? = nil, onAlreadyVisible: (NSWindow) -> Void) { guard let window else { return } if isFirstPresent { onFirstPresent?() let start = target.offsetBy(dx: 0, dy: startOffsetY) self.animatePresent(window: window, from: start, to: target) } else { onAlreadyVisible(window) } } @MainActor static func animateDismiss( window: NSWindow, offsetX: CGFloat = 6, offsetY: CGFloat = 6, duration: TimeInterval = 0.16, completion: @escaping @MainActor @Sendable () -> Void) { let target = window.frame.offsetBy(dx: offsetX, dy: offsetY) NSAnimationContext.runAnimationGroup { context in context.duration = duration context.timingFunction = CAMediaTimingFunction(name: .easeOut) window.animator().setFrame(target, display: true) window.animator().alphaValue = 0 } completionHandler: { Task { @MainActor in completion() } } } @MainActor static func animateDismissAndHide( window: NSWindow, offsetX: CGFloat = 6, offsetY: CGFloat = 6, duration: TimeInterval = 0.16, onHidden: @escaping @MainActor () -> Void) { self.animateDismiss(window: window, offsetX: offsetX, offsetY: offsetY, duration: duration) { window.orderOut(nil) onHidden() } } @MainActor static func clearGlobalEventMonitor(_ monitor: inout Any?) { if let current = monitor { NSEvent.removeMonitor(current) monitor = nil } } }