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

feat: Workaround for libraries that put the macOS keyboard focus on the window rather than the content view #266

Merged
merged 4 commits into from
Aug 7, 2023
Merged
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
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
mwcampbell marked this conversation as resolved.
Show resolved Hide resolved
/// 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.
mwcampbell marked this conversation as resolved.
Show resolved Hide resolved

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()
}