use crate::core::ObjectId;
use crate::platform::accessibility::{A11yState, AccessibilityBridge};
struct InstalledBridge {
ptr: *const dyn AccessibilityBridge,
}
thread_local! {
#[allow(clippy::missing_const_for_thread_local)]
static INSTALLED: core::cell::RefCell<Option<InstalledBridge>> =
const { core::cell::RefCell::new(None) };
#[allow(clippy::missing_const_for_thread_local)]
static PLATFORM_RESOLVED: core::cell::Cell<bool> = const { core::cell::Cell::new(false) };
}
pub fn install_bridge(bridge: &'static dyn AccessibilityBridge) -> bool {
let _ = PLATFORM_RESOLVED.try_with(|flag| flag.set(true));
INSTALLED
.try_with(|slot| slot.borrow_mut().replace(InstalledBridge { ptr: bridge }).is_some())
.unwrap_or(false)
}
pub fn uninstall_bridge() -> bool {
INSTALLED.try_with(|slot| slot.borrow_mut().take().is_some()).unwrap_or(false)
}
fn with_bridge<R>(f: impl FnOnce(&dyn AccessibilityBridge) -> R) -> Option<R> {
let installed = INSTALLED
.try_with(|slot| slot.borrow().as_ref().map(|installed| installed.ptr))
.ok()
.flatten();
let ptr = match installed {
Some(ptr) => ptr,
None => platform_bridge_ptr()?,
};
Some(f(unsafe { &*ptr }))
}
fn platform_bridge_ptr() -> Option<*const dyn AccessibilityBridge> {
let already = PLATFORM_RESOLVED.try_with(|flag| flag.get()).unwrap_or(false);
if !already {
let _ = PLATFORM_RESOLVED.try_with(|flag| flag.set(true));
}
let bridge = crate::platform::runtime::get_platform().accessibility_bridge()?;
Some(bridge as *const dyn AccessibilityBridge)
}
pub fn submit_mounted(id: ObjectId, state: &A11yState) {
with_bridge(|bridge| {
bridge.set_accessibility_name(id, &state.label);
bridge.notify_state_changed(id);
});
}
pub fn submit_unmounted(id: ObjectId) {
with_bridge(|bridge| {
bridge.set_accessibility_name(id, "");
bridge.notify_state_changed(id);
});
}
pub fn submit_geometry_changed(id: ObjectId) {
with_bridge(|bridge| bridge.notify_state_changed(id));
}
pub fn submit_state_changed(id: ObjectId) {
with_bridge(|bridge| bridge.notify_state_changed(id));
}
pub fn submit_semantic_changed(id: ObjectId) {
with_bridge(|bridge| bridge.notify_state_changed(id));
}
pub fn submit_value_changed(id: ObjectId) {
with_bridge(|bridge| bridge.notify_value_changed(id));
}
pub fn submit_focus_changed(id: ObjectId) {
with_bridge(|bridge| bridge.notify_focus_changed(id));
}
#[cfg(all(test, not(alloc_frugal)))]
mod tests {
use super::*;
use crate::platform::accessibility::{A11yRole, A11yState};
use std::sync::Mutex;
#[derive(Debug, Clone, PartialEq, Eq)]
enum Call {
Name(ObjectId, String),
State(ObjectId),
Value(ObjectId),
Focus(ObjectId),
}
struct Recording {
calls: Mutex<Vec<Call>>,
}
impl Recording {
fn new() -> Self {
Self { calls: Mutex::new(Vec::new()) }
}
fn calls(&self) -> Vec<Call> {
self.calls.lock().expect("recording mutex is never poisoned").clone()
}
}
impl AccessibilityBridge for Recording {
fn set_accessibility_name(&self, id: ObjectId, name: &str) {
self.calls.lock().expect("not poisoned").push(Call::Name(id, name.to_string()));
}
fn accessibility_name(&self, _id: ObjectId) -> Option<String> {
None
}
fn notify_name_changed(&self, _id: ObjectId) {}
fn notify_value_changed(&self, id: ObjectId) {
self.calls.lock().expect("not poisoned").push(Call::Value(id));
}
fn notify_state_changed(&self, id: ObjectId) {
self.calls.lock().expect("not poisoned").push(Call::State(id));
}
fn notify_focus_changed(&self, id: ObjectId) {
self.calls.lock().expect("not poisoned").push(Call::Focus(id));
}
}
fn with_recording<R>(f: impl FnOnce(&Recording) -> R) -> R {
let recorder: &'static Recording = Box::leak(Box::new(Recording::new()));
let had = install_bridge(recorder);
let result = f(recorder);
let removed = uninstall_bridge();
assert!(!had, "no bridge should have been installed before this test");
assert!(removed, "the substitute must be removable, or it leaks into the next test");
result
}
#[test]
fn mounting_submits_a_node() {
with_recording(|recorder| {
let state = A11yState {
role: A11yRole::Button,
label: "Save".to_string(),
..A11yState::default()
};
submit_mounted(7, &state);
let calls = recorder.calls();
assert!(
calls.contains(&Call::Name(7, "Save".to_string())),
"the node's label must reach the bridge: {calls:?}"
);
assert!(
calls.contains(&Call::State(7)),
"and its creation must be reported: {calls:?}"
);
});
}
#[test]
fn unmounting_submits_the_removal() {
with_recording(|recorder| {
submit_unmounted(9);
assert_eq!(recorder.calls(), vec![Call::Name(9, String::new()), Call::State(9)]);
});
}
#[test]
fn every_submit_point_reaches_the_bridge() {
with_recording(|recorder| {
submit_geometry_changed(1);
submit_state_changed(2);
submit_semantic_changed(3);
submit_value_changed(4);
submit_focus_changed(5);
assert_eq!(
recorder.calls(),
vec![
Call::State(1),
Call::State(2),
Call::State(3),
Call::Value(4),
Call::Focus(5),
]
);
});
}
#[test]
fn without_a_bridge_every_submit_point_is_a_no_op() {
let _ = uninstall_bridge();
let state = A11yState { role: A11yRole::Label, ..A11yState::default() };
submit_mounted(11, &state);
submit_unmounted(11);
submit_geometry_changed(11);
submit_state_changed(11);
submit_semantic_changed(11);
submit_value_changed(11);
submit_focus_changed(11);
}
}