Skip to content

Commit

Permalink
feat: Workaround for libraries that put the macOS keyboard focus on t…
Browse files Browse the repository at this point in the history
…he window rather than the content view (#266)
  • Loading branch information
mwcampbell authored Aug 7, 2023
1 parent b9b3cd1 commit c2db1b0
Show file tree
Hide file tree
Showing 3 changed files with 125 additions and 2 deletions.
32 changes: 30 additions & 2 deletions bindings/c/src/macos.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down Expand Up @@ -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);
}
3 changes: 3 additions & 0 deletions platforms/macos/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
92 changes: 92 additions & 0 deletions platforms/macos/src/patch.rs
Original file line number Diff line number Diff line change
@@ -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<T, F>(class: *mut Class, sel: Sel, func: F)
where
T: Message + ?Sized,
F: MethodImplementation<Callee = T>,
{
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()
}

0 comments on commit c2db1b0

Please sign in to comment.