From db7f9ceed788ebfcbbcb18e65927172f1dd7dde9 Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Tue, 15 Sep 2026 16:28:53 +0000 Subject: [PATCH] gpui_macos: Improve mouse tracking for normal windows (#64234) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit To reproduce: 1. Place Safari over part of Zed, then focus Zed. 2. Right-click Safari to open its context menu, then left-click Safari anywhere in it. 3. Move the pointer over Safari where it covers the Zed. Zed shows hover effects underneath. Use a view tracking area and disable `acceptsMouseMovedEvents` for normal macOS windows. This addresses hover effects on covered content when the pointer moves over another window. [Chromium also requests mouse movement through tracking areas](https://github.com/chromium/chromium/blob/5a43095e95ffe566d1512b9d67d534e3ae05b975/components/remote_cocoa/app_shim/bridged_content_view.mm#L436-L465). A search of its macOS window/view code found no explicit use of `acceptsMouseMovedEvents`, which [defaults to `false`](https://developer.apple.com/documentation/appkit/nswindow/acceptsmousemovedevents). Tracking stays enabled in unfocused windows while the app is active. Before: https://github.com/user-attachments/assets/f21cd3e5-04d5-4d34-b495-02f8d0f68ecf After: https://github.com/user-attachments/assets/89a8b9c9-5aa3-4075-aa91-13e200ea3613 Release Notes: - [GPUI] Fixed mouse tracking on macOS so moving the pointer over another app’s window no longer triggers hover effects in covered GPUI windows. - Fixed an issue on macOS where moving the pointer over another app could trigger hover effects in a Zed window underneath it. --------- Co-authored-by: Anthony Eid --- Cargo.toml | 1 + crates/gpui_macos/src/window.rs | 66 +++++++++++++++++++++------------ 2 files changed, 43 insertions(+), 24 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 44edcd8ada..eeeb733231 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -708,6 +708,7 @@ objc2-app-kit = { version = "0.3.2", default-features = false, features = [ "NSResponder", "NSSavePanel", "NSScreen", + "NSTrackingArea", "NSView", "NSWindow", "NSWorkspace", diff --git a/crates/gpui_macos/src/window.rs b/crates/gpui_macos/src/window.rs index 76f413a4a2..0867385a65 100644 --- a/crates/gpui_macos/src/window.rs +++ b/crates/gpui_macos/src/window.rs @@ -56,8 +56,9 @@ use objc2::{ }; use objc2_app_kit::{ NSAlert, NSAlertStyle, NSBeep, NSButton as Objc2NSButton, NSDraggingImageComponent, - NSDraggingImageComponentIconKey, NSDraggingItem, NSPasteboardWriting, NSView as Objc2NSView, - NSWindow as Objc2NSWindow, NSWindowButton as Objc2NSWindowButton, NSWorkspace, + NSDraggingImageComponentIconKey, NSDraggingItem, NSPasteboardWriting, NSTrackingArea, + NSTrackingAreaOptions, NSView as Objc2NSView, NSWindow as Objc2NSWindow, + NSWindowButton as Objc2NSWindowButton, NSWorkspace, }; use objc2_foundation::{NSPoint as Objc2NSPoint, NSRect as Objc2NSRect, NSURL}; use parking_lot::Mutex; @@ -101,14 +102,6 @@ const NSFloatingWindowLevel: NSInteger = 3; #[allow(non_upper_case_globals)] const NSPopUpWindowLevel: NSInteger = 101; #[allow(non_upper_case_globals)] -const NSTrackingMouseEnteredAndExited: NSUInteger = 0x01; -#[allow(non_upper_case_globals)] -const NSTrackingMouseMoved: NSUInteger = 0x02; -#[allow(non_upper_case_globals)] -const NSTrackingActiveAlways: NSUInteger = 0x80; -#[allow(non_upper_case_globals)] -const NSTrackingInVisibleRect: NSUInteger = 0x200; -#[allow(non_upper_case_globals)] const NSWindowAnimationBehaviorUtilityWindow: NSInteger = 4; #[allow(non_upper_case_globals)] const NSViewLayerContentsRedrawDuringViewResize: NSInteger = 2; @@ -1198,15 +1191,29 @@ impl MacWindow { let main_window: id = msg_send![app, mainWindow]; let mut sheet_parent = None; + // SAFETY: This is the live GPUIView (an NSView subclass) added to the + // content view above, and window creation runs on the main thread. + let tracking_view = &*native_view.cast::(); + match kind { WindowKind::Normal | WindowKind::Floating => { if kind == WindowKind::Floating { // Let the window float keep above normal windows. native_window.setLevel_(NSFloatingWindowLevel); + native_window.setAcceptsMouseMovedEvents_(YES); } else { native_window.setLevel_(NSNormalWindowLevel); + native_window.setAcceptsMouseMovedEvents_(NO); + add_mouse_tracking_area( + tracking_view, + NSTrackingAreaOptions::MouseEnteredAndExited + | NSTrackingAreaOptions::MouseMoved + // Track while this application is active, even if another + // window in the application has focus. + | NSTrackingAreaOptions::ActiveInActiveApp + | NSTrackingAreaOptions::InVisibleRect, + ); } - native_window.setAcceptsMouseMovedEvents_(YES); if let Some(tabbing_identifier) = tabbing_identifier { let tabbing_id = ns_string(tabbing_identifier.as_str()); @@ -1218,19 +1225,15 @@ impl MacWindow { // `AnchoredPopup` is rejected in `MacPlatform::open_window`, grouped here only // for exhaustiveness. WindowKind::PopUp | WindowKind::AnchoredPopup(_) => { - // Use a tracking area to allow receiving MouseMoved events even when - // the window or application aren't active, which is often the case - // e.g. for notification windows. - let tracking_area: id = msg_send![class!(NSTrackingArea), alloc]; - let _: () = msg_send![ - tracking_area, - initWithRect: NSRect::new(NSPoint::new(0., 0.), NSSize::new(0., 0.)) - options: NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | NSTrackingActiveAlways | NSTrackingInVisibleRect - owner: native_view - userInfo: nil - ]; - let _: () = - msg_send![native_view, addTrackingArea: tracking_area.autorelease()]; + add_mouse_tracking_area( + tracking_view, + NSTrackingAreaOptions::MouseEnteredAndExited + | NSTrackingAreaOptions::MouseMoved + // Track even when another application is active, e.g. so + // notification windows can respond to hover. + | NSTrackingAreaOptions::ActiveAlways + | NSTrackingAreaOptions::InVisibleRect, + ); native_window.setLevel_(NSPopUpWindowLevel); let _: () = msg_send![ @@ -2480,6 +2483,21 @@ extern "C" fn dealloc_view(this: &Object, _: Sel) { } } +fn add_mouse_tracking_area(native_view: &Objc2NSView, options: NSTrackingAreaOptions) { + // SAFETY: NSView provides the tracking-event callbacks, and the owner is + // the same view that retains the tracking area. No user info is supplied. + let tracking_area = unsafe { + NSTrackingArea::initWithRect_options_owner_userInfo( + NSTrackingArea::alloc(), + Objc2NSRect::ZERO, + options, + Some(native_view), + None, + ) + }; + native_view.addTrackingArea(&tracking_area); +} + extern "C" fn reset_cursor_rects(this: &Object, _: Sel) { // SAFETY: AppKit invokes cursor-rect updates on the main thread for GPUIView instances, // whose WINDOW_STATE_IVAR is initialized when the view is created. The cursor registered