From bc88c2e85af01343ef6d46cb561f48a7159e9735 Mon Sep 17 00:00:00 2001 From: Peter Pistorius Date: Tue, 28 Apr 2026 22:09:40 +0200 Subject: [PATCH 1/2] Add NSWindow fade animation helpers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds reusable fadeInAndUp / fadeOutAndDown extensions on NSWindow for smooth window transitions. No callers yet — landed standalone so future window UI (settings, status-item-driven panels) can adopt them. Lifted from #1 by @plyght. Co-Authored-By: plyght Co-Authored-By: Claude Opus 4.7 (1M context) --- .changeset/6732f3c3.md | 5 ++++ Sources/cmdcmd/NSWindowAnimations.swift | 33 +++++++++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 .changeset/6732f3c3.md create mode 100644 Sources/cmdcmd/NSWindowAnimations.swift 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?() + } + } +} From 58b6690c47ce9e8b1c4dd81b1e3219757c1d75a3 Mon Sep 17 00:00:00 2001 From: Peter Pistorius Date: Tue, 28 Apr 2026 22:17:47 +0200 Subject: [PATCH 2/2] Wire window fade into overlay show/hide When animations are on, the overlay now fades in on show and fades out on hide using the new NSWindow helpers. Distance is 0 (alpha-only) since the overlay is fullscreen and any slide reveals desktop edges. Hide defers orderOut and the layer-clear into the fade completion so the fade isn't on an already-emptied window. Co-Authored-By: plyght Co-Authored-By: Claude Opus 4.7 (1M context) --- Sources/cmdcmd/Overlay.swift | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) 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