1use rustc_hash::FxHashMap;
12use std::sync::{Arc, Mutex};
13
14use accesskit::{
15 Action, ActionHandler, ActionRequest, ActivationHandler, Live, Node, NodeId, Rect, Role,
16 Toggled, Tree, TreeId, TreeUpdate,
17};
18#[cfg(target_os = "linux")]
19use accesskit::DeactivationHandler;
20
21#[allow(unused_imports)]
22use crate::accessibility::{AccessibilityConfig, AccessibilityRole, LiveRegionMode};
23use crate::math::{BoundingBox, Dimensions};
24
25const ROOT_NODE_ID: NodeId = NodeId(u64::MAX);
28
29const DOCUMENT_NODE_ID: NodeId = NodeId(u64::MAX - 1);
33
34fn map_role(role: &AccessibilityRole) -> Role {
35 match role {
36 AccessibilityRole::None => Role::Unknown,
37 AccessibilityRole::Button => Role::Button,
38 AccessibilityRole::Link => Role::Link,
39 AccessibilityRole::Heading { .. } => Role::Heading,
40 AccessibilityRole::Label => Role::Label,
41 AccessibilityRole::StaticText => Role::Label,
42 AccessibilityRole::TextInput => Role::TextInput,
43 AccessibilityRole::TextArea => Role::MultilineTextInput,
44 AccessibilityRole::Checkbox => Role::CheckBox,
45 AccessibilityRole::RadioButton => Role::RadioButton,
46 AccessibilityRole::Slider => Role::Slider,
47 AccessibilityRole::Group => Role::Group,
48 AccessibilityRole::List => Role::List,
49 AccessibilityRole::ListItem => Role::ListItem,
50 AccessibilityRole::Menu => Role::Menu,
51 AccessibilityRole::MenuItem => Role::MenuItem,
52 AccessibilityRole::MenuBar => Role::MenuBar,
53 AccessibilityRole::Tab => Role::Tab,
54 AccessibilityRole::TabList => Role::TabList,
55 AccessibilityRole::TabPanel => Role::TabPanel,
56 AccessibilityRole::Dialog => Role::Dialog,
57 AccessibilityRole::AlertDialog => Role::AlertDialog,
58 AccessibilityRole::Toolbar => Role::Toolbar,
59 AccessibilityRole::Image => Role::Image,
60 AccessibilityRole::ProgressBar => Role::ProgressIndicator,
61 }
62}
63
64fn bounding_box_to_rect(bounds: BoundingBox) -> Rect {
65 Rect {
66 x0: bounds.x as f64,
67 y0: bounds.y as f64,
68 x1: (bounds.x + bounds.width) as f64,
69 y1: (bounds.y + bounds.height) as f64,
70 }
71}
72
73fn build_node(config: &AccessibilityConfig, bounds: BoundingBox) -> Node {
74 let role = map_role(&config.role);
75 let mut node = Node::new(role);
76 node.set_bounds(bounding_box_to_rect(bounds));
77
78 if !config.label.is_empty() {
80 node.set_label(config.label.as_str());
81 }
82
83 if !config.description.is_empty() {
85 node.set_description(config.description.as_str());
86 }
87
88 if !config.value.is_empty() {
90 node.set_value(config.value.as_str());
91 }
92
93 if let Some(min) = config.value_min {
95 node.set_min_numeric_value(min as f64);
96 }
97 if let Some(max) = config.value_max {
98 node.set_max_numeric_value(max as f64);
99 }
100
101 if !config.value.is_empty() {
103 if let Ok(num) = config.value.parse::<f64>() {
104 node.set_numeric_value(num);
105 }
106 }
107
108 if let AccessibilityRole::Heading { level } = &config.role {
110 node.set_level(*level as usize);
111 }
112
113 if let Some(checked) = config.checked {
115 node.set_toggled(if checked {
116 Toggled::True
117 } else {
118 Toggled::False
119 });
120 }
121
122 match config.live_region {
124 LiveRegionMode::Off => {}
125 LiveRegionMode::Polite => {
126 node.set_live(Live::Polite);
127 }
128 LiveRegionMode::Assertive => {
129 node.set_live(Live::Assertive);
130 }
131 }
132
133 if config.focusable {
135 node.add_action(Action::Focus);
136 }
137 match config.role {
138 AccessibilityRole::Button | AccessibilityRole::Link | AccessibilityRole::MenuItem => {
139 node.add_action(Action::Click);
140 }
141 AccessibilityRole::Checkbox | AccessibilityRole::RadioButton => {
142 node.add_action(Action::Click);
143 }
144 AccessibilityRole::Slider => {
145 node.add_action(Action::Increment);
146 node.add_action(Action::Decrement);
147 node.add_action(Action::SetValue);
148 }
149 _ => {}
150 }
151
152 node
153}
154
155fn build_tree_update(
156 configs: &FxHashMap<u32, AccessibilityConfig>,
157 bounds_by_id: &FxHashMap<u32, BoundingBox>,
158 element_order: &[u32],
159 focused_id: u32,
160 viewport: Dimensions,
161 include_tree: bool,
162) -> TreeUpdate {
163 let mut nodes: Vec<(NodeId, Node)> = Vec::with_capacity(element_order.len() + 2);
164
165 let child_ids: Vec<NodeId> = element_order
167 .iter()
168 .filter(|id| configs.contains_key(id) && bounds_by_id.contains_key(id))
169 .map(|&id| NodeId(id as u64))
170 .collect();
171
172 let viewport_bounds = BoundingBox::new(0.0, 0.0, viewport.width.max(0.0), viewport.height.max(0.0));
173
174 let mut root_node = Node::new(Role::Window);
176 root_node.set_label("Ply Application");
177 root_node.set_bounds(bounding_box_to_rect(viewport_bounds));
178 root_node.set_children(vec![DOCUMENT_NODE_ID]);
179 nodes.push((ROOT_NODE_ID, root_node));
180
181 let mut doc_node = Node::new(Role::Document);
183 doc_node.set_bounds(bounding_box_to_rect(viewport_bounds));
184 doc_node.set_children(child_ids);
185 nodes.push((DOCUMENT_NODE_ID, doc_node));
186
187 for &elem_id in element_order {
189 if let (Some(config), Some(bounds)) = (configs.get(&elem_id), bounds_by_id.get(&elem_id)) {
190 let node = build_node(config, *bounds);
191 nodes.push((NodeId(elem_id as u64), node));
192 }
193 }
194
195 let focus = if focused_id != 0 && configs.contains_key(&focused_id) && bounds_by_id.contains_key(&focused_id) {
197 NodeId(focused_id as u64)
198 } else {
199 ROOT_NODE_ID
200 };
201
202 let tree = if include_tree {
203 let mut t = Tree::new(ROOT_NODE_ID);
204 t.toolkit_name = Some("Ply Engine".to_string());
205 t.toolkit_version = Some(env!("CARGO_PKG_VERSION").to_string());
206 Some(t)
207 } else {
208 None
209 };
210
211 TreeUpdate {
212 nodes,
213 tree,
214 tree_id: TreeId::ROOT,
215 focus,
216 }
217}
218
219struct PlyActivationHandler {
222 initial_tree: Mutex<Option<TreeUpdate>>,
223}
224
225impl ActivationHandler for PlyActivationHandler {
226 fn request_initial_tree(&mut self) -> Option<TreeUpdate> {
227 self.initial_tree
228 .lock()
229 .ok()
230 .and_then(|mut t| t.take())
231 }
232}
233
234struct PlyActionHandler {
237 queue: Arc<Mutex<Vec<ActionRequest>>>,
238}
239
240impl ActionHandler for PlyActionHandler {
241 fn do_action(&mut self, request: ActionRequest) {
242 if let Ok(mut q) = self.queue.lock() {
243 q.push(request);
244 }
245 }
246}
247
248#[cfg(target_os = "linux")]
251struct PlyDeactivationHandler;
252
253#[cfg(target_os = "linux")]
254impl DeactivationHandler for PlyDeactivationHandler {
255 fn deactivate_accessibility(&mut self) {
256 }
258}
259
260enum PlatformAdapter {
261 #[cfg(target_os = "linux")]
262 Unix(accesskit_unix::Adapter),
263 #[cfg(target_os = "macos")]
264 MacOs(accesskit_macos::SubclassingAdapter),
265 #[cfg(target_os = "windows")]
266 Windows,
269 #[cfg(target_os = "android")]
270 Android(accesskit_android::InjectingAdapter),
271 None,
273}
274
275#[cfg(target_os = "windows")]
276struct WindowsA11yState {
277 adapter: accesskit_windows::Adapter,
278 activation_handler: PlyActivationHandler,
279}
280
281#[cfg(target_os = "windows")]
286static WINDOWS_A11Y: std::sync::Mutex<Option<WindowsA11yState>> = std::sync::Mutex::new(None);
287
288#[cfg(target_os = "windows")]
290#[link(name = "comctl32")]
291extern "system" {
292 fn SetWindowSubclass(
293 hwnd: isize,
294 pfn_subclass: unsafe extern "system" fn(isize, u32, usize, isize, usize, usize) -> isize,
295 uid_subclass: usize,
296 dw_ref_data: usize,
297 ) -> i32;
298 fn DefSubclassProc(hwnd: isize, msg: u32, wparam: usize, lparam: isize) -> isize;
299}
300
301#[cfg(target_os = "windows")]
305unsafe extern "system" fn a11y_subclass_proc(
306 hwnd: isize,
307 msg: u32,
308 wparam: usize,
309 lparam: isize,
310 _uid_subclass: usize,
311 _dw_ref_data: usize,
312) -> isize {
313 const WM_GETOBJECT: u32 = 0x003D;
314 const WM_SETFOCUS: u32 = 0x0007;
315 const WM_KILLFOCUS: u32 = 0x0008;
316
317 match msg {
318 WM_GETOBJECT => {
319 let pending = {
323 if let Ok(mut guard) = WINDOWS_A11Y.lock() {
324 if let Some(state) = guard.as_mut() {
325 state.adapter.handle_wm_getobject(
326 accesskit_windows::WPARAM(wparam),
327 accesskit_windows::LPARAM(lparam),
328 &mut state.activation_handler,
329 )
330 } else {
331 None
332 }
333 } else {
334 None
335 }
336 };
337 if let Some(r) = pending {
339 let lresult: accesskit_windows::LRESULT = r.into();
340 return lresult.0;
341 }
342 DefSubclassProc(hwnd, msg, wparam, lparam)
343 }
344 WM_SETFOCUS | WM_KILLFOCUS => {
345 let is_focused = msg == WM_SETFOCUS;
346 let pending = {
347 if let Ok(mut guard) = WINDOWS_A11Y.lock() {
348 if let Some(state) = guard.as_mut() {
349 state.adapter.update_window_focus_state(is_focused)
350 } else {
351 None
352 }
353 } else {
354 None
355 }
356 };
357 if let Some(events) = pending {
358 events.raise();
359 }
360 DefSubclassProc(hwnd, msg, wparam, lparam)
362 }
363 _ => DefSubclassProc(hwnd, msg, wparam, lparam),
364 }
365}
366
367#[cfg(target_os = "linux")]
373fn ensure_screen_reader_enabled() {
374 use std::process::Command;
375
376 let sr_output = Command::new("busctl")
378 .args([
379 "--user",
380 "get-property",
381 "org.a11y.Bus",
382 "/org/a11y/bus",
383 "org.a11y.Status",
384 "ScreenReaderEnabled",
385 ])
386 .output();
387
388 let sr_enabled = match &sr_output {
389 Ok(out) => {
390 let stdout = String::from_utf8_lossy(&out.stdout);
391 stdout.trim() == "b true"
392 }
393 Err(_) => return, };
395
396 if sr_enabled {
397 return;
399 }
400
401 let is_output = Command::new("busctl")
403 .args([
404 "--user",
405 "get-property",
406 "org.a11y.Bus",
407 "/org/a11y/bus",
408 "org.a11y.Status",
409 "IsEnabled",
410 ])
411 .output();
412
413 let is_enabled = match &is_output {
414 Ok(out) => {
415 let stdout = String::from_utf8_lossy(&out.stdout);
416 stdout.trim() == "b true"
417 }
418 Err(_) => return,
419 };
420
421 if !is_enabled {
422 return;
424 }
425
426 let _ = Command::new("busctl")
429 .args([
430 "--user",
431 "set-property",
432 "org.a11y.Bus",
433 "/org/a11y/bus",
434 "org.a11y.Status",
435 "ScreenReaderEnabled",
436 "b",
437 "true",
438 ])
439 .output();
440}
441
442pub struct NativeAccessibilityState {
443 adapter: PlatformAdapter,
444 action_queue: Arc<Mutex<Vec<ActionRequest>>>,
445 initialized: bool,
446}
447
448impl Default for NativeAccessibilityState {
449 fn default() -> Self {
450 Self {
451 adapter: PlatformAdapter::None,
452 action_queue: Arc::new(Mutex::new(Vec::new())),
453 initialized: false,
454 }
455 }
456}
457
458impl NativeAccessibilityState {
459 fn initialize(
460 &mut self,
461 configs: &FxHashMap<u32, AccessibilityConfig>,
462 bounds_by_id: &FxHashMap<u32, BoundingBox>,
463 element_order: &[u32],
464 focused_id: u32,
465 viewport: Dimensions,
466 ) {
467 let queue = self.action_queue.clone();
468 let initial_tree = build_tree_update(
469 configs,
470 bounds_by_id,
471 element_order,
472 focused_id,
473 viewport,
474 true,
475 );
476
477 #[cfg(target_os = "linux")]
478 {
479 let activation_handler = PlyActivationHandler {
480 initial_tree: Mutex::new(Some(initial_tree)),
481 };
482 let mut adapter = accesskit_unix::Adapter::new(
483 activation_handler,
484 PlyActionHandler { queue },
485 PlyDeactivationHandler,
486 );
487 adapter.update_window_focus_state(true);
489 self.adapter = PlatformAdapter::Unix(adapter);
490
491 std::thread::spawn(|| {
498 std::thread::sleep(std::time::Duration::from_millis(200));
500 ensure_screen_reader_enabled();
501 });
502 }
503
504 #[cfg(target_os = "macos")]
505 {
506 let view = macroquad::miniquad::window::apple_view() as *mut std::ffi::c_void;
513 let activation_handler = PlyActivationHandler {
514 initial_tree: Mutex::new(Some(initial_tree)),
515 };
516 let mut adapter = unsafe {
517 accesskit_macos::SubclassingAdapter::new(
518 view,
519 activation_handler,
520 PlyActionHandler { queue },
521 )
522 };
523 if let Some(events) = adapter.update_view_focus_state(true) {
525 events.raise();
526 }
527 self.adapter = PlatformAdapter::MacOs(adapter);
528 }
529
530 #[cfg(target_os = "windows")]
531 {
532 let hwnd_ptr = macroquad::miniquad::window::windows_hwnd();
544 let hwnd = accesskit_windows::HWND(hwnd_ptr);
545 let adapter = accesskit_windows::Adapter::new(
546 hwnd,
547 true, PlyActionHandler { queue },
549 );
550 let activation_handler = PlyActivationHandler {
551 initial_tree: Mutex::new(Some(initial_tree)),
552 };
553 *WINDOWS_A11Y.lock().unwrap() = Some(WindowsA11yState {
554 adapter,
555 activation_handler,
556 });
557 unsafe {
559 SetWindowSubclass(
560 hwnd_ptr as isize,
561 a11y_subclass_proc,
562 0xA11E, 0,
564 );
565 }
566 self.adapter = PlatformAdapter::Windows;
567 }
568
569 #[cfg(target_os = "android")]
570 {
571 use accesskit_android::jni::{self, objects::JValue};
579
580 let adapter = unsafe {
581 let raw_env = macroquad::miniquad::native::android::attach_jni_env();
582 let mut env = jni::JNIEnv::from_raw(raw_env as *mut _)
583 .expect("Failed to wrap JNIEnv");
584
585 let activity = jni::objects::JObject::from_raw(
586 macroquad::miniquad::native::android::ACTIVITY as _,
587 );
588
589 let focused_view = env
590 .call_method(&activity, "getCurrentFocus", "()Landroid/view/View;", &[])
591 .expect("getCurrentFocus() failed")
592 .l()
593 .unwrap_or(jni::objects::JObject::null());
594
595 let host_view = if !focused_view.is_null() {
596 focused_view
597 } else {
598 let content_view = env
599 .call_method(
600 &activity,
601 "findViewById",
602 "(I)Landroid/view/View;",
603 &[JValue::Int(16908290)],
604 )
605 .expect("findViewById(android.R.id.content) failed")
606 .l()
607 .expect("findViewById(android.R.id.content) did not return an object");
608
609 if !content_view.is_null() {
610 content_view
611 } else {
612 let window = env
613 .call_method(&activity, "getWindow", "()Landroid/view/Window;", &[])
614 .expect("getWindow() failed")
615 .l()
616 .expect("getWindow() did not return an object");
617 env.call_method(&window, "getDecorView", "()Landroid/view/View;", &[])
618 .expect("getDecorView() failed")
619 .l()
620 .expect("getDecorView() did not return an object")
621 }
622 };
623
624 accesskit_android::InjectingAdapter::new(
625 &mut env,
626 &host_view,
627 PlyActivationHandler {
628 initial_tree: Mutex::new(Some(initial_tree)),
629 },
630 PlyActionHandler { queue },
631 )
632 };
633 self.adapter = PlatformAdapter::Android(adapter);
634 }
635
636 #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows", target_os = "android")))]
637 {
638 let _ = (queue, initial_tree);
639 self.adapter = PlatformAdapter::None;
640 }
641
642 self.initialized = true;
643 }
644}
645
646pub enum PendingA11yAction {
648 Focus(u32),
650 Click(u32),
652}
653
654pub fn sync_accessibility_tree(
663 state: &mut NativeAccessibilityState,
664 accessibility_configs: &FxHashMap<u32, AccessibilityConfig>,
665 accessibility_bounds: &FxHashMap<u32, BoundingBox>,
666 accessibility_element_order: &[u32],
667 focused_element_id: u32,
668 viewport: Dimensions,
669) -> Vec<PendingA11yAction> {
670 if !state.initialized {
672 state.initialize(
673 accessibility_configs,
674 accessibility_bounds,
675 accessibility_element_order,
676 focused_element_id,
677 viewport,
678 );
679 }
680
681 let pending_actions: Vec<ActionRequest> = {
683 if let Ok(mut q) = state.action_queue.lock() {
684 q.drain(..).collect()
685 } else {
686 Vec::new()
687 }
688 };
689
690 let mut result = Vec::new();
692 for action in &pending_actions {
693 let target = action.target_node.0;
695 if target == ROOT_NODE_ID.0 || target == DOCUMENT_NODE_ID.0 {
696 continue;
697 }
698 let target_id = target as u32;
699 match action.action {
700 Action::Focus => {
701 result.push(PendingA11yAction::Focus(target_id));
702 }
703 Action::Click => {
704 result.push(PendingA11yAction::Click(target_id));
705 }
706 _ => {}
707 }
708 }
709
710 let update = build_tree_update(
712 accessibility_configs,
713 accessibility_bounds,
714 accessibility_element_order,
715 focused_element_id,
716 viewport,
717 false,
718 );
719
720 match &mut state.adapter {
721 #[cfg(target_os = "linux")]
722 PlatformAdapter::Unix(adapter) => {
723 adapter.update_if_active(|| update);
724 }
725 #[cfg(target_os = "macos")]
726 PlatformAdapter::MacOs(adapter) => {
727 if let Some(events) = adapter.update_if_active(|| update) {
728 events.raise();
729 }
730 }
731 #[cfg(target_os = "windows")]
732 PlatformAdapter::Windows => {
733 let pending = {
735 let mut guard = WINDOWS_A11Y.lock().unwrap();
736 if let Some(state) = guard.as_mut() {
737 state.adapter.update_if_active(|| update)
738 } else {
739 None
740 }
741 };
742 if let Some(events) = pending {
743 events.raise();
744 }
745 }
746 #[cfg(target_os = "android")]
747 PlatformAdapter::Android(adapter) => {
748 adapter.update_if_active(|| update);
749 }
750 PlatformAdapter::None => {
751 let _ = update;
752 }
753 }
754
755 result
756}
757
758#[cfg(test)]
759mod tests {
760 use super::*;
761 use crate::accessibility::{AccessibilityConfig, AccessibilityRole, LiveRegionMode};
762
763 fn make_config(role: AccessibilityRole, label: &str) -> AccessibilityConfig {
764 AccessibilityConfig {
765 focusable: true,
766 role,
767 label: label.to_string(),
768 show_ring: true,
769 ..Default::default()
770 }
771 }
772
773 #[test]
774 fn role_mapping_covers_all_variants() {
775 let roles = vec![
777 AccessibilityRole::None,
778 AccessibilityRole::Button,
779 AccessibilityRole::Link,
780 AccessibilityRole::Heading { level: 1 },
781 AccessibilityRole::Label,
782 AccessibilityRole::StaticText,
783 AccessibilityRole::TextInput,
784 AccessibilityRole::TextArea,
785 AccessibilityRole::Checkbox,
786 AccessibilityRole::RadioButton,
787 AccessibilityRole::Slider,
788 AccessibilityRole::Group,
789 AccessibilityRole::List,
790 AccessibilityRole::ListItem,
791 AccessibilityRole::Menu,
792 AccessibilityRole::MenuItem,
793 AccessibilityRole::MenuBar,
794 AccessibilityRole::Tab,
795 AccessibilityRole::TabList,
796 AccessibilityRole::TabPanel,
797 AccessibilityRole::Dialog,
798 AccessibilityRole::AlertDialog,
799 AccessibilityRole::Toolbar,
800 AccessibilityRole::Image,
801 AccessibilityRole::ProgressBar,
802 ];
803 for role in roles {
804 let _ = map_role(&role);
805 }
806 }
807
808 #[test]
809 fn build_node_button() {
810 let config = make_config(AccessibilityRole::Button, "Click me");
811 let node = build_node(&config, BoundingBox::new(10.0, 20.0, 30.0, 40.0));
812 assert_eq!(node.role(), Role::Button);
813 assert_eq!(node.label(), Some("Click me"));
814 }
815
816 #[test]
817 fn build_node_heading_with_level() {
818 let config = make_config(AccessibilityRole::Heading { level: 2 }, "Section");
819 let node = build_node(&config, BoundingBox::new(0.0, 0.0, 100.0, 24.0));
820 assert_eq!(node.role(), Role::Heading);
821 assert_eq!(node.level(), Some(2));
822 assert_eq!(node.label(), Some("Section"));
823 }
824
825 #[test]
826 fn build_node_checkbox_toggled() {
827 let mut config = make_config(AccessibilityRole::Checkbox, "Agree");
828 config.checked = Some(true);
829 let node = build_node(&config, BoundingBox::new(0.0, 0.0, 20.0, 20.0));
830 assert_eq!(node.role(), Role::CheckBox);
831 assert_eq!(node.toggled(), Some(Toggled::True));
832 }
833
834 #[test]
835 fn build_node_slider_values() {
836 let mut config = make_config(AccessibilityRole::Slider, "Volume");
837 config.value = "50".to_string();
838 config.value_min = Some(0.0);
839 config.value_max = Some(100.0);
840 let node = build_node(&config, BoundingBox::new(0.0, 0.0, 120.0, 24.0));
841 assert_eq!(node.role(), Role::Slider);
842 assert_eq!(node.numeric_value(), Some(50.0));
843 assert_eq!(node.min_numeric_value(), Some(0.0));
844 assert_eq!(node.max_numeric_value(), Some(100.0));
845 }
846
847 #[test]
848 fn build_node_live_region() {
849 let mut config = make_config(AccessibilityRole::Label, "Status");
850 config.live_region = LiveRegionMode::Polite;
851 let node = build_node(&config, BoundingBox::new(0.0, 0.0, 80.0, 24.0));
852 assert_eq!(node.live(), Some(Live::Polite));
853 }
854
855 #[test]
856 fn build_node_description() {
857 let mut config = make_config(AccessibilityRole::Button, "Submit");
858 config.description = "Submit the form".to_string();
859 let node = build_node(&config, BoundingBox::new(0.0, 0.0, 80.0, 24.0));
860 assert_eq!(node.description(), Some("Submit the form"));
861 }
862
863 #[test]
864 fn build_tree_update_structure() {
865 let mut configs = FxHashMap::default();
866 let mut bounds = FxHashMap::default();
867 configs.insert(101, make_config(AccessibilityRole::Button, "OK"));
868 configs.insert(102, make_config(AccessibilityRole::Button, "Cancel"));
869 bounds.insert(101, BoundingBox::new(10.0, 10.0, 80.0, 32.0));
870 bounds.insert(102, BoundingBox::new(100.0, 10.0, 80.0, 32.0));
871
872 let order = vec![101, 102];
873 let update = build_tree_update(&configs, &bounds, &order, 101, Dimensions::new(320.0, 240.0), true);
874
875 assert_eq!(update.nodes.len(), 4);
877
878 assert_eq!(update.nodes[0].0, ROOT_NODE_ID);
880 assert_eq!(update.nodes[0].1.role(), Role::Window);
881
882 assert_eq!(update.nodes[1].0, DOCUMENT_NODE_ID);
884 assert_eq!(update.nodes[1].1.role(), Role::Document);
885
886 assert_eq!(update.focus, NodeId(101));
888
889 let tree = update.tree.as_ref().unwrap();
891 assert_eq!(tree.root, ROOT_NODE_ID);
892 assert_eq!(tree.toolkit_name, Some("Ply Engine".to_string()));
893 }
894
895 #[test]
896 fn build_tree_update_no_focus() {
897 let configs = FxHashMap::default();
898 let bounds = FxHashMap::default();
899 let order = vec![];
900 let update = build_tree_update(&configs, &bounds, &order, 0, Dimensions::new(320.0, 240.0), true);
901
902 assert_eq!(update.nodes.len(), 2);
904 assert_eq!(update.focus, ROOT_NODE_ID);
906 }
907
908 #[test]
909 fn default_state_is_uninitialized() {
910 let state = NativeAccessibilityState::default();
911 assert!(!state.initialized);
912 }
913}