Skip to content

Commit 20bdfbd

Browse files
committed
Throttle location generation
1 parent a78ed53 commit 20bdfbd

2 files changed

Lines changed: 112 additions & 36 deletions

File tree

Core/Sources/SuggestionWidget/FeatureReducers/WidgetFeature.swift

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,10 @@ public struct WidgetFeature: ReducerProtocol {
195195
case .chatPanel(.presentChatPanel):
196196
let isDetached = state.chatPanelState.chatPanelInASeparateWindow
197197
return .run { _ in
198-
await windowsController?.updateWindowLocation(animated: false)
198+
await windowsController?.updateWindowLocation(
199+
animated: false,
200+
immediately: false
201+
)
199202
await windowsController?.updateWindowOpacity(immediately: false)
200203
if isDetached {
201204
Task { @MainActor in
@@ -207,7 +210,10 @@ public struct WidgetFeature: ReducerProtocol {
207210
case .chatPanel(.toggleChatPanelDetachedButtonClicked):
208211
let isDetached = state.chatPanelState.chatPanelInASeparateWindow
209212
return .run { _ in
210-
await windowsController?.updateWindowLocation(animated: !isDetached)
213+
await windowsController?.updateWindowLocation(
214+
animated: !isDetached,
215+
immediately: false
216+
)
211217
await windowsController?.updateWindowOpacity(immediately: false)
212218
}
213219
default: return .none

Core/Sources/SuggestionWidget/WidgetWindowsController.swift

Lines changed: 104 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,21 @@ final class WidgetWindowsController: NSObject {
1919
var currentApplicationProcessIdentifier: pid_t?
2020

2121
var cancellable: Set<AnyCancellable> = []
22+
@MainActor
2223
var observeToAppTask: Task<Void, Error>?
24+
@MainActor
2325
var observeToFocusedEditorTask: Task<Void, Error>?
26+
27+
@MainActor
2428
var updateWindowOpacityTask: Task<Void, Error>?
29+
@MainActor
2530
var lastUpdateWindowOpacityTime = Date(timeIntervalSince1970: 0)
2631

32+
@MainActor
33+
var updateWindowLocationTask: Task<Void, Error>?
34+
@MainActor
35+
var lastUpdateWindowLocationTime = Date(timeIntervalSince1970: 0)
36+
2737
deinit {
2838
userDefaultsObservers.presentationModeChangeObserver.onChange = {}
2939
observeToAppTask?.cancel()
@@ -47,7 +57,7 @@ final class WidgetWindowsController: NSObject {
4757

4858
xcodeInspector.$activeApplication.sink { [weak self] app in
4959
guard let app else { return }
50-
self?.activate(app)
60+
Task { [weak self] in await self?.activate(app) }
5161
}.store(in: &cancellable)
5262

5363
xcodeInspector.$completionPanel.sink { [weak self] newValue in
@@ -59,14 +69,14 @@ final class WidgetWindowsController: NSObject {
5969
// suggestion panel
6070
try await Task.sleep(nanoseconds: 400_000_000)
6171
}
62-
await self?.updateWindowLocation(animated: false)
72+
await self?.updateWindowLocation(animated: false, immediately: false)
6373
await self?.updateWindowOpacity(immediately: false)
6474
}
6575
}.store(in: &cancellable)
6676

6777
userDefaultsObservers.presentationModeChangeObserver.onChange = { [weak self] in
6878
Task { [weak self] in
69-
await self?.updateWindowLocation(animated: false)
79+
await self?.updateWindowLocation(animated: false, immediately: false)
7080
await self?.send(.updateColorScheme)
7181
}
7282
}
@@ -81,18 +91,21 @@ final class WidgetWindowsController: NSObject {
8191

8292
let isChatPanelDetached = state.chatPanelState.chatPanelInASeparateWindow
8393
let hasChat = !state.chatPanelState.chatTabGroup.tabInfo.isEmpty
84-
let shouldDebounce = !immediately &&
85-
Date().timeIntervalSince(lastUpdateWindowOpacityTime) < 1
86-
lastUpdateWindowOpacityTime = Date()
94+
let shouldDebounce = await MainActor.run {
95+
defer {lastUpdateWindowOpacityTime = Date() }
96+
return (!immediately &&
97+
!(Date().timeIntervalSince(lastUpdateWindowOpacityTime) > 5))
98+
}
8799
let activeApp = xcodeInspector.activeApplication
88100

89-
updateWindowOpacityTask?.cancel()
101+
await updateWindowOpacityTask?.cancel()
90102

91103
let task = Task {
92104
if shouldDebounce {
93105
try await Task.sleep(nanoseconds: 200_000_000)
94106
}
95107
try Task.checkCancellation()
108+
let xcodeInspector = self.xcodeInspector
96109
await MainActor.run {
97110
if let activeApp, activeApp.isXcode {
98111
let application = AXUIElementCreateApplication(
@@ -143,19 +156,26 @@ final class WidgetWindowsController: NSObject {
143156
}
144157
}
145158
}
146-
updateWindowOpacityTask = task
147-
_ = try? await task.value
159+
160+
await MainActor.run {
161+
updateWindowOpacityTask = task
162+
}
148163
}
149164

150-
func updateWindowLocation(animated: Bool) async {
165+
func updateWindowLocation(
166+
animated: Bool,
167+
immediately: Bool,
168+
function: StaticString = #function,
169+
line: UInt = #line
170+
) async {
151171
let state = store.withState { $0 }
152-
153-
guard let widgetLocation = generateWidgetLocation() else { return }
154-
await updatePanelState(widgetLocation)
155-
156172
let isChatPanelDetached = state.chatPanelState.chatPanelInASeparateWindow
157173

158-
await MainActor.run {
174+
@Sendable @MainActor
175+
func update() async {
176+
guard let widgetLocation = generateWidgetLocation() else { return }
177+
await updatePanelState(widgetLocation)
178+
159179
windows.widgetWindow.setFrame(
160180
widgetLocation.widgetFrame,
161181
display: false,
@@ -190,60 +210,101 @@ final class WidgetWindowsController: NSObject {
190210
)
191211
}
192212
}
213+
214+
let now = Date()
215+
let shouldThrottle = await MainActor.run {
216+
!immediately &&
217+
!(now.timeIntervalSince(lastUpdateWindowLocationTime) > 5)
218+
}
219+
220+
await updateWindowLocationTask?.cancel()
221+
let interval: TimeInterval = 0.1
222+
223+
if shouldThrottle {
224+
let delay = await max(
225+
0,
226+
interval - now.timeIntervalSince(lastUpdateWindowLocationTime)
227+
)
228+
229+
let task = Task { @MainActor in
230+
try await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000))
231+
try Task.checkCancellation()
232+
await update()
233+
}
234+
await MainActor.run {
235+
updateWindowLocationTask = task
236+
}
237+
} else {
238+
Task {
239+
await update()
240+
}
241+
}
242+
await MainActor.run {
243+
lastUpdateWindowLocationTime = Date()
244+
}
193245
}
194246
}
195247

196248
extension WidgetWindowsController: NSWindowDelegate {
249+
nonisolated
197250
func windowWillMove(_ notification: Notification) {
198-
guard (notification.object as? NSWindow) === windows.chatPanelWindow else { return }
251+
guard let window = notification.object as? NSWindow else { return }
199252
Task { @MainActor in
253+
guard window === windows.chatPanelWindow else { return }
200254
await Task.yield()
201255
store.send(.chatPanel(.detachChatPanel))
202256
}
203257
}
204258

259+
nonisolated
205260
func windowWillEnterFullScreen(_ notification: Notification) {
206-
guard (notification.object as? NSWindow) === windows.chatPanelWindow else { return }
261+
guard let window = notification.object as? NSWindow else { return }
207262
Task { @MainActor in
263+
guard window === windows.chatPanelWindow else { return }
208264
await Task.yield()
209265
store.send(.chatPanel(.enterFullScreen))
210266
}
211267
}
212268

269+
nonisolated
213270
func windowWillExitFullScreen(_ notification: Notification) {
214-
guard (notification.object as? NSWindow) === windows.chatPanelWindow else { return }
271+
guard let window = notification.object as? NSWindow else { return }
215272
Task { @MainActor in
273+
guard window === windows.chatPanelWindow else { return }
216274
await Task.yield()
217275
store.send(.chatPanel(.exitFullScreen))
218276
}
219277
}
220278
}
221279

222280
private extension WidgetWindowsController {
223-
func activate(_ app: AppInstanceInspector) {
281+
func activate(_ app: AppInstanceInspector) async {
224282
guard currentApplicationProcessIdentifier != app.processIdentifier else { return }
225283
currentApplicationProcessIdentifier = app.processIdentifier
226-
observe(to: app)
284+
await observe(to: app)
227285
}
228286

229-
func observe(to app: AppInstanceInspector) {
287+
func observe(to app: AppInstanceInspector) async {
230288
guard let app = app as? XcodeAppInstanceInspector else {
231289
Task {
232-
await updateWindowLocation(animated: false)
290+
await updateWindowLocation(animated: false, immediately: true)
233291
await updateWindowOpacity(immediately: true)
234292
}
235293
return
236294
}
237295
let notifications = app.axNotifications
238296
if let focusedEditor = xcodeInspector.focusedEditor {
239-
observe(to: focusedEditor)
297+
await observe(to: focusedEditor)
240298
}
241-
observeToAppTask?.cancel()
242-
observeToAppTask = Task {
299+
300+
let task = Task {
243301
await windows.orderFront()
244302

245303
let documentURL = await MainActor.run { store.withState { $0.focusingDocumentURL } }
246304
for await notification in notifications {
305+
if [.uiElementDestroyed, .created, .xcodeCompletionPanelChanged]
306+
.contains(notification.kind) { continue }
307+
247308
try Task.checkCancellation()
248309

249310
// Hide the widgets before switching to another window/editor
@@ -267,23 +328,27 @@ private extension WidgetWindowsController {
267328
.mainWindowChanged,
268329
.focusedWindowChanged,
269330
].contains(notification.kind) {
270-
await updateWindowLocation(animated: false)
331+
await updateWindowLocation(animated: false, immediately: false)
271332
await updateWindowOpacity(immediately: false)
272333
if let editor = xcodeInspector.focusedEditor {
273-
observe(to: editor)
334+
await observe(to: editor)
274335
}
275336
await send(.panel(.switchToAnotherEditorAndUpdateContent))
276337
} else {
277-
await updateWindowLocation(animated: false)
338+
await updateWindowLocation(animated: false, immediately: false)
278339
await updateWindowOpacity(immediately: false)
279340
}
280341
}
281342
}
343+
344+
await MainActor.run {
345+
observeToAppTask?.cancel()
346+
observeToAppTask = task
347+
}
282348
}
283349

284-
func observe(to editor: SourceEditor) {
285-
observeToFocusedEditorTask?.cancel()
286-
observeToFocusedEditorTask = Task {
350+
func observe(to editor: SourceEditor) async {
351+
let task = Task {
287352
let selectionRangeChange = editor.axNotifications
288353
.filter { $0.kind == .selectedTextChanged }
289354
let scroll = editor.axNotifications
@@ -292,22 +357,27 @@ private extension WidgetWindowsController {
292357
if #available(macOS 13.0, *) {
293358
for await _ in merge(
294359
selectionRangeChange.debounce(for: Duration.milliseconds(500)),
295-
scroll
360+
scroll.throttle(for: .milliseconds(200))
296361
) {
297362
guard xcodeInspector.latestActiveXcode != nil else { return }
298363
try Task.checkCancellation()
299-
await updateWindowLocation(animated: false)
364+
await updateWindowLocation(animated: false, immediately: false)
300365
await updateWindowOpacity(immediately: false)
301366
}
302367
} else {
303368
for await _ in merge(selectionRangeChange, scroll) {
304369
guard xcodeInspector.latestActiveXcode != nil else { return }
305370
try Task.checkCancellation()
306-
await updateWindowLocation(animated: false)
371+
await updateWindowLocation(animated: false, immediately: false)
307372
await updateWindowOpacity(immediately: false)
308373
}
309374
}
310375
}
376+
377+
await MainActor.run {
378+
observeToFocusedEditorTask?.cancel()
379+
observeToFocusedEditorTask = task
380+
}
311381
}
312382
}
313383

0 commit comments

Comments
 (0)