mirror of
https://github.com/rust-windowing/winit.git
synced 2026-09-02 23:00:05 -04:00
Swizzle sendEvent: instead of subclassing NSApplication
This is done to avoid order-dependent behavior that you'd otherwise encounter where `EventLoop::new` had to be called at the beginning of `fn main` to ensure that Winit's application was the one being registered as the main application by calling `sharedApplication`. Fixes https://github.com/rust-windowing/winit/issues/3772. This should also make it (more) possible to use multiple versions of Winit in the same application (though that's still untested). Finally, it should allow the user to override `NSApplication` themselves if they need to do that for some reason.
This commit is contained in:
committed by
Kirill Chibisov
parent
6556cde246
commit
53321dc6f5
@@ -46,6 +46,10 @@ changelog entry.
|
|||||||
- On Windows, add `CursorGrabMode::Locked`.
|
- On Windows, add `CursorGrabMode::Locked`.
|
||||||
- On Wayland, add `WindowExtWayland::xdg_toplevel`.
|
- On Wayland, add `WindowExtWayland::xdg_toplevel`.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- On macOS, no longer need control of the main `NSApplication` class (which means you can now override it yourself).
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- On Windows, fixed ~500 ms pause when clicking the title bar during continuous redraw.
|
- On Windows, fixed ~500 ms pause when clicking the title bar during continuous redraw.
|
||||||
|
|||||||
@@ -1,49 +1,104 @@
|
|||||||
#![allow(clippy::unnecessary_cast)]
|
#![allow(clippy::unnecessary_cast)]
|
||||||
|
#![allow(unknown_lints)] // New lint below
|
||||||
|
#![allow(static_mut_refs)] // Uses `MainThreadBound` in new version.
|
||||||
|
|
||||||
use objc2::{declare_class, msg_send, mutability, ClassType, DeclaredClass};
|
use std::cell::Cell;
|
||||||
use objc2_app_kit::{NSApplication, NSEvent, NSEventModifierFlags, NSEventType, NSResponder};
|
use std::mem;
|
||||||
use objc2_foundation::{MainThreadMarker, NSObject};
|
|
||||||
|
use objc2::runtime::{Imp, Sel};
|
||||||
|
use objc2::sel;
|
||||||
|
use objc2_app_kit::{NSApplication, NSEvent, NSEventModifierFlags, NSEventType};
|
||||||
|
use objc2_foundation::MainThreadMarker;
|
||||||
|
|
||||||
use super::app_state::ApplicationDelegate;
|
use super::app_state::ApplicationDelegate;
|
||||||
use crate::event::{DeviceEvent, ElementState};
|
use crate::event::{DeviceEvent, ElementState};
|
||||||
|
|
||||||
declare_class!(
|
type SendEvent = extern "C" fn(&NSApplication, Sel, &NSEvent);
|
||||||
pub(super) struct WinitApplication;
|
|
||||||
|
|
||||||
unsafe impl ClassType for WinitApplication {
|
// NOTE: Only used on the main thread. Ideally, we'd use `MainThreadBound`, but that isn't
|
||||||
#[inherits(NSResponder, NSObject)]
|
// constructible from `const` with this `objc2` version.
|
||||||
type Super = NSApplication;
|
static mut ORIGINAL: Cell<Option<SendEvent>> = Cell::new(None);
|
||||||
type Mutability = mutability::MainThreadOnly;
|
|
||||||
const NAME: &'static str = "WinitApplication";
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DeclaredClass for WinitApplication {}
|
extern "C" fn send_event(app: &NSApplication, sel: Sel, event: &NSEvent) {
|
||||||
|
let mtm = MainThreadMarker::from(app);
|
||||||
|
|
||||||
unsafe impl WinitApplication {
|
// Normally, holding Cmd + any key never sends us a `keyUp` event for that key.
|
||||||
// Normally, holding Cmd + any key never sends us a `keyUp` event for that key.
|
// Overriding `sendEvent:` fixes that. (https://stackoverflow.com/a/15294196)
|
||||||
// Overriding `sendEvent:` like this fixes that. (https://stackoverflow.com/a/15294196)
|
// Fun fact: Firefox still has this bug! (https://bugzilla.mozilla.org/show_bug.cgi?id=1299553)
|
||||||
// Fun fact: Firefox still has this bug! (https://bugzilla.mozilla.org/show_bug.cgi?id=1299553)
|
//
|
||||||
#[method(sendEvent:)]
|
// For posterity, there are some undocumented event types
|
||||||
fn send_event(&self, event: &NSEvent) {
|
// (https://github.com/servo/cocoa-rs/issues/155)
|
||||||
// For posterity, there are some undocumented event types
|
// but that doesn't really matter here.
|
||||||
// (https://github.com/servo/cocoa-rs/issues/155)
|
let event_type = unsafe { event.r#type() };
|
||||||
// but that doesn't really matter here.
|
let modifier_flags = unsafe { event.modifierFlags() };
|
||||||
let event_type = unsafe { event.r#type() };
|
if event_type == NSEventType::KeyUp
|
||||||
let modifier_flags = unsafe { event.modifierFlags() };
|
&& modifier_flags.contains(NSEventModifierFlags::NSEventModifierFlagCommand)
|
||||||
if event_type == NSEventType::KeyUp
|
{
|
||||||
&& modifier_flags.contains(NSEventModifierFlags::NSEventModifierFlagCommand)
|
if let Some(key_window) = app.keyWindow() {
|
||||||
{
|
key_window.sendEvent(event);
|
||||||
if let Some(key_window) = self.keyWindow() {
|
|
||||||
key_window.sendEvent(event);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let delegate = ApplicationDelegate::get(MainThreadMarker::from(self));
|
|
||||||
maybe_dispatch_device_event(&delegate, event);
|
|
||||||
unsafe { msg_send![super(self), sendEvent: event] }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
// Events are generally scoped to the window level, so the best way
|
||||||
|
// to get device events is to listen for them on NSApplication.
|
||||||
|
let delegate = ApplicationDelegate::get(mtm);
|
||||||
|
maybe_dispatch_device_event(&delegate, event);
|
||||||
|
|
||||||
|
let _ = mtm;
|
||||||
|
let original = unsafe { ORIGINAL.get().expect("no existing sendEvent: handler set") };
|
||||||
|
original(app, sel, event)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Override the [`sendEvent:`][NSApplication::sendEvent] method on the given application class.
|
||||||
|
///
|
||||||
|
/// The previous implementation created a subclass of [`NSApplication`], however we would like to
|
||||||
|
/// give the user full control over their `NSApplication`, so we override the method here using
|
||||||
|
/// method swizzling instead.
|
||||||
|
///
|
||||||
|
/// This _should_ also allow two versions of Winit to exist in the same application.
|
||||||
|
///
|
||||||
|
/// See the following links for more info on method swizzling:
|
||||||
|
/// - <https://nshipster.com/method-swizzling/>
|
||||||
|
/// - <https://spin.atomicobject.com/method-swizzling-objective-c/>
|
||||||
|
/// - <https://web.archive.org/web/20130308110627/http://cocoadev.com/wiki/MethodSwizzling>
|
||||||
|
///
|
||||||
|
/// NOTE: This function assumes that the passed in application object is the one returned from
|
||||||
|
/// [`NSApplication::sharedApplication`], i.e. the one and only global shared application object.
|
||||||
|
/// For testing though, we allow it to be a different object.
|
||||||
|
pub(crate) fn override_send_event(global_app: &NSApplication) {
|
||||||
|
let mtm = MainThreadMarker::from(global_app);
|
||||||
|
let class = global_app.class();
|
||||||
|
|
||||||
|
let method =
|
||||||
|
class.instance_method(sel!(sendEvent:)).expect("NSApplication must have sendEvent: method");
|
||||||
|
|
||||||
|
// SAFETY: Converting our `sendEvent:` implementation to an IMP.
|
||||||
|
let overridden = unsafe { mem::transmute::<SendEvent, Imp>(send_event) };
|
||||||
|
|
||||||
|
// If we've already overridden the method, don't do anything.
|
||||||
|
// FIXME(madsmtm): Use `std::ptr::fn_addr_eq` (Rust 1.85) once available in MSRV.
|
||||||
|
#[allow(unknown_lints, unpredictable_function_pointer_comparisons)]
|
||||||
|
if overridden == method.implementation() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// SAFETY: Our implementation has:
|
||||||
|
// 1. The same signature as `sendEvent:`.
|
||||||
|
// 2. Does not impose extra safety requirements on callers.
|
||||||
|
let original = unsafe { method.set_implementation(overridden) };
|
||||||
|
|
||||||
|
// SAFETY: This is the actual signature of `sendEvent:`.
|
||||||
|
let original = unsafe { mem::transmute::<Imp, SendEvent>(original) };
|
||||||
|
|
||||||
|
// NOTE: If NSApplication was safe to use from multiple threads, then this would potentially be
|
||||||
|
// a (checked) race-condition, since one could call `sendEvent:` before the original had been
|
||||||
|
// stored here.
|
||||||
|
//
|
||||||
|
// It is only usable from the main thread, however, so we're good!
|
||||||
|
let _ = mtm;
|
||||||
|
unsafe { ORIGINAL.set(Some(original)) };
|
||||||
|
}
|
||||||
|
|
||||||
fn maybe_dispatch_device_event(delegate: &ApplicationDelegate, event: &NSEvent) {
|
fn maybe_dispatch_device_event(delegate: &ApplicationDelegate, event: &NSEvent) {
|
||||||
let event_type = unsafe { event.r#type() };
|
let event_type = unsafe { event.r#type() };
|
||||||
@@ -85,3 +140,56 @@ fn maybe_dispatch_device_event(delegate: &ApplicationDelegate, event: &NSEvent)
|
|||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use objc2::rc::Retained;
|
||||||
|
use objc2::{declare_class, msg_send_id, mutability, ClassType, DeclaredClass};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_override() {
|
||||||
|
// FIXME(madsmtm): Ensure this always runs (maybe use cargo-nextest or `--test-threads=1`?)
|
||||||
|
let Some(mtm) = MainThreadMarker::new() else { return };
|
||||||
|
|
||||||
|
// Create a new application, without making it the shared application.
|
||||||
|
let app = unsafe { NSApplication::new(mtm) };
|
||||||
|
override_send_event(&app);
|
||||||
|
// Test calling twice works.
|
||||||
|
override_send_event(&app);
|
||||||
|
|
||||||
|
// FIXME(madsmtm): Can't test this yet, need some way to mock AppState.
|
||||||
|
// unsafe {
|
||||||
|
// let event = super::super::event::dummy_event().unwrap();
|
||||||
|
// app.sendEvent(&event)
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_custom_class() {
|
||||||
|
let Some(_mtm) = MainThreadMarker::new() else { return };
|
||||||
|
|
||||||
|
declare_class!(
|
||||||
|
struct TestApplication;
|
||||||
|
|
||||||
|
unsafe impl ClassType for TestApplication {
|
||||||
|
type Super = NSApplication;
|
||||||
|
type Mutability = mutability::MainThreadOnly;
|
||||||
|
const NAME: &'static str = "TestApplication";
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DeclaredClass for TestApplication {}
|
||||||
|
|
||||||
|
unsafe impl TestApplication {
|
||||||
|
#[method(sendEvent:)]
|
||||||
|
fn send_event(&self, _event: &NSEvent) {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let app: Retained<TestApplication> = unsafe { msg_send_id![TestApplication::class(), new] };
|
||||||
|
override_send_event(&app);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,11 +16,11 @@ use core_foundation::runloop::{
|
|||||||
};
|
};
|
||||||
use objc2::rc::{autoreleasepool, Retained};
|
use objc2::rc::{autoreleasepool, Retained};
|
||||||
use objc2::runtime::ProtocolObject;
|
use objc2::runtime::ProtocolObject;
|
||||||
use objc2::{msg_send_id, sel, ClassType};
|
use objc2::sel;
|
||||||
use objc2_app_kit::{NSApplication, NSApplicationActivationPolicy, NSWindow};
|
use objc2_app_kit::{NSApplication, NSApplicationActivationPolicy, NSWindow};
|
||||||
use objc2_foundation::{MainThreadMarker, NSObjectProtocol};
|
use objc2_foundation::{MainThreadMarker, NSObjectProtocol};
|
||||||
|
|
||||||
use super::app::WinitApplication;
|
use super::app::override_send_event;
|
||||||
use super::app_state::{ApplicationDelegate, HandlePendingUserEvents};
|
use super::app_state::{ApplicationDelegate, HandlePendingUserEvents};
|
||||||
use super::event::dummy_event;
|
use super::event::dummy_event;
|
||||||
use super::monitor::{self, MonitorHandle};
|
use super::monitor::{self, MonitorHandle};
|
||||||
@@ -220,15 +220,8 @@ impl<T> EventLoop<T> {
|
|||||||
let mtm = MainThreadMarker::new()
|
let mtm = MainThreadMarker::new()
|
||||||
.expect("on macOS, `EventLoop` must be created on the main thread!");
|
.expect("on macOS, `EventLoop` must be created on the main thread!");
|
||||||
|
|
||||||
let app: Retained<NSApplication> =
|
// Initialize the application (if it has not already been).
|
||||||
unsafe { msg_send_id![WinitApplication::class(), sharedApplication] };
|
let app = NSApplication::sharedApplication(mtm);
|
||||||
|
|
||||||
if !app.is_kind_of::<WinitApplication>() {
|
|
||||||
panic!(
|
|
||||||
"`winit` requires control over the principal class. You must create the event \
|
|
||||||
loop before other parts of your application initialize NSApplication"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let activation_policy = match attributes.activation_policy {
|
let activation_policy = match attributes.activation_policy {
|
||||||
None => None,
|
None => None,
|
||||||
@@ -247,6 +240,9 @@ impl<T> EventLoop<T> {
|
|||||||
app.setDelegate(Some(ProtocolObject::from_ref(&*delegate)));
|
app.setDelegate(Some(ProtocolObject::from_ref(&*delegate)));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Override `sendEvent:` on the application to forward to our application state.
|
||||||
|
override_send_event(&app);
|
||||||
|
|
||||||
let panic_info: Rc<PanicInfo> = Default::default();
|
let panic_info: Rc<PanicInfo> = Default::default();
|
||||||
setup_control_flow_observers(mtm, Rc::downgrade(&panic_info));
|
setup_control_flow_observers(mtm, Rc::downgrade(&panic_info));
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user