Skip to content

Commit 9e4e2f6

Browse files
committed
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.
1 parent 983e509 commit 9e4e2f6

File tree

5 files changed

+173
-16
lines changed

5 files changed

+173
-16
lines changed

winit-appkit/src/app_state.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,21 @@ impl AppState {
290290
}
291291
}
292292

293+
/// Try to call the handler synchronously and return a result.
294+
///
295+
/// Returns `None` if the handler is not set or is currently in use (re-entrant call).
296+
#[track_caller]
297+
pub fn try_with_handler_result<R>(
298+
self: &Rc<Self>,
299+
callback: impl FnOnce(&mut dyn ApplicationHandler, &ActiveEventLoop) -> R,
300+
) -> Option<R> {
301+
let this = self;
302+
self.event_handler.handle_with_result(|app| {
303+
let event_loop = ActiveEventLoop { app_state: Rc::clone(this), mtm: this.mtm };
304+
callback(app, &event_loop)
305+
})
306+
}
307+
293308
#[track_caller]
294309
fn with_handler(
295310
self: &Rc<Self>,

winit-appkit/src/view.rs

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use std::collections::{HashMap, VecDeque};
44
use std::ptr;
55
use std::rc::Rc;
66

7-
use dpi::{LogicalPosition, LogicalSize};
7+
use dpi::{LogicalPosition, LogicalSize, PhysicalPosition};
88
use objc2::rc::Retained;
99
use objc2::runtime::{AnyObject, Sel};
1010
use objc2::{DefinedClass, MainThreadMarker, define_class, msg_send};
@@ -781,9 +781,30 @@ define_class!(
781781
}
782782

783783
#[unsafe(method(acceptsFirstMouse:))]
784-
fn accepts_first_mouse(&self, _event: &NSEvent) -> bool {
784+
fn accepts_first_mouse(&self, event: Option<&NSEvent>) -> bool {
785785
trace_scope!("acceptsFirstMouse:");
786-
self.ivars().accepts_first_mouse
786+
// The event parameter can be nil according to Apple's API contract.
787+
// When nil, fall back to the static default.
788+
event
789+
.and_then(|event| {
790+
let window_id = window_id(&self.window());
791+
let point_in_window = event.locationInWindow();
792+
let point_in_view = self.convertPoint_fromView(point_in_window, None);
793+
let scale_factor = self.scale_factor();
794+
let position = PhysicalPosition::new(
795+
point_in_view.x * scale_factor,
796+
point_in_view.y * scale_factor,
797+
);
798+
self.ivars()
799+
.app_state
800+
.try_with_handler_result(move |app, event_loop| {
801+
app.macos_handler().map(|h| {
802+
h.accepts_first_mouse(event_loop, window_id, position)
803+
})
804+
})
805+
.flatten()
806+
})
807+
.unwrap_or(self.ivars().accepts_first_mouse)
787808
}
788809
}
789810
);

winit-common/src/event_handler.rs

Lines changed: 105 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -112,24 +112,39 @@ impl EventHandler {
112112
matches!(self.inner.try_borrow().as_deref(), Ok(Some(_)))
113113
}
114114

