mirror of
https://github.com/rust-windowing/winit.git
synced 2026-08-29 04:40:04 -04:00
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:
committed by
Simon Hausmann
parent
92b643c3f1
commit
6a8a334426
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -460,6 +460,7 @@ impl EventLoop {
|
||||
state,
|
||||
position: event_state.mouse_pos.into(),
|
||||
button: button.into(),
|
||||
is_macos_activation_click: false,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -187,6 +187,7 @@ impl PointerHandler for WinitState {
|
||||
state,
|
||||
position,
|
||||
button,
|
||||
is_macos_activation_click: false,
|
||||
},
|
||||
window_id,
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -235,6 +235,7 @@ impl Dispatch2<ZwpTabletToolV2, WinitState> for TabletToolData {
|
||||
button,
|
||||
data: data.tool_state.clone(),
|
||||
},
|
||||
is_macos_activation_click: false,
|
||||
}
|
||||
},
|
||||
TabletEvent::Left => WindowEvent::PointerLeft {
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
})));
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user