This repository was archived by the owner on Feb 24, 2025. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 16
/
Copy pathMouseOverAnimationButton.swift
183 lines (147 loc) · 4.85 KB
/
MouseOverAnimationButton.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
//
// MouseOverAnimationButton.swift
//
// Copyright © 2022 DuckDuckGo. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import AppKit
import Combine
import Foundation
import Lottie
final class MouseOverAnimationButton: AddressBarButton {
// MARK: - Events
override func awakeFromNib() {
super.awakeFromNib()
subscribeToIsMouseOver()
subscribeToEffectiveAppearance()
}
private var isMouseOverCancellable: AnyCancellable?
var isAnimationEnabled: Bool = true
private func subscribeToIsMouseOver() {
isMouseOverCancellable = publisher(for: \.isMouseOver)
.dropFirst()
.sink { [weak self] isMouseOver in
guard let self, self.isAnimationEnabled else { return }
if isMouseOver {
self.animate()
} else {
self.stopAnimation()
}
}
}
private var effectiveAppearanceCancellable: AnyCancellable?
private func subscribeToEffectiveAppearance() {
effectiveAppearanceCancellable = NSApp.publisher(for: \.effectiveAppearance)
.dropFirst()
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
self?.updateAnimationView()
}
}
// MARK: - Loading & Updating of Animation Views
struct AnimationNames: Equatable {
let aqua: String
let dark: String
}
var animationNames: AnimationNames? {
didSet {
if oldValue != animationNames {
loadAnimationViews()
updateAnimationView()
}
}
}
struct AnimationViews {
let aqua: LottieAnimationView
let dark: LottieAnimationView
}
private var animationViewCache: AnimationViews?
private func loadAnimationViews() {
guard let animationNames = animationNames,
let aquaAnimationView = LottieAnimationView(named: animationNames.aqua),
let darkAnimationView = LottieAnimationView(named: animationNames.dark) else {
assertionFailure("Missing animation names or animation files in the bundle")
return
}
animationViewCache = AnimationViews(
aqua: aquaAnimationView,
dark: darkAnimationView)
}
private var currentAnimationView: LottieAnimationView?
private func updateAnimationView() {
guard let animationViewCache = animationViewCache else {
return
}
let isAquaMode = NSApp.effectiveAppearance.name == .aqua
let newAnimationView: LottieAnimationView
// Animation view causes problems in tests
if case .normal = NSApp.runType {
newAnimationView = isAquaMode ? animationViewCache.aqua : animationViewCache.dark
} else {
newAnimationView = LottieAnimationView()
}
guard currentAnimationView?.identifier != newAnimationView.identifier else {
// No need to update
return
}
currentAnimationView?.removeFromSuperview()
currentAnimationView = newAnimationView
newAnimationView.isHidden = true
addAndLayout(newAnimationView)
}
// MARK: - Animating
@Published var isAnimationViewVisible = false
override var image: NSImage? {
get {
return super.image
}
set {
if imageCache !== newValue {
imageCache = newValue
}
if !isAnimationViewVisible {
super.image = newValue
}
}
}
var imageCache: NSImage?
private func hideImage() {
super.image = nil
}
private func showImage() {
if let imageCache = imageCache {
NSAppearance.withAppAppearance {
image = imageCache
}
}
}
private func hideAnimation() {
currentAnimationView?.isHidden = true
isAnimationViewVisible = false
}
private func showAnimation() {
currentAnimationView?.isHidden = false
isAnimationViewVisible = true
}
private func animate() {
hideImage()
showAnimation()
currentAnimationView?.play()
}
private func stopAnimation() {
hideAnimation()
showImage()
currentAnimationView?.stop()
}
}