feat: animate actions panel and footer transitions

This commit is contained in:
2026-10-12 17:06:53 +02:00
parent ff8407e771
commit 95cc55b8ce
2 changed files with 53 additions and 16 deletions
@@ -10,16 +10,19 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
private let applicationLabel = NSTextField(labelWithString: "")
private var mouseMonitor: Any?
private weak var coveredPill: NSView?
private var coveredPillOpacity: Float = 1
private var coveredPillAlpha: CGFloat = 1
private var isClosing = false
private var transitionGeneration = 0
private var panel: ActionsPanelView { view as! ActionsPanelView }
var uninstall: (() -> Void)?
var closed: (() -> Void)?
var isShown: Bool { viewIfLoaded?.superview != nil }
var isShown: Bool { viewIfLoaded?.superview != nil && !isClosing }
init() { super.init(nibName: nil, bundle: nil) }
required init?(coder: NSCoder) { fatalError("Conjure creates actions programmatically") }
override func loadView() {
view = NSView()
view = ActionsPanelView()
view.wantsLayer = true
view.layer?.shadowColor = NSColor.black.cgColor
view.layer?.shadowOpacity = 0.35
@@ -138,16 +141,27 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
applicationLabel.stringValue = application.name
search.stringValue = ""
controlTextDidChange(Notification(name: NSControl.textDidChangeNotification))
view.translatesAutoresizingMaskIntoConstraints = false
host.addSubview(view)
NSLayoutConstraint.activate([
view.trailingAnchor.constraint(equalTo: pill.trailingAnchor),
view.bottomAnchor.constraint(equalTo: pill.bottomAnchor)
])
coveredPill = pill
coveredPillOpacity = pill.layer?.opacity ?? 1
pill.layer?.opacity = 0
transitionGeneration += 1
isClosing = false
panel.acceptsInteraction = true
panel.setAccessibilityHidden(false)
if view.superview == nil {
view.alphaValue = 0
view.translatesAutoresizingMaskIntoConstraints = false
host.addSubview(view)
NSLayoutConstraint.activate([
view.trailingAnchor.constraint(equalTo: pill.trailingAnchor),
view.bottomAnchor.constraint(equalTo: pill.bottomAnchor)
])
coveredPill = pill
coveredPillAlpha = pill.alphaValue
}
pill.setAccessibilityHidden(true)
NSAnimationContext.runAnimationGroup { context in
context.duration = NSWorkspace.shared.accessibilityDisplayShouldReduceMotion ? 0 : 0.16
view.animator().alphaValue = 1
pill.animator().alphaValue = 0
}
window.makeFirstResponder(search)
(search.currentEditor() as? NSTextView)?.insertionPointColor = SunsetPalette.accent
mouseMonitor = NSEvent.addLocalMonitorForEvents(matching: [.leftMouseDown, .rightMouseDown]) { [weak self, weak pill] event in
@@ -165,12 +179,26 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
func close() {
guard isShown else { return }
transitionGeneration += 1
let generation = transitionGeneration
isClosing = true
panel.acceptsInteraction = false
panel.setAccessibilityHidden(true)
if let mouseMonitor { NSEvent.removeMonitor(mouseMonitor) }
mouseMonitor = nil
coveredPill?.layer?.opacity = coveredPillOpacity
coveredPill?.setAccessibilityHidden(false)
coveredPill = nil
view.removeFromSuperview()
NSAnimationContext.runAnimationGroup { context in
context.duration = NSWorkspace.shared.accessibilityDisplayShouldReduceMotion ? 0 : 0.12
view.animator().alphaValue = 0
coveredPill?.animator().alphaValue = coveredPillAlpha
} completionHandler: { [weak self] in
MainActor.assumeIsolated {
guard let self, self.transitionGeneration == generation, self.isClosing else { return }
self.view.removeFromSuperview()
self.coveredPill = nil
self.isClosing = false
}
}
closed?()
}
@@ -199,6 +227,15 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
}
}
@MainActor
private final class ActionsPanelView: NSView {
var acceptsInteraction = true
override func hitTest(_ point: NSPoint) -> NSView? {
acceptsInteraction ? super.hitTest(point) : nil
}
}
@MainActor
private final class ActionIconView: NSImageView {
// SF Symbol optical insets otherwise enlarge a 22-point icon box beyond application icon sizing.