1use std::collections::{HashMap, HashSet, VecDeque};
4use std::sync::atomic::{AtomicU64, Ordering};
5use std::sync::Mutex;
6
7use rayon::prelude::*;
8use xa11y_core::{
9 ElementData, Error, Provider, Rect, Result, Role, StateSet, Subscription, Toggled,
10};
11use zbus::blocking::{Connection, Proxy};
12
13static NEXT_HANDLE: AtomicU64 = AtomicU64::new(1);
15
16fn state_attr_to_string(name: &str, s: &StateSet) -> Option<String> {
19 match name {
20 "enabled" => Some(s.enabled.to_string()),
21 "visible" => Some(s.visible.to_string()),
22 "focused" => Some(s.focused.to_string()),
23 "focusable" => Some(s.focusable.to_string()),
24 "selected" => Some(s.selected.to_string()),
25 "editable" => Some(s.editable.to_string()),
26 "modal" => Some(s.modal.to_string()),
27 "required" => Some(s.required.to_string()),
28 "busy" => Some(s.busy.to_string()),
29 "expanded" => s.expanded.map(|b| b.to_string()),
30 "checked" => s.checked.map(|c| {
31 match c {
32 Toggled::On => "on",
33 Toggled::Off => "off",
34 Toggled::Mixed => "mixed",
35 }
36 .to_string()
37 }),
38 _ => None,
39 }
40}
41
42pub struct LinuxProvider {
44 a11y_bus: Connection,
45 handle_cache: Mutex<HashMap<u64, AccessibleRef>>,
47 action_indices: Mutex<HashMap<u64, HashMap<String, i32>>>,
50}
51
52#[derive(Debug, Clone)]
54pub(crate) struct AccessibleRef {
55 pub(crate) bus_name: String,
56 pub(crate) path: String,
57}
58
59impl LinuxProvider {
60 pub fn new() -> Result<Self> {
65 let a11y_bus = Self::connect_a11y_bus()?;
66 Ok(Self {
67 a11y_bus,
68 handle_cache: Mutex::new(HashMap::new()),
69 action_indices: Mutex::new(HashMap::new()),
70 })
71 }
72
73 pub(crate) fn connect_a11y_bus() -> Result<Connection> {
74 if let Ok(session) = Connection::session() {
78 let proxy = Proxy::new(&session, "org.a11y.Bus", "/org/a11y/bus", "org.a11y.Bus")
79 .map_err(|e| Error::Platform {
80 code: -1,
81 message: format!("Failed to create a11y bus proxy: {}", e),
82 })?;
83
84 if let Ok(addr_reply) = proxy.call_method("GetAddress", &()) {
85 if let Ok(address) = addr_reply.body().deserialize::<String>() {
86 if let Ok(addr) = zbus::Address::try_from(address.as_str()) {
87 if let Ok(Ok(conn)) =
88 zbus::blocking::connection::Builder::address(addr).map(|b| b.build())
89 {
90 return Ok(conn);
91 }
92 }
93 }
94 }
95
96 return Ok(session);
98 }
99
100 Connection::session().map_err(|e| Error::Platform {
101 code: -1,
102 message: format!("Failed to connect to D-Bus session bus: {}", e),
103 })
104 }
105
106 fn make_proxy(&self, bus_name: &str, path: &str, interface: &str) -> Result<Proxy<'_>> {
107 zbus::blocking::proxy::Builder::<Proxy>::new(&self.a11y_bus)
110 .destination(bus_name.to_owned())
111 .map_err(|e| Error::Platform {
112 code: -1,
113 message: format!("Failed to set proxy destination: {}", e),
114 })?
115 .path(path.to_owned())
116 .map_err(|e| Error::Platform {
117 code: -1,
118 message: format!("Failed to set proxy path: {}", e),
119 })?
120 .interface(interface.to_owned())
121 .map_err(|e| Error::Platform {
122 code: -1,
123 message: format!("Failed to set proxy interface: {}", e),
124 })?
125 .cache_properties(zbus::proxy::CacheProperties::No)
126 .build()
127 .map_err(|e| Error::Platform {
128 code: -1,
129 message: format!("Failed to create proxy: {}", e),
130 })
131 }
132
133 fn has_interface(&self, aref: &AccessibleRef, iface: &str) -> bool {
136 let proxy = match self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Accessible") {
137 Ok(p) => p,
138 Err(_) => return false,
139 };
140 let reply = match proxy.call_method("GetInterfaces", &()) {
141 Ok(r) => r,
142 Err(_) => return false,
143 };
144 let interfaces: Vec<String> = match reply.body().deserialize() {
145 Ok(v) => v,
146 Err(_) => return false,
147 };
148 interfaces.iter().any(|i| i.contains(iface))
149 }
150
151 fn get_role_number(&self, aref: &AccessibleRef) -> Result<u32> {
153 let proxy = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Accessible")?;
154 let reply = proxy
155 .call_method("GetRole", &())
156 .map_err(|e| Error::Platform {
157 code: -1,
158 message: format!("GetRole failed: {}", e),
159 })?;
160 reply
161 .body()
162 .deserialize::<u32>()
163 .map_err(|e| Error::Platform {
164 code: -1,
165 message: format!("GetRole deserialize failed: {}", e),
166 })
167 }
168
169 fn get_role_name(&self, aref: &AccessibleRef) -> Result<String> {
171 let proxy = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Accessible")?;
172 let reply = proxy
173 .call_method("GetRoleName", &())
174 .map_err(|e| Error::Platform {
175 code: -1,
176 message: format!("GetRoleName failed: {}", e),
177 })?;
178 reply
179 .body()
180 .deserialize::<String>()
181 .map_err(|e| Error::Platform {
182 code: -1,
183 message: format!("GetRoleName deserialize failed: {}", e),
184 })
185 }
186
187 fn check_chromium_a11y_enabled(
204 &self,
205 parent: &AccessibleRef,
206 role_hint: Option<Role>,
207 ) -> Result<()> {
208 let app_root = AccessibleRef {
209 bus_name: parent.bus_name.clone(),
210 path: "/org/a11y/atspi/accessible/root".to_string(),
211 };
212 let toolkit = match self
213 .make_proxy(
214 &app_root.bus_name,
215 &app_root.path,
216 "org.a11y.atspi.Application",
217 )
218 .ok()
219 .and_then(|proxy| proxy.get_property::<String>("ToolkitName").ok())
220 {
221 Some(t) => t,
222 None => return Ok(()),
223 };
224 if !toolkit.eq_ignore_ascii_case("Chromium") {
225 return Ok(());
226 }
227 let role = role_hint.unwrap_or_else(|| self.resolve_role(parent));
228 if role != Role::Window {
229 return Ok(());
230 }
231 let app_name = self.get_name(&app_root).unwrap_or_default();
232 Err(Error::AccessibilityNotEnabled {
233 app: app_name,
234 instructions: "Chromium/Electron app exposes an empty accessibility tree on Linux. \
235 Relaunch with `--force-renderer-accessibility` (or set the env var \
236 `ACCESSIBILITY_ENABLED=1`) so the renderer accessibility bridge is initialised."
237 .to_string(),
238 })
239 }
240
241 fn get_name(&self, aref: &AccessibleRef) -> Result<String> {
243 let proxy = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Accessible")?;
244 proxy
245 .get_property::<String>("Name")
246 .map_err(|e| Error::Platform {
247 code: -1,
248 message: format!("Get Name property failed: {}", e),
249 })
250 }
251
252 fn get_description(&self, aref: &AccessibleRef) -> Result<String> {
254 let proxy = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Accessible")?;
255 proxy
256 .get_property::<String>("Description")
257 .map_err(|e| Error::Platform {
258 code: -1,
259 message: format!("Get Description property failed: {}", e),
260 })
261 }
262
263 fn get_atspi_children(&self, aref: &AccessibleRef) -> Result<Vec<AccessibleRef>> {
267 let proxy = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Accessible")?;
268 let reply = proxy
269 .call_method("GetChildren", &())
270 .map_err(|e| Error::Platform {
271 code: -1,
272 message: format!("GetChildren failed: {}", e),
273 })?;
274 let children: Vec<(String, zbus::zvariant::OwnedObjectPath)> =
275 reply.body().deserialize().map_err(|e| Error::Platform {
276 code: -1,
277 message: format!("GetChildren deserialize failed: {}", e),
278 })?;
279 Ok(children
280 .into_iter()
281 .map(|(bus_name, path)| AccessibleRef {
282 bus_name,
283 path: path.to_string(),
284 })
285 .collect())
286 }
287
288 fn get_state(&self, aref: &AccessibleRef) -> Result<Vec<u32>> {
290 let proxy = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Accessible")?;
291 let reply = proxy
292 .call_method("GetState", &())
293 .map_err(|e| Error::Platform {
294 code: -1,
295 message: format!("GetState failed: {}", e),
296 })?;
297 reply
298 .body()
299 .deserialize::<Vec<u32>>()
300 .map_err(|e| Error::Platform {
301 code: -1,
302 message: format!("GetState deserialize failed: {}", e),
303 })
304 }
305
306 fn is_multi_line(&self, aref: &AccessibleRef) -> bool {
312 let state_bits = self.get_state(aref).unwrap_or_default();
313 let bits: u64 = if state_bits.len() >= 2 {
314 (state_bits[0] as u64) | ((state_bits[1] as u64) << 32)
315 } else if state_bits.len() == 1 {
316 state_bits[0] as u64
317 } else {
318 0
319 };
320 const MULTI_LINE: u64 = 1 << 17;
322 (bits & MULTI_LINE) != 0
323 }
324
325 fn get_extents(&self, aref: &AccessibleRef) -> Option<Rect> {
329 if !self.has_interface(aref, "Component") {
330 return None;
331 }
332 let proxy = self
333 .make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Component")
334 .ok()?;
335 let reply = proxy.call_method("GetExtents", &(0u32,)).ok()?;
338 let (x, y, w, h): (i32, i32, i32, i32) = reply.body().deserialize().ok()?;
339 if w <= 0 && h <= 0 {
340 return None;
341 }
342 Some(Rect {
343 x,
344 y,
345 width: w.max(0) as u32,
346 height: h.max(0) as u32,
347 })
348 }
349
350 fn get_actions(&self, aref: &AccessibleRef) -> (Vec<String>, HashMap<String, i32>) {
356 let mut actions = Vec::new();
357 let mut indices = HashMap::new();
358
359 if let Ok(proxy) = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Action") {
361 let n_actions = proxy
363 .get_property::<i32>("NActions")
364 .or_else(|_| proxy.get_property::<u32>("NActions").map(|n| n as i32))
365 .unwrap_or(0);
366 for i in 0..n_actions {
367 if let Ok(reply) = proxy.call_method("GetName", &(i,)) {
368 if let Ok(name) = reply.body().deserialize::<String>() {
369 if let Some(action_name) = map_atspi_action_name(&name) {
370 if !actions.contains(&action_name) {
371 indices.insert(action_name.clone(), i);
372 actions.push(action_name);
373 }
374 }
375 }
376 }
377 }
378 }
379
380 (actions, indices)
388 }
389
390 fn is_gtk_toolkit(&self, aref: &AccessibleRef) -> bool {
397 let app_root = AccessibleRef {
398 bus_name: aref.bus_name.clone(),
399 path: "/org/a11y/atspi/accessible/root".to_string(),
400 };
401 self.make_proxy(
402 &app_root.bus_name,
403 &app_root.path,
404 "org.a11y.atspi.Application",
405 )
406 .ok()
407 .and_then(|proxy| proxy.get_property::<String>("ToolkitName").ok())
408 .map(|t| t.eq_ignore_ascii_case("GTK"))
409 .unwrap_or(false)
410 }
411
412 fn find_gtk_press_fallback(
425 &self,
426 outer: &AccessibleRef,
427 outer_name: &str,
428 ) -> Option<(AccessibleRef, i32)> {
429 let mut queue: VecDeque<(AccessibleRef, u32)> = VecDeque::new();
430 queue.push_back((outer.clone(), 0));
431 let mut visited: usize = 0;
432 let mut shallowest_depth: Option<u32> = None;
433 let mut hits: Vec<(AccessibleRef, i32)> = Vec::new();
434
435 while let Some((node, depth)) = queue.pop_front() {
436 if let Some(best) = shallowest_depth {
438 if depth > best {
439 continue;
440 }
441 }
442 if visited > GTK_FALLBACK_MAX_NODES {
443 break;
444 }
445
446 let role_name = if depth == 0 {
447 String::new()
448 } else {
449 visited += 1;
450 self.get_role_name(&node).unwrap_or_default().to_lowercase()
451 };
452
453 if depth > 0 && is_actionable_atspi_role(&role_name) {
454 if let Some(idx) = self.gtk_fallback_pick(&node, outer_name) {
455 match shallowest_depth {
456 Some(d) if depth < d => {
457 shallowest_depth = Some(depth);
458 hits.clear();
459 hits.push((node.clone(), idx));
460 }
461 Some(d) if depth == d => hits.push((node.clone(), idx)),
462 Some(_) => {}
463 None => {
464 shallowest_depth = Some(depth);
465 hits.push((node.clone(), idx));
466 }
467 }
468 }
469 }
470
471 let stop_descending = depth >= GTK_FALLBACK_MAX_DEPTH
476 || (depth > 0 && is_never_descend_atspi_role(&role_name));
477 if stop_descending {
478 continue;
479 }
480 if let Ok(children) = self.get_atspi_children(&node) {
481 for c in children {
482 queue.push_back((c, depth + 1));
483 }
484 }
485 }
486
487 if hits.len() == 1 {
488 Some(hits.into_iter().next().unwrap())
489 } else {
490 None
491 }
492 }
493
494 fn gtk_fallback_pick(&self, aref: &AccessibleRef, outer_name: &str) -> Option<i32> {
497 let (_, index_map) = self.get_actions(aref);
498 let idx = *index_map.get("press")?;
499 if !self.is_showing_visible_sensitive(aref) {
500 return None;
501 }
502 if !outer_name.is_empty() {
506 let inner_name = self.get_name(aref).unwrap_or_default();
507 if !inner_name.is_empty() && inner_name != outer_name {
508 return None;
509 }
510 }
511 Some(idx)
512 }
513
514 fn is_showing_visible_sensitive(&self, aref: &AccessibleRef) -> bool {
519 let raw = self.get_state(aref).unwrap_or_default();
520 let bits: u64 = if raw.len() >= 2 {
521 (raw[0] as u64) | ((raw[1] as u64) << 32)
522 } else if raw.len() == 1 {
523 raw[0] as u64
524 } else {
525 0
526 };
527 const SENSITIVE: u64 = 1 << 24;
528 const SHOWING: u64 = 1 << 25;
529 const VISIBLE: u64 = 1 << 30;
530 (bits & (SHOWING | VISIBLE | SENSITIVE)) == (SHOWING | VISIBLE | SENSITIVE)
531 }
532
533 fn get_value(&self, aref: &AccessibleRef) -> Option<String> {
536 let text_value = self.get_text_content(aref);
540 if text_value.is_some() {
541 return text_value;
542 }
543 if let Ok(proxy) = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Value") {
545 if let Ok(val) = proxy.get_property::<f64>("CurrentValue") {
546 return Some(val.to_string());
547 }
548 }
549 None
550 }
551
552 fn get_text_content(&self, aref: &AccessibleRef) -> Option<String> {
554 let proxy = self
555 .make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Text")
556 .ok()?;
557 let char_count: i32 = proxy.get_property("CharacterCount").ok()?;
558 if char_count > 0 {
559 let reply = proxy.call_method("GetText", &(0i32, char_count)).ok()?;
560 let text: String = reply.body().deserialize().ok()?;
561 if !text.is_empty() {
562 return Some(text);
563 }
564 }
565 None
566 }
567
568 fn cache_element(&self, aref: AccessibleRef) -> u64 {
570 let handle = NEXT_HANDLE.fetch_add(1, Ordering::Relaxed);
571 self.handle_cache
572 .lock()
573 .unwrap_or_else(|e| e.into_inner())
574 .insert(handle, aref);
575 handle
576 }
577
578 fn get_cached(&self, handle: u64) -> Result<AccessibleRef> {
580 self.handle_cache
581 .lock()
582 .unwrap_or_else(|e| e.into_inner())
583 .get(&handle)
584 .cloned()
585 .ok_or(Error::ElementStale {
586 selector: format!("handle:{}", handle),
587 })
588 }
589
590 fn build_element_data(&self, aref: &AccessibleRef, pid: Option<u32>) -> ElementData {
595 let role_name = self.get_role_name(aref).unwrap_or_default();
596 let role_num = self.get_role_number(aref).unwrap_or(0);
597 let role = {
598 let by_name = if !role_name.is_empty() {
599 map_atspi_role(&role_name)
600 } else {
601 Role::Unknown
602 };
603 let coarse = if by_name != Role::Unknown {
604 by_name
605 } else {
606 map_atspi_role_number(role_num)
607 };
608 if coarse == Role::TextArea && !self.is_multi_line(aref) {
609 Role::TextField
610 } else {
611 coarse
612 }
613 };
614
615 let (
619 ((mut name, value), description),
620 (
621 (states, bounds),
622 ((actions, action_index_map), (numeric_value, min_value, max_value)),
623 ),
624 ) = rayon::join(
625 || {
626 rayon::join(
627 || {
628 let name = self.get_name(aref).ok().filter(|s| !s.is_empty());
629 let value = if role_has_value(role) {
630 self.get_value(aref)
631 } else {
632 None
633 };
634 (name, value)
635 },
636 || self.get_description(aref).ok().filter(|s| !s.is_empty()),
637 )
638 },
639 || {
640 rayon::join(
641 || {
642 rayon::join(
643 || self.parse_states(aref, role),
644 || {
645 if role != Role::Application {
646 self.get_extents(aref)
647 } else {
648 None
649 }
650 },
651 )
652 },
653 || {
654 rayon::join(
655 || {
656 if role_has_actions(role) {
657 self.get_actions(aref)
658 } else {
659 (vec![], HashMap::new())
660 }
661 },
662 || {
663 if matches!(
664 role,
665 Role::Slider
666 | Role::ProgressBar
667 | Role::ScrollBar
668 | Role::SpinButton
669 ) {
670 if let Ok(proxy) = self.make_proxy(
671 &aref.bus_name,
672 &aref.path,
673 "org.a11y.atspi.Value",
674 ) {
675 (
676 proxy.get_property::<f64>("CurrentValue").ok(),
677 proxy.get_property::<f64>("MinimumValue").ok(),
678 proxy.get_property::<f64>("MaximumValue").ok(),
679 )
680 } else {
681 (None, None, None)
682 }
683 } else {
684 (None, None, None)
685 }
686 },
687 )
688 },
689 )
690 },
691 );
692
693 if name.is_none() && role == Role::StaticText {
696 if let Some(ref v) = value {
697 name = Some(v.clone());
698 }
699 }
700
701 let raw = {
702 let raw_role = if role_name.is_empty() {
703 format!("role_num:{}", role_num)
704 } else {
705 role_name
706 };
707 let mut raw = HashMap::new();
708 raw.insert("atspi_role".into(), serde_json::Value::String(raw_role));
709 raw.insert(
710 "bus_name".into(),
711 serde_json::Value::String(aref.bus_name.clone()),
712 );
713 raw.insert(
714 "object_path".into(),
715 serde_json::Value::String(aref.path.clone()),
716 );
717 if let Some(ref n) = name {
720 raw.insert("atspi_name".into(), serde_json::Value::String(n.clone()));
721 }
722 if let Some(ref v) = value {
723 raw.insert("atspi_value".into(), serde_json::Value::String(v.clone()));
724 }
725 if let Some(ref d) = description {
726 raw.insert(
727 "atspi_description".into(),
728 serde_json::Value::String(d.clone()),
729 );
730 }
731 raw
732 };
733
734 let name = xa11y_core::text::strip_bidi_opt(name);
737 let value = xa11y_core::text::strip_bidi_opt(value);
738 let description = xa11y_core::text::strip_bidi_opt(description);
739
740 let handle = self.cache_element(aref.clone());
741 if !action_index_map.is_empty() {
742 self.action_indices
743 .lock()
744 .unwrap_or_else(|e| e.into_inner())
745 .insert(handle, action_index_map);
746 }
747
748 ElementData {
749 role,
750 name,
751 value,
752 description,
753 bounds,
754 actions,
755 states,
756 numeric_value,
757 min_value,
758 max_value,
759 pid,
760 stable_id: Some(aref.path.clone()),
761 raw,
762 handle,
763 }
764 }
765
766 fn get_atspi_parent(&self, aref: &AccessibleRef) -> Result<Option<AccessibleRef>> {
768 let proxy = self.make_proxy(
770 &aref.bus_name,
771 &aref.path,
772 "org.freedesktop.DBus.Properties",
773 )?;
774 let reply = proxy
775 .call_method("Get", &("org.a11y.atspi.Accessible", "Parent"))
776 .map_err(|e| Error::Platform {
777 code: -1,
778 message: format!("Get Parent property failed: {}", e),
779 })?;
780 let variant: zbus::zvariant::OwnedValue =
782 reply.body().deserialize().map_err(|e| Error::Platform {
783 code: -1,
784 message: format!("Parent deserialize variant failed: {}", e),
785 })?;
786 let (bus, path): (String, zbus::zvariant::OwnedObjectPath) =
787 zbus::zvariant::Value::from(variant).try_into().map_err(
788 |e: zbus::zvariant::Error| Error::Platform {
789 code: -1,
790 message: format!("Parent deserialize struct failed: {}", e),
791 },
792 )?;
793 let path_str = path.as_str();
794 if path_str == "/org/a11y/atspi/null" || bus.is_empty() || path_str.is_empty() {
795 return Ok(None);
796 }
797 if path_str == "/org/a11y/atspi/accessible/root" {
799 return Ok(None);
800 }
801 Ok(Some(AccessibleRef {
802 bus_name: bus,
803 path: path_str.to_string(),
804 }))
805 }
806
807 fn parse_states(&self, aref: &AccessibleRef, role: Role) -> StateSet {
809 let state_bits = self.get_state(aref).unwrap_or_default();
810
811 let bits: u64 = if state_bits.len() >= 2 {
813 (state_bits[0] as u64) | ((state_bits[1] as u64) << 32)
814 } else if state_bits.len() == 1 {
815 state_bits[0] as u64
816 } else {
817 0
818 };
819
820 const BUSY: u64 = 1 << 3;
822 const CHECKED: u64 = 1 << 4;
823 const EDITABLE: u64 = 1 << 7;
824 const ENABLED: u64 = 1 << 8;
825 const EXPANDABLE: u64 = 1 << 9;
826 const EXPANDED: u64 = 1 << 10;
827 const FOCUSABLE: u64 = 1 << 11;
828 const FOCUSED: u64 = 1 << 12;
829 const MODAL: u64 = 1 << 16;
830 const SELECTED: u64 = 1 << 23;
831 const SENSITIVE: u64 = 1 << 24;
832 const SHOWING: u64 = 1 << 25;
833 const VISIBLE: u64 = 1 << 30;
834 const INDETERMINATE: u64 = 1 << 32;
835 const REQUIRED: u64 = 1 << 33;
836
837 let enabled = (bits & ENABLED) != 0 || (bits & SENSITIVE) != 0;
838 let visible = (bits & VISIBLE) != 0 || (bits & SHOWING) != 0;
839
840 let checked = match role {
841 Role::CheckBox | Role::RadioButton | Role::MenuItem | Role::Switch => {
842 if (bits & INDETERMINATE) != 0 {
843 Some(Toggled::Mixed)
844 } else if (bits & CHECKED) != 0 {
845 Some(Toggled::On)
846 } else {
847 Some(Toggled::Off)
848 }
849 }
850 _ => None,
851 };
852
853 let expanded = if (bits & EXPANDABLE) != 0 {
854 Some((bits & EXPANDED) != 0)
855 } else {
856 None
857 };
858
859 StateSet {
860 enabled,
861 visible,
862 focused: (bits & FOCUSED) != 0,
863 checked,
864 selected: (bits & SELECTED) != 0,
865 expanded,
866 editable: (bits & EDITABLE) != 0,
867 focusable: (bits & FOCUSABLE) != 0,
868 modal: (bits & MODAL) != 0,
869 required: (bits & REQUIRED) != 0,
870 busy: (bits & BUSY) != 0,
871 }
872 }
873
874 pub(crate) fn find_app_by_pid(&self, pid: u32) -> Result<AccessibleRef> {
879 let registry = AccessibleRef {
880 bus_name: "org.a11y.atspi.Registry".to_string(),
881 path: "/org/a11y/atspi/accessible/root".to_string(),
882 };
883 let children = self.get_atspi_children(®istry)?;
884
885 for child in &children {
886 if child.path == "/org/a11y/atspi/null" {
887 continue;
888 }
889 if let Some(app_pid) = self.get_dbus_pid(&child.bus_name) {
895 if app_pid == pid {
896 return Ok(child.clone());
897 }
898 }
899 if let Ok(proxy) =
901 self.make_proxy(&child.bus_name, &child.path, "org.a11y.atspi.Application")
902 {
903 if let Ok(app_pid) = proxy.get_property::<i32>("Id") {
904 if app_pid as u32 == pid {
905 return Ok(child.clone());
906 }
907 }
908 }
909 }
910
911 Err(Error::Platform {
912 code: -1,
913 message: format!("No application found with PID {}", pid),
914 })
915 }
916
917 fn get_dbus_pid(&self, bus_name: &str) -> Option<u32> {
919 let proxy = self
920 .make_proxy(
921 "org.freedesktop.DBus",
922 "/org/freedesktop/DBus",
923 "org.freedesktop.DBus",
924 )
925 .ok()?;
926 let reply = proxy
927 .call_method("GetConnectionUnixProcessID", &(bus_name,))
928 .ok()?;
929 let pid: u32 = reply.body().deserialize().ok()?;
930 if pid > 0 {
931 Some(pid)
932 } else {
933 None
934 }
935 }
936
937 fn do_atspi_action_by_index(&self, aref: &AccessibleRef, index: i32) -> Result<()> {
939 let proxy = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Action")?;
940 proxy
941 .call_method("DoAction", &(index,))
942 .map_err(|e| Error::Platform {
943 code: -1,
944 message: format!("DoAction({}) failed: {}", index, e),
945 })?;
946 Ok(())
947 }
948
949 fn get_action_index(&self, handle: u64, action: &str) -> Result<i32> {
951 self.action_indices
952 .lock()
953 .unwrap_or_else(|e| e.into_inner())
954 .get(&handle)
955 .and_then(|map| map.get(action).copied())
956 .ok_or_else(|| Error::ActionNotSupported {
957 action: action.to_string(),
958 role: Role::Unknown, })
960 }
961
962 fn get_app_pid(&self, aref: &AccessibleRef) -> Option<u32> {
968 if let Some(pid) = self.get_dbus_pid(&aref.bus_name) {
969 return Some(pid);
970 }
971
972 if let Ok(proxy) = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Application")
974 {
975 if let Ok(pid) = proxy.get_property::<i32>("Id") {
976 if pid > 0 {
977 return Some(pid as u32);
978 }
979 }
980 }
981
982 None
983 }
984
985 fn resolve_role(&self, aref: &AccessibleRef) -> Role {
987 let role_name = self.get_role_name(aref).unwrap_or_default();
988 let by_name = if !role_name.is_empty() {
989 map_atspi_role(&role_name)
990 } else {
991 Role::Unknown
992 };
993 let coarse = if by_name != Role::Unknown {
994 by_name
995 } else {
996 let role_num = self.get_role_number(aref).unwrap_or(0);
998 map_atspi_role_number(role_num)
999 };
1000 if coarse == Role::TextArea && !self.is_multi_line(aref) {
1002 Role::TextField
1003 } else {
1004 coarse
1005 }
1006 }
1007
1008 fn matches_ref(
1022 &self,
1023 aref: &AccessibleRef,
1024 simple: &xa11y_core::selector::SimpleSelector,
1025 ) -> bool {
1026 let needs_role = simple.role.is_some()
1029 || simple
1030 .filters
1031 .iter()
1032 .any(|f| matches!(f.attr.as_str(), "role" | "checked"));
1033 let role = if needs_role {
1034 Some(self.resolve_role(aref))
1035 } else {
1036 None
1037 };
1038
1039 if let Some(ref role_match) = simple.role {
1040 match role_match {
1041 xa11y_core::selector::RoleMatch::Normalized(expected) => {
1042 if role != Some(*expected) {
1043 return false;
1044 }
1045 }
1046 xa11y_core::selector::RoleMatch::Platform(platform_role) => {
1047 let raw_role = self.get_role_name(aref).unwrap_or_default();
1048 if raw_role != *platform_role {
1049 return false;
1050 }
1051 }
1052 }
1053 }
1054
1055 let mut state_set: Option<StateSet> = None;
1056
1057 for filter in &simple.filters {
1058 let attr = filter.attr.as_str();
1059 let resolved: Option<Option<String>> = match attr {
1060 "role" => Some(role.map(|r| r.to_snake_case().to_string())),
1061 "name" => {
1062 let name = self.get_name(aref).ok().filter(|s| !s.is_empty());
1063 let resolved = if name.is_none() && role == Some(Role::StaticText) {
1066 self.get_value(aref)
1067 } else {
1068 name
1069 };
1070 Some(resolved)
1071 }
1072 "value" => Some(self.get_value(aref)),
1073 "description" => Some(self.get_description(aref).ok().filter(|s| !s.is_empty())),
1074 "enabled" | "visible" | "focused" | "focusable" | "selected" | "editable"
1075 | "modal" | "required" | "busy" | "expanded" | "checked" => {
1076 let s = state_set.get_or_insert_with(|| {
1077 self.parse_states(aref, role.unwrap_or(Role::Unknown))
1081 });
1082 Some(state_attr_to_string(attr, s))
1083 }
1084 _ => None, };
1086
1087 match resolved {
1088 Some(value) => {
1089 if !xa11y_core::selector::match_op(&filter.op, &filter.value, value.as_deref())
1090 {
1091 return false;
1092 }
1093 }
1094 None => {
1095 let pid = None; let data = self.build_element_data(aref, pid);
1103 return xa11y_core::selector::matches_simple(&data, simple);
1104 }
1105 }
1106 }
1107
1108 true
1109 }
1110
1111 fn collect_matching_refs_group(
1121 &self,
1122 parent: &AccessibleRef,
1123 clauses: &[&xa11y_core::selector::SimpleSelector],
1124 depth: u32,
1125 max_depth: u32,
1126 limit: Option<usize>,
1127 ) -> Result<Vec<(usize, AccessibleRef)>> {
1128 if depth > max_depth {
1129 return Ok(vec![]);
1130 }
1131
1132 let children = self.get_atspi_children(parent)?;
1133
1134 let parent_is_registry = parent.bus_name == "org.a11y.atspi.Registry";
1142 let mut to_search: Vec<AccessibleRef> = Vec::new();
1143 for child in children {
1144 if child.path == "/org/a11y/atspi/null"
1145 || child.bus_name.is_empty()
1146 || child.path.is_empty()
1147 {
1148 continue;
1149 }
1150
1151 if !parent_is_registry {
1152 let child_role = self.get_role_name(&child).unwrap_or_default();
1153 if child_role == "application" {
1154 let grandchildren = self.get_atspi_children(&child).unwrap_or_default();
1155 for gc in grandchildren {
1156 if gc.path == "/org/a11y/atspi/null"
1157 || gc.bus_name.is_empty()
1158 || gc.path.is_empty()
1159 {
1160 continue;
1161 }
1162 let gc_role = self.get_role_name(&gc).unwrap_or_default();
1163 if gc_role == "application" {
1164 continue;
1165 }
1166 to_search.push(gc);
1167 }
1168 continue;
1169 }
1170 }
1171 to_search.push(child);
1172 }
1173
1174 if to_search.is_empty() {
1177 self.check_chromium_a11y_enabled(parent, None)?;
1178 }
1179
1180 type ChildBatch = (Vec<(usize, AccessibleRef)>, Option<Error>);
1195 let per_child: Vec<ChildBatch> = to_search
1196 .par_iter()
1197 .map(|child| {
1198 let mut child_results: Vec<(usize, AccessibleRef)> = Vec::new();
1199 for (idx, simple) in clauses.iter().enumerate() {
1200 if self.matches_ref(child, simple) {
1201 child_results.push((idx, child.clone()));
1202 }
1203 }
1204 match self.collect_matching_refs_group(child, clauses, depth + 1, max_depth, limit)
1205 {
1206 Ok(sub) => {
1207 child_results.extend(sub);
1208 (child_results, None)
1209 }
1210 Err(e @ Error::AccessibilityNotEnabled { .. }) => (Vec::new(), Some(e)),
1211 Err(_) => (child_results, None),
1212 }
1213 })
1214 .collect();
1215
1216 let mut results = Vec::new();
1228 for (batch, maybe_err) in per_child {
1229 if let Some(err) = maybe_err {
1230 return Err(err);
1231 }
1232 for r in batch {
1233 results.push(r);
1234 if let Some(limit) = limit {
1235 if results.len() >= limit {
1236 return Ok(results);
1237 }
1238 }
1239 }
1240 }
1241 Ok(results)
1242 }
1243}
1244
1245impl Provider for LinuxProvider {
1246 fn get_children(&self, element: Option<&ElementData>) -> Result<Vec<ElementData>> {
1247 match element {
1248 None => {
1249 self.list_apps()
1252 }
1253 Some(element_data) => {
1254 let aref = self.get_cached(element_data.handle)?;
1255 let children = self.get_atspi_children(&aref).unwrap_or_default();
1256 let pid = element_data.pid;
1257
1258 let mut to_build: Vec<AccessibleRef> = Vec::new();
1261 for child_ref in &children {
1262 if child_ref.path == "/org/a11y/atspi/null"
1263 || child_ref.bus_name.is_empty()
1264 || child_ref.path.is_empty()
1265 {
1266 continue;
1267 }
1268 let child_role = self.get_role_name(child_ref).unwrap_or_default();
1269 if child_role == "application" {
1270 let grandchildren = self.get_atspi_children(child_ref).unwrap_or_default();
1271 for gc_ref in grandchildren {
1272 if gc_ref.path == "/org/a11y/atspi/null"
1273 || gc_ref.bus_name.is_empty()
1274 || gc_ref.path.is_empty()
1275 {
1276 continue;
1277 }
1278 let gc_role = self.get_role_name(&gc_ref).unwrap_or_default();
1279 if gc_role == "application" {
1280 continue;
1281 }
1282 to_build.push(gc_ref);
1283 }
1284 continue;
1285 }
1286 to_build.push(child_ref.clone());
1287 }
1288
1289 if to_build.is_empty() {
1290 self.check_chromium_a11y_enabled(&aref, Some(element_data.role))?;
1291 }
1292
1293 let results: Vec<ElementData> = to_build
1294 .par_iter()
1295 .map(|r| self.build_element_data(r, pid))
1296 .collect();
1297
1298 Ok(results)
1299 }
1300 }
1301 }
1302
1303 fn find_elements_group(
1304 &self,
1305 root: &ElementData,
1306 group: &xa11y_core::selector::SelectorGroup,
1307 limit: Option<usize>,
1308 max_depth: Option<u32>,
1309 ) -> Result<Vec<ElementData>> {
1310 if group.clauses.is_empty() {
1311 return Ok(vec![]);
1312 }
1313 if group.clauses.iter().any(|c| c.segments.is_empty()) {
1316 return Ok(vec![]);
1317 }
1318
1319 let max_depth_val = max_depth.unwrap_or(xa11y_core::MAX_TREE_DEPTH);
1329
1330 let firsts: Vec<&xa11y_core::selector::SimpleSelector> = group
1331 .clauses
1332 .iter()
1333 .map(|c| &c.segments[0].simple)
1334 .collect();
1335
1336 let start_ref = self.get_cached(root.handle)?;
1337
1338 let phase1_limit = if group.clauses.len() == 1 {
1347 let clause = &group.clauses[0];
1348 let first = firsts[0];
1349 let outer = if clause.segments.len() == 1 {
1350 limit
1351 } else {
1352 None
1353 };
1354 match (outer, first.nth) {
1355 (Some(l), Some(n)) => Some(l.max(n)),
1356 (_, Some(n)) => Some(n),
1357 (l, None) => l,
1358 }
1359 } else {
1360 None
1361 };
1362
1363 let phase1: Vec<(usize, AccessibleRef)> =
1364 self.collect_matching_refs_group(&start_ref, &firsts, 0, max_depth_val, phase1_limit)?;
1365
1366 let pid_from_root = root.pid;
1367
1368 let mut by_clause: Vec<Vec<(usize, AccessibleRef)>> =
1372 (0..group.clauses.len()).map(|_| Vec::new()).collect();
1373 for (walk_pos, (clause_idx, aref)) in phase1.into_iter().enumerate() {
1374 by_clause[clause_idx].push((walk_pos, aref));
1375 }
1376
1377 let mut merged: Vec<(usize, ElementData)> = Vec::new();
1385 for (clause_idx, hits) in by_clause.into_iter().enumerate() {
1386 if hits.is_empty() {
1387 continue;
1388 }
1389 let clause = &group.clauses[clause_idx];
1390 let phase1_data: Vec<(usize, ElementData)> = hits
1392 .par_iter()
1393 .map(|(pos, aref)| {
1394 (*pos, self.build_element_data(aref, pid_from_root))
1398 })
1399 .collect();
1400
1401 if clause.segments.len() == 1 {
1402 let mut candidates = phase1_data;
1404 if let Some(nth) = clause.segments[0].simple.nth {
1405 if nth <= candidates.len() {
1406 let kept = candidates.remove(nth - 1);
1407 candidates.clear();
1408 candidates.push(kept);
1409 } else {
1410 candidates.clear();
1411 }
1412 }
1413 merged.extend(candidates);
1414 continue;
1415 }
1416
1417 for (anchor_pos, head) in phase1_data {
1422 let narrowed = self.narrow_multi_segment(
1423 vec![head],
1424 &clause.segments[1..],
1425 max_depth_val,
1426 None,
1427 )?;
1428 for n in narrowed {
1429 merged.push((anchor_pos, n));
1430 }
1431 }
1432 }
1433
1434 merged.sort_by_key(|(pos, _)| *pos);
1438 let mut seen: HashSet<(String, String)> = HashSet::new();
1439 let mut out: Vec<ElementData> = Vec::with_capacity(merged.len());
1440 for (_, data) in merged {
1441 if let Ok(aref) = self.get_cached(data.handle) {
1445 if !seen.insert((aref.bus_name.clone(), aref.path.clone())) {
1446 continue;
1447 }
1448 }
1449 out.push(data);
1450 }
1451 if let Some(l) = limit {
1452 out.truncate(l);
1453 }
1454 Ok(out)
1455 }
1456
1457 fn get_parent(&self, element: &ElementData) -> Result<Option<ElementData>> {
1458 let aref = self.get_cached(element.handle)?;
1459 match self.get_atspi_parent(&aref)? {
1460 Some(parent_ref) => {
1461 let data = self.build_element_data(&parent_ref, element.pid);
1462 Ok(Some(data))
1463 }
1464 None => Ok(None),
1465 }
1466 }
1467
1468 fn list_apps(&self) -> Result<Vec<ElementData>> {
1475 let registry = AccessibleRef {
1476 bus_name: "org.a11y.atspi.Registry".to_string(),
1477 path: "/org/a11y/atspi/accessible/root".to_string(),
1478 };
1479 let children = self.get_atspi_children(®istry)?;
1480
1481 let valid: Vec<(&AccessibleRef, String)> = children
1483 .iter()
1484 .filter(|c| c.path != "/org/a11y/atspi/null")
1485 .filter_map(|c| {
1486 let name = self.get_name(c).unwrap_or_default();
1487 if name.is_empty() {
1488 None
1489 } else {
1490 Some((c, name))
1491 }
1492 })
1493 .collect();
1494
1495 let results: Vec<ElementData> = valid
1496 .par_iter()
1497 .map(|(child, app_name)| {
1498 let pid = self.get_app_pid(child);
1499 let mut data = self.build_element_data(child, pid);
1500 data.name = Some(app_name.clone());
1501 data
1502 })
1503 .collect();
1504
1505 Ok(results)
1506 }
1507
1508 fn press(&self, element: &ElementData) -> Result<()> {
1509 let target = self.get_cached(element.handle)?;
1510 if let Ok(index) = self.get_action_index(element.handle, "press") {
1512 return self.do_atspi_action_by_index(&target, index);
1513 }
1514 if element.role == Role::Button && self.is_gtk_toolkit(&target) {
1533 let outer_name = self.get_name(&target).unwrap_or_default();
1534 if let Some((inner, index)) = self.find_gtk_press_fallback(&target, &outer_name) {
1535 return self.do_atspi_action_by_index(&inner, index);
1536 }
1537 }
1538 Err(Error::ActionNotSupported {
1539 action: "press".to_string(),
1540 role: element.role,
1541 })
1542 }
1543
1544 fn focus(&self, element: &ElementData) -> Result<()> {
1545 let target = self.get_cached(element.handle)?;
1546 if let Ok(proxy) =
1550 self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Component")
1551 {
1552 if let Ok(reply) = proxy.call_method("GrabFocus", &()) {
1553 if let Ok(true) = reply.body().deserialize::<bool>() {
1555 return Ok(());
1556 }
1557 }
1559 }
1560 if let Ok(index) = self.get_action_index(element.handle, "focus") {
1561 return self.do_atspi_action_by_index(&target, index);
1562 }
1563 Err(Error::ActionNotSupported {
1564 action: "focus".to_string(),
1565 role: element.role,
1566 })
1567 }
1568
1569 fn blur(&self, element: &ElementData) -> Result<()> {
1570 let target = self.get_cached(element.handle)?;
1571 if let Some(parent_ref) = self.get_atspi_parent(&target)? {
1575 if parent_ref.path != "/org/a11y/atspi/null" {
1576 let p = self.make_proxy(
1577 &parent_ref.bus_name,
1578 &parent_ref.path,
1579 "org.a11y.atspi.Component",
1580 )?;
1581 p.call_method("GrabFocus", &())
1582 .map_err(|e| Error::Platform {
1583 code: -1,
1584 message: format!("Component.GrabFocus on parent failed: {}", e),
1585 })?;
1586 return Ok(());
1587 }
1588 }
1589 Err(Error::ActionNotSupported {
1590 action: "blur".to_string(),
1591 role: element.role,
1592 })
1593 }
1594
1595 fn toggle(&self, element: &ElementData) -> Result<()> {
1596 let target = self.get_cached(element.handle)?;
1597 let index = self
1598 .get_action_index(element.handle, "toggle")
1599 .map_err(|_| Error::ActionNotSupported {
1600 action: "toggle".to_string(),
1601 role: element.role,
1602 })?;
1603 self.do_atspi_action_by_index(&target, index)
1604 }
1605
1606 fn select(&self, element: &ElementData) -> Result<()> {
1607 let target = self.get_cached(element.handle)?;
1608 let index = self
1609 .get_action_index(element.handle, "select")
1610 .map_err(|_| Error::ActionNotSupported {
1611 action: "select".to_string(),
1612 role: element.role,
1613 })?;
1614 self.do_atspi_action_by_index(&target, index)
1615 }
1616
1617 fn expand(&self, element: &ElementData) -> Result<()> {
1618 let target = self.get_cached(element.handle)?;
1619 let index = self
1620 .get_action_index(element.handle, "expand")
1621 .map_err(|_| Error::ActionNotSupported {
1622 action: "expand".to_string(),
1623 role: element.role,
1624 })?;
1625 self.do_atspi_action_by_index(&target, index)
1626 }
1627
1628 fn collapse(&self, element: &ElementData) -> Result<()> {
1629 let target = self.get_cached(element.handle)?;
1630 let index = self
1631 .get_action_index(element.handle, "collapse")
1632 .map_err(|_| Error::ActionNotSupported {
1633 action: "collapse".to_string(),
1634 role: element.role,
1635 })?;
1636 self.do_atspi_action_by_index(&target, index)
1637 }
1638
1639 fn show_menu(&self, element: &ElementData) -> Result<()> {
1640 let target = self.get_cached(element.handle)?;
1641 let index = self
1642 .get_action_index(element.handle, "show_menu")
1643 .map_err(|_| Error::ActionNotSupported {
1644 action: "show_menu".to_string(),
1645 role: element.role,
1646 })?;
1647 self.do_atspi_action_by_index(&target, index)
1648 }
1649
1650 fn increment(&self, element: &ElementData) -> Result<()> {
1651 let target = self.get_cached(element.handle)?;
1652 if let Ok(index) = self.get_action_index(element.handle, "increment") {
1654 return self.do_atspi_action_by_index(&target, index);
1655 }
1656 let proxy = self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Value")?;
1657 let current: f64 = proxy
1658 .get_property("CurrentValue")
1659 .map_err(|e| Error::Platform {
1660 code: -1,
1661 message: format!("Value.CurrentValue failed: {}", e),
1662 })?;
1663 let step: f64 = proxy.get_property("MinimumIncrement").unwrap_or(1.0);
1664 let step = if step <= 0.0 { 1.0 } else { step };
1665 proxy
1666 .set_property("CurrentValue", current + step)
1667 .map_err(|e| Error::Platform {
1668 code: -1,
1669 message: format!("Value.SetCurrentValue failed: {}", e),
1670 })
1671 }
1672
1673 fn decrement(&self, element: &ElementData) -> Result<()> {
1674 let target = self.get_cached(element.handle)?;
1675 if let Ok(index) = self.get_action_index(element.handle, "decrement") {
1676 return self.do_atspi_action_by_index(&target, index);
1677 }
1678 let proxy = self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Value")?;
1679 let current: f64 = proxy
1680 .get_property("CurrentValue")
1681 .map_err(|e| Error::Platform {
1682 code: -1,
1683 message: format!("Value.CurrentValue failed: {}", e),
1684 })?;
1685 let step: f64 = proxy.get_property("MinimumIncrement").unwrap_or(1.0);
1686 let step = if step <= 0.0 { 1.0 } else { step };
1687 proxy
1688 .set_property("CurrentValue", current - step)
1689 .map_err(|e| Error::Platform {
1690 code: -1,
1691 message: format!("Value.SetCurrentValue failed: {}", e),
1692 })
1693 }
1694
1695 fn scroll_into_view(&self, element: &ElementData) -> Result<()> {
1696 let target = self.get_cached(element.handle)?;
1697 let proxy = self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Component")?;
1698 proxy
1699 .call_method("ScrollTo", &(0u32,))
1700 .map_err(|e| Error::Platform {
1701 code: -1,
1702 message: format!("ScrollTo failed: {}", e),
1703 })?;
1704 Ok(())
1705 }
1706
1707 fn set_value(&self, element: &ElementData, value: &str) -> Result<()> {
1708 let target = self.get_cached(element.handle)?;
1709 let proxy = self
1710 .make_proxy(
1711 &target.bus_name,
1712 &target.path,
1713 "org.a11y.atspi.EditableText",
1714 )
1715 .map_err(|_| Error::TextValueNotSupported)?;
1716 if proxy.call_method("SetTextContents", &(value,)).is_ok() {
1718 return Ok(());
1719 }
1720 let classify_editable_text_error = |op: &str, e: zbus::Error| -> Error {
1726 let msg = e.to_string();
1727 if msg.contains("UnknownMethod") || msg.contains("UnknownInterface") {
1728 Error::TextValueNotSupported
1729 } else {
1730 Error::Platform {
1731 code: -1,
1732 message: format!("EditableText.{} failed: {}", op, msg),
1733 }
1734 }
1735 };
1736 if let Err(e) = proxy.call_method("DeleteText", &(0i32, i32::MAX)) {
1737 return Err(classify_editable_text_error("DeleteText", e));
1738 }
1739 if let Err(e) = proxy.call_method("InsertText", &(0i32, value, value.len() as i32)) {
1740 return Err(classify_editable_text_error("InsertText", e));
1741 }
1742 Ok(())
1743 }
1744
1745 fn set_numeric_value(&self, element: &ElementData, value: f64) -> Result<()> {
1746 let target = self.get_cached(element.handle)?;
1747 let proxy = self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Value")?;
1748 proxy
1749 .set_property("CurrentValue", value)
1750 .map_err(|e| Error::Platform {
1751 code: -1,
1752 message: format!("SetValue failed: {}", e),
1753 })
1754 }
1755
1756 fn type_text(&self, element: &ElementData, text: &str) -> Result<()> {
1757 let target = self.get_cached(element.handle)?;
1758 let text_proxy = self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Text");
1761 let insert_pos = text_proxy
1762 .as_ref()
1763 .ok()
1764 .and_then(|p| p.get_property::<i32>("CaretOffset").ok())
1765 .unwrap_or(-1); let proxy = self
1768 .make_proxy(
1769 &target.bus_name,
1770 &target.path,
1771 "org.a11y.atspi.EditableText",
1772 )
1773 .map_err(|_| Error::TextValueNotSupported)?;
1774 let pos = if insert_pos >= 0 {
1775 insert_pos
1776 } else {
1777 i32::MAX
1778 };
1779 proxy
1780 .call_method("InsertText", &(pos, text, text.len() as i32))
1781 .map_err(|e| Error::Platform {
1782 code: -1,
1783 message: format!("EditableText.InsertText failed: {}", e),
1784 })?;
1785 Ok(())
1786 }
1787
1788 fn set_text_selection(&self, element: &ElementData, start: u32, end: u32) -> Result<()> {
1789 let target = self.get_cached(element.handle)?;
1790 let proxy = self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Text")?;
1791 proxy
1792 .call_method("SetSelection", &(0i32, start as i32, end as i32))
1793 .map_err(|e| Error::Platform {
1794 code: -1,
1795 message: format!("Text.SetSelection failed: {}", e),
1796 })?;
1797 Ok(())
1798 }
1799
1800 fn perform_action(&self, element: &ElementData, action: &str) -> Result<()> {
1801 match action {
1802 "press" => self.press(element),
1803 "focus" => self.focus(element),
1804 "blur" => self.blur(element),
1805 "toggle" => self.toggle(element),
1806 "select" => self.select(element),
1807 "expand" => self.expand(element),
1808 "collapse" => self.collapse(element),
1809 "show_menu" => self.show_menu(element),
1810 "increment" => self.increment(element),
1811 "decrement" => self.decrement(element),
1812 "scroll_into_view" => self.scroll_into_view(element),
1813 _ => Err(Error::ActionNotSupported {
1814 action: action.to_string(),
1815 role: element.role,
1816 }),
1817 }
1818 }
1819
1820 fn subscribe(&self, element: &ElementData) -> Result<Subscription> {
1821 let pid = element.pid.ok_or(Error::Platform {
1822 code: -1,
1823 message: "Element has no PID for subscribe".to_string(),
1824 })?;
1825 let app_name = element.name.clone().unwrap_or_default();
1826 crate::events::subscribe_for_pid(self, pid, app_name)
1827 }
1828}
1829
1830fn role_has_value(role: Role) -> bool {
1833 !matches!(
1834 role,
1835 Role::Application
1836 | Role::Window
1837 | Role::Dialog
1838 | Role::Group
1839 | Role::MenuBar
1840 | Role::Toolbar
1841 | Role::TabGroup
1842 | Role::SplitGroup
1843 | Role::Table
1844 | Role::TableRow
1845 | Role::Separator
1846 )
1847}
1848
1849fn role_has_actions(role: Role) -> bool {
1852 matches!(
1853 role,
1854 Role::Button
1855 | Role::CheckBox
1856 | Role::RadioButton
1857 | Role::MenuItem
1858 | Role::Link
1859 | Role::ComboBox
1860 | Role::TextField
1861 | Role::TextArea
1862 | Role::SpinButton
1863 | Role::Tab
1864 | Role::TreeItem
1865 | Role::ListItem
1866 | Role::ScrollBar
1867 | Role::Slider
1868 | Role::Menu
1869 | Role::Image
1870 | Role::Unknown
1871 )
1872}
1873
1874pub(crate) fn map_atspi_role(role_name: &str) -> Role {
1876 match role_name.to_lowercase().as_str() {
1877 "application" => Role::Application,
1878 "window" | "frame" => Role::Window,
1879 "dialog" | "file chooser" => Role::Dialog,
1880 "alert" | "notification" => Role::Alert,
1881 "push button" | "push button menu" => Role::Button,
1882 "toggle button" => Role::Switch,
1883 "check box" | "check menu item" => Role::CheckBox,
1884 "radio button" | "radio menu item" => Role::RadioButton,
1885 "entry" | "password text" => Role::TextField,
1886 "spin button" | "spinbutton" => Role::SpinButton,
1887 "text" | "textbox" => Role::TextArea,
1891 "label" | "static" | "caption" => Role::StaticText,
1892 "combo box" | "combobox" => Role::ComboBox,
1893 "list" | "list box" | "listbox" => Role::List,
1895 "list item" => Role::ListItem,
1896 "menu" => Role::Menu,
1897 "menu item" | "tearoff menu item" => Role::MenuItem,
1898 "menu bar" => Role::MenuBar,
1899 "page tab" => Role::Tab,
1900 "page tab list" => Role::TabGroup,
1901 "table" | "tree table" => Role::Table,
1902 "table row" => Role::TableRow,
1903 "table cell" | "table column header" | "table row header" => Role::TableCell,
1904 "tool bar" => Role::Toolbar,
1905 "scroll bar" => Role::ScrollBar,
1906 "slider" => Role::Slider,
1907 "image" | "icon" | "desktop icon" => Role::Image,
1908 "link" => Role::Link,
1909 "panel" | "section" | "form" | "filler" | "viewport" | "scroll pane" => Role::Group,
1910 "progress bar" => Role::ProgressBar,
1911 "tree item" => Role::TreeItem,
1912 "document web" | "document frame" => Role::WebArea,
1913 "heading" => Role::Heading,
1914 "separator" => Role::Separator,
1915 "split pane" => Role::SplitGroup,
1916 "tooltip" | "tool tip" => Role::Tooltip,
1917 "status bar" | "statusbar" => Role::Status,
1918 "landmark" | "navigation" => Role::Navigation,
1919 _ => xa11y_core::unknown_role(role_name),
1920 }
1921}
1922
1923pub(crate) fn map_atspi_role_number(role: u32) -> Role {
1926 match role {
1927 2 => Role::Alert, 7 => Role::CheckBox, 8 => Role::CheckBox, 11 => Role::ComboBox, 16 => Role::Dialog, 19 => Role::Dialog, 20 => Role::Group, 23 => Role::Window, 26 => Role::Image, 27 => Role::Image, 29 => Role::StaticText, 31 => Role::List, 32 => Role::ListItem, 33 => Role::Menu, 34 => Role::MenuBar, 35 => Role::MenuItem, 37 => Role::Tab, 38 => Role::TabGroup, 39 => Role::Group, 40 => Role::TextField, 42 => Role::ProgressBar, 43 => Role::Button, 44 => Role::RadioButton, 45 => Role::RadioButton, 48 => Role::ScrollBar, 49 => Role::Group, 50 => Role::Separator, 51 => Role::Slider, 52 => Role::SpinButton, 53 => Role::SplitGroup, 55 => Role::Table, 56 => Role::TableCell, 57 => Role::TableCell, 58 => Role::TableCell, 61 => Role::TextArea, 62 => Role::Switch, 63 => Role::Toolbar, 65 => Role::Group, 66 => Role::Table, 67 => Role::Unknown, 68 => Role::Group, 69 => Role::Window, 75 => Role::Application, 78 => Role::TextArea, 79 => Role::TextField, 82 => Role::WebArea, 83 => Role::Heading, 85 => Role::Group, 86 => Role::Group, 87 => Role::Group, 88 => Role::Link, 90 => Role::TableRow, 91 => Role::TreeItem, 95 => Role::WebArea, 97 => Role::List, 98 => Role::List, 93 => Role::Tooltip, 101 => Role::Alert, 116 => Role::StaticText, 129 => Role::Button, _ => xa11y_core::unknown_role(&format!("AT-SPI role number {role}")),
1989 }
1990}
1991
1992const GTK_FALLBACK_MAX_DEPTH: u32 = 3;
1996
1997const GTK_FALLBACK_MAX_NODES: usize = 200;
2000
2001fn is_actionable_atspi_role(role: &str) -> bool {
2006 matches!(
2007 role,
2008 "push button"
2009 | "toggle button"
2010 | "check box"
2011 | "radio button"
2012 | "menu item"
2013 | "check menu item"
2014 | "radio menu item"
2015 | "link"
2016 | "page tab"
2017 | "tab"
2018 | "list item"
2019 | "tree item"
2020 )
2021}
2022
2023fn is_never_descend_atspi_role(role: &str) -> bool {
2028 matches!(
2029 role,
2030 "label" | "separator" | "image" | "icon" | "static" | "caption"
2031 )
2032}
2033
2034fn map_atspi_action_name(action_name: &str) -> Option<String> {
2048 let lower = action_name.to_lowercase();
2057 let collapsed: String = lower.chars().filter(|c| !matches!(c, '_' | ' ')).collect();
2058 let canonical = match collapsed.as_str() {
2059 "click" | "activate" | "press" | "invoke" | "dodefault" => "press",
2060 "toggle" | "check" | "uncheck" => "toggle",
2061 "expand" | "open" => "expand",
2062 "collapse" | "close" => "collapse",
2063 "select" => "select",
2064 "menu" | "showmenu" | "showcontextmenu" | "contextmenu" | "popup" => "show_menu",
2065 "increment" => "increment",
2066 "decrement" => "decrement",
2067 _ => return None,
2068 };
2069 Some(canonical.to_string())
2070}
2071
2072#[cfg(test)]
2073mod tests {
2074 use super::*;
2075
2076 #[test]
2077 fn test_role_mapping() {
2078 assert_eq!(map_atspi_role("push button"), Role::Button);
2079 assert_eq!(map_atspi_role("toggle button"), Role::Switch);
2080 assert_eq!(map_atspi_role("check box"), Role::CheckBox);
2081 assert_eq!(map_atspi_role("entry"), Role::TextField);
2082 assert_eq!(map_atspi_role("label"), Role::StaticText);
2083 assert_eq!(map_atspi_role("window"), Role::Window);
2084 assert_eq!(map_atspi_role("frame"), Role::Window);
2085 assert_eq!(map_atspi_role("dialog"), Role::Dialog);
2086 assert_eq!(map_atspi_role("combo box"), Role::ComboBox);
2087 assert_eq!(map_atspi_role("slider"), Role::Slider);
2088 assert_eq!(map_atspi_role("panel"), Role::Group);
2089 assert_eq!(map_atspi_role("unknown_thing"), Role::Unknown);
2090 }
2091
2092 #[test]
2093 fn test_numeric_role_mapping() {
2094 assert_eq!(map_atspi_role_number(62), Role::Switch);
2097 assert_eq!(map_atspi_role_number(43), Role::Button); assert_eq!(map_atspi_role_number(7), Role::CheckBox);
2100 assert_eq!(map_atspi_role_number(67), Role::Unknown); }
2102
2103 #[test]
2104 fn test_action_name_mapping() {
2105 assert_eq!(map_atspi_action_name("click"), Some("press".to_string()));
2106 assert_eq!(map_atspi_action_name("activate"), Some("press".to_string()));
2107 assert_eq!(map_atspi_action_name("press"), Some("press".to_string()));
2108 assert_eq!(map_atspi_action_name("invoke"), Some("press".to_string()));
2109 assert_eq!(
2111 map_atspi_action_name("doDefault"),
2112 Some("press".to_string())
2113 );
2114 assert_eq!(
2115 map_atspi_action_name("do_default"),
2116 Some("press".to_string())
2117 );
2118 assert_eq!(map_atspi_action_name("toggle"), Some("toggle".to_string()));
2119 assert_eq!(map_atspi_action_name("check"), Some("toggle".to_string()));
2120 assert_eq!(map_atspi_action_name("uncheck"), Some("toggle".to_string()));
2121 assert_eq!(map_atspi_action_name("expand"), Some("expand".to_string()));
2122 assert_eq!(map_atspi_action_name("open"), Some("expand".to_string()));
2123 assert_eq!(
2124 map_atspi_action_name("collapse"),
2125 Some("collapse".to_string())
2126 );
2127 assert_eq!(map_atspi_action_name("close"), Some("collapse".to_string()));
2128 assert_eq!(map_atspi_action_name("select"), Some("select".to_string()));
2129 assert_eq!(map_atspi_action_name("menu"), Some("show_menu".to_string()));
2130 assert_eq!(
2131 map_atspi_action_name("showmenu"),
2132 Some("show_menu".to_string())
2133 );
2134 assert_eq!(
2135 map_atspi_action_name("popup"),
2136 Some("show_menu".to_string())
2137 );
2138 assert_eq!(
2139 map_atspi_action_name("show menu"),
2140 Some("show_menu".to_string())
2141 );
2142 assert_eq!(
2145 map_atspi_action_name("showContextMenu"),
2146 Some("show_menu".to_string())
2147 );
2148 assert_eq!(
2149 map_atspi_action_name("show_context_menu"),
2150 Some("show_menu".to_string())
2151 );
2152 assert_eq!(
2153 map_atspi_action_name("increment"),
2154 Some("increment".to_string())
2155 );
2156 assert_eq!(
2157 map_atspi_action_name("decrement"),
2158 Some("decrement".to_string())
2159 );
2160 assert_eq!(map_atspi_action_name("foobar"), None);
2161 }
2162
2163 #[test]
2166 fn test_action_name_aliases_roundtrip() {
2167 let atspi_names = [
2168 "click",
2169 "activate",
2170 "press",
2171 "invoke",
2172 "doDefault",
2173 "do_default",
2174 "toggle",
2175 "check",
2176 "uncheck",
2177 "expand",
2178 "open",
2179 "collapse",
2180 "close",
2181 "select",
2182 "menu",
2183 "showmenu",
2184 "showContextMenu",
2185 "show_context_menu",
2186 "popup",
2187 "show menu",
2188 "increment",
2189 "decrement",
2190 ];
2191 for name in atspi_names {
2192 let canonical = map_atspi_action_name(name).unwrap_or_else(|| {
2193 panic!("AT-SPI2 name {:?} should map to a canonical name", name)
2194 });
2195 let back = map_atspi_action_name(&canonical)
2197 .unwrap_or_else(|| panic!("canonical {:?} should map back to itself", canonical));
2198 assert_eq!(
2199 canonical, back,
2200 "AT-SPI2 {:?} -> {:?} -> {:?} (expected {:?})",
2201 name, canonical, back, canonical
2202 );
2203 }
2204 }
2205
2206 #[test]
2208 fn test_action_name_case_insensitive() {
2209 assert_eq!(map_atspi_action_name("Click"), Some("press".to_string()));
2210 assert_eq!(map_atspi_action_name("TOGGLE"), Some("toggle".to_string()));
2211 assert_eq!(
2212 map_atspi_action_name("Increment"),
2213 Some("increment".to_string())
2214 );
2215 }
2216
2217 #[test]
2222 fn test_gtk_fallback_actionable_roles() {
2223 for role in [
2224 "push button",
2225 "toggle button",
2226 "check box",
2227 "radio button",
2228 "menu item",
2229 "link",
2230 "tab",
2231 "list item",
2232 "tree item",
2233 ] {
2234 assert!(
2235 is_actionable_atspi_role(role),
2236 "{role:?} should be actionable"
2237 );
2238 }
2239 }
2240
2241 #[test]
2246 fn test_gtk_fallback_non_actionable_roles() {
2247 for role in [
2248 "label",
2249 "panel",
2250 "filler",
2251 "section",
2252 "group",
2253 "image",
2254 "separator",
2255 "static",
2256 "frame",
2257 "window",
2258 ] {
2259 assert!(
2260 !is_actionable_atspi_role(role),
2261 "{role:?} must not be treated as actionable"
2262 );
2263 }
2264 }
2265
2266 #[test]
2270 fn test_gtk_fallback_never_descend_roles() {
2271 for role in ["label", "separator", "image", "icon", "static", "caption"] {
2272 assert!(
2273 is_never_descend_atspi_role(role),
2274 "{role:?} must block BFS descent"
2275 );
2276 }
2277 for role in ["panel", "filler", "section", "group", "frame"] {
2280 assert!(
2281 !is_never_descend_atspi_role(role),
2282 "container role {role:?} must remain descendable"
2283 );
2284 }
2285 }
2286}