macOS: add WindowEvent::PointerButton::is_macos_activation_click

Apps need per-click decisions to follow the macOS convention of accepting
first mouse for low-risk actions (selection, scrolling) but rejecting it
for buttons and destructive actions. Motivated by slint-ui/slint#10451.

Always return `true` from `acceptsFirstMouse:`, and tag the resulting
`PointerButton` events with `is_macos_activation_click: true` (on both
the activating left press and its matching release) so the app can
short-circuit the whole gesture with a single check:

    WindowEvent::PointerButton { is_macos_activation_click: true, .. } => return,

Replaces an earlier callback-based design (`accepts_first_mouse` on
`ApplicationHandlerExtMacOS`) that mapped more closely to AppKit but
didn't fit winit's event-driven model and required re-entrancy handling.

Also removes `WindowAttributesMacOS::with_accepts_first_mouse`, which is
now redundant — apps that want to reject activation clicks check the
flag on the per-event instead.
This commit is contained in:
Till Adam
2026-06-18 22:39:35 +02:00
committed by Simon Hausmann
parent 92b643c3f1
commit 6a8a334426
15 changed files with 95 additions and 21 deletions

View File

@@ -367,7 +367,6 @@ pub struct WindowAttributesMacOS {
pub(crate) fullsize_content_view: bool,
pub(crate) disallow_hidpi: bool,
pub(crate) has_shadow: bool,
pub(crate) accepts_first_mouse: bool,
pub(crate) tabbing_identifier: Option<String>,
pub(crate) option_as_alt: OptionAsAlt,
pub(crate) borderless_game: bool,
@@ -431,13 +430,6 @@ impl WindowAttributesMacOS {
self
}
/// Window accepts click-through mouse events.
#[inline]
pub fn with_accepts_first_mouse(mut self, accepts_first_mouse: bool) -> Self {
self.accepts_first_mouse = accepts_first_mouse;
self
}
/// Defines the window tabbing identifier.
///
/// <https://developer.apple.com/documentation/appkit/nswindow/1644704-tabbingidentifier>
@@ -506,7 +498,6 @@ impl Default for WindowAttributesMacOS {
fullsize_content_view: false,
disallow_hidpi: false,
has_shadow: true,
accepts_first_mouse: true,
tabbing_identifier: None,
option_as_alt: Default::default(),
borderless_game: false,

View File

@@ -138,7 +138,10 @@ pub struct ViewState {
/// to the application, even during IME
forward_key_to_app: Cell<bool>,
marked_text: RefCell<Retained<NSMutableAttributedString>>,
accepts_first_mouse: bool,
/// Set by `acceptsFirstMouse:` so the next `mouseDown:` can tag its
/// [`WindowEvent::PointerButton`] with `is_macos_activation_click: true`.
next_click_is_activation: Cell<bool>,
/// The state of the `Option` as `Alt`.
option_as_alt: Cell<OptionAsAlt>,
@@ -780,9 +783,18 @@ 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
// Always accept the click. The following `mouseDown:` is tagged with
// `is_macos_activation_click: true` so the application can decide per click
// whether to act on it.
//
// AppKit may pass nil here for synthetic queries that are not tied to a
// specific click; only set the flag when we actually have an event.
if event.is_some() {
self.ivars().next_click_is_activation.set(true);
}
true
}
}
);
@@ -790,7 +802,6 @@ define_class!(
impl WinitView {
pub(super) fn new(
app_state: &Rc<AppState>,
accepts_first_mouse: bool,
option_as_alt: OptionAsAlt,
mtm: MainThreadMarker,
) -> Retained<Self> {
@@ -808,7 +819,7 @@ impl WinitView {
ime_capabilities: Default::default(),
forward_key_to_app: Default::default(),
marked_text: Default::default(),
accepts_first_mouse,
next_click_is_activation: Default::default(),
option_as_alt: Cell::new(option_as_alt),
});
let this: Retained<Self> = unsafe { msg_send![super(this), init] };
@@ -1155,12 +1166,23 @@ impl WinitView {
self.update_modifiers(event, false);
// `acceptsFirstMouse:` fires only for the left button, so the activation flag is
// applied to the left press and held until the matching left release. This lets
// apps short-circuit both events with a single `if is_macos_activation_click` check
// without having to track gesture state themselves.
let is_macos_activation_click = matches!(button, MouseButton::Left)
&& match button_state {
ElementState::Pressed => self.ivars().next_click_is_activation.get(),
ElementState::Released => self.ivars().next_click_is_activation.replace(false),
};
self.queue_event(WindowEvent::PointerButton {
device_id: None,
primary: true,
state: button_state,
position,
button: button.into(),
is_macos_activation_click,
});
}

View File

@@ -842,12 +842,7 @@ fn new_window(
window.center();
}
let view = WinitView::new(
app_state,
macos_attrs.accepts_first_mouse,
macos_attrs.option_as_alt,
mtm,
);
let view = WinitView::new(app_state, macos_attrs.option_as_alt, mtm);
// The default value of `setWantsBestResolutionOpenGLSurface:` was `false` until
// macos 10.14 and `true` after 10.15, we should set it to `YES` or `NO` to avoid