use super::{A11yState, AccessibilityBridge};
use crate::compat::{HashMap, MiniToString, Mutex, String};
use crate::core::ObjectId;
use cocoa::base::{id, nil};
use cocoa::foundation::{NSAutoreleasePool, NSString};
extern "C" {
fn NSAccessibilityPostNotification(element: id, notification: id);
}
pub struct MacOSAccessibilityBridge {
names: Mutex<HashMap<ObjectId, String>>,
nodes: Mutex<HashMap<ObjectId, A11yState>>,
native_handles: Mutex<HashMap<ObjectId, usize>>,
}
unsafe fn autoreleased_nsstring(s: &str) -> id {
NSString::alloc(nil).init_str(s).autorelease()
}
impl MacOSAccessibilityBridge {
pub fn new() -> Self {
Self {
names: Mutex::new(HashMap::new()),
nodes: Mutex::new(HashMap::new()),
native_handles: Mutex::new(HashMap::new()),
}
}
pub fn register_handle(&self, id: ObjectId, ptr: usize) {
if let Some(mut handles) = crate::compat::try_lock(&self.native_handles) {
handles.insert(id, ptr);
}
}
pub fn unregister_handle(&self, id: ObjectId) {
if let Some(mut handles) = crate::compat::try_lock(&self.native_handles) {
handles.remove(&id);
}
}
fn post_notification(&self, id: ObjectId, notification_name: &str) -> bool {
let ptr = match crate::compat::try_lock(&self.native_handles) {
Some(h) => h.get(&id).copied(),
None => return false,
};
let Some(ptr) = ptr else { return false };
let result = std::panic::catch_unwind(|| unsafe {
let element: id = std::mem::transmute(ptr);
let ns_name = autoreleased_nsstring(notification_name);
NSAccessibilityPostNotification(element, ns_name);
true
});
result.unwrap_or(false)
}
}
crate::impl_default_via_new!(MacOSAccessibilityBridge);
impl AccessibilityBridge for MacOSAccessibilityBridge {
fn set_accessibility_name(&self, id: ObjectId, name: &str) {
if let Some(mut names) = crate::compat::try_lock(&self.names) {
names.insert(id, name.to_string());
}
}
fn accessibility_name(&self, id: ObjectId) -> Option<String> {
crate::compat::try_lock(&self.names).and_then(|names| names.get(&id).cloned())
}
fn submit_node_state(&self, id: ObjectId, state: &A11yState) {
if let Some(mut names) = crate::compat::try_lock(&self.names) {
names.insert(id, state.label.clone());
}
if let Some(mut nodes) = crate::compat::try_lock(&self.nodes) {
nodes.insert(id, state.clone());
}
}
fn node_state(&self, id: ObjectId) -> Option<A11yState> {
crate::compat::try_lock(&self.nodes).and_then(|nodes| nodes.get(&id).cloned())
}
fn unregister_node(&self, id: ObjectId) {
if let Some(mut names) = crate::compat::try_lock(&self.names) {
names.remove(&id);
}
if let Some(mut nodes) = crate::compat::try_lock(&self.nodes) {
nodes.remove(&id);
}
}
fn node_count(&self) -> usize {
crate::compat::try_lock(&self.nodes).map(|nodes| nodes.len()).unwrap_or(0)
}
fn notify_name_changed(&self, id: ObjectId) {
self.post_notification(id, "NSAccessibilityNameChangedNotification");
}
fn notify_value_changed(&self, id: ObjectId) {
self.post_notification(id, "NSAccessibilityValueChangedNotification");
}
fn notify_state_changed(&self, id: ObjectId) {
self.post_notification(id, "NSAccessibilityValueChangedNotification");
}
fn notify_focus_changed(&self, id: ObjectId) {
self.post_notification(id, "NSAccessibilityFocusedUIElementChangedNotification");
}
}
#[cfg(target_os = "macos")]
pub fn ns_accessibility_role(role: &super::A11yRole) -> &'static str {
match role {
super::A11yRole::Button => "NSAccessibilityButtonRole",
super::A11yRole::Label | super::A11yRole::Heading | super::A11yRole::Paragraph => {
"NSAccessibilityStaticTextRole"
}
super::A11yRole::TextField => "NSAccessibilityTextFieldRole",
super::A11yRole::CheckBox => "NSAccessibilityCheckBoxRole",
super::A11yRole::RadioButton => "NSAccessibilityRadioButtonRole",
super::A11yRole::Slider => "NSAccessibilitySliderRole",
super::A11yRole::ProgressBar => "NSAccessibilityProgressIndicatorRole",
super::A11yRole::List => "NSAccessibilityListRole",
super::A11yRole::Table => "NSAccessibilityTableRole",
super::A11yRole::Image => "NSAccessibilityImageRole",
super::A11yRole::Link => "NSAccessibilityLinkRole",
super::A11yRole::Group => "NSAccessibilityGroupRole",
super::A11yRole::Window => "NSAccessibilityWindowRole",
super::A11yRole::Dialog | super::A11yRole::Alert => "NSAccessibilityDialogRole",
super::A11yRole::Menu => "NSAccessibilityMenuRole",
super::A11yRole::MenuItem => "NSAccessibilityMenuItemRole",
super::A11yRole::Tab => "NSAccessibilityTabRole",
super::A11yRole::Switch => "NSAccessibilityCheckBoxRole",
super::A11yRole::ComboBox => "NSAccessibilityComboBoxRole",
super::A11yRole::SpinButton => "NSAccessibilityIncrementorRole",
super::A11yRole::StatusBar => "NSAccessibilityGroupRole",
super::A11yRole::ToolTip => "NSAccessibilityHelpTagRole",
super::A11yRole::Tree => "NSAccessibilityOutlineRole",
super::A11yRole::Unknown => "NSAccessibilityUnknownRole",
}
}
#[cfg(target_os = "macos")]
pub fn ns_accessibility_subrole(role: &super::A11yRole) -> Option<&'static str> {
match role {
super::A11yRole::Switch => Some("NSAccessibilitySwitchSubrole"),
super::A11yRole::ToolTip => Some("NSAccessibilityToolbarSubrole"),
_ => None,
}
}
#[cfg(target_os = "macos")]
pub fn post_ns_accessibility_notification(element_ptr: usize, notification: &str) {
unsafe {
let element: id = std::mem::transmute(element_ptr);
let ns_name = autoreleased_nsstring(notification);
NSAccessibilityPostNotification(element, ns_name);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_role_mapping() {
assert_eq!(
ns_accessibility_role(&crate::platform::accessibility::A11yRole::Button),
"NSAccessibilityButtonRole"
);
assert_eq!(
ns_accessibility_role(&crate::platform::accessibility::A11yRole::TextField),
"NSAccessibilityTextFieldRole"
);
assert_eq!(
ns_accessibility_role(&crate::platform::accessibility::A11yRole::CheckBox),
"NSAccessibilityCheckBoxRole"
);
assert_eq!(
ns_accessibility_role(&crate::platform::accessibility::A11yRole::Window),
"NSAccessibilityWindowRole"
);
assert_eq!(
ns_accessibility_role(&crate::platform::accessibility::A11yRole::Unknown),
"NSAccessibilityUnknownRole"
);
}
#[test]
fn test_subrole_mapping() {
assert_eq!(
ns_accessibility_subrole(&crate::platform::accessibility::A11yRole::Switch),
Some("NSAccessibilitySwitchSubrole")
);
assert_eq!(
ns_accessibility_subrole(&crate::platform::accessibility::A11yRole::Button),
None
);
}
#[test]
fn test_bridge_send_sync() {
fn assert_send<T: Send>() {}
fn assert_sync<T: Sync>() {}
assert_send::<MacOSAccessibilityBridge>();
assert_sync::<MacOSAccessibilityBridge>();
}
#[test]
fn submitted_state_is_stored_in_full_not_only_the_label() {
use crate::platform::accessibility::{A11yRole, A11yState};
let bridge = MacOSAccessibilityBridge::new();
let full = A11yState {
role: A11yRole::CheckBox,
label: "Dark mode".to_string(),
description: "Toggle the theme".to_string(),
enabled: true,
checked: Some(true),
children: vec![8, 9],
..A11yState::default()
};
bridge.submit_node_state(4, &full);
assert_eq!(bridge.node_state(4), Some(full), "every field round-trips");
assert_eq!(bridge.accessibility_name(4).as_deref(), Some("Dark mode"));
}
#[test]
fn unmount_removes_entries_so_counts_return_to_baseline() {
use crate::platform::accessibility::{A11yRole, A11yState};
let bridge = MacOSAccessibilityBridge::new();
let id = 12u64;
bridge.submit_node_state(
id,
&A11yState { role: A11yRole::Button, label: "Go".to_string(), ..A11yState::default() },
);
assert_eq!(bridge.node_count(), 1);
assert!(bridge.accessibility_name(id).is_some());
bridge.unregister_node(id);
assert_eq!(bridge.node_count(), 0, "the node is gone");
assert!(bridge.accessibility_name(id).is_none(), "and the name is gone");
assert!(bridge.node_state(id).is_none(), "and the state is gone");
}
}