Skip to content

Commit fe6ecfa

Browse files
Prevent pointer warp during menu rehide
1 parent 3afd48c commit fe6ecfa

2 files changed

Lines changed: 46 additions & 16 deletions

File tree

OverflowBar/Services/MenuBarItemStore.swift

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ final class MenuBarItemStore: ObservableObject {
2525
private var menuDismissMonitor: Any?
2626
private var transientDismissCheck: DispatchWorkItem?
2727
private var pendingRehideItem: MenuBarItem?
28-
private var pendingRehidePointer: CGPoint?
2928
// NSMenu can nest tracking sessions (for example, a submenu opened from
3029
// a status-item menu). Keep a depth instead of a Boolean so an inner
3130
// didEndTracking notification cannot make us rehide while the parent
@@ -462,7 +461,7 @@ final class MenuBarItemStore: ObservableObject {
462461
self.retryActivation(item, mouseButton: .left, retryCount: retryCount, message: "Unable to activate \(item.tooltip).")
463462
return
464463
}
465-
self.rehideAfterNextUserClick(item, restoreCursorLocation: nil)
464+
self.rehideAfterNextUserClick(item)
466465
self.finishActivation()
467466
}
468467
return
@@ -524,7 +523,7 @@ final class MenuBarItemStore: ObservableObject {
524523
self.itemUsesAccessibility(item),
525524
self.activator.activateDirectly(item)
526525
|| self.activator.activateViaAccessibilityHitTest(item) {
527-
self.rehideAfterNextUserClick(item, restoreCursorLocation: restoreCursorLocation)
526+
self.rehideAfterNextUserClick(item)
528527
self.finishActivation()
529528
return
530529
}
@@ -536,7 +535,7 @@ final class MenuBarItemStore: ObservableObject {
536535
self.retryActivation(item, mouseButton: mouseButton, retryCount: retryCount, message: "Unable to activate \(item.tooltip).")
537536
return
538537
}
539-
self.rehideAfterNextUserClick(item, restoreCursorLocation: restoreCursorLocation)
538+
self.rehideAfterNextUserClick(item)
540539
self.finishActivation()
541540
}
542541
}
@@ -564,9 +563,8 @@ final class MenuBarItemStore: ObservableObject {
564563
item.windowID == nil
565564
}
566565

