#[cfg(target_arch = "wasm32")]
use crate::accessibility::{AccessibilityRole, LiveRegionMode};
#[cfg(target_arch = "wasm32")]
use rustc_hash::FxHashSet;
#[cfg(target_arch = "wasm32")]
extern "C" {
fn ply_a11y_init();
fn ply_a11y_upsert_node(
id: u32,
role_ptr: *const u8,
role_len: u32,
label_ptr: *const u8,
label_len: u32,
tab_index: i32,
);
fn ply_a11y_set_heading_level(id: u32, level: u32);
fn ply_a11y_set_checked(id: u32, checked: u32);
fn ply_a11y_set_value(
id: u32,
value_ptr: *const u8,
value_len: u32,
min: f32,
max: f32,
);
fn ply_a11y_set_live(id: u32, mode: u32);
fn ply_a11y_remove_node(id: u32);
fn ply_a11y_set_focus(id: u32);
fn ply_a11y_clear();
fn ply_a11y_announce(id: u32, text_ptr: *const u8, text_len: u32);
fn ply_a11y_set_description(id: u32, desc_ptr: *const u8, desc_len: u32);
fn ply_a11y_reorder(ids_ptr: *const u32, count: u32);
fn ply_a11y_set_bounds(id: u32, x: f32, y: f32, width: f32, height: f32);
fn ply_a11y_set_viewport(width: f32, height: f32);
}
#[cfg(target_arch = "wasm32")]
fn role_to_aria_string(role: &AccessibilityRole) -> &'static str {
match role {
AccessibilityRole::None => "none",
AccessibilityRole::Button => "button",
AccessibilityRole::Link => "link",
AccessibilityRole::Heading { .. } => "heading",
AccessibilityRole::Label => "note",
AccessibilityRole::StaticText => "none",
AccessibilityRole::TextInput => "textbox",
AccessibilityRole::TextArea => "textbox",
AccessibilityRole::Checkbox => "checkbox",
AccessibilityRole::RadioButton => "radio",
AccessibilityRole::Slider => "slider",
AccessibilityRole::Group => "group",
AccessibilityRole::List => "list",
AccessibilityRole::ListItem => "listitem",
AccessibilityRole::Menu => "menu",
AccessibilityRole::MenuItem => "menuitem",
AccessibilityRole::MenuBar => "menubar",
AccessibilityRole::Tab => "tab",
AccessibilityRole::TabList => "tablist",
AccessibilityRole::TabPanel => "tabpanel",
AccessibilityRole::Dialog => "dialog",
AccessibilityRole::AlertDialog => "alertdialog",
AccessibilityRole::Toolbar => "toolbar",
AccessibilityRole::Image => "img",
AccessibilityRole::ProgressBar => "progressbar",
}
}
#[cfg(target_arch = "wasm32")]
pub struct WebAccessibilityState {
initialized: bool,
previous_ids: FxHashSet<u32>,
previous_focus: u32,
previous_order: Vec<u32>,
}
#[cfg(target_arch = "wasm32")]
impl Default for WebAccessibilityState {
fn default() -> Self {
Self {
initialized: false,
previous_ids: FxHashSet::default(),
previous_focus: 0,
previous_order: Vec::new(),
}
}
}
#[cfg(target_arch = "wasm32")]
pub fn sync_accessibility_tree(
state: &mut WebAccessibilityState,
accessibility_configs: &rustc_hash::FxHashMap<u32, crate::accessibility::AccessibilityConfig>,
accessibility_bounds: &rustc_hash::FxHashMap<u32, crate::math::BoundingBox>,
accessibility_element_order: &[u32],
focused_element_id: u32,
viewport: crate::math::Dimensions,
) {
if !state.initialized {
unsafe { ply_a11y_init(); }
state.initialized = true;
}
unsafe { ply_a11y_set_viewport(viewport.width, viewport.height); }
let mut current_ids = FxHashSet::with_capacity_and_hasher(accessibility_configs.len(), Default::default());
for &elem_id in accessibility_element_order {
let (config, bounds) = match (
accessibility_configs.get(&elem_id),
accessibility_bounds.get(&elem_id),
) {
(Some(config), Some(bounds)) => (config, bounds),
_ => continue,
};
current_ids.insert(elem_id);
let role_str = role_to_aria_string(&config.role);
let tab_index = if config.focusable {
config.tab_index.unwrap_or(0)
} else {
-1
};
unsafe {
ply_a11y_upsert_node(
elem_id,
role_str.as_ptr(),
role_str.len() as u32,
config.label.as_ptr(),
config.label.len() as u32,
tab_index,
);
ply_a11y_set_bounds(elem_id, bounds.x, bounds.y, bounds.width, bounds.height);
}
if let AccessibilityRole::Heading { level } = &config.role {
unsafe { ply_a11y_set_heading_level(elem_id, *level as u32); }
}
if let Some(checked) = config.checked {
unsafe { ply_a11y_set_checked(elem_id, if checked { 1 } else { 0 }); }
}
if !config.value.is_empty() {
unsafe {
ply_a11y_set_value(
elem_id,
config.value.as_ptr(),
config.value.len() as u32,
config.value_min.unwrap_or(f32::NAN),
config.value_max.unwrap_or(f32::NAN),
);
}
}
if !config.description.is_empty() {
unsafe {
ply_a11y_set_description(
elem_id,
config.description.as_ptr(),
config.description.len() as u32,
);
}
}
let live_mode = match config.live_region {
LiveRegionMode::Off => 0u32,
LiveRegionMode::Polite => 1,
LiveRegionMode::Assertive => 2,
};
if live_mode > 0 {
unsafe { ply_a11y_set_live(elem_id, live_mode); }
}
}
for old_id in &state.previous_ids {
if !current_ids.contains(old_id) {
unsafe { ply_a11y_remove_node(*old_id); }
}
}
if accessibility_element_order != state.previous_order.as_slice() {
unsafe {
ply_a11y_reorder(
accessibility_element_order.as_ptr(),
accessibility_element_order.len() as u32,
);
}
state.previous_order = accessibility_element_order.to_vec();
}
if focused_element_id != state.previous_focus {
unsafe { ply_a11y_set_focus(focused_element_id); }
state.previous_focus = focused_element_id;
}
state.previous_ids = current_ids;
}