Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support handling any event on macOS #2141

Closed
Closed
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
76 changes: 76 additions & 0 deletions examples/native_events.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
#[cfg(target_os = "macos")]
fn main() {
use simple_logger::SimpleLogger;
use winit::{
event::{Event, WindowEvent},
event_loop::{ControlFlow, EventLoop},
platform::macos::{
objc::{sel, sel_impl},
EventLoopExtMacOS,
},
window::WindowBuilder,
};
SimpleLogger::new().init().unwrap();
let mut event_loop = EventLoop::new();

unsafe {
// ------------------------------------------------------------------
// It's allowed to register multiple callbacks for the same selector
// All of them are called in the order they were registered
event_loop
.add_application_method(
sel!(applicationDidChangeOcclusionState:),
Box::new(|_notification: *mut objc::runtime::Object| {
println!("First callback: The occlusion state has changed!");
}) as Box<dyn Fn(_)>,
)
.unwrap();
event_loop
.add_application_method(
sel!(applicationDidChangeOcclusionState:),
Box::new(|_notification: *mut objc::runtime::Object| {
println!("SECOND callback: The occlusion state has changed!");
}) as Box<dyn Fn(_)>,
)
.unwrap();
// ------------------------------------------------------------------
// It's also valid to register a callback for something
// that winit already has a callback for
// (both of them are called in this case)
event_loop
.add_application_method(
sel!(applicationDidFinishLaunching:),
Box::new(|_: *mut objc::runtime::Object| {
println!("User callback: applicationDidFinishLaunching");
}) as Box<dyn Fn(_)>,
)
.unwrap();
}

let window = WindowBuilder::new()
.with_title("A fantastic window!")
.with_inner_size(winit::dpi::LogicalSize::new(128.0, 128.0))
.build(&event_loop)
.unwrap();

event_loop.run(move |event, _, control_flow| {
*control_flow = ControlFlow::Wait;
// println!("{:?}", event);

match event {
Event::WindowEvent {
event: WindowEvent::CloseRequested,
window_id,
} if window_id == window.id() => *control_flow = ControlFlow::Exit,
Event::MainEventsCleared => {
window.request_redraw();
}
_ => (),
}
});
}

#[cfg(not(target_os = "macos"))]
fn main() {
println!("There's currently no example for how to register handlers for native events on this platform");
}
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ extern crate serde;
extern crate bitflags;
#[cfg(any(target_os = "macos", target_os = "ios"))]
#[macro_use]
extern crate objc;
pub extern crate objc;

pub mod dpi;
#[macro_use]
Expand Down
141 changes: 139 additions & 2 deletions src/platform/macos.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
#![cfg(target_os = "macos")]

use std::os::raw::c_void;
use std::{collections::hash_map::Entry, os::raw::c_void};

use cocoa::appkit::NSApp;
pub use objc;
use objc::{
msg_send,
rc::autoreleasepool,
runtime::{Object, Sel},
Encode,
};

use crate::{
dpi::LogicalSize,
event_loop::{EventLoop, EventLoopWindowTarget},
monitor::MonitorHandle,
platform_impl::get_aux_state_mut,
platform_impl::{
create_delegate_class, get_aux_state_mut, get_aux_state_ref,
EventLoop as PlatformEventLoop, IdRef, BASE_APP_DELEGATE_METHODS,
},
window::{Window, WindowBuilder},
};

Expand Down Expand Up @@ -179,6 +191,112 @@ impl WindowBuilderExtMacOS for WindowBuilder {
}
}