567-
private func rehideAfterNextUserClick(_ item: MenuBarItem, restoreCursorLocation: CGPoint?) {
566+
private func rehideAfterNextUserClick(_ item: MenuBarItem) {
568567
pendingRehideItem = item
569-
pendingRehidePointer = restoreCursorLocation
570568

571569
// Popovers do not emit NSMenu tracking notifications. Install this
572570
// monitor after the originating click has completed. While an NSMenu
@@ -609,9 +607,7 @@ final class MenuBarItemStore: ObservableObject {
609607

610608
private func rehidePendingItem() {
611609
guard let item = pendingRehideItem else { return }
612-
let pointer = pendingRehidePointer
613610
pendingRehideItem = nil
614-
pendingRehidePointer = nil
615611
rehideWorkItem?.cancel()
616612
rehideWorkItem = nil
617613
transientDismissCheck?.cancel()
@@ -620,7 +616,10 @@ final class MenuBarItemStore: ObservableObject {
620616
NSEvent.removeMonitor(menuDismissMonitor)
621617
self.menuDismissMonitor = nil
622618
}
623-
layoutManager.rehide(item, restoreCursorLocation: pointer)
619+
// Capture the pointer inside the actual menu/popover interaction. The
620+
// original panel position is stale by this point and restoring it
621+
// would reopen hover UI or leave other apps with a false hit target.
622+
layoutManager.rehide(item, restoreCursorLocation: nil)
624623
}
625624

626625
private func cancelPendingRehide() {
@@ -633,7 +632,6 @@ final class MenuBarItemStore: ObservableObject {
633632
self.menuDismissMonitor = nil
634633
}
635634
pendingRehideItem = nil
636-
pendingRehidePointer = nil
637635
}
638636

639637
/// Control Center's menus are foreign-process windows and never produce

OverflowBar/Services/MenuBarLayoutManager.swift

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -218,12 +218,13 @@ final class MenuBarLayoutManager {
218218
// WindowServer location still matches the real hardware pointer,
219219
// including when the user clicked inside OverflowBar itself.
220220
let physicalPointerLocation = restoreCursorLocation ?? CGEvent(source: nil)?.location
221+
let eventPoint = safeEventPoint(preferred: physicalPointerLocation, fallback: currentFrame(windowID: targetWindowID) ?? .zero)
221222
guard let itemWindowID = item.windowID, let ownerPID = item.ownerPID,
222223
currentFrame(windowID: itemWindowID) != nil,
223224
let targetFrame = currentFrame(windowID: targetWindowID),
224225
let source = eventSource(for: ownerPID),
225-
let down = targetedEvent(type: .leftMouseDown, point: CGPoint(x: 20_000, y: 20_000), windowID: itemWindowID, pid: ownerPID, source: source, command: true),
226-
let up = targetedEvent(type: .leftMouseUp, point: CGPoint(x: placement == .left ? targetFrame.minX : targetFrame.maxX, y: targetFrame.midY), windowID: targetWindowID, pid: ownerPID, source: source, command: false) else {
226+
let down = targetedEvent(type: .leftMouseDown, point: eventPoint, windowID: itemWindowID, pid: ownerPID, source: source, command: true),
227+
let up = targetedEvent(type: .leftMouseUp, point: eventPoint, windowID: targetWindowID, pid: ownerPID, source: source, command: false) else {
227228
completion(false)
228229
return
229230
}
@@ -233,10 +234,15 @@ final class MenuBarLayoutManager {
233234
DispatchQueue.main.asyncAfter(deadline: .now() + 0.03) {
234235
self?.relay(up, to: ownerPID) { success in
235236
self?.logger.info("Mouse-up relay window \(itemWindowID, privacy: .public) success=\(success, privacy: .public)")
236-
// The relay consumes the synthetic event after forwarding
237-
// it to the owner process. Do not warp or otherwise
238-
// restore the global pointer here; that reintroduces the
239-
// stale-hover state this manager is designed to avoid.
237+
// A hide drag still has to target the off-screen staging
238+
// window, so WindowServer may clamp its logical pointer
239+
// while processing the event. Restore the pointer only
240+
// after the hide transaction (never during reveal), and
241+
// only to the location captured immediately before this
242+
// transaction began.
243+
if placement == .left {
244+
self?.restoreSyntheticPointer(physicalPointerLocation)
245+
}
240246
self?.verifyMove(item, relativeTo: targetWindowID, placement: placement, attempt: attempt, check: 0, restoreCursorLocation: physicalPointerLocation, completion: completion)
241247
}
242248
}
@@ -264,6 +270,9 @@ final class MenuBarLayoutManager {
264270
self.move(item, relativeTo: targetWindowID, placement: placement, attempt: attempt + 1, restoreCursorLocation: restoreCursorLocation, completion: completion)
265271
} else {
266272
self.logger.info("Move verification window \(itemWindowID, privacy: .public) failed")
273+
if placement == .left {
274+
self.restoreSyntheticPointer(restoreCursorLocation)
275+
}
267276
completion(false)
268277
}
269278
}
@@ -291,6 +300,29 @@ final class MenuBarLayoutManager {
291300
return CGEventSource(stateID: state)
292301
}
293302

303+
/// All synthetic drag events must carry an on-screen cursor coordinate.
304+
/// The target window fields select the status-item source/destination;
305+
/// using an off-screen point (20,000 or a negative hidden-section frame)
306+
/// makes WindowServer clamp the logical pointer to the top-left corner.
307+
private func safeEventPoint(preferred: CGPoint?, fallback: CGRect) -> CGPoint {
308+
let displays = Self.activeDisplayBounds()
309+
if let preferred, displays.contains(where: { $0.contains(preferred) }) {
310+
return preferred
311+
}
312+
if let display = displays.first {
313+
let x = min(max(fallback.midX, display.minX + 8), display.maxX - 8)
314+
let y = min(max(fallback.midY, display.minY + 8), display.maxY - 8)
315+
return CGPoint(x: x, y: y)
316+
}
317+
return CGPoint(x: 8, y: 8)
318+
}
319+
320+
private func restoreSyntheticPointer(_ point: CGPoint?) {
321+
guard let point, Self.activeDisplayBounds().contains(where: { $0.contains(point) }) else { return }
322+
CGWarpMouseCursorPosition(point)
323+
CGAssociateMouseAndMouseCursorPosition(1)
324+
}
325+
294326
private func targetedEvent(type: CGEventType, point: CGPoint, windowID: CGWindowID, pid: pid_t, source: CGEventSource, command: Bool) -> CGEvent? {
295327
guard let event = CGEvent(mouseEventSource: source, mouseType: type, mouseCursorPosition: point, mouseButton: .left) else { return nil }
296328
event.flags = command ? .maskCommand : []

0 commit comments

Comments
 (0)