11import Cocoa
22
3+ private enum Timing {
4+ static let slideIn : TimeInterval = 0.3
5+ static let expansion : TimeInterval = 0.25
6+ static let fadeIn : TimeInterval = 0.15
7+ }
8+
9+ private enum Padding {
10+ static let horizontal : CGFloat = 16
11+ static let vertical : CGFloat = 14
12+ }
13+
314extension NotificationManager {
415 func showWithAnimation(
516 notification: NotificationInstance , screen: NSScreen , timeoutSeconds: Double
617 ) {
718 let screenRect = screen. visibleFrame
8- let finalXPos = screenRect. maxX - panelWidth( ) - Config. rightMargin + Config. buttonOverhang
9- let currentFrame = notification. panel. frame
19+ let finalX = screenRect. maxX - panelWidth( ) - Config. rightMargin + Config. buttonOverhang
20+ let y = notification. panel. frame. minY
1021
1122 notification. panel. setFrame (
1223 NSRect (
1324 x: screenRect. maxX + Config. slideInOffset,
14- y: currentFrame . minY ,
25+ y: y ,
1526 width: panelWidth ( ) ,
1627 height: panelHeight ( )
1728 ) ,
@@ -21,93 +32,118 @@ extension NotificationManager {
2132 notification. panel. orderFrontRegardless ( )
2233 notification. panel. makeKeyAndOrderFront ( nil )
2334
24- NSAnimationContext . runAnimationGroup ( { context in
25- context. duration = 0.3
26- context. timingFunction = CAMediaTimingFunction ( name: . easeOut)
35+ animate ( duration: Timing . slideIn, timing: . easeOut) {
2736 notification. panel. animator ( ) . setFrame (
28- NSRect (
29- x: finalXPos, y: currentFrame. minY, width: panelWidth ( ) ,
30- height: panelHeight ( ) ) ,
37+ NSRect ( x: finalX, y: y, width: self . panelWidth ( ) , height: self . panelHeight ( ) ) ,
3138 display: true
3239 )
3340 notification. panel. animator ( ) . alphaValue = 1.0
34- } ) {
35- DispatchQueue . main. async {
36- notification. clickableView. updateTrackingAreas ( )
37- notification. clickableView. window? . invalidateCursorRects ( for: notification. clickableView)
38- notification. clickableView. window? . resetCursorRects ( )
39- self . updateHoverForAll ( atScreenPoint: NSEvent . mouseLocation)
40- }
41+ } completion: {
42+ self . refreshTrackingAreas ( for: notification)
43+ self . updateHoverForAll ( atScreenPoint: NSEvent . mouseLocation)
4144 notification. startDismissTimer ( timeoutSeconds: timeoutSeconds)
4245 }
4346 }
4447
4548 func animateExpansion( notification: NotificationInstance , isExpanded: Bool ) {
46- let targetHeight = panelHeight ( expanded: isExpanded)
4749 let currentFrame = notification. panel. frame
48-
49- let heightDiff = targetHeight - currentFrame. height
50+ let targetHeight = panelHeight ( expanded: isExpanded)
5051 let newFrame = NSRect (
5152 x: currentFrame. minX,
52- y: currentFrame. minY - heightDiff ,
53+ y: currentFrame. minY - ( targetHeight - currentFrame . height ) ,
5354 width: currentFrame. width,
5455 height: targetHeight
5556 )
5657
57- guard
58- let effectView = notification. clickableView. subviews. first? . subviews. first
59- as? NSVisualEffectView
60- else {
58+ guard let effectView = findEffectView ( in: notification) else {
6159 notification. isAnimating = false
6260 return
6361 }
6462
6563 if isExpanded {
66- notification. compactContentView? . alphaValue = 1.0
64+ animateToExpanded ( notification: notification, effectView: effectView, frame: newFrame)
65+ } else {
66+ animateToCompact ( notification: notification, frame: newFrame)
67+ }
68+ }
6769
68- let expandedView = createExpandedNotificationView ( notification : notification )
69- expandedView . translatesAutoresizingMaskIntoConstraints = false
70- expandedView . alphaValue = 0
71- effectView . addSubview ( expandedView )
70+ private func animateToExpanded (
71+ notification : NotificationInstance , effectView : NSVisualEffectView , frame : NSRect
72+ ) {
73+ notification . compactContentView ? . isHidden = true
7274
73- NSLayoutConstraint . activate ( [
74- expandedView. leadingAnchor. constraint ( equalTo: effectView. leadingAnchor, constant: 16 ) ,
75- expandedView. trailingAnchor. constraint ( equalTo: effectView. trailingAnchor, constant: - 16 ) ,
76- expandedView. topAnchor. constraint ( equalTo: effectView. topAnchor, constant: 14 ) ,
77- expandedView. bottomAnchor. constraint ( equalTo: effectView. bottomAnchor, constant: - 14 ) ,
78- ] )
75+ let expandedView = createExpandedNotificationView ( notification: notification)
76+ expandedView. translatesAutoresizingMaskIntoConstraints = false
77+ expandedView. alphaValue = 0
78+ notification. expandedContentView = expandedView
7979
80- notification. expandedContentView = expandedView
80+ animate ( duration: Timing . expansion, timing: . easeInEaseOut) {
81+ notification. panel. animator ( ) . setFrame ( frame, display: true )
82+ } completion: {
83+ effectView. addSubview ( expandedView)
84+ self . pinToEdges ( expandedView, in: effectView)
8185
82- NSAnimationContext . runAnimationGroup ( { context in
83- context. duration = 0.25
84- context. timingFunction = CAMediaTimingFunction ( name: . easeInEaseOut)
85- notification. panel. animator ( ) . setFrame ( newFrame, display: true )
86- notification. compactContentView? . animator ( ) . alphaValue = 0
86+ self . animate ( duration: Timing . fadeIn, timing: . easeOut) {
8787 expandedView. animator ( ) . alphaValue = 1.0
88- } ) {
89- notification. compactContentView? . isHidden = true
90- notification. isAnimating = false
91- notification. clickableView. updateTrackingAreas ( )
92- self . repositionNotifications ( )
93- }
94- } else {
95- notification. stopCountdown ( )
96- notification. expandedContentView? . removeFromSuperview ( )
97- notification. expandedContentView = nil
98- notification. compactContentView? . alphaValue = 0
99- notification. compactContentView? . isHidden = false
100-
101- NSAnimationContext . runAnimationGroup ( { context in
102- context. duration = 0.25
103- context. timingFunction = CAMediaTimingFunction ( name: . easeInEaseOut)
104- notification. panel. animator ( ) . setFrame ( newFrame, display: true )
105- notification. compactContentView? . animator ( ) . alphaValue = 1.0
106- } ) {
107- notification. isAnimating = false
108- notification. clickableView. updateTrackingAreas ( )
109- self . repositionNotifications ( )
88+ } completion: {
89+ self . finishExpansionAnimation ( notification)
11090 }
11191 }
11292 }
93+
94+ private func animateToCompact( notification: NotificationInstance , frame: NSRect ) {
95+ notification. stopCountdown ( )
96+ notification. expandedContentView? . removeFromSuperview ( )
97+ notification. expandedContentView = nil
98+ notification. compactContentView? . alphaValue = 0
99+ notification. compactContentView? . isHidden = false
100+
101+ animate ( duration: Timing . expansion, timing: . easeInEaseOut) {
102+ notification. panel. animator ( ) . setFrame ( frame, display: true )
103+ notification. compactContentView? . animator ( ) . alphaValue = 1.0
104+ } completion: {
105+ self . finishExpansionAnimation ( notification)
106+ }
107+ }
108+
109+ private func finishExpansionAnimation( _ notification: NotificationInstance ) {
110+ notification. isAnimating = false
111+ notification. clickableView. updateTrackingAreas ( )
112+ repositionNotifications ( )
113+ }
114+
115+ private func findEffectView( in notification: NotificationInstance ) -> NSVisualEffectView ? {
116+ notification. clickableView. subviews. first? . subviews. first as? NSVisualEffectView
117+ }
118+
119+ private func pinToEdges( _ view: NSView , in container: NSView ) {
120+ NSLayoutConstraint . activate ( [
121+ view. leadingAnchor. constraint ( equalTo: container. leadingAnchor, constant: Padding . horizontal) ,
122+ view. trailingAnchor. constraint (
123+ equalTo: container. trailingAnchor, constant: - Padding. horizontal) ,
124+ view. topAnchor. constraint ( equalTo: container. topAnchor, constant: Padding . vertical) ,
125+ view. bottomAnchor. constraint ( equalTo: container. bottomAnchor, constant: - Padding. vertical) ,
126+ ] )
127+ }
128+
129+ private func refreshTrackingAreas( for notification: NotificationInstance ) {
130+ DispatchQueue . main. async {
131+ notification. clickableView. updateTrackingAreas ( )
132+ notification. clickableView. window? . invalidateCursorRects ( for: notification. clickableView)
133+ notification. clickableView. window? . resetCursorRects ( )
134+ }
135+ }
136+
137+ private func animate(
138+ duration: TimeInterval ,
139+ timing: CAMediaTimingFunctionName ,
140+ animations: @escaping ( ) -> Void ,
141+ completion: @escaping ( ) -> Void
142+ ) {
143+ NSAnimationContext . runAnimationGroup ( { context in
144+ context. duration = duration
145+ context. timingFunction = CAMediaTimingFunction ( name: timing)
146+ animations ( )
147+ } , completionHandler: completion)
148+ }
113149}
0 commit comments