use super::{A11yState, AccessibilityBridge};
use crate::compat::{lock, HashMap, Mutex, String, ToString};
use crate::core::ObjectId;
pub struct WindowsAccessibilityBridge {
names: Mutex<HashMap<ObjectId, String>>,
nodes: Mutex<HashMap<ObjectId, A11yState>>,
native_handles: Mutex<HashMap<ObjectId, usize>>,
}
impl WindowsAccessibilityBridge {
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) {
lock(&self.native_handles).insert(id, ptr);
}
pub fn unregister_handle(&self, id: ObjectId) {
lock(&self.native_handles).remove(&id);
}
fn post_event(&self, id: ObjectId, event: u32) -> bool {
let Some(hwnd_val) = lock(&self.native_handles).get(&id).copied() else {
return false;
};
#[cfg(target_os = "windows")]
{
let hwnd = hwnd_val as winapi::shared::windef::HWND;
unsafe {
winapi::um::winuser::NotifyWinEvent(
event,
hwnd,
winapi::um::winuser::OBJID_CLIENT,
0,
);
}
}
#[cfg(not(target_os = "windows"))]
{
let _ = event;
}
true
}
}
crate::impl_default_via_new!(WindowsAccessibilityBridge);
impl AccessibilityBridge for WindowsAccessibilityBridge {
fn set_accessibility_name(&self, id: ObjectId, name: &str) {
lock(&self.names).insert(id, name.to_string());
}
fn accessibility_name(&self, id: ObjectId) -> Option<String> {
lock(&self.names).get(&id).cloned()
}
fn submit_node_state(&self, id: ObjectId, state: &A11yState) {
lock(&self.names).insert(id, state.label.clone());
lock(&self.nodes).insert(id, state.clone());
}
fn node_state(&self, id: ObjectId) -> Option<A11yState> {
lock(&self.nodes).get(&id).cloned()
}
fn unregister_node(&self, id: ObjectId) {
lock(&self.names).remove(&id);
lock(&self.nodes).remove(&id);
}
fn node_count(&self) -> usize {
lock(&self.nodes).len()
}
fn notify_name_changed(&self, id: ObjectId) {
self.post_event(id, winapi::um::winuser::EVENT_OBJECT_NAMECHANGE);
}
fn notify_value_changed(&self, id: ObjectId) {
self.post_event(id, winapi::um::winuser::EVENT_OBJECT_VALUECHANGE);
}
fn notify_state_changed(&self, id: ObjectId) {
self.post_event(id, winapi::um::winuser::EVENT_OBJECT_STATECHANGE);
}
fn notify_focus_changed(&self, id: ObjectId) {
self.post_event(id, winapi::um::winuser::EVENT_OBJECT_FOCUS);
}
}
#[cfg(target_os = "windows")]
pub mod uia_constants {
pub const UIA_BUTTON_CONTROL_TYPE: u32 = 50000u32;
pub const UIA_CHECKBOX_CONTROL_TYPE: u32 = 50001u32;
pub const UIA_COMBOBOX_CONTROL_TYPE: u32 = 50003u32;
pub const UIA_EDIT_CONTROL_TYPE: u32 = 50004u32;
pub const UIA_GROUP_CONTROL_TYPE: u32 = 50008u32;
pub const UIA_IMAGE_CONTROL_TYPE: u32 = 50009u32;
pub const UIA_LISTITEM_CONTROL_TYPE: u32 = 50010u32;
pub const UIA_LIST_CONTROL_TYPE: u32 = 50011u32;
pub const UIA_MENU_CONTROL_TYPE: u32 = 50012u32;
pub const UIA_MENUBAR_CONTROL_TYPE: u32 = 50013u32;
pub const UIA_MENUITEM_CONTROL_TYPE: u32 = 50014u32;
pub const UIA_PROGRESSBAR_CONTROL_TYPE: u32 = 50016u32;
pub const UIA_RADIOBUTTON_CONTROL_TYPE: u32 = 50017u32;
pub const UIA_SCROLLBAR_CONTROL_TYPE: u32 = 50018u32;
pub const UIA_SLIDER_CONTROL_TYPE: u32 = 50019u32;
pub const UIA_SPINBUTTON_CONTROL_TYPE: u32 = 50020u32;
pub const UIA_STATUSBAR_CONTROL_TYPE: u32 = 50021u32;
pub const UIA_TAB_CONTROL_TYPE: u32 = 50022u32;
pub const UIA_TABLE_CONTROL_TYPE: u32 = 50023u32;
pub const UIA_TEXT_CONTROL_TYPE: u32 = 50024u32;
pub const UIA_TOOLTIP_CONTROL_TYPE: u32 = 50026u32;
pub const UIA_TREE_CONTROL_TYPE: u32 = 50028u32;
pub const UIA_WINDOW_CONTROL_TYPE: u32 = 50031u32;
pub const UIA_HYPERLINK_CONTROL_TYPE: u32 = 50032u32;
pub const UIA_DATAGRID_CONTROL_TYPE: u32 = 50035u32;
}
#[cfg(target_os = "windows")]
pub fn uia_control_type_id(role: &super::A11yRole) -> u32 {
use uia_constants::*;
match role {
super::A11yRole::Button | super::A11yRole::Switch => UIA_BUTTON_CONTROL_TYPE,
super::A11yRole::Label | super::A11yRole::Heading | super::A11yRole::Paragraph => {
UIA_TEXT_CONTROL_TYPE
}
super::A11yRole::TextField => UIA_EDIT_CONTROL_TYPE,
super::A11yRole::CheckBox => UIA_CHECKBOX_CONTROL_TYPE,
super::A11yRole::RadioButton => UIA_RADIOBUTTON_CONTROL_TYPE,
super::A11yRole::Slider => UIA_SLIDER_CONTROL_TYPE,
super::A11yRole::ProgressBar => UIA_PROGRESSBAR_CONTROL_TYPE,
super::A11yRole::List => UIA_LIST_CONTROL_TYPE,
super::A11yRole::Table => UIA_TABLE_CONTROL_TYPE,
super::A11yRole::Image => UIA_IMAGE_CONTROL_TYPE,
super::A11yRole::Link => UIA_HYPERLINK_CONTROL_TYPE,
super::A11yRole::Group | super::A11yRole::StatusBar => UIA_GROUP_CONTROL_TYPE,
super::A11yRole::Window => UIA_WINDOW_CONTROL_TYPE,
super::A11yRole::Dialog | super::A11yRole::Alert => UIA_WINDOW_CONTROL_TYPE,
super::A11yRole::Menu => UIA_MENU_CONTROL_TYPE,
super::A11yRole::MenuItem => UIA_MENUITEM_CONTROL_TYPE,
super::A11yRole::Tab => UIA_TAB_CONTROL_TYPE,
super::A11yRole::ComboBox => UIA_COMBOBOX_CONTROL_TYPE,
super::A11yRole::SpinButton => UIA_SPINBUTTON_CONTROL_TYPE,
super::A11yRole::ToolTip => UIA_TOOLTIP_CONTROL_TYPE,
super::A11yRole::Tree => UIA_TREE_CONTROL_TYPE,
super::A11yRole::Unknown => UIA_GROUP_CONTROL_TYPE,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::platform::accessibility::{A11yRole, A11yState};
#[test]
fn test_bridge_send_sync() {
fn assert_send<T: Send>() {}
fn assert_sync<T: Sync>() {}
assert_send::<WindowsAccessibilityBridge>();
assert_sync::<WindowsAccessibilityBridge>();
}
#[test]
fn submitted_state_is_stored_in_full_not_only_the_label() {
let bridge = WindowsAccessibilityBridge::new();
let full = A11yState {
role: A11yRole::Slider,
label: "Volume".to_string(),
description: "Output level".to_string(),
enabled: true,
value: "60".to_string(),
children: vec![2],
..A11yState::default()
};
bridge.submit_node_state(3, &full);
assert_eq!(bridge.node_state(3), Some(full), "every field round-trips");
assert_eq!(bridge.accessibility_name(3).as_deref(), Some("Volume"));
}
#[test]
fn unmount_removes_entries_so_counts_return_to_baseline() {
let bridge = WindowsAccessibilityBridge::new();
let id = 11u64;
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");
}
#[test]
fn test_uia_control_type_mapping() {
#[cfg(target_os = "windows")]
{
assert_eq!(
uia_control_type_id(&A11yRole::Button),
uia_constants::UIA_BUTTON_CONTROL_TYPE
);
assert_eq!(
uia_control_type_id(&A11yRole::TextField),
uia_constants::UIA_EDIT_CONTROL_TYPE
);
assert_eq!(
uia_control_type_id(&A11yRole::Window),
uia_constants::UIA_WINDOW_CONTROL_TYPE
);
}
}
}