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 refine_text_role(&self, coarse: Role, aref: &AccessibleRef) -> Role {
334 match coarse {
335 Role::TextArea | Role::TextField => {
336 if self.is_multi_line(aref) {
337 Role::TextArea
338 } else {
339 Role::TextField
340 }
341 }
342 other => other,
343 }
344 }
345
346 fn get_extents(&self, aref: &AccessibleRef) -> Option<Rect> {
350 if !self.has_interface(aref, "Component") {
351 return None;
352 }
353 let proxy = self
354 .make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Component")
355 .ok()?;
356 let reply = proxy.call_method("GetExtents", &(0u32,)).ok()?;
359 let (x, y, w, h): (i32, i32, i32, i32) = reply.body().deserialize().ok()?;
360 if w <= 0 && h <= 0 {
361 return None;
362 }
363 Some(Rect {
364 x,
365 y,
366 width: w.max(0) as u32,
367 height: h.max(0) as u32,
368 })
369 }
370
371 fn get_actions(&self, aref: &AccessibleRef) -> (Vec<String>, HashMap<String, i32>) {
377 let mut actions = Vec::new();
378 let mut indices = HashMap::new();
379
380 if let Ok(proxy) = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Action") {
382 let n_actions = proxy
384 .get_property::<i32>("NActions")
385 .or_else(|_| proxy.get_property::<u32>("NActions").map(|n| n as i32))
386 .unwrap_or(0);
387 for i in 0..n_actions {
388 if let Ok(reply) = proxy.call_method("GetName", &(i,)) {
389 if let Ok(name) = reply.body().deserialize::<String>() {
390 if let Some(action_name) = map_atspi_action_name(&name) {
391 if !actions.contains(&action_name) {
392 indices.insert(action_name.clone(), i);
393 actions.push(action_name);
394 }
395 }
396 }
397 }
398 }
399 }
400
401 (actions, indices)
409 }
410
411 fn is_gtk_toolkit(&self, aref: &AccessibleRef) -> bool {
418 let app_root = AccessibleRef {
419 bus_name: aref.bus_name.clone(),
420 path: "/org/a11y/atspi/accessible/root".to_string(),
421 };
422 self.make_proxy(
423 &app_root.bus_name,
424 &app_root.path,
425 "org.a11y.atspi.Application",
426 )
427 .ok()
428 .and_then(|proxy| proxy.get_property::<String>("ToolkitName").ok())
429 .map(|t| t.eq_ignore_ascii_case("GTK"))
430 .unwrap_or(false)
431 }
432
433 fn find_gtk_press_fallback(
446 &self,
447 outer: &AccessibleRef,
448 outer_name: &str,
449 ) -> Option<(AccessibleRef, i32)> {
450 let mut queue: VecDeque<(AccessibleRef, u32)> = VecDeque::new();
451 queue.push_back((outer.clone(), 0));
452 let mut visited: usize = 0;
453 let mut shallowest_depth: Option<u32> = None;
454 let mut hits: Vec<(AccessibleRef, i32)> = Vec::new();
455
456 while let Some((node, depth)) = queue.pop_front() {
457 if let Some(best) = shallowest_depth {
459 if depth > best {
460 continue;
461 }
462 }
463 if visited > GTK_FALLBACK_MAX_NODES {
464 break;
465 }
466
467 let role_name = if depth == 0 {
468 String::new()
469 } else {
470 visited += 1;
471 self.get_role_name(&node).unwrap_or_default().to_lowercase()
472 };
473
474 if depth > 0 && is_actionable_atspi_role(&role_name) {
475 if let Some(idx) = self.gtk_fallback_pick(&node, outer_name) {
476 match shallowest_depth {
477 Some(d) if depth < d => {
478 shallowest_depth = Some(depth);
479 hits.clear();
480 hits.push((node.clone(), idx));
481 }
482 Some(d) if depth == d => hits.push((node.clone(), idx)),
483 Some(_) => {}
484 None => {
485 shallowest_depth = Some(depth);
486 hits.push((node.clone(), idx));
487 }
488 }
489 }
490 }
491
492 let stop_descending = depth >= GTK_FALLBACK_MAX_DEPTH
497 || (depth > 0 && is_never_descend_atspi_role(&role_name));
498 if stop_descending {
499 continue;
500 }
501 if let Ok(children) = self.get_atspi_children(&node) {
502 for c in children {
503 queue.push_back((c, depth + 1));
504 }
505 }
506 }
507
508 if hits.len() == 1 {
509 Some(hits.into_iter().next().unwrap())
510 } else {
511 None
512 }
513 }
514
515 fn gtk_fallback_pick(&self, aref: &AccessibleRef, outer_name: &str) -> Option<i32> {
518 let (_, index_map) = self.get_actions(aref);
519 let idx = *index_map.get("press")?;
520 if !self.is_showing_visible_sensitive(aref) {
521 return None;
522 }
523 if !outer_name.is_empty() {
527 let inner_name = self.get_name(aref).unwrap_or_default();
528 if !inner_name.is_empty() && inner_name != outer_name {
529 return None;
530 }
531 }
532 Some(idx)
533 }
534
535 fn is_showing_visible_sensitive(&self, aref: &AccessibleRef) -> bool {
540 let raw = self.get_state(aref).unwrap_or_default();
541 let bits: u64 = if raw.len() >= 2 {
542 (raw[0] as u64) | ((raw[1] as u64) << 32)
543 } else if raw.len() == 1 {
544 raw[0] as u64
545 } else {
546 0
547 };
548 const SENSITIVE: u64 = 1 << 24;
549 const SHOWING: u64 = 1 << 25;
550 const VISIBLE: u64 = 1 << 30;
551 (bits & (SHOWING | VISIBLE | SENSITIVE)) == (SHOWING | VISIBLE | SENSITIVE)
552 }
553
554 fn get_value(&self, aref: &AccessibleRef) -> Option<String> {
557 let text_value = self.get_text_content(aref);
561 if text_value.is_some() {
562 return text_value;
563 }
564 if let Ok(proxy) = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Value") {
566 if let Ok(val) = proxy.get_property::<f64>("CurrentValue") {
567 return Some(val.to_string());
568 }
569 }
570 None
571 }
572
573 fn get_text_content(&self, aref: &AccessibleRef) -> Option<String> {
575 let proxy = self
576 .make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Text")
577 .ok()?;
578 let char_count: i32 = proxy.get_property("CharacterCount").ok()?;
579 if char_count > 0 {
580 let reply = proxy.call_method("GetText", &(0i32, char_count)).ok()?;
581 let text: String = reply.body().deserialize().ok()?;
582 if !text.is_empty() {
583 return Some(text);
584 }
585 }
586 None
587 }
588
589 fn cache_element(&self, aref: AccessibleRef) -> u64 {
591 let handle = NEXT_HANDLE.fetch_add(1, Ordering::Relaxed);
592 self.handle_cache
593 .lock()
594 .unwrap_or_else(|e| e.into_inner())
595 .insert(handle, aref);
596 handle
597 }
598
599 fn get_cached(&self, handle: u64) -> Result<AccessibleRef> {
601 self.handle_cache
602 .lock()
603 .unwrap_or_else(|e| e.into_inner())
604 .get(&handle)
605 .cloned()
606 .ok_or(Error::ElementStale {
607 selector: format!("handle:{}", handle),
608 })
609 }
610
611 fn build_element_data(&self, aref: &AccessibleRef, pid: Option<u32>) -> ElementData {
616 let role_name = self.get_role_name(aref).unwrap_or_default();
617 let role_num = self.get_role_number(aref).unwrap_or(0);
618 let role = {
619 let by_name = if !role_name.is_empty() {
620 map_atspi_role(&role_name)
621 } else {
622 Role::Unknown
623 };
624 let coarse = if by_name != Role::Unknown {
625 by_name
626 } else {
627 map_atspi_role_number(role_num)
628 };
629 self.refine_text_role(coarse, aref)
630 };
631
632 let (
636 ((mut name, value), description),
637 (
638 (states, bounds),
639 ((actions, action_index_map), (numeric_value, min_value, max_value)),
640 ),
641 ) = rayon::join(
642 || {
643 rayon::join(
644 || {
645 let name = self.get_name(aref).ok().filter(|s| !s.is_empty());
646 let value = if role_has_value(role) {
647 self.get_value(aref)
648 } else {
649 None
650 };
651 (name, value)
652 },
653 || self.get_description(aref).ok().filter(|s| !s.is_empty()),
654 )
655 },
656 || {
657 rayon::join(
658 || {
659 rayon::join(
660 || self.parse_states(aref, role),
661 || {
662 if role != Role::Application {
663 self.get_extents(aref)
664 } else {
665 None
666 }
667 },
668 )
669 },
670 || {
671 rayon::join(
672 || {
673 if role_has_actions(role) {
674 self.get_actions(aref)
675 } else {
676 (vec![], HashMap::new())
677 }
678 },
679 || {
680 if matches!(
681 role,
682 Role::Slider
683 | Role::ProgressBar
684 | Role::ScrollBar
685 | Role::SpinButton
686 ) {
687 if let Ok(proxy) = self.make_proxy(
688 &aref.bus_name,
689 &aref.path,
690 "org.a11y.atspi.Value",
691 ) {
692 (
693 proxy.get_property::<f64>("CurrentValue").ok(),
694 proxy.get_property::<f64>("MinimumValue").ok(),
695 proxy.get_property::<f64>("MaximumValue").ok(),
696 )
697 } else {
698 (None, None, None)
699 }
700 } else {
701 (None, None, None)
702 }
703 },
704 )
705 },
706 )
707 },
708 );
709
710 if name.is_none() && role == Role::StaticText {
713 if let Some(ref v) = value {
714 name = Some(v.clone());
715 }
716 }
717
718 let raw = {
719 let raw_role = if role_name.is_empty() {
720 format!("role_num:{}", role_num)
721 } else {
722 role_name
723 };
724 let mut raw = HashMap::new();
725 raw.insert("atspi_role".into(), serde_json::Value::String(raw_role));
726 raw.insert(
727 "bus_name".into(),
728 serde_json::Value::String(aref.bus_name.clone()),
729 );
730 raw.insert(
731 "object_path".into(),
732 serde_json::Value::String(aref.path.clone()),
733 );
734 if let Some(ref n) = name {
737 raw.insert("atspi_name".into(), serde_json::Value::String(n.clone()));
738 }
739 if let Some(ref v) = value {
740 raw.insert("atspi_value".into(), serde_json::Value::String(v.clone()));
741 }
742 if let Some(ref d) = description {
743 raw.insert(
744 "atspi_description".into(),
745 serde_json::Value::String(d.clone()),
746 );
747 }
748 raw
749 };
750
751 let name = xa11y_core::text::strip_bidi_opt(name);
754 let value = xa11y_core::text::strip_bidi_opt(value);
755 let description = xa11y_core::text::strip_bidi_opt(description);
756
757 let handle = self.cache_element(aref.clone());
758 if !action_index_map.is_empty() {
759 self.action_indices
760 .lock()
761 .unwrap_or_else(|e| e.into_inner())
762 .insert(handle, action_index_map);
763 }
764
765 ElementData {
766 role,
767 name,
768 value,
769 description,
770 bounds,
771 actions,
772 states,
773 numeric_value,
774 min_value,
775 max_value,
776 pid,
777 stable_id: Some(aref.path.clone()),
778 raw,
779 handle,
780 }
781 }
782
783 fn get_atspi_parent(&self, aref: &AccessibleRef) -> Result<Option<AccessibleRef>> {
785 let proxy = self.make_proxy(
787 &aref.bus_name,
788 &aref.path,
789 "org.freedesktop.DBus.Properties",
790 )?;
791 let reply = proxy
792 .call_method("Get", &("org.a11y.atspi.Accessible", "Parent"))
793 .map_err(|e| Error::Platform {
794 code: -1,
795 message: format!("Get Parent property failed: {}", e),
796 })?;
797 let variant: zbus::zvariant::OwnedValue =
799 reply.body().deserialize().map_err(|e| Error::Platform {
800 code: -1,
801 message: format!("Parent deserialize variant failed: {}", e),
802 })?;
803 let (bus, path): (String, zbus::zvariant::OwnedObjectPath) =
804 zbus::zvariant::Value::from(variant).try_into().map_err(
805 |e: zbus::zvariant::Error| Error::Platform {
806 code: -1,
807 message: format!("Parent deserialize struct failed: {}", e),
808 },
809 )?;
810 let path_str = path.as_str();
811 if path_str == "/org/a11y/atspi/null" || bus.is_empty() || path_str.is_empty() {
812 return Ok(None);
813 }
814 if path_str == "/org/a11y/atspi/accessible/root" {
816 return Ok(None);
817 }
818 Ok(Some(AccessibleRef {
819 bus_name: bus,
820 path: path_str.to_string(),
821 }))
822 }
823
824 fn state_bits(&self, aref: &AccessibleRef) -> u64 {
829 let state_bits = self.get_state(aref).unwrap_or_default();
830 if state_bits.len() >= 2 {
831 (state_bits[0] as u64) | ((state_bits[1] as u64) << 32)
832 } else if state_bits.len() == 1 {
833 state_bits[0] as u64
834 } else {
835 0
836 }
837 }
838
839 fn parse_states(&self, aref: &AccessibleRef, role: Role) -> StateSet {
841 let bits = self.state_bits(aref);
842
843 const BUSY: u64 = 1 << 3;
845 const CHECKED: u64 = 1 << 4;
846 const EDITABLE: u64 = 1 << 7;
847 const ENABLED: u64 = 1 << 8;
848 const EXPANDABLE: u64 = 1 << 9;
849 const EXPANDED: u64 = 1 << 10;
850 const FOCUSABLE: u64 = 1 << 11;
851 const FOCUSED: u64 = 1 << 12;
852 const MODAL: u64 = 1 << 16;
853 const SELECTED: u64 = 1 << 23;
854 const SENSITIVE: u64 = 1 << 24;
855 const SHOWING: u64 = 1 << 25;
856 const VISIBLE: u64 = 1 << 30;
857 const INDETERMINATE: u64 = 1 << 32;
858 const REQUIRED: u64 = 1 << 33;
859
860 let enabled = (bits & ENABLED) != 0 || (bits & SENSITIVE) != 0;
861 let visible = (bits & VISIBLE) != 0 || (bits & SHOWING) != 0;
862
863 let checked = match role {
864 Role::CheckBox | Role::RadioButton | Role::MenuItem | Role::Switch => {
865 if (bits & INDETERMINATE) != 0 {
866 Some(Toggled::Mixed)
867 } else if (bits & CHECKED) != 0 {
868 Some(Toggled::On)
869 } else {
870 Some(Toggled::Off)
871 }
872 }
873 _ => None,
874 };
875
876 let expanded = if (bits & EXPANDABLE) != 0 {
877 Some((bits & EXPANDED) != 0)
878 } else {
879 None
880 };
881
882 StateSet {
883 enabled,
884 visible,
885 focused: (bits & FOCUSED) != 0,
886 checked,
887 selected: (bits & SELECTED) != 0,
888 expanded,
889 editable: (bits & EDITABLE) != 0,
890 focusable: (bits & FOCUSABLE) != 0,
891 modal: (bits & MODAL) != 0,
892 required: (bits & REQUIRED) != 0,
893 busy: (bits & BUSY) != 0,
894 }
895 }
896
897 pub(crate) fn find_app_by_pid(&self, pid: u32) -> Result<AccessibleRef> {
902 let registry = AccessibleRef {
903 bus_name: "org.a11y.atspi.Registry".to_string(),
904 path: "/org/a11y/atspi/accessible/root".to_string(),
905 };
906 let children = self.get_atspi_children(®istry)?;
907
908 for child in &children {
909 if child.path == "/org/a11y/atspi/null" {
910 continue;
911 }
912 if let Some(app_pid) = self.get_dbus_pid(&child.bus_name) {
918 if app_pid == pid {
919 return Ok(child.clone());
920 }
921 }
922 if let Ok(proxy) =
924 self.make_proxy(&child.bus_name, &child.path, "org.a11y.atspi.Application")
925 {
926 if let Ok(app_pid) = proxy.get_property::<i32>("Id") {
927 if app_pid as u32 == pid {
928 return Ok(child.clone());
929 }
930 }
931 }
932 }
933
934 Err(Error::Platform {
935 code: -1,
936 message: format!("No application found with PID {}", pid),
937 })
938 }
939
940 fn get_dbus_pid(&self, bus_name: &str) -> Option<u32> {
942 let proxy = self
943 .make_proxy(
944 "org.freedesktop.DBus",
945 "/org/freedesktop/DBus",
946 "org.freedesktop.DBus",
947 )
948 .ok()?;
949 let reply = proxy
950 .call_method("GetConnectionUnixProcessID", &(bus_name,))
951 .ok()?;
952 let pid: u32 = reply.body().deserialize().ok()?;
953 if pid > 0 {
954 Some(pid)
955 } else {
956 None
957 }
958 }
959
960 fn do_atspi_action_by_index(&self, aref: &AccessibleRef, index: i32) -> Result<()> {
962 let proxy = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Action")?;
963 proxy
964 .call_method("DoAction", &(index,))
965 .map_err(|e| Error::Platform {
966 code: -1,
967 message: format!("DoAction({}) failed: {}", index, e),
968 })?;
969 Ok(())
970 }
971
972 fn get_action_index(&self, handle: u64, action: &str) -> Result<i32> {
974 self.action_indices
975 .lock()
976 .unwrap_or_else(|e| e.into_inner())
977 .get(&handle)
978 .and_then(|map| map.get(action).copied())
979 .ok_or_else(|| Error::ActionNotSupported {
980 action: action.to_string(),
981 role: Role::Unknown, })
983 }
984
985 fn get_app_pid(&self, aref: &AccessibleRef) -> Option<u32> {
991 if let Some(pid) = self.get_dbus_pid(&aref.bus_name) {
992 return Some(pid);
993 }
994
995 if let Ok(proxy) = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Application")
1002 {
1003 if let Ok(pid) = proxy.get_property::<i32>("Id") {
1004 if pid > 0 && std::path::Path::new(&format!("/proc/{pid}")).exists() {
1005 return Some(pid as u32);
1006 }
1007 }
1008 }
1009
1010 None
1011 }
1012
1013 fn resolve_role(&self, aref: &AccessibleRef) -> Role {
1015 let role_name = self.get_role_name(aref).unwrap_or_default();
1016 let by_name = if !role_name.is_empty() {
1017 map_atspi_role(&role_name)
1018 } else {
1019 Role::Unknown
1020 };
1021 let coarse = if by_name != Role::Unknown {
1022 by_name
1023 } else {
1024 let role_num = self.get_role_number(aref).unwrap_or(0);
1026 map_atspi_role_number(role_num)
1027 };
1028 self.refine_text_role(coarse, aref)
1029 }
1030
1031 fn matches_ref(
1045 &self,
1046 aref: &AccessibleRef,
1047 simple: &xa11y_core::selector::SimpleSelector,
1048 ) -> bool {
1049 let needs_role = simple.role.is_some()
1052 || simple
1053 .filters
1054 .iter()
1055 .any(|f| matches!(f.attr.as_str(), "role" | "checked"));
1056 let role = if needs_role {
1057 Some(self.resolve_role(aref))
1058 } else {
1059 None
1060 };
1061
1062 if let Some(ref role_match) = simple.role {
1063 match role_match {
1064 xa11y_core::selector::RoleMatch::Normalized(expected) => {
1065 if role != Some(*expected) {
1066 return false;
1067 }
1068 }
1069 xa11y_core::selector::RoleMatch::Platform(platform_role) => {
1070 let raw_role = self.get_role_name(aref).unwrap_or_default();
1071 if raw_role != *platform_role {
1072 return false;
1073 }
1074 }
1075 }
1076 }
1077
1078 let mut state_set: Option<StateSet> = None;
1079
1080 for filter in &simple.filters {
1081 let attr = filter.attr.as_str();
1082 let resolved: Option<Option<String>> = match attr {
1083 "role" => Some(role.map(|r| r.to_snake_case().to_string())),
1084 "name" => {
1085 let name = self.get_name(aref).ok().filter(|s| !s.is_empty());
1086 let resolved = if name.is_none() && role == Some(Role::StaticText) {
1089 self.get_value(aref)
1090 } else {
1091 name
1092 };
1093 Some(resolved)
1094 }
1095 "value" => Some(self.get_value(aref)),
1096 "description" => Some(self.get_description(aref).ok().filter(|s| !s.is_empty())),
1097 "enabled" | "visible" | "focused" | "focusable" | "selected" | "editable"
1098 | "modal" | "required" | "busy" | "expanded" | "checked" => {
1099 let s = state_set.get_or_insert_with(|| {
1100 self.parse_states(aref, role.unwrap_or(Role::Unknown))
1104 });
1105 Some(state_attr_to_string(attr, s))
1106 }
1107 _ => None, };
1109
1110 match resolved {
1111 Some(value) => {
1112 if !xa11y_core::selector::match_op(&filter.op, &filter.value, value.as_deref())
1113 {
1114 return false;
1115 }
1116 }
1117 None => {
1118 let pid = None; let data = self.build_element_data(aref, pid);
1126 return xa11y_core::selector::matches_simple(&data, simple);
1127 }
1128 }
1129 }
1130
1131 true
1132 }
1133
1134 fn collect_matching_refs_group(
1144 &self,
1145 parent: &AccessibleRef,
1146 clauses: &[&xa11y_core::selector::SimpleSelector],
1147 depth: u32,
1148 max_depth: u32,
1149 limit: Option<usize>,
1150 ) -> Result<Vec<(usize, AccessibleRef)>> {
1151 if depth > max_depth {
1152 return Ok(vec![]);
1153 }
1154
1155 let children = self.get_atspi_children(parent)?;
1156
1157 let parent_is_registry = parent.bus_name == "org.a11y.atspi.Registry";
1165 let mut to_search: Vec<AccessibleRef> = Vec::new();
1166 for child in children {
1167 if child.path == "/org/a11y/atspi/null"
1168 || child.bus_name.is_empty()
1169 || child.path.is_empty()
1170 {
1171 continue;
1172 }
1173
1174 if !parent_is_registry {
1175 let child_role = self.get_role_name(&child).unwrap_or_default();
1176 if child_role == "application" {
1177 let grandchildren = self.get_atspi_children(&child).unwrap_or_default();
1178 for gc in grandchildren {
1179 if gc.path == "/org/a11y/atspi/null"
1180 || gc.bus_name.is_empty()
1181 || gc.path.is_empty()
1182 {
1183 continue;
1184 }
1185 let gc_role = self.get_role_name(&gc).unwrap_or_default();
1186 if gc_role == "application" {
1187 continue;
1188 }
1189 to_search.push(gc);
1190 }
1191 continue;
1192 }
1193 }
1194 to_search.push(child);
1195 }
1196
1197 if to_search.is_empty() {
1200 self.check_chromium_a11y_enabled(parent, None)?;
1201 }
1202
1203 type ChildBatch = (Vec<(usize, AccessibleRef)>, Option<Error>);
1218 let per_child: Vec<ChildBatch> = to_search
1219 .par_iter()
1220 .map(|child| {
1221 let mut child_results: Vec<(usize, AccessibleRef)> = Vec::new();
1222 for (idx, simple) in clauses.iter().enumerate() {
1223 if self.matches_ref(child, simple) {
1224 child_results.push((idx, child.clone()));
1225 }
1226 }
1227 match self.collect_matching_refs_group(child, clauses, depth + 1, max_depth, limit)
1228 {
1229 Ok(sub) => {
1230 child_results.extend(sub);
1231 (child_results, None)
1232 }
1233 Err(e @ Error::AccessibilityNotEnabled { .. }) => (Vec::new(), Some(e)),
1234 Err(_) => (child_results, None),
1235 }
1236 })
1237 .collect();
1238
1239 let mut results = Vec::new();
1251 for (batch, maybe_err) in per_child {
1252 if let Some(err) = maybe_err {
1253 return Err(err);
1254 }
1255 for r in batch {
1256 results.push(r);
1257 if let Some(limit) = limit {
1258 if results.len() >= limit {
1259 return Ok(results);
1260 }
1261 }
1262 }
1263 }
1264 Ok(results)
1265 }
1266}
1267
1268impl Provider for LinuxProvider {
1269 fn get_children(&self, element: Option<&ElementData>) -> Result<Vec<ElementData>> {
1270 match element {
1271 None => {
1272 self.list_apps()
1275 }
1276 Some(element_data) => {
1277 let aref = self.get_cached(element_data.handle)?;
1278 let children = self.get_atspi_children(&aref).unwrap_or_default();
1279 let pid = element_data.pid;
1280
1281 let mut to_build: Vec<AccessibleRef> = Vec::new();
1284 for child_ref in &children {
1285 if child_ref.path == "/org/a11y/atspi/null"
1286 || child_ref.bus_name.is_empty()
1287 || child_ref.path.is_empty()
1288 {
1289 continue;
1290 }
1291 let child_role = self.get_role_name(child_ref).unwrap_or_default();
1292 if child_role == "application" {
1293 let grandchildren = self.get_atspi_children(child_ref).unwrap_or_default();
1294 for gc_ref in grandchildren {
1295 if gc_ref.path == "/org/a11y/atspi/null"
1296 || gc_ref.bus_name.is_empty()
1297 || gc_ref.path.is_empty()
1298 {
1299 continue;
1300 }
1301 let gc_role = self.get_role_name(&gc_ref).unwrap_or_default();
1302 if gc_role == "application" {
1303 continue;
1304 }
1305 to_build.push(gc_ref);
1306 }
1307 continue;
1308 }
1309 to_build.push(child_ref.clone());
1310 }
1311
1312 if to_build.is_empty() {
1313 self.check_chromium_a11y_enabled(&aref, Some(element_data.role))?;
1314 }
1315
1316 let results: Vec<ElementData> = to_build
1317 .par_iter()
1318 .map(|r| self.build_element_data(r, pid))
1319 .collect();
1320
1321 Ok(results)
1322 }
1323 }
1324 }
1325
1326 fn find_elements_group(
1327 &self,
1328 root: &ElementData,
1329 group: &xa11y_core::selector::SelectorGroup,
1330 limit: Option<usize>,
1331 max_depth: Option<u32>,
1332 ) -> Result<Vec<ElementData>> {
1333 if group.clauses.is_empty() {
1334 return Ok(vec![]);
1335 }
1336 if group.clauses.iter().any(|c| c.segments.is_empty()) {
1339 return Ok(vec![]);
1340 }
1341
1342 let max_depth_val = max_depth.unwrap_or(xa11y_core::MAX_TREE_DEPTH);
1352
1353 let firsts: Vec<&xa11y_core::selector::SimpleSelector> = group
1354 .clauses
1355 .iter()
1356 .map(|c| &c.segments[0].simple)
1357 .collect();
1358
1359 let start_ref = self.get_cached(root.handle)?;
1360
1361 let phase1_limit = if group.clauses.len() == 1 {
1370 let clause = &group.clauses[0];
1371 let first = firsts[0];
1372 let outer = if clause.segments.len() == 1 {
1373 limit
1374 } else {
1375 None
1376 };
1377 match (outer, first.nth) {
1378 (Some(l), Some(n)) => Some(l.max(n)),
1379 (_, Some(n)) => Some(n),
1380 (l, None) => l,
1381 }
1382 } else {
1383 None
1384 };
1385
1386 let phase1: Vec<(usize, AccessibleRef)> =
1387 self.collect_matching_refs_group(&start_ref, &firsts, 0, max_depth_val, phase1_limit)?;
1388
1389 let pid_from_root = root.pid;
1390
1391 let mut by_clause: Vec<Vec<(usize, AccessibleRef)>> =
1395 (0..group.clauses.len()).map(|_| Vec::new()).collect();
1396 for (walk_pos, (clause_idx, aref)) in phase1.into_iter().enumerate() {
1397 by_clause[clause_idx].push((walk_pos, aref));
1398 }
1399
1400 let mut merged: Vec<(usize, ElementData)> = Vec::new();
1408 for (clause_idx, hits) in by_clause.into_iter().enumerate() {
1409 if hits.is_empty() {
1410 continue;
1411 }
1412 let clause = &group.clauses[clause_idx];
1413 let phase1_data: Vec<(usize, ElementData)> = hits
1415 .par_iter()
1416 .map(|(pos, aref)| {
1417 (*pos, self.build_element_data(aref, pid_from_root))
1421 })
1422 .collect();
1423
1424 if clause.segments.len() == 1 {
1425 let mut candidates = phase1_data;
1427 if let Some(nth) = clause.segments[0].simple.nth {
1428 if nth <= candidates.len() {
1429 let kept = candidates.remove(nth - 1);
1430 candidates.clear();
1431 candidates.push(kept);
1432 } else {
1433 candidates.clear();
1434 }
1435 }
1436 merged.extend(candidates);
1437 continue;
1438 }
1439
1440 for (anchor_pos, head) in phase1_data {
1445 let narrowed = self.narrow_multi_segment(
1446 vec![head],
1447 &clause.segments[1..],
1448 max_depth_val,
1449 None,
1450 )?;
1451 for n in narrowed {
1452 merged.push((anchor_pos, n));
1453 }
1454 }
1455 }
1456
1457 merged.sort_by_key(|(pos, _)| *pos);
1461 let mut seen: HashSet<(String, String)> = HashSet::new();
1462 let mut out: Vec<ElementData> = Vec::with_capacity(merged.len());
1463 for (_, data) in merged {
1464 if let Ok(aref) = self.get_cached(data.handle) {
1468 if !seen.insert((aref.bus_name.clone(), aref.path.clone())) {
1469 continue;
1470 }
1471 }
1472 out.push(data);
1473 }
1474 if let Some(l) = limit {
1475 out.truncate(l);
1476 }
1477 Ok(out)
1478 }
1479
1480 fn get_parent(&self, element: &ElementData) -> Result<Option<ElementData>> {
1481 let aref = self.get_cached(element.handle)?;
1482 match self.get_atspi_parent(&aref)? {
1483 Some(parent_ref) => {
1484 let data = self.build_element_data(&parent_ref, element.pid);
1485 Ok(Some(data))
1486 }
1487 None => Ok(None),
1488 }
1489 }
1490
1491 fn list_apps(&self) -> Result<Vec<ElementData>> {
1498 let registry = AccessibleRef {
1499 bus_name: "org.a11y.atspi.Registry".to_string(),
1500 path: "/org/a11y/atspi/accessible/root".to_string(),
1501 };
1502 let children = self.get_atspi_children(®istry)?;
1503
1504 let valid: Vec<(&AccessibleRef, String)> = children
1506 .iter()
1507 .filter(|c| c.path != "/org/a11y/atspi/null")
1508 .filter_map(|c| {
1509 let name = self.get_name(c).unwrap_or_default();
1510 if name.is_empty() {
1511 None
1512 } else {
1513 Some((c, name))
1514 }
1515 })
1516 .collect();
1517
1518 let results: Vec<ElementData> = valid
1519 .par_iter()
1520 .map(|(child, app_name)| {
1521 let pid = self.get_app_pid(child);
1522 let mut data = self.build_element_data(child, pid);
1523 data.name = Some(app_name.clone());
1524 data
1525 })
1526 .collect();
1527
1528 Ok(results)
1529 }
1530
1531 fn app_by_pid(&self, pid: u32) -> Result<ElementData> {
1543 let registry = AccessibleRef {
1544 bus_name: "org.a11y.atspi.Registry".to_string(),
1545 path: "/org/a11y/atspi/accessible/root".to_string(),
1546 };
1547 let children = self.get_atspi_children(®istry)?;
1548
1549 let mut total = 0usize;
1550 let mut unresolved = 0usize;
1551 for child in children.iter().filter(|c| c.path != "/org/a11y/atspi/null") {
1552 total += 1;
1553 match self.get_app_pid(child) {
1554 Some(p) if p == pid => {
1555 let mut data = self.build_element_data(child, Some(p));
1556 let name = self.get_name(child).unwrap_or_default();
1558 if !name.is_empty() {
1559 data.name = Some(name);
1560 }
1561 return Ok(data);
1562 }
1563 Some(_) => {}
1564 None => unresolved += 1,
1565 }
1566 }
1567 Err(
1568 Error::selector_not_matched(format!("application[pid={pid}]")).diagnose(
1569 xa11y_core::Diagnosis {
1570 last_observed: Some(format!(
1571 "{total} AT-SPI registry entries examined, {unresolved} without a \
1572 resolvable pid"
1573 )),
1574 ..Default::default()
1575 },
1576 ),
1577 )
1578 }
1579
1580 fn focused_app(&self) -> Result<ElementData> {
1593 const ACTIVE: u64 = 1 << 1;
1595
1596 let registry = AccessibleRef {
1597 bus_name: "org.a11y.atspi.Registry".to_string(),
1598 path: "/org/a11y/atspi/accessible/root".to_string(),
1599 };
1600 let apps = self.get_atspi_children(®istry)?;
1601
1602 for app in apps.iter().filter(|c| c.path != "/org/a11y/atspi/null") {
1603 let windows = match self.get_atspi_children(app) {
1604 Ok(w) => w,
1605 Err(_) => continue,
1608 };
1609 let has_active_window = windows
1610 .iter()
1611 .filter(|w| w.path != "/org/a11y/atspi/null")
1612 .any(|w| self.state_bits(w) & ACTIVE != 0);
1613 if has_active_window {
1614 let pid = self.get_app_pid(app);
1615 let mut data = self.build_element_data(app, pid);
1616 let name = self.get_name(app).unwrap_or_default();
1617 if !name.is_empty() {
1618 data.name = Some(name);
1619 }
1620 return Ok(data);
1621 }
1622 }
1623
1624 Err(Error::selector_not_matched("focused application"))
1625 }
1626
1627 fn press(&self, element: &ElementData) -> Result<()> {
1628 let target = self.get_cached(element.handle)?;
1629 if let Ok(index) = self.get_action_index(element.handle, "press") {
1631 return self.do_atspi_action_by_index(&target, index);
1632 }
1633 if element.role == Role::Button && self.is_gtk_toolkit(&target) {
1652 let outer_name = self.get_name(&target).unwrap_or_default();
1653 if let Some((inner, index)) = self.find_gtk_press_fallback(&target, &outer_name) {
1654 return self.do_atspi_action_by_index(&inner, index);
1655 }
1656 }
1657 Err(Error::ActionNotSupported {
1658 action: "press".to_string(),
1659 role: element.role,
1660 })
1661 }
1662
1663 fn focus(&self, element: &ElementData) -> Result<()> {
1664 let target = self.get_cached(element.handle)?;
1665 if let Ok(proxy) =
1669 self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Component")
1670 {
1671 if let Ok(reply) = proxy.call_method("GrabFocus", &()) {
1672 if let Ok(true) = reply.body().deserialize::<bool>() {
1674 return Ok(());
1675 }
1676 }
1678 }
1679 if let Ok(index) = self.get_action_index(element.handle, "focus") {
1680 return self.do_atspi_action_by_index(&target, index);
1681 }
1682 Err(Error::ActionNotSupported {
1683 action: "focus".to_string(),
1684 role: element.role,
1685 })
1686 }
1687
1688 fn blur(&self, element: &ElementData) -> Result<()> {
1689 let target = self.get_cached(element.handle)?;
1690 if let Some(parent_ref) = self.get_atspi_parent(&target)? {
1694 if parent_ref.path != "/org/a11y/atspi/null" {
1695 let p = self.make_proxy(
1696 &parent_ref.bus_name,
1697 &parent_ref.path,
1698 "org.a11y.atspi.Component",
1699 )?;
1700 p.call_method("GrabFocus", &())
1701 .map_err(|e| Error::Platform {
1702 code: -1,
1703 message: format!("Component.GrabFocus on parent failed: {}", e),
1704 })?;
1705 return Ok(());
1706 }
1707 }
1708 Err(Error::ActionNotSupported {
1709 action: "blur".to_string(),
1710 role: element.role,
1711 })
1712 }
1713
1714 fn toggle(&self, element: &ElementData) -> Result<()> {
1715 let target = self.get_cached(element.handle)?;
1716 if let Ok(index) = self.get_action_index(element.handle, "toggle") {
1721 return self.do_atspi_action_by_index(&target, index);
1722 }
1723 if matches!(
1746 element.role,
1747 Role::CheckBox | Role::RadioButton | Role::Switch
1748 ) {
1749 if let Ok(index) = self.get_action_index(element.handle, "press") {
1750 return self.do_atspi_action_by_index(&target, index);
1751 }
1752 }
1753 Err(Error::ActionNotSupported {
1754 action: "toggle".to_string(),
1755 role: element.role,
1756 })
1757 }
1758
1759 fn select(&self, element: &ElementData) -> Result<()> {
1760 let target = self.get_cached(element.handle)?;
1761 let index = self
1762 .get_action_index(element.handle, "select")
1763 .map_err(|_| Error::ActionNotSupported {
1764 action: "select".to_string(),
1765 role: element.role,
1766 })?;
1767 self.do_atspi_action_by_index(&target, index)
1768 }
1769
1770 fn expand(&self, element: &ElementData) -> Result<()> {
1771 let target = self.get_cached(element.handle)?;
1772 let index = self
1773 .get_action_index(element.handle, "expand")
1774 .map_err(|_| Error::ActionNotSupported {
1775 action: "expand".to_string(),
1776 role: element.role,
1777 })?;
1778 self.do_atspi_action_by_index(&target, index)
1779 }
1780
1781 fn collapse(&self, element: &ElementData) -> Result<()> {
1782 let target = self.get_cached(element.handle)?;
1783 let index = self
1784 .get_action_index(element.handle, "collapse")
1785 .map_err(|_| Error::ActionNotSupported {
1786 action: "collapse".to_string(),
1787 role: element.role,
1788 })?;
1789 self.do_atspi_action_by_index(&target, index)
1790 }
1791
1792 fn show_menu(&self, element: &ElementData) -> Result<()> {
1793 let target = self.get_cached(element.handle)?;
1794 let index = self
1795 .get_action_index(element.handle, "show_menu")
1796 .map_err(|_| Error::ActionNotSupported {
1797 action: "show_menu".to_string(),
1798 role: element.role,
1799 })?;
1800 self.do_atspi_action_by_index(&target, index)
1801 }
1802
1803 fn increment(&self, element: &ElementData) -> Result<()> {
1804 let target = self.get_cached(element.handle)?;
1805 if let Ok(index) = self.get_action_index(element.handle, "increment") {
1807 return self.do_atspi_action_by_index(&target, index);
1808 }
1809 let proxy = self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Value")?;
1810 let current: f64 = proxy
1811 .get_property("CurrentValue")
1812 .map_err(|e| Error::Platform {
1813 code: -1,
1814 message: format!("Value.CurrentValue failed: {}", e),
1815 })?;
1816 let step: f64 = proxy.get_property("MinimumIncrement").unwrap_or(1.0);
1817 let step = if step <= 0.0 { 1.0 } else { step };
1818 proxy
1819 .set_property("CurrentValue", current + step)
1820 .map_err(|e| Error::Platform {
1821 code: -1,
1822 message: format!("Value.SetCurrentValue failed: {}", e),
1823 })
1824 }
1825
1826 fn decrement(&self, element: &ElementData) -> Result<()> {
1827 let target = self.get_cached(element.handle)?;
1828 if let Ok(index) = self.get_action_index(element.handle, "decrement") {
1829 return self.do_atspi_action_by_index(&target, index);
1830 }
1831 let proxy = self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Value")?;
1832 let current: f64 = proxy
1833 .get_property("CurrentValue")
1834 .map_err(|e| Error::Platform {
1835 code: -1,
1836 message: format!("Value.CurrentValue failed: {}", e),
1837 })?;
1838 let step: f64 = proxy.get_property("MinimumIncrement").unwrap_or(1.0);
1839 let step = if step <= 0.0 { 1.0 } else { step };
1840 proxy
1841 .set_property("CurrentValue", current - step)
1842 .map_err(|e| Error::Platform {
1843 code: -1,
1844 message: format!("Value.SetCurrentValue failed: {}", e),
1845 })
1846 }
1847
1848 fn scroll_into_view(&self, element: &ElementData) -> Result<()> {
1849 let target = self.get_cached(element.handle)?;
1850 let proxy = self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Component")?;
1851 proxy
1852 .call_method("ScrollTo", &(0u32,))
1853 .map_err(|e| Error::Platform {
1854 code: -1,
1855 message: format!("ScrollTo failed: {}", e),
1856 })?;
1857 Ok(())
1858 }
1859
1860 fn set_value(&self, element: &ElementData, value: &str) -> Result<()> {
1861 let target = self.get_cached(element.handle)?;
1862 let proxy = self
1863 .make_proxy(
1864 &target.bus_name,
1865 &target.path,
1866 "org.a11y.atspi.EditableText",
1867 )
1868 .map_err(|_| Error::TextValueNotSupported)?;
1869 if proxy.call_method("SetTextContents", &(value,)).is_ok() {
1871 return Ok(());
1872 }
1873 let classify_editable_text_error = |op: &str, e: zbus::Error| -> Error {
1879 let msg = e.to_string();
1880 if msg.contains("UnknownMethod") || msg.contains("UnknownInterface") {
1881 Error::TextValueNotSupported
1882 } else {
1883 Error::Platform {
1884 code: -1,
1885 message: format!("EditableText.{} failed: {}", op, msg),
1886 }
1887 }
1888 };
1889 if let Err(e) = proxy.call_method("DeleteText", &(0i32, i32::MAX)) {
1890 return Err(classify_editable_text_error("DeleteText", e));
1891 }
1892 if let Err(e) = proxy.call_method("InsertText", &(0i32, value, value.len() as i32)) {
1893 return Err(classify_editable_text_error("InsertText", e));
1894 }
1895 Ok(())
1896 }
1897
1898 fn set_numeric_value(&self, element: &ElementData, value: f64) -> Result<()> {
1899 let target = self.get_cached(element.handle)?;
1900 let proxy = self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Value")?;
1901 proxy
1902 .set_property("CurrentValue", value)
1903 .map_err(|e| Error::Platform {
1904 code: -1,
1905 message: format!("SetValue failed: {}", e),
1906 })
1907 }
1908
1909 fn type_text(&self, element: &ElementData, text: &str) -> Result<()> {
1910 let target = self.get_cached(element.handle)?;
1911 let text_proxy = self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Text");
1914 let insert_pos = text_proxy
1915 .as_ref()
1916 .ok()
1917 .and_then(|p| p.get_property::<i32>("CaretOffset").ok())
1918 .unwrap_or(-1); let proxy = self
1921 .make_proxy(
1922 &target.bus_name,
1923 &target.path,
1924 "org.a11y.atspi.EditableText",
1925 )
1926 .map_err(|_| Error::TextValueNotSupported)?;
1927 let pos = if insert_pos >= 0 {
1928 insert_pos
1929 } else {
1930 i32::MAX
1931 };
1932 proxy
1933 .call_method("InsertText", &(pos, text, text.len() as i32))
1934 .map_err(|e| Error::Platform {
1935 code: -1,
1936 message: format!("EditableText.InsertText failed: {}", e),
1937 })?;
1938 Ok(())
1939 }
1940
1941 fn set_text_selection(&self, element: &ElementData, start: u32, end: u32) -> Result<()> {
1942 let target = self.get_cached(element.handle)?;
1943 let proxy = self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Text")?;
1944 proxy
1945 .call_method("SetSelection", &(0i32, start as i32, end as i32))
1946 .map_err(|e| Error::Platform {
1947 code: -1,
1948 message: format!("Text.SetSelection failed: {}", e),
1949 })?;
1950 Ok(())
1951 }
1952
1953 fn perform_action(&self, element: &ElementData, action: &str) -> Result<()> {
1954 match action {
1955 "press" => self.press(element),
1956 "focus" => self.focus(element),
1957 "blur" => self.blur(element),
1958 "toggle" => self.toggle(element),
1959 "select" => self.select(element),
1960 "expand" => self.expand(element),
1961 "collapse" => self.collapse(element),
1962 "show_menu" => self.show_menu(element),
1963 "increment" => self.increment(element),
1964 "decrement" => self.decrement(element),
1965 "scroll_into_view" => self.scroll_into_view(element),
1966 _ => Err(Error::ActionNotSupported {
1967 action: action.to_string(),
1968 role: element.role,
1969 }),
1970 }
1971 }
1972
1973 fn subscribe(&self, element: &ElementData) -> Result<Subscription> {
1974 let pid = element.pid.ok_or(Error::Platform {
1975 code: -1,
1976 message: "Element has no PID for subscribe".to_string(),
1977 })?;
1978 let app_name = element.name.clone().unwrap_or_default();
1979 crate::events::subscribe_for_pid(self, pid, app_name)
1980 }
1981}
1982
1983fn role_has_value(role: Role) -> bool {
1986 !matches!(
1987 role,
1988 Role::Application
1989 | Role::Window
1990 | Role::Dialog
1991 | Role::Group
1992 | Role::MenuBar
1993 | Role::Toolbar
1994 | Role::TabGroup
1995 | Role::SplitGroup
1996 | Role::Table
1997 | Role::TableRow
1998 | Role::Separator
1999 )
2000}
2001
2002fn role_has_actions(role: Role) -> bool {
2005 matches!(
2006 role,
2007 Role::Button
2008 | Role::CheckBox
2009 | Role::RadioButton
2010 | Role::MenuItem
2011 | Role::Link
2012 | Role::ComboBox
2013 | Role::TextField
2014 | Role::TextArea
2015 | Role::SpinButton
2016 | Role::Tab
2017 | Role::TreeItem
2018 | Role::ListItem
2019 | Role::ScrollBar
2020 | Role::Slider
2021 | Role::Menu
2022 | Role::Image
2023 | Role::Unknown
2024 )
2025}
2026
2027pub(crate) fn map_atspi_role(role_name: &str) -> Role {
2029 match role_name.to_lowercase().as_str() {
2030 "application" => Role::Application,
2031 "window" | "frame" => Role::Window,
2032 "dialog" | "file chooser" => Role::Dialog,
2033 "alert" | "notification" => Role::Alert,
2034 "push button" | "push button menu" => Role::Button,
2035 "toggle button" => Role::Switch,
2036 "check box" | "check menu item" => Role::CheckBox,
2037 "radio button" | "radio menu item" => Role::RadioButton,
2038 "entry" | "password text" => Role::TextField,
2039 "spin button" | "spinbutton" => Role::SpinButton,
2040 "text" | "textbox" => Role::TextArea,
2044 "label" | "static" | "caption" => Role::StaticText,
2045 "combo box" | "combobox" => Role::ComboBox,
2046 "list" | "list box" | "listbox" => Role::List,
2048 "list item" => Role::ListItem,
2049 "menu" => Role::Menu,
2050 "menu item" | "tearoff menu item" => Role::MenuItem,
2051 "menu bar" => Role::MenuBar,
2052 "page tab" => Role::Tab,
2053 "page tab list" => Role::TabGroup,
2054 "table" | "tree table" => Role::Table,
2055 "table row" => Role::TableRow,
2056 "table cell" | "table column header" | "table row header" => Role::TableCell,
2057 "tool bar" => Role::Toolbar,
2058 "scroll bar" => Role::ScrollBar,
2059 "slider" => Role::Slider,
2060 "image" | "icon" | "desktop icon" => Role::Image,
2061 "link" => Role::Link,
2062 "panel" | "section" | "form" | "filler" | "viewport" | "scroll pane" => Role::Group,
2063 "progress bar" => Role::ProgressBar,
2064 "tree item" => Role::TreeItem,
2065 "document web" | "document frame" => Role::WebArea,
2066 "heading" => Role::Heading,
2067 "separator" => Role::Separator,
2068 "split pane" => Role::SplitGroup,
2069 "tooltip" | "tool tip" => Role::Tooltip,
2070 "status bar" | "statusbar" => Role::Status,
2071 "landmark" | "navigation" => Role::Navigation,
2072 _ => xa11y_core::unknown_role(role_name),
2073 }
2074}
2075
2076pub(crate) fn map_atspi_role_number(role: u32) -> Role {
2079 match role {
2080 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}")),
2142 }
2143}
2144
2145const GTK_FALLBACK_MAX_DEPTH: u32 = 3;
2149
2150const GTK_FALLBACK_MAX_NODES: usize = 200;
2153
2154fn is_actionable_atspi_role(role: &str) -> bool {
2159 matches!(
2160 role,
2161 "push button"
2162 | "toggle button"
2163 | "check box"
2164 | "radio button"
2165 | "menu item"
2166 | "check menu item"
2167 | "radio menu item"
2168 | "link"
2169 | "page tab"
2170 | "tab"
2171 | "list item"
2172 | "tree item"
2173 )
2174}
2175
2176fn is_never_descend_atspi_role(role: &str) -> bool {
2181 matches!(
2182 role,
2183 "label" | "separator" | "image" | "icon" | "static" | "caption"
2184 )
2185}
2186
2187fn map_atspi_action_name(action_name: &str) -> Option<String> {
2201 let lower = action_name.to_lowercase();
2210 let collapsed: String = lower.chars().filter(|c| !matches!(c, '_' | ' ')).collect();
2211 let canonical = match collapsed.as_str() {
2212 "click" | "activate" | "press" | "invoke" | "dodefault" => "press",
2213 "toggle" | "check" | "uncheck" => "toggle",
2214 "expand" | "open" => "expand",
2215 "collapse" | "close" => "collapse",
2216 "select" => "select",
2217 "menu" | "showmenu" | "showcontextmenu" | "contextmenu" | "popup" => "show_menu",
2218 "increment" => "increment",
2219 "decrement" => "decrement",
2220 _ => return None,
2221 };
2222 Some(canonical.to_string())
2223}
2224
2225#[cfg(test)]
2226mod tests {
2227 use super::*;
2228
2229 #[test]
2230 fn test_role_mapping() {
2231 assert_eq!(map_atspi_role("push button"), Role::Button);
2232 assert_eq!(map_atspi_role("toggle button"), Role::Switch);
2233 assert_eq!(map_atspi_role("check box"), Role::CheckBox);
2234 assert_eq!(map_atspi_role("entry"), Role::TextField);
2235 assert_eq!(map_atspi_role("label"), Role::StaticText);
2236 assert_eq!(map_atspi_role("window"), Role::Window);
2237 assert_eq!(map_atspi_role("frame"), Role::Window);
2238 assert_eq!(map_atspi_role("dialog"), Role::Dialog);
2239 assert_eq!(map_atspi_role("combo box"), Role::ComboBox);
2240 assert_eq!(map_atspi_role("slider"), Role::Slider);
2241 assert_eq!(map_atspi_role("panel"), Role::Group);
2242 assert_eq!(map_atspi_role("unknown_thing"), Role::Unknown);
2243 }
2244
2245 #[test]
2246 fn test_numeric_role_mapping() {
2247 assert_eq!(map_atspi_role_number(62), Role::Switch);
2250 assert_eq!(map_atspi_role_number(43), Role::Button); assert_eq!(map_atspi_role_number(7), Role::CheckBox);
2253 assert_eq!(map_atspi_role_number(67), Role::Unknown); }
2255
2256 #[test]
2257 fn test_action_name_mapping() {
2258 assert_eq!(map_atspi_action_name("click"), Some("press".to_string()));
2259 assert_eq!(map_atspi_action_name("activate"), Some("press".to_string()));
2260 assert_eq!(map_atspi_action_name("press"), Some("press".to_string()));
2261 assert_eq!(map_atspi_action_name("invoke"), Some("press".to_string()));
2262 assert_eq!(
2264 map_atspi_action_name("doDefault"),
2265 Some("press".to_string())
2266 );
2267 assert_eq!(
2268 map_atspi_action_name("do_default"),
2269 Some("press".to_string())
2270 );
2271 assert_eq!(map_atspi_action_name("toggle"), Some("toggle".to_string()));
2272 assert_eq!(map_atspi_action_name("check"), Some("toggle".to_string()));
2273 assert_eq!(map_atspi_action_name("uncheck"), Some("toggle".to_string()));
2274 assert_eq!(map_atspi_action_name("expand"), Some("expand".to_string()));
2275 assert_eq!(map_atspi_action_name("open"), Some("expand".to_string()));
2276 assert_eq!(
2277 map_atspi_action_name("collapse"),
2278 Some("collapse".to_string())
2279 );
2280 assert_eq!(map_atspi_action_name("close"), Some("collapse".to_string()));
2281 assert_eq!(map_atspi_action_name("select"), Some("select".to_string()));
2282 assert_eq!(map_atspi_action_name("menu"), Some("show_menu".to_string()));
2283 assert_eq!(
2284 map_atspi_action_name("showmenu"),
2285 Some("show_menu".to_string())
2286 );
2287 assert_eq!(
2288 map_atspi_action_name("popup"),
2289 Some("show_menu".to_string())
2290 );
2291 assert_eq!(
2292 map_atspi_action_name("show menu"),
2293 Some("show_menu".to_string())
2294 );
2295 assert_eq!(
2298 map_atspi_action_name("showContextMenu"),
2299 Some("show_menu".to_string())
2300 );
2301 assert_eq!(
2302 map_atspi_action_name("show_context_menu"),
2303 Some("show_menu".to_string())
2304 );
2305 assert_eq!(
2306 map_atspi_action_name("increment"),
2307 Some("increment".to_string())
2308 );
2309 assert_eq!(
2310 map_atspi_action_name("decrement"),
2311 Some("decrement".to_string())
2312 );
2313 assert_eq!(map_atspi_action_name("foobar"), None);
2314 }
2315
2316 #[test]
2319 fn test_action_name_aliases_roundtrip() {
2320 let atspi_names = [
2321 "click",
2322 "activate",
2323 "press",
2324 "invoke",
2325 "doDefault",
2326 "do_default",
2327 "toggle",
2328 "check",
2329 "uncheck",
2330 "expand",
2331 "open",
2332 "collapse",
2333 "close",
2334 "select",
2335 "menu",
2336 "showmenu",
2337 "showContextMenu",
2338 "show_context_menu",
2339 "popup",
2340 "show menu",
2341 "increment",
2342 "decrement",
2343 ];
2344 for name in atspi_names {
2345 let canonical = map_atspi_action_name(name).unwrap_or_else(|| {
2346 panic!("AT-SPI2 name {:?} should map to a canonical name", name)
2347 });
2348 let back = map_atspi_action_name(&canonical)
2350 .unwrap_or_else(|| panic!("canonical {:?} should map back to itself", canonical));
2351 assert_eq!(
2352 canonical, back,
2353 "AT-SPI2 {:?} -> {:?} -> {:?} (expected {:?})",
2354 name, canonical, back, canonical
2355 );
2356 }
2357 }
2358
2359 #[test]
2361 fn test_action_name_case_insensitive() {
2362 assert_eq!(map_atspi_action_name("Click"), Some("press".to_string()));
2363 assert_eq!(map_atspi_action_name("TOGGLE"), Some("toggle".to_string()));
2364 assert_eq!(
2365 map_atspi_action_name("Increment"),
2366 Some("increment".to_string())
2367 );
2368 }
2369
2370 #[test]
2375 fn test_gtk_fallback_actionable_roles() {
2376 for role in [
2377 "push button",
2378 "toggle button",
2379 "check box",
2380 "radio button",
2381 "menu item",
2382 "link",
2383 "tab",
2384 "list item",
2385 "tree item",
2386 ] {
2387 assert!(
2388 is_actionable_atspi_role(role),
2389 "{role:?} should be actionable"
2390 );
2391 }
2392 }
2393
2394 #[test]
2399 fn test_gtk_fallback_non_actionable_roles() {
2400 for role in [
2401 "label",
2402 "panel",
2403 "filler",
2404 "section",
2405 "group",
2406 "image",
2407 "separator",
2408 "static",
2409 "frame",
2410 "window",
2411 ] {
2412 assert!(
2413 !is_actionable_atspi_role(role),
2414 "{role:?} must not be treated as actionable"
2415 );
2416 }
2417 }
2418
2419 #[test]
2423 fn test_gtk_fallback_never_descend_roles() {
2424 for role in ["label", "separator", "image", "icon", "static", "caption"] {
2425 assert!(
2426 is_never_descend_atspi_role(role),
2427 "{role:?} must block BFS descent"
2428 );
2429 }
2430 for role in ["panel", "filler", "section", "group", "frame"] {
2433 assert!(
2434 !is_never_descend_atspi_role(role),
2435 "container role {role:?} must remain descendable"
2436 );
2437 }
2438 }
2439}