Skip to content

Commit d5298f2

Browse files
committed
Add blinking animation on prefetch
1 parent 81246d2 commit d5298f2

File tree

1 file changed

+46
-8
lines changed

1 file changed

+46
-8
lines changed

Core/Sources/Service/RealtimeSuggestionController.swift

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ public actor RealtimeSuggestionController {
138138
else { return }
139139
if Task.isCancelled { return }
140140
os_log(.info, "Prefetch suggestions.")
141+
realtimeSuggestionIndicatorController?.triggerPrefetchAnimation()
141142
do {
142143
try await Environment.triggerAction("Prefetch Suggestions")
143144
} catch {
@@ -149,20 +150,49 @@ public actor RealtimeSuggestionController {
149150

150151
/// Present a tiny dot next to mouse cursor if real-time suggestion is enabled.
151152
final class RealtimeSuggestionIndicatorController {
153+
class IndicatorContentViewModel: ObservableObject {
154+
@Published var isPrefetching = false
155+
private var prefetchTask: Task<Void, Error>?
156+
157+
@MainActor
158+
func prefetch() {
159+
prefetchTask?.cancel()
160+
withAnimation(.easeIn(duration: 0.2)) {
161+
isPrefetching = true
162+
}
163+
prefetchTask = Task {
164+
try await Task.sleep(nanoseconds: 2 * 1_000_000_000)
165+
withAnimation(.easeOut(duration: 0.2)) {
166+
isPrefetching = false
167+
}
168+
}
169+
}
170+
}
171+
152172
struct IndicatorContentView: View {
153-
@State var opacity: CGFloat = 1
154-
@State var scale: CGFloat = 1
173+
@ObservedObject var viewModel: IndicatorContentViewModel
174+
@State var progress: CGFloat = 1
175+
var opacityA: CGFloat { progress }
176+
var opacityB: CGFloat { (1 - progress) }
177+
var scaleA: CGFloat { progress / 2 + 0.5 }
178+
var scaleB: CGFloat { 1 - progress }
179+
155180
var body: some View {
156181
Circle()
157-
.fill(Color.accentColor.opacity(opacity))
158-
.scaleEffect(.init(width: scale, height: scale))
182+
.fill(Color.accentColor.opacity(opacityA))
183+
.scaleEffect(.init(width: scaleA, height: scaleA))
159184
.frame(width: 8, height: 8)
185+
.background(
186+
Circle()
187+
.fill(Color.white.opacity(viewModel.isPrefetching ? opacityB : 0))
188+
.scaleEffect(.init(width: scaleB, height: scaleB))
189+
.frame(width: 8, height: 8)
190+
)
160191
.onAppear {
161192
Task {
162193
await Task.yield() // to avoid unwanted translations.
163194
withAnimation(.easeInOut(duration: 1).repeatForever(autoreverses: true)) {
164-
opacity = 0.5
165-
scale = 0.5
195+
progress = 0
166196
}
167197
}
168198
}
@@ -182,6 +212,7 @@ final class RealtimeSuggestionIndicatorController {
182212
}
183213
}
184214

215+
private let viewModel = IndicatorContentViewModel()
185216
private var displayLink: CVDisplayLink!
186217
private var isDisplayLinkStarted: Bool = false
187218
private var userDefaultsObserver = UserDefaultsObserver()
@@ -194,7 +225,7 @@ final class RealtimeSuggestionIndicatorController {
194225
}
195226

196227
@MainActor
197-
let window = {
228+
lazy var window = {
198229
let it = NSWindow(
199230
contentRect: .zero,
200231
styleMask: .borderless,
@@ -206,7 +237,8 @@ final class RealtimeSuggestionIndicatorController {
206237
it.backgroundColor = .white.withAlphaComponent(0)
207238
it.level = .statusBar
208239
it.contentView = NSHostingView(
209-
rootView: IndicatorContentView().frame(minWidth: 10, minHeight: 10)
240+
rootView: IndicatorContentView(viewModel: self.viewModel)
241+
.frame(minWidth: 10, minHeight: 10)
210242
)
211243
return it
212244
}()
@@ -291,4 +323,10 @@ final class RealtimeSuggestionIndicatorController {
291323
window.makeKey()
292324
}
293325
}
326+
327+
func triggerPrefetchAnimation() {
328+
Task { @MainActor in
329+
viewModel.prefetch()
330+
}
331+
}
294332
}

0 commit comments

Comments
 (0)