diff --git a/.changeset/6732f3c3.md b/.changeset/6732f3c3.md new file mode 100644 index 0000000..f2a1ac3 --- /dev/null +++ b/.changeset/6732f3c3.md @@ -0,0 +1,5 @@ +--- +bump: patch +--- + +Add reusable NSWindow fade-in/out animation helpers. diff --git a/Sources/cmdcmd/NSWindowAnimations.swift b/Sources/cmdcmd/NSWindowAnimations.swift new file mode 100644 index 0000000..4fa0e69 --- /dev/null +++ b/Sources/cmdcmd/NSWindowAnimations.swift @@ -0,0 +1,33 @@ +import AppKit + +extension NSWindow { + func fadeInAndUp(distance: CGFloat = 50, duration: TimeInterval = 0.125, callback: (() -> Void)? = nil) { + let toFrame = frame + let fromFrame = NSRect(x: toFrame.minX, y: toFrame.minY - distance, width: toFrame.width, height: toFrame.height) + setFrame(fromFrame, display: true) + alphaValue = 0 + NSAnimationContext.runAnimationGroup { context in + context.duration = duration + context.timingFunction = CAMediaTimingFunction(controlPoints: 0.4, 0, 0.2, 1) + animator().alphaValue = 1 + animator().setFrame(toFrame, display: true) + } completionHandler: { + callback?() + } + } + + func fadeOutAndDown(distance: CGFloat = 50, duration: TimeInterval = 0.125, callback: (() -> Void)? = nil) { + let fromFrame = frame + let toFrame = NSRect(x: fromFrame.minX, y: fromFrame.minY - distance, width: fromFrame.width, height: fromFrame.height) + setFrame(fromFrame, display: true) + alphaValue = 1 + NSAnimationContext.runAnimationGroup { context in + context.duration = duration + context.timingFunction = CAMediaTimingFunction(controlPoints: 0.4, 0, 0.2, 1) + animator().alphaValue = 0 + animator().setFrame(toFrame, display: true) + } completionHandler: { + callback?() + } + } +} diff --git a/Sources/cmdcmd/Overlay.swift b/Sources/cmdcmd/Overlay.swift index 7490136..5ffdba7 100644 --- a/Sources/cmdcmd/Overlay.swift +++ b/Sources/cmdcmd/Overlay.swift @@ -151,7 +151,11 @@ final class Overlay { let w = window ?? makeWindow(frame: visibleFrame) window = w w.setFrame(visibleFrame, display: false) - w.alphaValue = 1 + if config.animations { + w.alphaValue = 0 + } else { + w.alphaValue = 1 + } let tWindow = CFAbsoluteTimeGetCurrent() CATransaction.begin() CATransaction.setDisableActions(true) @@ -162,6 +166,9 @@ final class Overlay { NSApp.activate(ignoringOtherApps: true) if let v = view { w.makeFirstResponder(v) } let tFront = CFAbsoluteTimeGetCurrent() + if config.animations { + w.fadeInAndUp(distance: 0, duration: 0.10) + } animateShowFromFocused(in: w) let tEnd = CFAbsoluteTimeGetCurrent() Log.debug(String(format: "render: filter=%.1f window=%.1f(new=%@) installTiles=%.1f orderFront+activate=%.1f animate=%.1f total=%.1f n=%d", @@ -483,7 +490,8 @@ private static func windowMostlyOn(displayBounds: CGRect, window: SCWindow) -> B let toStop = allTiles for t in toStop { t.suppressFrames = true } stopActivityTimer() - window?.orderOut(nil) + let w = window + let animate = config.animations && w != nil && w!.alphaValue > 0 visible = false if activatePrevious, prevFrontPID != 0, let app = NSRunningApplication(processIdentifier: prevFrontPID) { @@ -505,8 +513,22 @@ private static func windowMostlyOn(displayBounds: CGRect, window: SCWindow) -> B } isZoomed = false savedFrames = [] - if let root = window?.contentView?.layer { - root.sublayers?.forEach { $0.removeFromSuperlayer() } + let clearLayers = { [weak self] in + if let root = self?.window?.contentView?.layer { + root.sublayers?.forEach { $0.removeFromSuperlayer() } + } + } + if animate, let w { + w.fadeOutAndDown(distance: 0, duration: 0.10) { [weak self] in + guard let self else { return } + if !self.visible { + w.orderOut(nil) + clearLayers() + } + } + } else { + w?.orderOut(nil) + clearLayers() } hint.reset() DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in