From 9b0b42ba35f68358d9f805a0a79ecaffdbabf0b4 Mon Sep 17 00:00:00 2001 From: Till Adam Date: Sun, 15 Feb 2026 22:21:43 +0100 Subject: [PATCH 1/6] macOS: add dynamic `ApplicationHandlerExtMacOS::accepts_first_mouse` Add an `accepts_first_mouse` callback to `ApplicationHandlerExtMacOS` that receives the window ID and click position, enabling per-click decisions about whether to accept first mouse on inactive windows. This is needed for applications that want to follow the macOS convention of accepting first mouse for low-risk actions (selection, scrolling) but rejecting it for buttons and destructive actions. The implementation dispatches synchronously via a new `EventHandler::handle_with_result` method. When the handler is unavailable (not set or re-entrant), the static `accepts_first_mouse` value from `WindowAttributes` is used as a fallback. --- winit-appkit/src/app_state.rs | 15 ++++ winit-appkit/src/view.rs | 26 +++++- winit-common/src/event_handler.rs | 120 +++++++++++++++++++++++++--- winit-core/src/application/macos.rs | 28 +++++++ winit/src/changelog/unreleased.md | 1 + 5 files changed, 174 insertions(+), 16 deletions(-) diff --git a/winit-appkit/src/app_state.rs b/winit-appkit/src/app_state.rs index 5b057b79b3..1bf376b611 100644 --- a/winit-appkit/src/app_state.rs +++ b/winit-appkit/src/app_state.rs @@ -288,6 +288,21 @@ impl AppState { } } + /// Try to call the handler synchronously and return a result. + /// + /// Returns `None` if the handler is not set or is currently in use (re-entrant call). + #[track_caller] + pub fn try_with_handler_result( + self: &Rc, + callback: impl FnOnce(&mut dyn ApplicationHandler, &ActiveEventLoop) -> R, + ) -> Option { + let this = self; + self.event_handler.handle_with_result(|app| { + let event_loop = ActiveEventLoop { app_state: Rc::clone(this), mtm: this.mtm }; + callback(app, &event_loop) + }) + } + #[track_caller] fn with_handler( self: &Rc, diff --git a/winit-appkit/src/view.rs b/winit-appkit/src/view.rs index 818e460277..a3208d9d23 100644 --- a/winit-appkit/src/view.rs +++ b/winit-appkit/src/view.rs @@ -3,7 +3,7 @@ use std::cell::{Cell, RefCell}; use std::collections::{HashMap, VecDeque}; use std::rc::Rc; -use dpi::{LogicalPosition, LogicalSize}; +use dpi::{LogicalPosition, LogicalSize, PhysicalPosition}; use objc2::rc::Retained; use objc2::runtime::{AnyObject, Sel}; use objc2::{AnyThread, DefinedClass, MainThreadMarker, define_class, msg_send}; @@ -764,9 +764,29 @@ define_class!( } #[unsafe(method(acceptsFirstMouse:))] - fn accepts_first_mouse(&self, _event: &NSEvent) -> bool { + fn accepts_first_mouse(&self, event: Option<&NSEvent>) -> bool { let _entered = debug_span!("acceptsFirstMouse:").entered(); - self.ivars().accepts_first_mouse + // The event parameter can be nil according to Apple's API contract. + // When nil, fall back to the static default. + event + .and_then(|event| { + let window_id = window_id(&self.window()); + let point_in_window = event.locationInWindow(); + let point_in_view = self.convertPoint_fromView(point_in_window, None); + let scale_factor = self.scale_factor(); + let position = PhysicalPosition::new( + point_in_view.x * scale_factor, + point_in_view.y * scale_factor, + ); + self.ivars() + .app_state + .try_with_handler_result(move |app, event_loop| { + app.macos_handler() + .map(|h| h.accepts_first_mouse(event_loop, window_id, position)) + }) + .flatten() + }) + .unwrap_or(self.ivars().accepts_first_mouse) } } ); diff --git a/winit-common/src/event_handler.rs b/winit-common/src/event_handler.rs index f19d2f218a..b9eec2c6b0 100644 --- a/winit-common/src/event_handler.rs +++ b/winit-common/src/event_handler.rs @@ -116,24 +116,39 @@ impl EventHandler { matches!(self.inner.try_borrow().as_deref(), Ok(Some(_))) } - pub fn handle(&self, callback: impl FnOnce(&mut (dyn ApplicationHandler + '_))) { + /// Try to call the handler and return a value. + /// + /// Returns `None` if the handler is not set or is currently in use (re-entrant call). + /// + /// It is important that we keep the `RefMut` borrowed during the callback, so that `in_use` + /// can properly detect that the handler is still in use. If the handler unwinds, the `RefMut` + /// will ensure that the handler is no longer borrowed. + pub fn handle_with_result( + &self, + callback: impl FnOnce(&mut (dyn ApplicationHandler + '_)) -> R, + ) -> Option { match self.inner.try_borrow_mut().as_deref_mut() { - Ok(Some(user_app)) => { - // It is important that we keep the reference borrowed here, - // so that `in_use` can properly detect that the handler is - // still in use. - // - // If the handler unwinds, the `RefMut` will ensure that the - // handler is no longer borrowed. - callback(&mut **user_app); - }, + Ok(Some(user_app)) => Some(callback(&mut **user_app)), Ok(None) => { - // `NSApplication`, our app state and this handler are all - // global state and so it's not impossible that we could get - // an event after the application has exited the `EventLoop`. + // `NSApplication`, our app state and this handler are all global state and so + // it's not impossible that we could get an event after the application has + // exited the `EventLoop`. tracing::error!("tried to run event handler, but no handler was set"); + None }, Err(_) => { + // Handler is currently in use, return None instead of panicking. + None + }, + } + } + + pub fn handle(&self, callback: impl FnOnce(&mut (dyn ApplicationHandler + '_))) { + match self.handle_with_result(callback) { + Some(()) => {}, + // Handler not set — already logged by handle_with_result. + None if !self.in_use() => {}, + None => { // Prevent re-entrancy. panic!("tried to handle event while another event is currently being handled"); }, @@ -157,3 +172,82 @@ impl EventHandler { } } } + +#[cfg(test)] +mod tests { + use winit_core::application::ApplicationHandler; + use winit_core::event::WindowEvent; + use winit_core::event_loop::ActiveEventLoop; + use winit_core::window::WindowId; + + use super::EventHandler; + + struct DummyApp; + + impl ApplicationHandler for DummyApp { + fn can_create_surfaces(&mut self, _event_loop: &dyn ActiveEventLoop) {} + + fn window_event( + &mut self, + _event_loop: &dyn ActiveEventLoop, + _window_id: WindowId, + _event: WindowEvent, + ) { + } + } + + #[test] + fn handle_with_result_returns_value() { + let handler = EventHandler::new(); + handler.set(Box::new(DummyApp), || { + let result = handler.handle_with_result(|_app| 42); + assert_eq!(result, Some(42)); + }); + } + + #[test] + fn handle_with_result_returns_none_when_not_set() { + let handler = EventHandler::new(); + let result = handler.handle_with_result(|_app| 42); + assert_eq!(result, None); + } + + #[test] + fn handle_with_result_returns_none_when_in_use() { + let handler = EventHandler::new(); + handler.set(Box::new(DummyApp), || { + // Borrow the handler via `handle`, then try `handle_with_result` + // from within — simulating re-entrancy. + handler.handle(|_app| { + let result = handler.handle_with_result(|_app| 42); + assert_eq!(result, None); + }); + }); + } + + #[test] + fn handle_with_result_returns_none_when_reentrant_through_self() { + let handler = EventHandler::new(); + handler.set(Box::new(DummyApp), || { + let result = handler.handle_with_result(|_app| { + // Re-entrant call through handle_with_result itself. + handler.handle_with_result(|_app| 42) + }); + assert_eq!(result, Some(None)); + }); + } + + #[test] + #[should_panic( + expected = "tried to handle event while another event is currently being handled" + )] + fn handle_panics_on_reentrant_call() { + let handler = EventHandler::new(); + handler.set(Box::new(DummyApp), || { + handler.handle(|_app| { + // Re-entrant handle must still panic after the refactoring. + handler.handle(|_app| {}); + }); + }); + } +} diff --git a/winit-core/src/application/macos.rs b/winit-core/src/application/macos.rs index d18b4090e5..fe4e3fd6c0 100644 --- a/winit-core/src/application/macos.rs +++ b/winit-core/src/application/macos.rs @@ -1,3 +1,5 @@ +use dpi::PhysicalPosition; + use crate::application::ApplicationHandler; use crate::event_loop::ActiveEventLoop; use crate::window::WindowId; @@ -49,4 +51,30 @@ pub trait ApplicationHandlerExtMacOS: ApplicationHandler { let _ = window_id; let _ = action; } + + /// Called when the user clicks on an inactive window to determine whether the click should + /// also be processed as a normal mouse event. + /// + /// This corresponds to the [`acceptsFirstMouse:`] method on `NSView`, which receives the + /// triggering mouse event. Winit extracts the click position from that event and passes it + /// here so that the application can make per-click decisions, e.g. accept first mouse for + /// low-risk actions (selection, scrolling) but reject it for buttons or destructive actions. + /// + /// The default implementation returns `true`. + /// + /// If this method cannot be called synchronously (e.g. the handler is already in use), the + /// static `accepts_first_mouse` value from + /// [`WindowAttributes`][crate::window::WindowAttributes] is used as a fallback. + /// + /// [`acceptsFirstMouse:`]: https://developer.apple.com/documentation/appkit/nsview/acceptsfirstmouse(_:) + #[doc(alias = "acceptsFirstMouse:")] + fn accepts_first_mouse( + &mut self, + event_loop: &dyn ActiveEventLoop, + window_id: WindowId, + position: PhysicalPosition, + ) -> bool { + let _ = (event_loop, window_id, position); + true + } } diff --git a/winit/src/changelog/unreleased.md b/winit/src/changelog/unreleased.md index 32910b707c..63f22c9aa8 100644 --- a/winit/src/changelog/unreleased.md +++ b/winit/src/changelog/unreleased.md @@ -48,6 +48,7 @@ changelog entry. - Implement `Send` and `Sync` for `OwnedDisplayHandle`. - Use new macOS 15 cursors for resize icons. - On Android, added scancode conversions for more obscure key codes. +- On macOS, add `ApplicationHandlerExtMacOS::accepts_first_mouse` for dynamic per-click decisions. ### Changed From 959c126fa37f195fcadada5b3338d4e2d40e268f Mon Sep 17 00:00:00 2001 From: Till Adam Date: Sat, 28 Mar 2026 14:23:59 +0100 Subject: [PATCH 2/6] Remove stale 'after the refactoring' from comment --- winit-common/src/event_handler.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/winit-common/src/event_handler.rs b/winit-common/src/event_handler.rs index b9eec2c6b0..45b1709674 100644 --- a/winit-common/src/event_handler.rs +++ b/winit-common/src/event_handler.rs @@ -245,7 +245,7 @@ mod tests { let handler = EventHandler::new(); handler.set(Box::new(DummyApp), || { handler.handle(|_app| { - // Re-entrant handle must still panic after the refactoring. + // Re-entrant handle must still panic. handler.handle(|_app| {}); }); }); From 6cf84806dd9f8bed94d8d80823b8d53799498ce1 Mon Sep 17 00:00:00 2001 From: Till Adam Date: Sat, 28 Mar 2026 14:26:52 +0100 Subject: [PATCH 3/6] Clarify that dynamic accepts_first_mouse takes precedence over static setting --- winit-core/src/application/macos.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/winit-core/src/application/macos.rs b/winit-core/src/application/macos.rs index fe4e3fd6c0..428baa59d6 100644 --- a/winit-core/src/application/macos.rs +++ b/winit-core/src/application/macos.rs @@ -62,9 +62,10 @@ pub trait ApplicationHandlerExtMacOS: ApplicationHandler { /// /// The default implementation returns `true`. /// - /// If this method cannot be called synchronously (e.g. the handler is already in use), the - /// static `accepts_first_mouse` value from - /// [`WindowAttributes`][crate::window::WindowAttributes] is used as a fallback. + /// When [`ApplicationHandler::macos_handler`] returns `Some`, this method takes precedence + /// over the static `WindowAttributesMacOS::with_accepts_first_mouse` setting. The static + /// value is only used as a fallback when this method cannot be called synchronously (e.g. + /// the handler is already borrowed due to re-entrancy). /// /// [`acceptsFirstMouse:`]: https://developer.apple.com/documentation/appkit/nsview/acceptsfirstmouse(_:) #[doc(alias = "acceptsFirstMouse:")] From 7ea93ef5ad51a2449780726ba95c2d696924d23f Mon Sep 17 00:00:00 2001 From: Till Adam Date: Sat, 28 Mar 2026 14:37:01 +0100 Subject: [PATCH 4/6] Warn when accepts_first_mouse handler cannot be called due to re-entrancy --- winit-appkit/src/view.rs | 45 ++++++++++++++++++++++------------------ 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/winit-appkit/src/view.rs b/winit-appkit/src/view.rs index a3208d9d23..802fa126a4 100644 --- a/winit-appkit/src/view.rs +++ b/winit-appkit/src/view.rs @@ -16,7 +16,7 @@ use objc2_foundation::{ NSArray, NSAttributedString, NSAttributedStringKey, NSCopying, NSMutableAttributedString, NSNotFound, NSObject, NSPoint, NSRange, NSRect, NSSize, NSString, NSUInteger, }; -use tracing::{debug_span, trace_span}; +use tracing::{debug_span, trace_span, warn}; use winit_core::event::{ DeviceEvent, ElementState, Ime, KeyEvent, Modifiers, MouseButton, MouseScrollDelta, PointerKind, PointerSource, TouchPhase, WindowEvent, @@ -768,25 +768,30 @@ define_class!( let _entered = debug_span!("acceptsFirstMouse:").entered(); // The event parameter can be nil according to Apple's API contract. // When nil, fall back to the static default. - event - .and_then(|event| { - let window_id = window_id(&self.window()); - let point_in_window = event.locationInWindow(); - let point_in_view = self.convertPoint_fromView(point_in_window, None); - let scale_factor = self.scale_factor(); - let position = PhysicalPosition::new( - point_in_view.x * scale_factor, - point_in_view.y * scale_factor, - ); - self.ivars() - .app_state - .try_with_handler_result(move |app, event_loop| { - app.macos_handler() - .map(|h| h.accepts_first_mouse(event_loop, window_id, position)) - }) - .flatten() - }) - .unwrap_or(self.ivars().accepts_first_mouse) + let result = event.and_then(|event| { + let window_id = window_id(&self.window()); + let point_in_window = event.locationInWindow(); + let point_in_view = self.convertPoint_fromView(point_in_window, None); + let scale_factor = self.scale_factor(); + let position = PhysicalPosition::new( + point_in_view.x * scale_factor, + point_in_view.y * scale_factor, + ); + self.ivars() + .app_state + .try_with_handler_result(move |app, event_loop| { + app.macos_handler() + .map(|h| h.accepts_first_mouse(event_loop, window_id, position)) + }) + .flatten() + }); + if event.is_some() && result.is_none() { + warn!( + "could not call `accepts_first_mouse` handler (re-entrant call), \ + falling back to static value" + ); + } + result.unwrap_or(self.ivars().accepts_first_mouse) } } ); From 2cb9dceea53582d40e343a25414d96517c94f08f Mon Sep 17 00:00:00 2001 From: Till Adam Date: Sat, 28 Mar 2026 14:39:25 +0100 Subject: [PATCH 5/6] Add accepts_first_mouse example usage and document when to use static vs dynamic --- winit-core/src/application/macos.rs | 5 +++++ winit/examples/application.rs | 10 ++++++++++ 2 files changed, 15 insertions(+) diff --git a/winit-core/src/application/macos.rs b/winit-core/src/application/macos.rs index 428baa59d6..55dbe1077a 100644 --- a/winit-core/src/application/macos.rs +++ b/winit-core/src/application/macos.rs @@ -62,6 +62,11 @@ pub trait ApplicationHandlerExtMacOS: ApplicationHandler { /// /// The default implementation returns `true`. /// + /// Use `WindowAttributesMacOS::with_accepts_first_mouse` if a static, per-window value is + /// sufficient. Implement this method instead when you need to decide dynamically based on + /// *where* in the window the click landed (e.g. accept for the canvas area but reject for + /// toolbar buttons). + /// /// When [`ApplicationHandler::macos_handler`] returns `Some`, this method takes precedence /// over the static `WindowAttributesMacOS::with_accepts_first_mouse` setting. The static /// value is only used as a fallback when this method cannot be called synchronously (e.g. diff --git a/winit/examples/application.rs b/winit/examples/application.rs index 4098ebff68..75906a1d8c 100644 --- a/winit/examples/application.rs +++ b/winit/examples/application.rs @@ -600,6 +600,16 @@ impl ApplicationHandlerExtMacOS for Application { ) { info!(?window_id, ?action, "macOS standard key binding"); } + + fn accepts_first_mouse( + &mut self, + _event_loop: &dyn ActiveEventLoop, + window_id: WindowId, + position: PhysicalPosition, + ) -> bool { + info!(?window_id, ?position, "macOS accepts_first_mouse"); + true + } } /// State of the window. From aca5bc0cecfa4ad72200444bbe6f81974c2facdc Mon Sep 17 00:00:00 2001 From: Till Adam Date: Sat, 28 Mar 2026 15:20:05 +0100 Subject: [PATCH 6/6] Fix formatting --- winit-appkit/src/view.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/winit-appkit/src/view.rs b/winit-appkit/src/view.rs index 802fa126a4..91bcfb0351 100644 --- a/winit-appkit/src/view.rs +++ b/winit-appkit/src/view.rs @@ -787,8 +787,8 @@ define_class!( }); if event.is_some() && result.is_none() { warn!( - "could not call `accepts_first_mouse` handler (re-entrant call), \ - falling back to static value" + "could not call `accepts_first_mouse` handler (re-entrant call), falling back \ + to static value" ); } result.unwrap_or(self.ivars().accepts_first_mouse)