@@ -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.
151152final 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