From 81db44f584eead19deaac57053cc50af8c9c19d7 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 8 Dec 2025 16:49:58 +0100 Subject: [PATCH] feat: add outcome-based dismiss animations --- .../Sources/Clawdis/VoiceWakeOverlay.swift | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/apps/macos/Sources/Clawdis/VoiceWakeOverlay.swift b/apps/macos/Sources/Clawdis/VoiceWakeOverlay.swift index 6c2704643..8bcd70ddf 100644 --- a/apps/macos/Sources/Clawdis/VoiceWakeOverlay.swift +++ b/apps/macos/Sources/Clawdis/VoiceWakeOverlay.swift @@ -65,12 +65,12 @@ final class VoiceWakeOverlayController: ObservableObject { func sendNow() { self.autoSendTask?.cancel() guard let forwardConfig, forwardConfig.enabled else { - self.dismiss() + self.dismiss(reason: .explicit) return } let text = self.model.text.trimmingCharacters(in: .whitespacesAndNewlines) guard !text.isEmpty else { - self.dismiss() + self.dismiss(reason: .empty) return } @@ -80,17 +80,21 @@ final class VoiceWakeOverlayController: ObservableObject { await VoiceWakeForwarder.forward(transcript: payload, config: forwardConfig) } DispatchQueue.main.asyncAfter(deadline: .now() + 0.28) { - self.dismiss() + self.dismiss(reason: .explicit, outcome: .sent) } } - func dismiss(reason: DismissReason = .explicit) { + func dismiss(reason: DismissReason = .explicit, outcome: SendOutcome = .empty) { self.autoSendTask?.cancel() self.model.isSending = false guard let window else { return } + let target = self.dismissTargetFrame(for: window.frame, reason: reason, outcome: outcome) NSAnimationContext.runAnimationGroup { context in context.duration = 0.18 context.timingFunction = CAMediaTimingFunction(name: .easeOut) + if let target { + window.animator().setFrame(target, display: true) + } window.animator().alphaValue = 0 } completionHandler: { Task { @MainActor in @@ -101,6 +105,7 @@ final class VoiceWakeOverlayController: ObservableObject { } enum DismissReason { case explicit, empty } + enum SendOutcome { case sent, empty } // MARK: - Private @@ -183,6 +188,21 @@ final class VoiceWakeOverlayController: ObservableObject { } } + private func dismissTargetFrame(for frame: NSRect, reason: DismissReason, outcome: SendOutcome) -> NSRect? { + switch (reason, outcome) { + case (.empty, _): + let scale: CGFloat = 0.95 + let newSize = NSSize(width: frame.size.width * scale, height: frame.size.height * scale) + let dx = (frame.size.width - newSize.width) / 2 + let dy = (frame.size.height - newSize.height) / 2 + return NSRect(x: frame.origin.x + dx, y: frame.origin.y + dy, width: newSize.width, height: newSize.height) + case (.explicit, .sent): + return frame.offsetBy(dx: 8, dy: 6) + default: + return frame + } + } + private func scheduleAutoSend(after delay: TimeInterval) { guard let forwardConfig, forwardConfig.enabled else { return } self.autoSendTask = Task { [weak self] in