Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions winit-appkit/src/app_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<R>(
self: &Rc<Self>,
callback: impl FnOnce(&mut dyn ApplicationHandler, &ActiveEventLoop) -> R,
) -> Option<R> {
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<Self>,
Expand Down
33 changes: 29 additions & 4 deletions winit-appkit/src/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -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,
Expand Down Expand Up @@ -764,9 +764,34 @@ 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.
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)
}
}
);
Expand Down
120 changes: 107 additions & 13 deletions winit-common/src/event_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<R>(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's the right approach to the problem, but boy do I not like the fallback behavior when we're re-entrant, I wish we could do something different - but I don't see a good solution, AppKit is hella re-entrant and that's hard to avoid. Spitballing, maybe making ApplicationHandlerExtMacOS take &self, and do fn macos_handler(&self) -> Option<Box<dyn macos::ApplicationHandlerExtMacOS>> instead? That would push the re-entrancy to the user.

Anyhow, not something we need to fix in this PR, such changes can be made later when we better understand the requirements.

&self,
callback: impl FnOnce(&mut (dyn ApplicationHandler + '_)) -> R,
) -> Option<R> {
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");
},
Expand All @@ -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.
handler.handle(|_app| {});
});
});
}
}
34 changes: 34 additions & 0 deletions winit-core/src/application/macos.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use dpi::PhysicalPosition;

use crate::application::ApplicationHandler;
use crate::event_loop::ActiveEventLoop;
use crate::window::WindowId;
Expand Down Expand Up @@ -49,4 +51,36 @@ 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`.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, but that'd mean that by returning Some(self) in macos_handler, you overwrite WindowAttributesMacOS.accepts_first_mouse?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point — I've clarified the docs in 6cf8480 to make the precedence explicit: when macos_handler() returns Some, the dynamic callback takes precedence over the static WindowAttributesMacOS::with_accepts_first_mouse setting. The static value is only used as a fallback during re-entrancy.

I've also added guidance in 2cb9dce on when to use one vs the other: use the static setting when a per-window value is sufficient, use the dynamic callback when you need to decide based on click position.

///
/// 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.
/// the handler is already borrowed due to re-entrancy).
///
/// [`acceptsFirstMouse:`]: https://developer.apple.com/documentation/appkit/nsview/acceptsfirstmouse(_:)
#[doc(alias = "acceptsFirstMouse:")]
fn accepts_first_mouse(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add usage to the app example?

Or maybe just document when, as a user, you'd want to implement this vs. WindowAttributesMacOS.accepts_first_mouse.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 2cb9dce — added accepts_first_mouse to the application example and documented when you'd use the static setting vs the dynamic callback.

&mut self,
event_loop: &dyn ActiveEventLoop,
window_id: WindowId,
position: PhysicalPosition<f64>,
) -> bool {
let _ = (event_loop, window_id, position);
true
}
}
10 changes: 10 additions & 0 deletions winit/examples/application.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<f64>,
) -> bool {
info!(?window_id, ?position, "macOS accepts_first_mouse");
true
}
}

/// State of the window.
Expand Down
1 change: 1 addition & 0 deletions winit/src/changelog/unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
- On Wayland, added ext-background-effect-v1 support.

### Changed
Expand Down