pub trait DelegateMethod {
fn register_method<T>(self, sel: Sel, el: &mut PlatformEventLoop<T>) -> Result<(), String>;
Copy link
Member

Choose a reason for hiding this comment

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

This should probably be __-namespaced and #[doc(hidden)]

}
macro_rules! impl_delegate_method {
($($p:ident: $t:ident),*) => {
// method_decl_impl!(-T, R, extern fn(&T, Sel $(, $t)*) -> R, $($t),*);
impl<$($t, )* R> DelegateMethod for Box<dyn Fn($($t, )*) -> R + 'static>
where
$($t: Clone + Encode + 'static, )*
R: Encode + 'static
{
fn register_method<T>(self, sel: Sel, el: &mut PlatformEventLoop<T>) -> Result<(), String> {

// -------------------------------------------------------------------------
// HANDLER
// Allowing non-snake-case because we use the typename in the parameter name
// `param_$t`
#[allow(non_snake_case)]
extern "C" fn method_handler<$($t, )* R>(this: &Object, sel: Sel, $($p: $t, )*) -> R
where
$($t: Clone + 'static, )*
R: 'static,
{
// Let's call the base winit handler first.
{
let guard = BASE_APP_DELEGATE_METHODS.read().unwrap();
if let Some(base_method) = guard.get(sel.name()) {
unsafe {
let base_method = std::mem::transmute::<
unsafe extern fn(),
extern fn(&Object, Sel, $($t, )*) -> R
>(*base_method);
base_method(this, sel, $($p.clone(), )*);
}
}
}
let mut retval: Option<R> = None;
let aux = unsafe { get_aux_state_ref(this) };
if let Some(callbacks) = aux.user_methods.get(sel.name()) {
// The `methods` is a `Vec<Box<Box<Fn(...)>>>`
for cb in callbacks.iter() {
// Could this be done with fewer indirections?
if let Some(cb) = cb.downcast_ref::<Box<dyn Fn($($t, )*) -> R>>() {
let v = (cb)($($p.clone(), )*);
if retval.is_none() {
retval = Some(v);
}
} else {
warn!("Failed to downcast closure when handling {}", sel.name());
}
}
}
retval.expect(&format!(
"Couldn't get a return value during {:?}. This probably indicates that no appropriate callback was found", sel.name()
))
}
// -------------------------------------------------------------------------

let self_boxed = Box::new(self as Box<dyn Fn($($t, )*) -> R>);

// println!("created delegate class {}", delegate_class.name());
let mut delegate_state = unsafe {get_aux_state_mut(&mut **el.delegate)};
match delegate_state.user_methods.entry(sel.name().to_string()) {
Entry::Occupied(mut e) => {
e.get_mut().push(self_boxed);
}
Entry::Vacant(e) => {
e.insert(vec![self_boxed]);

// This user method doesn't have a defined callback in the app delegate class yet,
// so let's create a new class for this method
unsafe {
let prev_delegate_class = (**el.delegate).class();
let mut decl = create_delegate_class(prev_delegate_class);
decl.add_method(
// sel!(application:openFiles:),
sel,
method_handler::<$($t, )* R> as extern "C" fn(&Object, Sel, $($t, )*) -> R,
);
let delegate_class = decl.register();
let new_delegate = IdRef::new(msg_send![delegate_class, new]);
let mut new_state = get_aux_state_mut(&mut **new_delegate);
std::mem::swap(&mut *new_state, &mut *delegate_state);
let app = NSApp();
autoreleasepool(|| {
let _: () = msg_send![app, setDelegate:*new_delegate];
});
el.delegate = new_delegate;
}
}
}
Ok(())
}
}
}
}
impl_delegate_method!();
impl_delegate_method!(a: A);
impl_delegate_method!(a: A, b: B);
impl_delegate_method!(a: A, b: B, c: C);
impl_delegate_method!(a: A, b: B, c: C, d: D);
impl_delegate_method!(a: A, b: B, c: C, d: D, e: E);
impl_delegate_method!(a: A, b: B, c: C, d: D, e: E, f: F);
impl_delegate_method!(a: A, b: B, c: C, d: D, e: E, f: F, g: G);
impl_delegate_method!(a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H);

pub trait EventLoopExtMacOS {
/// Sets the activation policy for the application. It is set to
/// `NSApplicationActivationPolicyRegular` by default.
Expand All @@ -195,6 +313,17 @@ pub trait EventLoopExtMacOS {
/// [`run`](crate::event_loop::EventLoop::run) or
/// [`run_return`](crate::platform::run_return::EventLoopExtRunReturn::run_return)
fn enable_default_menu_creation(&mut self, enable: bool);

/// Adds a new callback method for the application delegate.
///
/// ### Safety
/// As the underlying `add_method` documentation writes:
/// > Unsafe because the caller must ensure that the types match those that are expected when the method is invoked from Objective-C.
unsafe fn add_application_method<F: DelegateMethod>(
&mut self,
sel: Sel,
Copy link
Member

Choose a reason for hiding this comment

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

Let's not expose objc as a public dependency; use sel: *const c_void instead.

It may be updated, and then users would have to always use the same version that winit uses. Also, it would now be a breaking change to update our version of objc (or swap it out with something else like my fork, though I realise that's an argument that mostly benefits me 😁).

method: F,
) -> Result<(), String>;
}
impl<T> EventLoopExtMacOS for EventLoop<T> {
#[inline]
Expand All @@ -210,6 +339,14 @@ impl<T> EventLoopExtMacOS for EventLoop<T> {
get_aux_state_mut(&**self.event_loop.delegate).create_default_menu = enable;
}
}

unsafe fn add_application_method<F: DelegateMethod>(
&mut self,
sel: Sel,
method: F,
) -> Result<(), String> {
method.register_method(sel, &mut self.event_loop)
}
}

/// Additional methods on `MonitorHandle` that are specific to MacOS.
Expand Down
64 changes: 58 additions & 6 deletions src/platform_impl/macos/app_delegate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,30 +6,65 @@ use objc::{
runtime::{Class, Object, Sel},
};
use std::{
cell::{RefCell, RefMut},
any::Any,
cell::{Ref, RefCell, RefMut},
collections::HashMap,
os::raw::c_void,
sync::{
atomic::{AtomicUsize, Ordering},
RwLock,
},
};

static AUX_DELEGATE_STATE_NAME: &str = "auxState";

#[derive(Default)]
pub struct AuxDelegateState {
/// We store this value in order to be able to defer setting the activation policy until
/// after the app has finished launching. If the activation policy is set earlier, the
/// menubar is initially unresponsive on macOS 10.15 for example.
pub activation_policy: ActivationPolicy,

pub create_default_menu: bool,

// pub winit_methods: HashMap<String, objc::runtime::Imp>,
/// Contains the closures that the application added on top of the ones
/// that winit already had.
///
/// Each key is the name of the selector
/// Each value is a vec of colsures that handle the callback
/// The first callback in the vec was registered first
pub user_methods: HashMap<String, Vec<Box<dyn Any>>>,
}

pub struct AppDelegateClass(pub *const Class);
unsafe impl Send for AppDelegateClass {}
unsafe impl Sync for AppDelegateClass {}

lazy_static! {
pub static ref APP_DELEGATE_CLASS: AppDelegateClass = unsafe {
let superclass = class!(NSResponder);
let mut decl = ClassDecl::new("WinitAppDelegate", superclass).unwrap();
/// Contains the function pointers to the event
/// listener methods defined by winit on the application
/// delegate
///
/// Each key is the name of a selector
/// Each value is the corresponding function pointer
pub static ref BASE_APP_DELEGATE_METHODS: RwLock<HashMap<String, objc::runtime::Imp>> = {
Default::default()
};
}

pub fn create_delegate_class(superclass: &Class) -> ClassDecl {
static CLASS_SEQ_NUM: AtomicUsize = AtomicUsize::new(0);
let curr_seq_num = CLASS_SEQ_NUM.fetch_add(1, Ordering::Relaxed);
// let superclass = class!(NSResponder);
let class_name = format!("WinitAppDelegate{}", curr_seq_num);
let decl = ClassDecl::new(&class_name, superclass).unwrap();
decl
}

pub fn create_base_app_delegate_class() -> *const Class {
let mut decl = create_delegate_class(class!(NSResponder));
let result = unsafe {
decl.add_class_method(sel!(new), new as extern "C" fn(&Class, Sel) -> id);
decl.add_method(sel!(dealloc), dealloc as extern "C" fn(&Object, Sel));

Expand All @@ -38,9 +73,18 @@ lazy_static! {
did_finish_launching as extern "C" fn(&Object, Sel, id),
);
decl.add_ivar::<*mut c_void>(AUX_DELEGATE_STATE_NAME);

AppDelegateClass(decl.register())
decl.register()
};

let methods: HashMap<_, _> = result
.instance_methods()
.iter()
.map(|x| (x.name().name().to_owned(), x.implementation()))
.collect();
let mut methods_guard = BASE_APP_DELEGATE_METHODS.write().unwrap();
*methods_guard = methods;

result
}

/// Safety: Assumes that Object is an instance of APP_DELEGATE_CLASS
Expand All @@ -50,6 +94,13 @@ pub unsafe fn get_aux_state_mut(this: &Object) -> RefMut<'_, AuxDelegateState> {
(*(ptr as *mut RefCell<AuxDelegateState>)).borrow_mut()
}

/// Safety: Assumes that Object is an instance of APP_DELEGATE_CLASS
pub unsafe fn get_aux_state_ref(this: &Object) -> Ref<'_, AuxDelegateState> {
let ptr: *mut c_void = *this.get_ivar(AUX_DELEGATE_STATE_NAME);
// Watch out that this needs to be the correct type
(*(ptr as *mut RefCell<AuxDelegateState>)).borrow()
}

extern "C" fn new(class: &Class, _: Sel) -> id {
unsafe {
let this: id = msg_send![class, alloc];
Expand All @@ -59,6 +110,7 @@ extern "C" fn new(class: &Class, _: Sel) -> id {
Box::into_raw(Box::new(RefCell::new(AuxDelegateState {
activation_policy: ActivationPolicy::Regular,
create_default_menu: true,
user_methods: HashMap::default(),
}))) as *mut c_void,
);
this
Expand Down
7 changes: 5 additions & 2 deletions src/platform_impl/macos/event_loop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ use crate::{
monitor::MonitorHandle as RootMonitorHandle,
platform_impl::platform::{
app::APP_CLASS,
app_delegate::APP_DELEGATE_CLASS,
app_delegate::create_base_app_delegate_class,
app_state::AppState,
monitor::{self, MonitorHandle},
observer::*,
Expand Down Expand Up @@ -127,7 +127,10 @@ impl<T> EventLoop<T> {
// be marked as main.
let app: id = msg_send![APP_CLASS.0, sharedApplication];

let delegate = IdRef::new(msg_send![APP_DELEGATE_CLASS.0, new]);
let app_delegate_class = create_base_app_delegate_class();

let delegate = IdRef::new(msg_send![app_delegate_class, new]);

autoreleasepool(|| {
let _: () = msg_send![app, setDelegate:*delegate];
});
Expand Down
Loading