115-
pub fn handle(&self, callback: impl FnOnce(&mut (dyn ApplicationHandler + '_))) {
115+
/// Try to call the handler and return a value.
116+
///
117+
/// Returns `None` if the handler is not set or is currently in use (re-entrant call).
118+
///
119+
/// It is important that we keep the `RefMut` borrowed during the callback, so that `in_use`
120+
/// can properly detect that the handler is still in use. If the handler unwinds, the `RefMut`
121+
/// will ensure that the handler is no longer borrowed.
122+
pub fn handle_with_result<R>(
123+
&self,
124+
callback: impl FnOnce(&mut (dyn ApplicationHandler + '_)) -> R,
125+
) -> Option<R> {
116126
match self.inner.try_borrow_mut().as_deref_mut() {
117-
Ok(Some(user_app)) => {
118-
// It is important that we keep the reference borrowed here,
119-
// so that `in_use` can properly detect that the handler is
120-
// still in use.
121-
//
122-
// If the handler unwinds, the `RefMut` will ensure that the
123-
// handler is no longer borrowed.
124-
callback(&mut **user_app);
125-
},
127+
Ok(Some(user_app)) => Some(callback(&mut **user_app)),
126128
Ok(None) => {
127-
// `NSApplication`, our app state and this handler are all
128-
// global state and so it's not impossible that we could get
129-
// an event after the application has exited the `EventLoop`.
129+
// `NSApplication`, our app state and this handler are all global state and so
130+
// it's not impossible that we could get an event after the application has
131+
// exited the `EventLoop`.
130132
tracing::error!("tried to run event handler, but no handler was set");
133+
None
131134
},
132135
Err(_) => {
136+
// Handler is currently in use, return None instead of panicking.
137+
None
138+
},
139+
}
140+
}
141+
142+
pub fn handle(&self, callback: impl FnOnce(&mut (dyn ApplicationHandler + '_))) {
143+
match self.handle_with_result(callback) {
144+
Some(()) => {},
145+
// Handler not set — already logged by handle_with_result.
146+
None if !self.in_use() => {},
147+
None => {
133148
// Prevent re-entrancy.
134149
panic!("tried to handle event while another event is currently being handled");
135150
},
@@ -153,3 +168,80 @@ impl EventHandler {
153168
}
154169
}
155170
}
171+
172+
#[cfg(test)]
173+
mod tests {
174+
use winit_core::application::ApplicationHandler;
175+
use winit_core::event::WindowEvent;
176+
use winit_core::event_loop::ActiveEventLoop;
177+
use winit_core::window::WindowId;
178+
179+
use super::EventHandler;
180+
181+
struct DummyApp;
182+
183+
impl ApplicationHandler for DummyApp {
184+
fn can_create_surfaces(&mut self, _event_loop: &dyn ActiveEventLoop) {}
185+
186+
fn window_event(
187+
&mut self,
188+
_event_loop: &dyn ActiveEventLoop,
189+
_window_id: WindowId,
190+
_event: WindowEvent,
191+
) {
192+
}
193+
}
194+
195+
#[test]
196+
fn handle_with_result_returns_value() {
197+
let handler = EventHandler::new();
198+
handler.set(Box::new(DummyApp), || {
199+
let result = handler.handle_with_result(|_app| 42);
200+
assert_eq!(result, Some(42));
201+
});
202+
}
203+
204+
#[test]
205+
fn handle_with_result_returns_none_when_not_set() {
206+
let handler = EventHandler::new();
207+
let result = handler.handle_with_result(|_app| 42);
208+
assert_eq!(result, None);
209+
}
210+
211+
#[test]
212+
fn handle_with_result_returns_none_when_in_use() {
213+
let handler = EventHandler::new();
214+
handler.set(Box::new(DummyApp), || {
215+
// Borrow the handler via `handle`, then try `handle_with_result`
216+
// from within — simulating re-entrancy.
217+
handler.handle(|_app| {
218+
let result = handler.handle_with_result(|_app| 42);
219+
assert_eq!(result, None);
220+
});
221+
});
222+
}
223+
224+
#[test]
225+
fn handle_with_result_returns_none_when_reentrant_through_self() {
226+
let handler = EventHandler::new();
227+
handler.set(Box::new(DummyApp), || {
228+
let result = handler.handle_with_result(|_app| {
229+
// Re-entrant call through handle_with_result itself.
230+
handler.handle_with_result(|_app| 42)
231+
});
232+
assert_eq!(result, Some(None));
233+
});
234+
}
235+
236+
#[test]
237+
#[should_panic(expected = "tried to handle event while another event is currently being handled")]
238+
fn handle_panics_on_reentrant_call() {
239+
let handler = EventHandler::new();
240+
handler.set(Box::new(DummyApp), || {
241+
handler.handle(|_app| {
242+
// Re-entrant handle must still panic after the refactoring.
243+
handler.handle(|_app| {});
244+
});
245+
});
246+
}
247+
}

winit-core/src/application/macos.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use dpi::PhysicalPosition;
2+
13
use crate::application::ApplicationHandler;
24
use crate::event_loop::ActiveEventLoop;
35
use crate::window::WindowId;
@@ -49,4 +51,30 @@ pub trait ApplicationHandlerExtMacOS: ApplicationHandler {
4951
let _ = window_id;
5052
let _ = action;
5153
}
54+
55+
/// Called when the user clicks on an inactive window to determine whether the click should
56+
/// also be processed as a normal mouse event.
57+
///
58+
/// This corresponds to the [`acceptsFirstMouse:`] method on `NSView`, which receives the
59+
/// triggering mouse event. Winit extracts the click position from that event and passes it
60+
/// here so that the application can make per-click decisions, e.g. accept first mouse for
61+
/// low-risk actions (selection, scrolling) but reject it for buttons or destructive actions.
62+
///
63+
/// The default implementation returns `true`.
64+
///
65+
/// If this method cannot be called synchronously (e.g. the handler is already in use), the
66+
/// static `accepts_first_mouse` value from
67+
/// [`WindowAttributes`][crate::window::WindowAttributes] is used as a fallback.
68+
///
69+
/// [`acceptsFirstMouse:`]: https://developer.apple.com/documentation/appkit/nsview/acceptsfirstmouse(_:)
70+
#[doc(alias = "acceptsFirstMouse:")]
71+
fn accepts_first_mouse(
72+
&mut self,
73+
event_loop: &dyn ActiveEventLoop,
74+
window_id: WindowId,
75+
position: PhysicalPosition<f64>,
76+
) -> bool {
77+
let _ = (event_loop, window_id, position);
78+
true
79+
}
5280
}

winit/src/changelog/unreleased.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ changelog entry.
4444

4545
- Add `keyboard` support for OpenHarmony.
4646
- On iOS, add Apple Pencil support with force, altitude, and azimuth data.
47+
- On macOS, add `ApplicationHandlerExtMacOS::accepts_first_mouse` for dynamic per-click decisions.
4748

4849
### Changed
4950

0 commit comments

Comments
 (0)