diff --git a/bindings/c/src/macos.rs b/bindings/c/src/macos.rs index 0e778562e..729ecce28 100644 --- a/bindings/c/src/macos.rs +++ b/bindings/c/src/macos.rs @@ -7,8 +7,14 @@ use crate::{ action_handler, box_from_ptr, ref_from_ptr, tree_update, tree_update_factory, BoxCastPtr, CastPtr, }; -use accesskit_macos::{Adapter, NSPoint, QueuedEvents, SubclassingAdapter}; -use std::{os::raw::c_void, ptr}; +use accesskit_macos::{ + add_focus_forwarder_to_window_class, Adapter, NSPoint, QueuedEvents, SubclassingAdapter, +}; +use std::{ + ffi::CStr, + os::raw::{c_char, c_void}, + ptr, +}; pub struct macos_queued_events { _private: [u8; 0], @@ -200,3 +206,25 @@ impl macos_subclassing_adapter { } } } + +/// Modifies the specified class, which must be a subclass of `NSWindow`, +/// to include an `accessibilityFocusedUIElement` method that calls +/// the corresponding method on the window's content view. This is needed +/// for windowing libraries such as SDL that place the keyboard focus +/// directly on the window rather than the content view. +/// +/// # Safety +/// +/// This function is declared unsafe because the caller must ensure that the +/// code for this library is never unloaded from the application process, +/// since it's not possible to reverse this operation. It's safest +/// if this library is statically linked into the application's main executable. +/// Also, this function assumes that the specified class is a subclass +/// of `NSWindow`. +#[no_mangle] +pub unsafe extern "C" fn accesskit_macos_add_focus_forwarder_to_window_class( + class_name: *const c_char, +) { + let class_name = unsafe { CStr::from_ptr(class_name).to_string_lossy() }; + add_focus_forwarder_to_window_class(&class_name); +} diff --git a/platforms/macos/src/lib.rs b/platforms/macos/src/lib.rs index 2a13e03d2..0debbc7ed 100644 --- a/platforms/macos/src/lib.rs +++ b/platforms/macos/src/lib.rs @@ -16,6 +16,9 @@ pub use adapter::Adapter; mod event; pub use event::QueuedEvents; +mod patch; +pub use patch::add_focus_forwarder_to_window_class; + mod subclass; pub use subclass::SubclassingAdapter; diff --git a/platforms/macos/src/patch.rs b/platforms/macos/src/patch.rs new file mode 100644 index 000000000..0a131ebd7 --- /dev/null +++ b/platforms/macos/src/patch.rs @@ -0,0 +1,92 @@ +// Copyright 2023 The AccessKit Authors. All rights reserved. +// Licensed under the Apache License, Version 2.0 (found in +// the LICENSE-APACHE file) or the MIT license (found in +// the LICENSE-MIT file), at your option. + +use objc2::{ + declare::MethodImplementation, + encode::{Encode, EncodeArguments, Encoding}, + ffi::class_addMethod, + msg_send, + runtime::{Bool, Class, Object, Sel}, + sel, Message, +}; +use std::{ffi::CString, ptr::null_mut}; + +use crate::appkit::NSWindow; + +extern "C" fn focus_forwarder(this: &NSWindow, _cmd: Sel) -> *mut Object { + this.content_view().map_or_else(null_mut, |view| unsafe { + msg_send![&*view, accessibilityFocusedUIElement] + }) +} + +/// Modifies the specified class, which must be a subclass of `NSWindow`, +/// to include an `accessibilityFocusedUIElement` method that calls +/// the corresponding method on the window's content view. This is needed +/// for windowing libraries such as SDL that place the keyboard focus +/// directly on the window rather than the content view. +/// +/// # Safety +/// +/// This function is declared unsafe because the caller must ensure that the +/// code for this crate is never unloaded from the application process, +/// since it's not possible to reverse this operation. It's safest +/// if this crate is statically linked into the application's main executable. +/// Also, this function assumes that the specified class is a subclass +/// of `NSWindow`. +pub unsafe fn add_focus_forwarder_to_window_class(class_name: &str) { + let class = Class::get(class_name).unwrap(); + unsafe { + add_method( + class as *const Class as *mut Class, + sel!(accessibilityFocusedUIElement), + focus_forwarder as unsafe extern "C" fn(_, _) -> _, + ) + }; +} + +// The rest of this file is copied from objc2 with only minor adaptations, +// to allow a method to be added to an existing class. + +unsafe fn add_method(class: *mut Class, sel: Sel, func: F) +where + T: Message + ?Sized, + F: MethodImplementation, +{ + let encs = F::Args::ENCODINGS; + let sel_args = count_args(sel); + assert_eq!( + sel_args, + encs.len(), + "Selector {:?} accepts {} arguments, but function accepts {}", + sel, + sel_args, + encs.len(), + ); + + let types = method_type_encoding(&F::Ret::ENCODING, encs); + let success = Bool::from_raw(unsafe { + class_addMethod( + class as *mut _, + sel.as_ptr(), + Some(func.__imp()), + types.as_ptr(), + ) + }); + assert!(success.as_bool(), "Failed to add method {:?}", sel); +} + +fn count_args(sel: Sel) -> usize { + sel.name().chars().filter(|&c| c == ':').count() +} + +fn method_type_encoding(ret: &Encoding, args: &[Encoding]) -> CString { + // First two arguments are always self and the selector + let mut types = format!("{}{}{}", ret, <*mut Object>::ENCODING, Sel::ENCODING); + for enc in args { + use core::fmt::Write; + write!(&mut types, "{}", enc).unwrap(); + } + CString::new(types).unwrap() +}