From 4d81f4aa620896ece7520e67e0cadebb097e5bd7 Mon Sep 17 00:00:00 2001 From: RandomScientist <37155686+Random-Scientist@users.noreply.github.com> Date: Tue, 17 Mar 2026 18:03:16 -0700 Subject: [PATCH] internal(macOS) use `NSTrackingArea` instead of `trackingRect` (#4514) --- winit-appkit/Cargo.toml | 1 + winit-appkit/src/view.rs | 81 ++++++++++++++++++------------- winit-core/src/event.rs | 6 +++ winit/src/changelog/unreleased.md | 1 + 4 files changed, 56 insertions(+), 33 deletions(-) diff --git a/winit-appkit/Cargo.toml b/winit-appkit/Cargo.toml index a4f590ea2..b5e423695 100644 --- a/winit-appkit/Cargo.toml +++ b/winit-appkit/Cargo.toml @@ -51,6 +51,7 @@ objc2-app-kit = { workspace = true, features = [ "NSScreen", "NSTextInputClient", "NSTextInputContext", + "NSTrackingArea", "NSToolbar", "NSView", "NSWindow", diff --git a/winit-appkit/src/view.rs b/winit-appkit/src/view.rs index 056ec93a2..9c7f3e362 100644 --- a/winit-appkit/src/view.rs +++ b/winit-appkit/src/view.rs @@ -1,16 +1,15 @@ #![allow(clippy::unnecessary_cast)] use std::cell::{Cell, RefCell}; use std::collections::{HashMap, VecDeque}; -use std::ptr; use std::rc::Rc; use dpi::{LogicalPosition, LogicalSize}; use objc2::rc::Retained; use objc2::runtime::{AnyObject, Sel}; -use objc2::{DefinedClass, MainThreadMarker, define_class, msg_send}; +use objc2::{AnyThread, DefinedClass, MainThreadMarker, define_class, msg_send}; use objc2_app_kit::{ - NSApplication, NSCursor, NSEvent, NSEventPhase, NSResponder, NSTextInputClient, - NSTrackingRectTag, NSView, NSWindow, + NSApplication, NSCursor, NSEvent, NSEventPhase, NSResponder, NSTextInputClient, NSTrackingArea, + NSTrackingAreaOptions, NSView, NSWindow, }; use objc2_core_foundation::CGRect; use objc2_foundation::{ @@ -119,7 +118,6 @@ pub struct ViewState { ime_size: Cell, modifiers: Cell, phys_modifiers: RefCell>, - tracking_rect: Cell>, ime_state: Cell, input_source: RefCell, @@ -131,7 +129,6 @@ pub struct ViewState { /// True if the current key event should be forwarded /// to the application, even during IME forward_key_to_app: Cell, - marked_text: RefCell>, accepts_first_mouse: bool, @@ -153,41 +150,17 @@ define_class!( true } - #[unsafe(method(viewDidMoveToWindow))] - fn view_did_move_to_window(&self) { - trace_scope!("viewDidMoveToWindow"); - if let Some(tracking_rect) = self.ivars().tracking_rect.take() { - self.removeTrackingRect(tracking_rect); - } - - let rect = self.frame(); - let tracking_rect = unsafe { - self.addTrackingRect_owner_userData_assumeInside(rect, self, ptr::null_mut(), false) - }; - assert_ne!(tracking_rect, 0, "failed adding tracking rect"); - self.ivars().tracking_rect.set(Some(tracking_rect)); - } - // Not a normal method on `NSView`, it's triggered by `NSViewFrameDidChangeNotification`. #[unsafe(method(viewFrameDidChangeNotification:))] fn frame_did_change(&self, _notification: Option<&AnyObject>) { trace_scope!("NSViewFrameDidChangeNotification"); - if let Some(tracking_rect) = self.ivars().tracking_rect.take() { - self.removeTrackingRect(tracking_rect); - } - - let rect = self.frame(); - let tracking_rect = unsafe { - self.addTrackingRect_owner_userData_assumeInside(rect, self, ptr::null_mut(), false) - }; - assert_ne!(tracking_rect, 0, "failed adding tracking rect"); - self.ivars().tracking_rect.set(Some(tracking_rect)); // Emit resize event here rather than from windowDidResize because: // 1. When a new window is created as a tab, the frame size may change without a window // resize occurring. // 2. Even when a window resize does occur on a new tabbed window, it contains the wrong // size (includes tab height). + let rect = self.frame(); let logical_size = LogicalSize::new(rect.size.width as f64, rect.size.height as f64); let size = logical_size.to_physical::(self.scale_factor()); self.queue_event(WindowEvent::SurfaceResized(size)); @@ -808,7 +781,6 @@ impl WinitView { ime_size: Default::default(), modifiers: Default::default(), phys_modifiers: Default::default(), - tracking_rect: Default::default(), ime_state: Default::default(), input_source: Default::default(), ime_capabilities: Default::default(), @@ -818,9 +790,52 @@ impl WinitView { option_as_alt: Cell::new(option_as_alt), }); let this: Retained = unsafe { msg_send![super(this), init] }; - *this.ivars().input_source.borrow_mut() = this.current_input_source(); + // `MouseEnteredAndExited` enables receiving events through `mouseEntered:` and + // `mouseExited:`. + // + // `MouseMoved` enables receiving events through `mouseMoved:` + // + // We do not set `CursorUpdate` because it is part of the "flexible" alternative to + // `cursorRect` based cursor image updates, and we currently still use + // `cursorRect`s. We also can't really switch to this approach because "The + // cursorUpdate(with:) message is not sent when the NSTrackingCursorUpdate option is + // specified along with [`ActiveAlways`]." + // + // `ActiveAlways` indicates we want to receive events when the window is not + // focused ("key window" in Cocoa terms), which matches the behavior on other + // platforms. + // + // We do not set `AssumeInside` because we want to avoid emitting `Left` events without a + // correspondering `Entered` to our consumers, and not setting this flag tells AppKit to + // handle this for us by synthesizing entry and exit events in some cases. + // + // `InVisibleRect` instructs the tracking area's `owner` (our `NSView`) to ignore the value + // we provide in `rect` and keep the tracking area's bounds up to date with the + // current view bounds automatically. + // + // We do not set `EnabledDuringMouseDrag` to match the platform behavior on Windows + // and Wayland, since neither emit events while being dragged over with an empty + // cursor without focus. + // + // See also https://developer.apple.com/documentation/appkit/nstrackingareaoptions. + + // Safety: the type of `owner` should be `NSView` and is. + // The type of `user_info` is irrelevant because it is None. + this.addTrackingArea(&*unsafe { + NSTrackingArea::initWithRect_options_owner_userInfo( + NSTrackingArea::alloc(), + NSRect::ZERO, + NSTrackingAreaOptions::MouseEnteredAndExited + | NSTrackingAreaOptions::MouseMoved + | NSTrackingAreaOptions::ActiveAlways + | NSTrackingAreaOptions::InVisibleRect, + Some(&this), + None, + ) + }); + this } diff --git a/winit-core/src/event.rs b/winit-core/src/event.rs index 031fde5c6..e6500c213 100644 --- a/winit-core/src/event.rs +++ b/winit-core/src/event.rs @@ -156,6 +156,8 @@ pub enum WindowEvent { Ime(Ime), /// The pointer has moved on the window. + /// + /// Should be emitted regardless of window focus. PointerMoved { device_id: Option, @@ -184,6 +186,8 @@ pub enum WindowEvent { }, /// The pointer has entered the window. + /// + /// Should be emitted regardless of window focus. PointerEntered { device_id: Option, @@ -209,6 +213,8 @@ pub enum WindowEvent { }, /// The pointer has left the window. + /// + /// Should be emitted regardless of window focus. PointerLeft { device_id: Option, diff --git a/winit/src/changelog/unreleased.md b/winit/src/changelog/unreleased.md index 8f183e334..66f864c04 100644 --- a/winit/src/changelog/unreleased.md +++ b/winit/src/changelog/unreleased.md @@ -50,6 +50,7 @@ changelog entry. ### Changed - Updated `windows-sys` to `v0.61`. +- On older macOS versions (tested up to 12.7.6), applications now receive mouse movement events for unfocused windows, matching the behavior on other platforms. ### Fixed