From 6a8a3344269ce0a69ae3fa88645dd4d4e7f360c3 Mon Sep 17 00:00:00 2001 From: Till Adam Date: Thu, 18 Jun 2026 22:39:35 +0200 Subject: [PATCH] macOS: add `WindowEvent::PointerButton::is_macos_activation_click` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- winit-android/src/event_loop.rs | 2 ++ winit-appkit/src/lib.rs | 9 ------ winit-appkit/src/view.rs | 32 ++++++++++++++++--- winit-appkit/src/window_delegate.rs | 7 +--- winit-core/src/event.rs | 21 ++++++++++++ winit-orbital/src/event_loop.rs | 1 + winit-uikit/src/view.rs | 2 ++ winit-wayland/src/seat/pointer/mod.rs | 1 + winit-wayland/src/seat/touch/mod.rs | 2 ++ winit-wayland/src/types/wp_tablet_input_v2.rs | 1 + winit-web/src/event_loop/window_target.rs | 3 ++ winit-win32/src/event_loop.rs | 8 +++++ winit-x11/src/event_processor.rs | 7 ++++ winit/examples/application.rs | 9 +++++- winit/src/changelog/unreleased.md | 11 +++++++ 15 files changed, 95 insertions(+), 21 deletions(-) diff --git a/winit-android/src/event_loop.rs b/winit-android/src/event_loop.rs index a106ab97f..430191535 100644 --- a/winit-android/src/event_loop.rs +++ b/winit-android/src/event_loop.rs @@ -383,6 +383,7 @@ impl EventLoop { android_activity::input::ToolType::Mouse => continue, _ => event::ButtonSource::Unknown(0), }, + is_macos_activation_click: false, }; app.window_event(&self.window_target, GLOBAL_WINDOW, event); }, @@ -426,6 +427,7 @@ impl EventLoop { android_activity::input::ToolType::Mouse => continue, _ => event::ButtonSource::Unknown(0), }, + is_macos_activation_click: false, }; app.window_event(&self.window_target, GLOBAL_WINDOW, event); } diff --git a/winit-appkit/src/lib.rs b/winit-appkit/src/lib.rs index 3ff9c82f4..3d3df653a 100644 --- a/winit-appkit/src/lib.rs +++ b/winit-appkit/src/lib.rs @@ -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, 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. /// /// @@ -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, diff --git a/winit-appkit/src/view.rs b/winit-appkit/src/view.rs index d461e77df..74cd8c2e4 100644 --- a/winit-appkit/src/view.rs +++ b/winit-appkit/src/view.rs @@ -138,7 +138,10 @@ pub struct ViewState { /// to the application, even during IME forward_key_to_app: Cell, marked_text: RefCell>, - 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, /// The state of the `Option` as `Alt`. option_as_alt: Cell, @@ -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, - accepts_first_mouse: bool, option_as_alt: OptionAsAlt, mtm: MainThreadMarker, ) -> Retained { @@ -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 = 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, }); } diff --git a/winit-appkit/src/window_delegate.rs b/winit-appkit/src/window_delegate.rs index 8bc14e3df..f28d34b97 100644 --- a/winit-appkit/src/window_delegate.rs +++ b/winit-appkit/src/window_delegate.rs @@ -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 diff --git a/winit-core/src/event.rs b/winit-core/src/event.rs index 045a40a77..8780d3bda 100644 --- a/winit-core/src/event.rs +++ b/winit-core/src/event.rs @@ -329,6 +329,25 @@ pub enum WindowEvent { primary: bool, button: ButtonSource, + + /// Whether this event is part of the click that activated an otherwise inactive window. + /// + /// On macOS, AppKit normally consumes the click that brings a window forward without + /// delivering it as a regular mouse event (controlled by [`acceptsFirstMouse:`]). Winit + /// always delivers it, but tags both the activating press *and* its matching release + /// with this flag so applications can short-circuit the whole gesture with a single + /// check — e.g. ignore activation clicks for destructive or button-like targets while + /// accepting them for low-risk actions (selection, scrolling). + /// + /// ## Platform-specific + /// + /// - **Only available on macOS.** Always `false` on every other platform. + /// - Only ever `true` for the left mouse button. Intervening drag motion (delivered as + /// [`WindowEvent::PointerMoved`]) is not tagged; applications that care about drags + /// during the activation gesture must track that state themselves. + /// + /// [`acceptsFirstMouse:`]: https://developer.apple.com/documentation/appkit/nsview/acceptsfirstmouse(_:) + is_macos_activation_click: bool, }, /// Multi-finger hold gesture on the touchpad or touchscreen without movement. @@ -1689,6 +1708,7 @@ mod tests { state: event::ElementState::Pressed, position: (0, 0).into(), button: event::ButtonSource::Unknown(0), + is_macos_activation_click: false, }); with_window_event(PointerButton { device_id: None, @@ -1699,6 +1719,7 @@ mod tests { finger_id: fid, force: Some(event::Force::Normalized(0.0)), }, + is_macos_activation_click: false, }); with_window_event(PinchGesture { device_id: None, diff --git a/winit-orbital/src/event_loop.rs b/winit-orbital/src/event_loop.rs index 2175df706..2c1aa43c1 100644 --- a/winit-orbital/src/event_loop.rs +++ b/winit-orbital/src/event_loop.rs @@ -460,6 +460,7 @@ impl EventLoop { state, position: event_state.mouse_pos.into(), button: button.into(), + is_macos_activation_click: false, }); } }, diff --git a/winit-uikit/src/view.rs b/winit-uikit/src/view.rs index 19cbeacf6..dd1e3d76c 100644 --- a/winit-uikit/src/view.rs +++ b/winit-uikit/src/view.rs @@ -571,6 +571,7 @@ impl WinitView { } else { ButtonSource::Touch { finger_id, force } }, + is_macos_activation_click: false, }, }); }, @@ -629,6 +630,7 @@ impl WinitView { } else { ButtonSource::Touch { finger_id, force } }, + is_macos_activation_click: false, }, }); } diff --git a/winit-wayland/src/seat/pointer/mod.rs b/winit-wayland/src/seat/pointer/mod.rs index d683d449f..e4e113e38 100644 --- a/winit-wayland/src/seat/pointer/mod.rs +++ b/winit-wayland/src/seat/pointer/mod.rs @@ -187,6 +187,7 @@ impl PointerHandler for WinitState { state, position, button, + is_macos_activation_click: false, }, window_id, ); diff --git a/winit-wayland/src/seat/touch/mod.rs b/winit-wayland/src/seat/touch/mod.rs index 82b48db66..e7b3b0456 100644 --- a/winit-wayland/src/seat/touch/mod.rs +++ b/winit-wayland/src/seat/touch/mod.rs @@ -67,6 +67,7 @@ impl TouchHandler for WinitState { state: ElementState::Pressed, position, button: ButtonSource::Touch { finger_id, force: None }, + is_macos_activation_click: false, }, window_id, ); @@ -120,6 +121,7 @@ impl TouchHandler for WinitState { state: ElementState::Released, position, button: ButtonSource::Touch { finger_id, force: None }, + is_macos_activation_click: false, }, window_id, ); diff --git a/winit-wayland/src/types/wp_tablet_input_v2.rs b/winit-wayland/src/types/wp_tablet_input_v2.rs index 7da798c39..1b02ddbd3 100644 --- a/winit-wayland/src/types/wp_tablet_input_v2.rs +++ b/winit-wayland/src/types/wp_tablet_input_v2.rs @@ -235,6 +235,7 @@ impl Dispatch2 for TabletToolData { button, data: data.tool_state.clone(), }, + is_macos_activation_click: false, } }, TabletEvent::Left => WindowEvent::PointerLeft { diff --git a/winit-web/src/event_loop/window_target.rs b/winit-web/src/event_loop/window_target.rs index dcfce3bb7..8db54caf1 100644 --- a/winit-web/src/event_loop/window_target.rs +++ b/winit-web/src/event_loop/window_target.rs @@ -296,6 +296,7 @@ impl ActiveEventLoop { state, position, button, + is_macos_activation_click: false, }, }])); } @@ -323,6 +324,7 @@ impl ActiveEventLoop { state: ElementState::Pressed, position, button, + is_macos_activation_click: false, }, }))); } @@ -351,6 +353,7 @@ impl ActiveEventLoop { state: ElementState::Released, position, button, + is_macos_activation_click: false, }, }))); } diff --git a/winit-win32/src/event_loop.rs b/winit-win32/src/event_loop.rs index daa8717c5..af9265b92 100644 --- a/winit-win32/src/event_loop.rs +++ b/winit-win32/src/event_loop.rs @@ -1873,6 +1873,7 @@ unsafe fn public_window_callback_inner( _ => unreachable!(), } .into(), + is_macos_activation_click: false, }); result = ProcResult::Value(0); }, @@ -1902,6 +1903,7 @@ unsafe fn public_window_callback_inner( _ => unreachable!(), } .into(), + is_macos_activation_click: false, }); result = ProcResult::Value(0); }, @@ -1930,6 +1932,7 @@ unsafe fn public_window_callback_inner( position, // 1 is defined as back, 2 as forward; other codes are unexpected. button: MouseButton::try_from_u8(b).unwrap().into(), + is_macos_activation_click: false, }); result = ProcResult::Value(0); }, @@ -1959,6 +1962,7 @@ unsafe fn public_window_callback_inner( position, // 1 is defined as back, 2 as forward; other codes are unexpected. button: MouseButton::try_from_u8(b).unwrap().into(), + is_macos_activation_click: false, }); result = ProcResult::Value(0); }, @@ -2018,6 +2022,7 @@ unsafe fn public_window_callback_inner( state: Pressed, position, button: Touch { finger_id, force: None }, + is_macos_activation_click: false, }); } else if util::has_flag(input.dwFlags, TOUCHEVENTF_UP) { userdata.send_window_event(window, WindowEvent::PointerButton { @@ -2026,6 +2031,7 @@ unsafe fn public_window_callback_inner( state: Released, position, button: Touch { finger_id, force: None }, + is_macos_activation_click: false, }); userdata.send_window_event(window, WindowEvent::PointerLeft { device_id: None, @@ -2187,6 +2193,7 @@ unsafe fn public_window_callback_inner( state: Pressed, position, button, + is_macos_activation_click: false, }); } else { userdata.send_window_event(window, WindowEvent::PointerButton { @@ -2195,6 +2202,7 @@ unsafe fn public_window_callback_inner( state: Released, position, button, + is_macos_activation_click: false, }); userdata.send_window_event(window, WindowEvent::PointerLeft { device_id: None, diff --git a/winit-x11/src/event_processor.rs b/winit-x11/src/event_processor.rs index 425c904a4..494bb615d 100644 --- a/winit-x11/src/event_processor.rs +++ b/winit-x11/src/event_processor.rs @@ -1108,6 +1108,7 @@ impl EventProcessor { state, position, button: MouseButton::Left.into(), + is_macos_activation_click: false, }, xlib::Button2 => WindowEvent::PointerButton { device_id, @@ -1115,6 +1116,7 @@ impl EventProcessor { state, position, button: MouseButton::Middle.into(), + is_macos_activation_click: false, }, xlib::Button3 => WindowEvent::PointerButton { device_id, @@ -1122,6 +1124,7 @@ impl EventProcessor { state, position, button: MouseButton::Right.into(), + is_macos_activation_click: false, }, // Suppress emulated scroll wheel clicks, since we handle the real motion events for @@ -1151,6 +1154,7 @@ impl EventProcessor { // Button 8 maps to MouseButton::BACK = 3; 36 maps to MouseButton::Button32. // 255 is the largest code yielded on X11 (tested). button: MouseButton::try_from_u8((x - 5) as u8).unwrap().into(), + is_macos_activation_click: false, }, x @ 37..=0xff => WindowEvent::PointerButton { device_id, @@ -1159,6 +1163,7 @@ impl EventProcessor { position, // 255 is the largest code yielded on X11 (tested). button: ButtonSource::Unknown(x as u16), + is_macos_activation_click: false, }, _ => return, }; @@ -1450,6 +1455,7 @@ impl EventProcessor { state: ElementState::Pressed, position, button: ButtonSource::Touch { finger_id, force: None }, + is_macos_activation_click: false, }; app.window_event(&self.target, window_id, event); }, @@ -1469,6 +1475,7 @@ impl EventProcessor { state: ElementState::Released, position, button: ButtonSource::Touch { finger_id, force: None }, + is_macos_activation_click: false, }; app.window_event(&self.target, window_id, event); let event = WindowEvent::PointerLeft { diff --git a/winit/examples/application.rs b/winit/examples/application.rs index 33c942e91..0f213940b 100644 --- a/winit/examples/application.rs +++ b/winit/examples/application.rs @@ -474,8 +474,15 @@ impl ApplicationHandler for Application { } } }, - WindowEvent::PointerButton { button, state, .. } => { + WindowEvent::PointerButton { button, state, is_macos_activation_click, .. } => { info!("Pointer button {button:?} {state:?}"); + // On macOS, drop both press and release of the click that activated this + // window — real apps would typically skip destructive or button-target + // actions for such clicks; this example just logs them. + if is_macos_activation_click { + info!("(macOS activation click — ignoring)"); + return; + } let mods = window.modifiers; if let Some(action) = state .is_pressed() diff --git a/winit/src/changelog/unreleased.md b/winit/src/changelog/unreleased.md index e2f7326ea..3f64ded48 100644 --- a/winit/src/changelog/unreleased.md +++ b/winit/src/changelog/unreleased.md @@ -57,6 +57,10 @@ changelog entry. window to be shown on the same Space as a fullscreen window (`NSWindowCollectionBehaviorFullScreenAuxiliary`) instead of triggering a Space switch or Split View tiling. +- Add `WindowEvent::PointerButton::is_macos_activation_click`. On macOS, both the press and + matching release of a click that activated a previously inactive window are tagged, so + applications can ignore activation clicks for buttons or destructive actions while accepting + them for low-risk actions like selection or scrolling. Always `false` on other platforms. ### Changed @@ -64,6 +68,13 @@ changelog entry. - 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. - On macOS, using the private API `CGSSetWindowBackgroundBlurRadius` for `Window::set_blur` is now disabled by default. It can be re-enabled using the Cargo feature `private-apple-apis`. +### Removed + +- On macOS, remove `WindowAttributesMacOS::with_accepts_first_mouse`. Use the new per-event + `WindowEvent::PointerButton::is_macos_activation_click` flag instead. To preserve the old + `with_accepts_first_mouse(false)` behavior, ignore `PointerButton` press events (and their + matching releases / drags) where `is_macos_activation_click` is `true`. + ### Fixed - On Windows, fix a freeze that occurs when the keyboard layout is switched by