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(
368 Rect {
369 x,
370 y,
371 width: w.max(0) as u32,
372 height: h.max(0) as u32,
373 }
374 .to_logical(crate::scale::coordinate_scale()),
375 )
376 }
377
378 fn get_actions(&self, aref: &AccessibleRef) -> (Vec<String>, HashMap<String, i32>) {
384 let mut actions = Vec::new();
385 let mut indices = HashMap::new();
386
387 if let Ok(proxy) = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Action") {
389 let n_actions = proxy
391 .get_property::<i32>("NActions")
392 .or_else(|_| proxy.get_property::<u32>("NActions").map(|n| n as i32))
393 .unwrap_or(0);
394 for i in 0..n_actions {
395 if let Ok(reply) = proxy.call_method("GetName", &(i,)) {
396 if let Ok(name) = reply.body().deserialize::<String>() {
397 if let Some(action_name) = map_atspi_action_name(&name) {
398 if !actions.contains(&action_name) {
399 indices.insert(action_name.clone(), i);
400 actions.push(action_name);
401 }
402 }
403 }
404 }
405 }
406 }
407
408 (actions, indices)
416 }
417
418 fn is_gtk_toolkit(&self, aref: &AccessibleRef) -> bool {
425 let app_root = AccessibleRef {
426 bus_name: aref.bus_name.clone(),
427 path: "/org/a11y/atspi/accessible/root".to_string(),
428 };
429 self.make_proxy(
430 &app_root.bus_name,
431 &app_root.path,
432 "org.a11y.atspi.Application",
433 )
434 .ok()
435 .and_then(|proxy| proxy.get_property::<String>("ToolkitName").ok())
436 .map(|t| t.eq_ignore_ascii_case("GTK"))
437 .unwrap_or(false)
438 }
439
440 fn find_gtk_press_fallback(
453 &self,
454 outer: &AccessibleRef,
455 outer_name: &str,
456 ) -> Option<(AccessibleRef, i32)> {
457 let mut queue: VecDeque<(AccessibleRef, u32)> = VecDeque::new();
458 queue.push_back((outer.clone(), 0));
459 let mut visited: usize = 0;
460 let mut shallowest_depth: Option<u32> = None;
461 let mut hits: Vec<(AccessibleRef, i32)> = Vec::new();
462
463 while let Some((node, depth)) = queue.pop_front() {
464 if let Some(best) = shallowest_depth {
466 if depth > best {
467 continue;
468 }
469 }
470 if visited > GTK_FALLBACK_MAX_NODES {
471 break;
472 }
473
474 let role_name = if depth == 0 {
475 String::new()
476 } else {
477 visited += 1;
478 self.get_role_name(&node).unwrap_or_default().to_lowercase()
479 };
480
481 if depth > 0 && is_actionable_atspi_role(&role_name) {
482 if let Some(idx) = self.gtk_fallback_pick(&node, outer_name) {
483 match shallowest_depth {
484 Some(d) if depth < d => {
485 shallowest_depth = Some(depth);
486 hits.clear();
487 hits.push((node.clone(), idx));
488 }
489 Some(d) if depth == d => hits.push((node.clone(), idx)),
490 Some(_) => {}
491 None => {
492 shallowest_depth = Some(depth);
493 hits.push((node.clone(), idx));
494 }
495 }
496 }
497 }
498
499 let stop_descending = depth >= GTK_FALLBACK_MAX_DEPTH
504 || (depth > 0 && is_never_descend_atspi_role(&role_name));
505 if stop_descending {
506 continue;
507 }
508 if let Ok(children) = self.get_atspi_children(&node) {
509 for c in children {
510 queue.push_back((c, depth + 1));
511 }
512 }
513 }
514
515 if hits.len() == 1 {
516 Some(hits.into_iter().next().unwrap())
517 } else {
518 None
519 }
520 }
521
522 fn gtk_fallback_pick(&self, aref: &AccessibleRef, outer_name: &str) -> Option<i32> {
525 let (_, index_map) = self.get_actions(aref);
526 let idx = *index_map.get("press")?;
527 if !self.is_showing_visible_sensitive(aref) {
528 return None;
529 }
530 if !outer_name.is_empty() {
534 let inner_name = self.get_name(aref).unwrap_or_default();
535 if !inner_name.is_empty() && inner_name != outer_name {
536 return None;
537 }
538 }
539 Some(idx)
540 }
541
542 fn is_showing_visible_sensitive(&self, aref: &AccessibleRef) -> bool {
547 let raw = self.get_state(aref).unwrap_or_default();
548 let bits: u64 = if raw.len() >= 2 {
549 (raw[0] as u64) | ((raw[1] as u64) << 32)
550 } else if raw.len() == 1 {
551 raw[0] as u64
552 } else {
553 0
554 };
555 const SENSITIVE: u64 = 1 << 24;
556 const SHOWING: u64 = 1 << 25;
557 const VISIBLE: u64 = 1 << 30;
558 (bits & (SHOWING | VISIBLE | SENSITIVE)) == (SHOWING | VISIBLE | SENSITIVE)
559 }
560
561 fn get_value(&self, aref: &AccessibleRef) -> Option<String> {
564 let text_value = self.get_text_content(aref);
568 if text_value.is_some() {
569 return text_value;
570 }
571 if let Ok(proxy) = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Value") {
573 if let Ok(val) = proxy.get_property::<f64>("CurrentValue") {
574 return Some(val.to_string());
575 }
576 }
577 None
578 }
579
580 fn get_text_content(&self, aref: &AccessibleRef) -> Option<String> {
582 let proxy = self
583 .make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Text")
584 .ok()?;
585 let char_count: i32 = proxy.get_property("CharacterCount").ok()?;
586 if char_count > 0 {
587 let reply = proxy.call_method("GetText", &(0i32, char_count)).ok()?;
588 let text: String = reply.body().deserialize().ok()?;
589 if !text.is_empty() {
590 return Some(text);
591 }
592 }
593 None
594 }
595
596 fn cache_element(&self, aref: AccessibleRef) -> u64 {
598 let handle = NEXT_HANDLE.fetch_add(1, Ordering::Relaxed);
599 self.handle_cache
600 .lock()
601 .unwrap_or_else(|e| e.into_inner())
602 .insert(handle, aref);
603 handle
604 }
605
606 fn get_cached(&self, handle: u64) -> Result<AccessibleRef> {
608 self.handle_cache
609 .lock()
610 .unwrap_or_else(|e| e.into_inner())
611 .get(&handle)
612 .cloned()
613 .ok_or(Error::ElementStale {
614 selector: format!("handle:{}", handle),
615 })
616 }
617
618 fn build_element_data(&self, aref: &AccessibleRef, pid: Option<u32>) -> ElementData {
623 let role_name = self.get_role_name(aref).unwrap_or_default();
624 let role_num = self.get_role_number(aref).unwrap_or(0);
625 let role = {
626 let by_name = if !role_name.is_empty() {
627 map_atspi_role(&role_name)
628 } else {
629 Role::Unknown
630 };
631 let coarse = if by_name != Role::Unknown {
632 by_name
633 } else {
634 map_atspi_role_number(role_num)
635 };
636 self.refine_text_role(coarse, aref)
637 };
638
639 let (
643 ((mut name, value), description),
644 (
645 (states, bounds),
646 ((actions, action_index_map), (numeric_value, min_value, max_value)),
647 ),
648 ) = rayon::join(
649 || {
650 rayon::join(
651 || {
652 let name = self.get_name(aref).ok().filter(|s| !s.is_empty());
653 let value = if role_has_value(role) {
654 self.get_value(aref)
655 } else {
656 None
657 };
658 (name, value)
659 },
660 || self.get_description(aref).ok().filter(|s| !s.is_empty()),
661 )
662 },
663 || {
664 rayon::join(
665 || {
666 rayon::join(
667 || self.parse_states(aref, role),
668 || {
669 if role != Role::Application {
670 self.get_extents(aref)
671 } else {
672 None
673 }
674 },
675 )
676 },
677 || {
678 rayon::join(
679 || {
680 if role_has_actions(role) {
681 self.get_actions(aref)
682 } else {
683 (vec![], HashMap::new())
684 }
685 },
686 || {
687 if matches!(
688 role,
689 Role::Slider
690 | Role::ProgressBar
691 | Role::ScrollBar
692 | Role::SpinButton
693 ) {
694 if let Ok(proxy) = self.make_proxy(
695 &aref.bus_name,
696 &aref.path,
697 "org.a11y.atspi.Value",
698 ) {
699 (
700 proxy.get_property::<f64>("CurrentValue").ok(),
701 proxy.get_property::<f64>("MinimumValue").ok(),
702 proxy.get_property::<f64>("MaximumValue").ok(),
703 )
704 } else {
705 (None, None, None)
706 }
707 } else {
708 (None, None, None)
709 }
710 },
711 )
712 },
713 )
714 },
715 );
716
717 if name.is_none() && role == Role::StaticText {
720 if let Some(ref v) = value {
721 name = Some(v.clone());
722 }
723 }
724
725 let raw = {
726 let raw_role = if role_name.is_empty() {
727 format!("role_num:{}", role_num)
728 } else {
729 role_name
730 };
731 let mut raw = HashMap::new();
732 raw.insert("atspi_role".into(), serde_json::Value::String(raw_role));
733 raw.insert(
734 "bus_name".into(),
735 serde_json::Value::String(aref.bus_name.clone()),
736 );
737 raw.insert(
738 "object_path".into(),
739 serde_json::Value::String(aref.path.clone()),
740 );
741 if let Some(ref n) = name {
744 raw.insert("atspi_name".into(), serde_json::Value::String(n.clone()));
745 }
746 if let Some(ref v) = value {
747 raw.insert("atspi_value".into(), serde_json::Value::String(v.clone()));
748 }
749 if let Some(ref d) = description {
750 raw.insert(
751 "atspi_description".into(),
752 serde_json::Value::String(d.clone()),
753 );
754 }
755 raw
756 };
757
758 let name = xa11y_core::text::strip_bidi_opt(name);
761 let value = xa11y_core::text::strip_bidi_opt(value);
762 let description = xa11y_core::text::strip_bidi_opt(description);
763
764 let handle = self.cache_element(aref.clone());
765 if !action_index_map.is_empty() {
766 self.action_indices
767 .lock()
768 .unwrap_or_else(|e| e.into_inner())
769 .insert(handle, action_index_map);
770 }
771
772 ElementData {
773 role,
774 name,
775 value,
776 description,
777 bounds,
778 actions,
779 states,
780 numeric_value,
781 min_value,
782 max_value,
783 pid,
784 stable_id: Some(aref.path.clone()),
785 raw,
786 handle,
787 }
788 }
789
790 fn get_atspi_parent(&self, aref: &AccessibleRef) -> Result<Option<AccessibleRef>> {
792 let proxy = self.make_proxy(
794 &aref.bus_name,
795 &aref.path,
796 "org.freedesktop.DBus.Properties",
797 )?;
798 let reply = proxy
799 .call_method("Get", &("org.a11y.atspi.Accessible", "Parent"))
800 .map_err(|e| Error::Platform {
801 code: -1,
802 message: format!("Get Parent property failed: {}", e),
803 })?;
804 let variant: zbus::zvariant::OwnedValue =
806 reply.body().deserialize().map_err(|e| Error::Platform {
807 code: -1,
808 message: format!("Parent deserialize variant failed: {}", e),
809 })?;
810 let (bus, path): (String, zbus::zvariant::OwnedObjectPath) =
811 zbus::zvariant::Value::from(variant).try_into().map_err(
812 |e: zbus::zvariant::Error| Error::Platform {
813 code: -1,
814 message: format!("Parent deserialize struct failed: {}", e),
815 },
816 )?;
817 let path_str = path.as_str();
818 if path_str == "/org/a11y/atspi/null" || bus.is_empty() || path_str.is_empty() {
819 return Ok(None);
820 }
821 if path_str == "/org/a11y/atspi/accessible/root" {
823 return Ok(None);
824 }
825 Ok(Some(AccessibleRef {
826 bus_name: bus,
827 path: path_str.to_string(),
828 }))
829 }
830
831 fn state_bits(&self, aref: &AccessibleRef) -> u64 {
836 let state_bits = self.get_state(aref).unwrap_or_default();
837 if state_bits.len() >= 2 {
838 (state_bits[0] as u64) | ((state_bits[1] as u64) << 32)
839 } else if state_bits.len() == 1 {
840 state_bits[0] as u64
841 } else {
842 0
843 }
844 }
845
846 fn parse_states(&self, aref: &AccessibleRef, role: Role) -> StateSet {
848 let bits = self.state_bits(aref);
849
850 const BUSY: u64 = 1 << 3;
852 const CHECKED: u64 = 1 << 4;
853 const EDITABLE: u64 = 1 << 7;
854 const ENABLED: u64 = 1 << 8;
855 const EXPANDABLE: u64 = 1 << 9;
856 const EXPANDED: u64 = 1 << 10;
857 const FOCUSABLE: u64 = 1 << 11;
858 const FOCUSED: u64 = 1 << 12;
859 const MODAL: u64 = 1 << 16;
860 const SELECTED: u64 = 1 << 23;
861 const SENSITIVE: u64 = 1 << 24;
862 const SHOWING: u64 = 1 << 25;
863 const VISIBLE: u64 = 1 << 30;
864 const INDETERMINATE: u64 = 1 << 32;
865 const REQUIRED: u64 = 1 << 33;
866
867 let enabled = (bits & ENABLED) != 0 || (bits & SENSITIVE) != 0;
868 let visible = (bits & VISIBLE) != 0 || (bits & SHOWING) != 0;
869
870 let checked = match role {
871 Role::CheckBox | Role::RadioButton | Role::MenuItem | Role::Switch => {
872 if (bits & INDETERMINATE) != 0 {
873 Some(Toggled::Mixed)
874 } else if (bits & CHECKED) != 0 {
875 Some(Toggled::On)
876 } else {
877 Some(Toggled::Off)
878 }
879 }
880 _ => None,
881 };
882
883 let expanded = if (bits & EXPANDABLE) != 0 {
884 Some((bits & EXPANDED) != 0)
885 } else {
886 None
887 };
888
889 StateSet {
890 enabled,
891 visible,
892 focused: (bits & FOCUSED) != 0,
893 checked,
894 selected: (bits & SELECTED) != 0,
895 expanded,
896 editable: (bits & EDITABLE) != 0,
897 focusable: (bits & FOCUSABLE) != 0,
898 modal: (bits & MODAL) != 0,
899 required: (bits & REQUIRED) != 0,
900 busy: (bits & BUSY) != 0,
901 }
902 }
903
904 pub(crate) fn find_app_by_pid(&self, pid: u32) -> Result<AccessibleRef> {
909 let registry = AccessibleRef {
910 bus_name: "org.a11y.atspi.Registry".to_string(),
911 path: "/org/a11y/atspi/accessible/root".to_string(),
912 };
913 let children = self.get_atspi_children(®istry)?;
914
915 for child in &children {
916 if child.path == "/org/a11y/atspi/null" {
917 continue;
918 }
919 if let Some(app_pid) = self.get_dbus_pid(&child.bus_name) {
925 if app_pid == pid {
926 return Ok(child.clone());
927 }
928 }
929 if let Ok(proxy) =
931 self.make_proxy(&child.bus_name, &child.path, "org.a11y.atspi.Application")
932 {
933 if let Ok(app_pid) = proxy.get_property::<i32>("Id") {
934 if app_pid as u32 == pid {
935 return Ok(child.clone());
936 }
937 }
938 }
939 }
940
941 Err(Error::Platform {
942 code: -1,
943 message: format!("No application found with PID {}", pid),
944 })
945 }
946
947 fn get_dbus_pid(&self, bus_name: &str) -> Option<u32> {
949 let proxy = self
950 .make_proxy(
951 "org.freedesktop.DBus",
952 "/org/freedesktop/DBus",
953 "org.freedesktop.DBus",
954 )
955 .ok()?;
956 let reply = proxy
957 .call_method("GetConnectionUnixProcessID", &(bus_name,))
958 .ok()?;
959 let pid: u32 = reply.body().deserialize().ok()?;
960 if pid > 0 {
961 Some(pid)
962 } else {
963 None
964 }
965 }
966
967 fn do_atspi_action_by_index(&self, aref: &AccessibleRef, index: i32) -> Result<()> {
969 let proxy = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Action")?;
970 proxy
971 .call_method("DoAction", &(index,))
972 .map_err(|e| Error::Platform {
973 code: -1,
974 message: format!("DoAction({}) failed: {}", index, e),
975 })?;
976 Ok(())
977 }
978
979 fn get_action_index(&self, handle: u64, action: &str) -> Result<i32> {
981 self.action_indices
982 .lock()
983 .unwrap_or_else(|e| e.into_inner())
984 .get(&handle)
985 .and_then(|map| map.get(action).copied())
986 .ok_or_else(|| Error::ActionNotSupported {
987 action: action.to_string(),
988 role: Role::Unknown, })
990 }
991
992 fn get_app_pid(&self, aref: &AccessibleRef) -> Option<u32> {
998 if let Some(pid) = self.get_dbus_pid(&aref.bus_name) {
999 return Some(pid);
1000 }
1001
1002 if let Ok(proxy) = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Application")
1009 {
1010 if let Ok(pid) = proxy.get_property::<i32>("Id") {
1011 if pid > 0 && std::path::Path::new(&format!("/proc/{pid}")).exists() {
1012 return Some(pid as u32);
1013 }
1014 }
1015 }
1016
1017 None
1018 }
1019
1020 fn resolve_role(&self, aref: &AccessibleRef) -> Role {
1022 let role_name = self.get_role_name(aref).unwrap_or_default();
1023 let by_name = if !role_name.is_empty() {
1024 map_atspi_role(&role_name)
1025 } else {
1026 Role::Unknown
1027 };
1028 let coarse = if by_name != Role::Unknown {
1029 by_name
1030 } else {
1031 let role_num = self.get_role_number(aref).unwrap_or(0);
1033 map_atspi_role_number(role_num)
1034 };
1035 self.refine_text_role(coarse, aref)
1036 }
1037
1038 fn matches_ref(
1052 &self,
1053 aref: &AccessibleRef,
1054 simple: &xa11y_core::selector::SimpleSelector,
1055 ) -> bool {
1056 let needs_role = simple.role.is_some()
1059 || simple
1060 .filters
1061 .iter()
1062 .any(|f| matches!(f.attr.as_str(), "role" | "checked"));
1063 let role = if needs_role {
1064 Some(self.resolve_role(aref))
1065 } else {
1066 None
1067 };
1068
1069 if let Some(ref role_match) = simple.role {
1070 match role_match {
1071 xa11y_core::selector::RoleMatch::Normalized(expected) => {
1072 if role != Some(*expected) {
1073 return false;
1074 }
1075 }
1076 xa11y_core::selector::RoleMatch::Platform(platform_role) => {
1077 let raw_role = self.get_role_name(aref).unwrap_or_default();
1078 if raw_role != *platform_role {
1079 return false;
1080 }
1081 }
1082 }
1083 }
1084
1085 let mut state_set: Option<StateSet> = None;
1086
1087 for filter in &simple.filters {
1088 let attr = filter.attr.as_str();
1089 let resolved: Option<Option<String>> = match attr {
1090 "role" => Some(role.map(|r| r.to_snake_case().to_string())),
1091 "name" => {
1092 let name = self.get_name(aref).ok().filter(|s| !s.is_empty());
1093 let resolved = if name.is_none() && role == Some(Role::StaticText) {
1096 self.get_value(aref)
1097 } else {
1098 name
1099 };
1100 Some(resolved)
1101 }
1102 "value" => Some(self.get_value(aref)),
1103 "description" => Some(self.get_description(aref).ok().filter(|s| !s.is_empty())),
1104 "enabled" | "visible" | "focused" | "focusable" | "selected" | "editable"
1105 | "modal" | "required" | "busy" | "expanded" | "checked" => {
1106 let s = state_set.get_or_insert_with(|| {
1107 self.parse_states(aref, role.unwrap_or(Role::Unknown))
1111 });
1112 Some(state_attr_to_string(attr, s))
1113 }
1114 _ => None, };
1116
1117 match resolved {
1118 Some(value) => {
1119 if !xa11y_core::selector::match_op(&filter.op, &filter.value, value.as_deref())
1120 {
1121 return false;
1122 }
1123 }
1124 None => {
1125 let pid = None; let data = self.build_element_data(aref, pid);
1133 return xa11y_core::selector::matches_simple(&data, simple);
1134 }
1135 }
1136 }
1137
1138 true
1139 }
1140
1141 fn collect_matching_refs_group(
1151 &self,
1152 parent: &AccessibleRef,
1153 clauses: &[&xa11y_core::selector::SimpleSelector],
1154 depth: u32,
1155 max_depth: u32,
1156 limit: Option<usize>,
1157 ) -> Result<Vec<(usize, AccessibleRef)>> {
1158 if depth > max_depth {
1159 return Ok(vec![]);
1160 }
1161
1162 let children = self.get_atspi_children(parent)?;
1163
1164 let parent_is_registry = parent.bus_name == "org.a11y.atspi.Registry";
1172 let mut to_search: Vec<AccessibleRef> = Vec::new();
1173 for child in children {
1174 if child.path == "/org/a11y/atspi/null"
1175 || child.bus_name.is_empty()
1176 || child.path.is_empty()
1177 {
1178 continue;
1179 }
1180
1181 if !parent_is_registry {
1182 let child_role = self.get_role_name(&child).unwrap_or_default();
1183 if child_role == "application" {
1184 let grandchildren = self.get_atspi_children(&child).unwrap_or_default();
1185 for gc in grandchildren {
1186 if gc.path == "/org/a11y/atspi/null"
1187 || gc.bus_name.is_empty()
1188 || gc.path.is_empty()
1189 {
1190 continue;
1191 }
1192 let gc_role = self.get_role_name(&gc).unwrap_or_default();
1193 if gc_role == "application" {
1194 continue;
1195 }
1196 to_search.push(gc);
1197 }
1198 continue;
1199 }
1200 }
1201 to_search.push(child);
1202 }
1203
1204 if to_search.is_empty() {
1207 self.check_chromium_a11y_enabled(parent, None)?;
1208 }
1209
1210 type ChildBatch = (Vec<(usize, AccessibleRef)>, Option<Error>);
1225 let per_child: Vec<ChildBatch> = to_search
1226 .par_iter()
1227 .map(|child| {
1228 let mut child_results: Vec<(usize, AccessibleRef)> = Vec::new();
1229 for (idx, simple) in clauses.iter().enumerate() {
1230 if self.matches_ref(child, simple) {
1231 child_results.push((idx, child.clone()));
1232 }
1233 }
1234 match self.collect_matching_refs_group(child, clauses, depth + 1, max_depth, limit)
1235 {
1236 Ok(sub) => {
1237 child_results.extend(sub);
1238 (child_results, None)
1239 }
1240 Err(e @ Error::AccessibilityNotEnabled { .. }) => (Vec::new(), Some(e)),
1241 Err(_) => (child_results, None),
1242 }
1243 })
1244 .collect();
1245
1246 let mut results = Vec::new();
1258 for (batch, maybe_err) in per_child {
1259 if let Some(err) = maybe_err {
1260 return Err(err);
1261 }
1262 for r in batch {
1263 results.push(r);
1264 if let Some(limit) = limit {
1265 if results.len() >= limit {
1266 return Ok(results);
1267 }
1268 }
1269 }
1270 }
1271 Ok(results)
1272 }
1273}
1274
1275impl Provider for LinuxProvider {
1276 fn get_children(&self, element: Option<&ElementData>) -> Result<Vec<ElementData>> {
1277 match element {
1278 None => {
1279 self.list_apps()
1282 }
1283 Some(element_data) => {
1284 let aref = self.get_cached(element_data.handle)?;
1285 let children = self.get_atspi_children(&aref).unwrap_or_default();
1286 let pid = element_data.pid;
1287
1288 let mut to_build: Vec<AccessibleRef> = Vec::new();
1291 for child_ref in &children {
1292 if child_ref.path == "/org/a11y/atspi/null"
1293 || child_ref.bus_name.is_empty()
1294 || child_ref.path.is_empty()
1295 {
1296 continue;
1297 }
1298 let child_role = self.get_role_name(child_ref).unwrap_or_default();
1299 if child_role == "application" {
1300 let grandchildren = self.get_atspi_children(child_ref).unwrap_or_default();
1301 for gc_ref in grandchildren {
1302 if gc_ref.path == "/org/a11y/atspi/null"
1303 || gc_ref.bus_name.is_empty()
1304 || gc_ref.path.is_empty()
1305 {
1306 continue;
1307 }
1308 let gc_role = self.get_role_name(&gc_ref).unwrap_or_default();
1309 if gc_role == "application" {
1310 continue;
1311 }
1312 to_build.push(gc_ref);
1313 }
1314 continue;
1315 }
1316 to_build.push(child_ref.clone());
1317 }
1318
1319 if to_build.is_empty() {
1320 self.check_chromium_a11y_enabled(&aref, Some(element_data.role))?;
1321 }
1322
1323 let results: Vec<ElementData> = to_build
1324 .par_iter()
1325 .map(|r| self.build_element_data(r, pid))
1326 .collect();
1327
1328 Ok(results)
1329 }
1330 }
1331 }
1332
1333 fn find_elements_group(
1334 &self,
1335 root: &ElementData,
1336 group: &xa11y_core::selector::SelectorGroup,
1337 limit: Option<usize>,
1338 max_depth: Option<u32>,
1339 ) -> Result<Vec<ElementData>> {
1340 if group.clauses.is_empty() {
1341 return Ok(vec![]);
1342 }
1343 if group.clauses.iter().any(|c| c.segments.is_empty()) {
1346 return Ok(vec![]);
1347 }
1348
1349 let max_depth_val = max_depth.unwrap_or(xa11y_core::MAX_TREE_DEPTH);
1359
1360 let firsts: Vec<&xa11y_core::selector::SimpleSelector> = group
1361 .clauses
1362 .iter()
1363 .map(|c| &c.segments[0].simple)
1364 .collect();
1365
1366 let start_ref = self.get_cached(root.handle)?;
1367
1368 let phase1_limit = if group.clauses.len() == 1 {
1377 let clause = &group.clauses[0];
1378 let first = firsts[0];
1379 let outer = if clause.segments.len() == 1 {
1380 limit
1381 } else {
1382 None
1383 };
1384 match (outer, first.nth) {
1385 (Some(l), Some(n)) => Some(l.max(n)),
1386 (_, Some(n)) => Some(n),
1387 (l, None) => l,
1388 }
1389 } else {
1390 None
1391 };
1392
1393 let phase1: Vec<(usize, AccessibleRef)> =
1394 self.collect_matching_refs_group(&start_ref, &firsts, 0, max_depth_val, phase1_limit)?;
1395
1396 let pid_from_root = root.pid;
1397
1398 let mut by_clause: Vec<Vec<(usize, AccessibleRef)>> =
1402 (0..group.clauses.len()).map(|_| Vec::new()).collect();
1403 for (walk_pos, (clause_idx, aref)) in phase1.into_iter().enumerate() {
1404 by_clause[clause_idx].push((walk_pos, aref));
1405 }
1406
1407 let mut merged: Vec<(usize, ElementData)> = Vec::new();
1415 for (clause_idx, hits) in by_clause.into_iter().enumerate() {
1416 if hits.is_empty() {
1417 continue;
1418 }
1419 let clause = &group.clauses[clause_idx];
1420 let phase1_data: Vec<(usize, ElementData)> = hits
1422 .par_iter()
1423 .map(|(pos, aref)| {
1424 (*pos, self.build_element_data(aref, pid_from_root))
1428 })
1429 .collect();
1430
1431 if clause.segments.len() == 1 {
1432 let mut candidates = phase1_data;
1434 if let Some(nth) = clause.segments[0].simple.nth {
1435 if nth <= candidates.len() {
1436 let kept = candidates.remove(nth - 1);
1437 candidates.clear();
1438 candidates.push(kept);
1439 } else {
1440 candidates.clear();
1441 }
1442 }
1443 merged.extend(candidates);
1444 continue;
1445 }
1446
1447 for (anchor_pos, head) in phase1_data {
1452 let narrowed = self.narrow_multi_segment(
1453 vec![head],
1454 &clause.segments[1..],
1455 max_depth_val,
1456 None,
1457 )?;
1458 for n in narrowed {
1459 merged.push((anchor_pos, n));
1460 }
1461 }
1462 }
1463
1464 merged.sort_by_key(|(pos, _)| *pos);
1468 let mut seen: HashSet<(String, String)> = HashSet::new();
1469 let mut out: Vec<ElementData> = Vec::with_capacity(merged.len());
1470 for (_, data) in merged {
1471 if let Ok(aref) = self.get_cached(data.handle) {
1475 if !seen.insert((aref.bus_name.clone(), aref.path.clone())) {
1476 continue;
1477 }
1478 }
1479 out.push(data);
1480 }
1481 if let Some(l) = limit {
1482 out.truncate(l);
1483 }
1484 Ok(out)
1485 }
1486
1487 fn get_parent(&self, element: &ElementData) -> Result<Option<ElementData>> {
1488 let aref = self.get_cached(element.handle)?;
1489 match self.get_atspi_parent(&aref)? {
1490 Some(parent_ref) => {
1491 let data = self.build_element_data(&parent_ref, element.pid);
1492 Ok(Some(data))
1493 }
1494 None => Ok(None),
1495 }
1496 }
1497
1498 fn list_apps(&self) -> Result<Vec<ElementData>> {
1505 let registry = AccessibleRef {
1506 bus_name: "org.a11y.atspi.Registry".to_string(),
1507 path: "/org/a11y/atspi/accessible/root".to_string(),
1508 };
1509 let children = self.get_atspi_children(®istry)?;
1510
1511 let valid: Vec<(&AccessibleRef, String)> = children
1513 .iter()
1514 .filter(|c| c.path != "/org/a11y/atspi/null")
1515 .filter_map(|c| {
1516 let name = self.get_name(c).unwrap_or_default();
1517 if name.is_empty() {
1518 None
1519 } else {
1520 Some((c, name))
1521 }
1522 })
1523 .collect();
1524
1525 let results: Vec<ElementData> = valid
1526 .par_iter()
1527 .map(|(child, app_name)| {
1528 let pid = self.get_app_pid(child);
1529 let mut data = self.build_element_data(child, pid);
1530 data.name = Some(app_name.clone());
1531 data
1532 })
1533 .collect();
1534
1535 Ok(results)
1536 }
1537
1538 fn app_by_pid(&self, pid: u32) -> Result<ElementData> {
1550 let registry = AccessibleRef {
1551 bus_name: "org.a11y.atspi.Registry".to_string(),
1552 path: "/org/a11y/atspi/accessible/root".to_string(),
1553 };
1554 let children = self.get_atspi_children(®istry)?;
1555
1556 let mut total = 0usize;
1557 let mut unresolved = 0usize;
1558 for child in children.iter().filter(|c| c.path != "/org/a11y/atspi/null") {
1559 total += 1;
1560 match self.get_app_pid(child) {
1561 Some(p) if p == pid => {
1562 let mut data = self.build_element_data(child, Some(p));
1563 let name = self.get_name(child).unwrap_or_default();
1565 if !name.is_empty() {
1566 data.name = Some(name);
1567 }
1568 return Ok(data);
1569 }
1570 Some(_) => {}
1571 None => unresolved += 1,
1572 }
1573 }
1574 Err(
1575 Error::selector_not_matched(format!("application[pid={pid}]")).diagnose(
1576 xa11y_core::Diagnosis {
1577 last_observed: Some(format!(
1578 "{total} AT-SPI registry entries examined, {unresolved} without a \
1579 resolvable pid"
1580 )),
1581 ..Default::default()
1582 },
1583 ),
1584 )
1585 }
1586
1587 fn focused_app(&self) -> Result<ElementData> {
1600 const ACTIVE: u64 = 1 << 1;
1602
1603 let registry = AccessibleRef {
1604 bus_name: "org.a11y.atspi.Registry".to_string(),
1605 path: "/org/a11y/atspi/accessible/root".to_string(),
1606 };
1607 let apps = self.get_atspi_children(®istry)?;
1608
1609 for app in apps.iter().filter(|c| c.path != "/org/a11y/atspi/null") {
1610 let windows = match self.get_atspi_children(app) {
1611 Ok(w) => w,
1612 Err(_) => continue,
1615 };
1616 let has_active_window = windows
1617 .iter()
1618 .filter(|w| w.path != "/org/a11y/atspi/null")
1619 .any(|w| self.state_bits(w) & ACTIVE != 0);
1620 if has_active_window {
1621 let pid = self.get_app_pid(app);
1622 let mut data = self.build_element_data(app, pid);
1623 let name = self.get_name(app).unwrap_or_default();
1624 if !name.is_empty() {
1625 data.name = Some(name);
1626 }
1627 return Ok(data);
1628 }
1629 }
1630
1631 Err(Error::selector_not_matched("focused application"))
1632 }
1633
1634 fn press(&self, element: &ElementData) -> Result<()> {
1635 let target = self.get_cached(element.handle)?;
1636 if let Ok(index) = self.get_action_index(element.handle, "press") {
1638 return self.do_atspi_action_by_index(&target, index);
1639 }
1640 if element.role == Role::Button && self.is_gtk_toolkit(&target) {
1659 let outer_name = self.get_name(&target).unwrap_or_default();
1660 if let Some((inner, index)) = self.find_gtk_press_fallback(&target, &outer_name) {
1661 return self.do_atspi_action_by_index(&inner, index);
1662 }
1663 }
1664 Err(Error::ActionNotSupported {
1665 action: "press".to_string(),
1666 role: element.role,
1667 })
1668 }
1669
1670 fn focus(&self, element: &ElementData) -> Result<()> {
1671 let target = self.get_cached(element.handle)?;
1672 if let Ok(proxy) =
1676 self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Component")
1677 {
1678 if let Ok(reply) = proxy.call_method("GrabFocus", &()) {
1679 if let Ok(true) = reply.body().deserialize::<bool>() {
1681 return Ok(());
1682 }
1683 }
1685 }
1686 if let Ok(index) = self.get_action_index(element.handle, "focus") {
1687 return self.do_atspi_action_by_index(&target, index);
1688 }
1689 Err(Error::ActionNotSupported {
1690 action: "focus".to_string(),
1691 role: element.role,
1692 })
1693 }
1694
1695 fn blur(&self, element: &ElementData) -> Result<()> {
1696 let target = self.get_cached(element.handle)?;
1697 if let Some(parent_ref) = self.get_atspi_parent(&target)? {
1701 if parent_ref.path != "/org/a11y/atspi/null" {
1702 let p = self.make_proxy(
1703 &parent_ref.bus_name,
1704 &parent_ref.path,
1705 "org.a11y.atspi.Component",
1706 )?;
1707 p.call_method("GrabFocus", &())
1708 .map_err(|e| Error::Platform {
1709 code: -1,
1710 message: format!("Component.GrabFocus on parent failed: {}", e),
1711 })?;
1712 return Ok(());
1713 }
1714 }
1715 Err(Error::ActionNotSupported {
1716 action: "blur".to_string(),
1717 role: element.role,
1718 })
1719 }
1720
1721 fn toggle(&self, element: &ElementData) -> Result<()> {
1722 let target = self.get_cached(element.handle)?;
1723 if let Ok(index) = self.get_action_index(element.handle, "toggle") {
1728 return self.do_atspi_action_by_index(&target, index);
1729 }
1730 if matches!(
1753 element.role,
1754 Role::CheckBox | Role::RadioButton | Role::Switch
1755 ) {
1756 if let Ok(index) = self.get_action_index(element.handle, "press") {
1757 return self.do_atspi_action_by_index(&target, index);
1758 }
1759 }
1760 Err(Error::ActionNotSupported {
1761 action: "toggle".to_string(),
1762 role: element.role,
1763 })
1764 }
1765
1766 fn select(&self, element: &ElementData) -> Result<()> {
1767 let target = self.get_cached(element.handle)?;
1768 let index = self
1769 .get_action_index(element.handle, "select")
1770 .map_err(|_| Error::ActionNotSupported {
1771 action: "select".to_string(),
1772 role: element.role,
1773 })?;
1774 self.do_atspi_action_by_index(&target, index)
1775 }
1776
1777 fn expand(&self, element: &ElementData) -> Result<()> {
1778 let target = self.get_cached(element.handle)?;
1779 let index = self
1780 .get_action_index(element.handle, "expand")
1781 .map_err(|_| Error::ActionNotSupported {
1782 action: "expand".to_string(),
1783 role: element.role,
1784 })?;
1785 self.do_atspi_action_by_index(&target, index)
1786 }
1787
1788 fn collapse(&self, element: &ElementData) -> Result<()> {
1789 let target = self.get_cached(element.handle)?;
1790 let index = self
1791 .get_action_index(element.handle, "collapse")
1792 .map_err(|_| Error::ActionNotSupported {
1793 action: "collapse".to_string(),
1794 role: element.role,
1795 })?;
1796 self.do_atspi_action_by_index(&target, index)
1797 }
1798
1799 fn show_menu(&self, element: &ElementData) -> Result<()> {
1800 let target = self.get_cached(element.handle)?;
1801 let index = self
1802 .get_action_index(element.handle, "show_menu")
1803 .map_err(|_| Error::ActionNotSupported {
1804 action: "show_menu".to_string(),
1805 role: element.role,
1806 })?;
1807 self.do_atspi_action_by_index(&target, index)
1808 }
1809
1810 fn increment(&self, element: &ElementData) -> Result<()> {
1811 let target = self.get_cached(element.handle)?;
1812 if let Ok(index) = self.get_action_index(element.handle, "increment") {
1814 return self.do_atspi_action_by_index(&target, index);
1815 }
1816 let proxy = self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Value")?;
1817 let current: f64 = proxy
1818 .get_property("CurrentValue")
1819 .map_err(|e| Error::Platform {
1820 code: -1,
1821 message: format!("Value.CurrentValue failed: {}", e),
1822 })?;
1823 let step: f64 = proxy.get_property("MinimumIncrement").unwrap_or(1.0);
1824 let step = if step <= 0.0 { 1.0 } else { step };
1825 proxy
1826 .set_property("CurrentValue", current + step)
1827 .map_err(|e| Error::Platform {
1828 code: -1,
1829 message: format!("Value.SetCurrentValue failed: {}", e),
1830 })
1831 }
1832
1833 fn decrement(&self, element: &ElementData) -> Result<()> {
1834 let target = self.get_cached(element.handle)?;
1835 if let Ok(index) = self.get_action_index(element.handle, "decrement") {
1836 return self.do_atspi_action_by_index(&target, index);
1837 }
1838 let proxy = self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Value")?;
1839 let current: f64 = proxy
1840 .get_property("CurrentValue")
1841 .map_err(|e| Error::Platform {
1842 code: -1,
1843 message: format!("Value.CurrentValue failed: {}", e),
1844 })?;
1845 let step: f64 = proxy.get_property("MinimumIncrement").unwrap_or(1.0);
1846 let step = if step <= 0.0 { 1.0 } else { step };
1847 proxy
1848 .set_property("CurrentValue", current - step)
1849 .map_err(|e| Error::Platform {
1850 code: -1,
1851 message: format!("Value.SetCurrentValue failed: {}", e),
1852 })
1853 }
1854
1855 fn scroll_into_view(&self, element: &ElementData) -> Result<()> {
1856 let target = self.get_cached(element.handle)?;
1857 let proxy = self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Component")?;
1858 proxy
1859 .call_method("ScrollTo", &(0u32,))
1860 .map_err(|e| Error::Platform {
1861 code: -1,
1862 message: format!("ScrollTo failed: {}", e),
1863 })?;
1864 Ok(())
1865 }
1866
1867 fn set_value(&self, element: &ElementData, value: &str) -> Result<()> {
1868 let target = self.get_cached(element.handle)?;
1869 let proxy = self
1870 .make_proxy(
1871 &target.bus_name,
1872 &target.path,
1873 "org.a11y.atspi.EditableText",
1874 )
1875 .map_err(|_| Error::TextValueNotSupported)?;
1876 if proxy.call_method("SetTextContents", &(value,)).is_ok() {
1878 return Ok(());
1879 }
1880 let classify_editable_text_error = |op: &str, e: zbus::Error| -> Error {
1886 let msg = e.to_string();
1887 if msg.contains("UnknownMethod") || msg.contains("UnknownInterface") {
1888 Error::TextValueNotSupported
1889 } else {
1890 Error::Platform {
1891 code: -1,
1892 message: format!("EditableText.{} failed: {}", op, msg),
1893 }
1894 }
1895 };
1896 if let Err(e) = proxy.call_method("DeleteText", &(0i32, i32::MAX)) {
1897 return Err(classify_editable_text_error("DeleteText", e));
1898 }
1899 if let Err(e) = proxy.call_method("InsertText", &(0i32, value, value.len() as i32)) {
1900 return Err(classify_editable_text_error("InsertText", e));
1901 }
1902 Ok(())
1903 }
1904
1905 fn set_numeric_value(&self, element: &ElementData, value: f64) -> Result<()> {
1906 let target = self.get_cached(element.handle)?;
1907 let proxy = self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Value")?;
1908 proxy
1909 .set_property("CurrentValue", value)
1910 .map_err(|e| Error::Platform {
1911 code: -1,
1912 message: format!("SetValue failed: {}", e),
1913 })
1914 }
1915
1916 fn type_text(&self, element: &ElementData, text: &str) -> Result<()> {
1917 let target = self.get_cached(element.handle)?;
1918 let text_proxy = self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Text");
1921 let insert_pos = text_proxy
1922 .as_ref()
1923 .ok()
1924 .and_then(|p| p.get_property::<i32>("CaretOffset").ok())
1925 .unwrap_or(-1); let proxy = self
1928 .make_proxy(
1929 &target.bus_name,
1930 &target.path,
1931 "org.a11y.atspi.EditableText",
1932 )
1933 .map_err(|_| Error::TextValueNotSupported)?;
1934 let pos = if insert_pos >= 0 {
1935 insert_pos
1936 } else {
1937 i32::MAX
1938 };
1939 proxy
1940 .call_method("InsertText", &(pos, text, text.len() as i32))
1941 .map_err(|e| Error::Platform {
1942 code: -1,
1943 message: format!("EditableText.InsertText failed: {}", e),
1944 })?;
1945 Ok(())
1946 }
1947
1948 fn set_text_selection(&self, element: &ElementData, start: u32, end: u32) -> Result<()> {
1949 let target = self.get_cached(element.handle)?;
1950 let proxy = self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Text")?;
1951 proxy
1952 .call_method("SetSelection", &(0i32, start as i32, end as i32))
1953 .map_err(|e| Error::Platform {
1954 code: -1,
1955 message: format!("Text.SetSelection failed: {}", e),
1956 })?;
1957 Ok(())
1958 }
1959
1960 fn perform_action(&self, element: &ElementData, action: &str) -> Result<()> {
1961 match action {
1962 "press" => self.press(element),
1963 "focus" => self.focus(element),
1964 "blur" => self.blur(element),
1965 "toggle" => self.toggle(element),
1966 "select" => self.select(element),
1967 "expand" => self.expand(element),
1968 "collapse" => self.collapse(element),
1969 "show_menu" => self.show_menu(element),
1970 "increment" => self.increment(element),
1971 "decrement" => self.decrement(element),
1972 "scroll_into_view" => self.scroll_into_view(element),
1973 _ => Err(Error::ActionNotSupported {
1974 action: action.to_string(),
1975 role: element.role,
1976 }),
1977 }
1978 }
1979
1980 fn subscribe(&self, element: &ElementData) -> Result<Subscription> {
1981 let pid = element.pid.ok_or(Error::Platform {
1982 code: -1,
1983 message: "Element has no PID for subscribe".to_string(),
1984 })?;
1985 let app_name = element.name.clone().unwrap_or_default();
1986 crate::events::subscribe_for_pid(self, pid, app_name)
1987 }
1988}
1989
1990fn role_has_value(role: Role) -> bool {
1993 !matches!(
1994 role,
1995 Role::Application
1996 | Role::Window
1997 | Role::Dialog
1998 | Role::Group
1999 | Role::MenuBar
2000 | Role::Toolbar
2001 | Role::TabGroup
2002 | Role::SplitGroup
2003 | Role::Table
2004 | Role::TableRow
2005 | Role::Separator
2006 )
2007}
2008
2009fn role_has_actions(role: Role) -> bool {
2012 matches!(
2013 role,
2014 Role::Button
2015 | Role::CheckBox
2016 | Role::RadioButton
2017 | Role::MenuItem
2018 | Role::Link
2019 | Role::ComboBox
2020 | Role::TextField
2021 | Role::TextArea
2022 | Role::SpinButton
2023 | Role::Tab
2024 | Role::TreeItem
2025 | Role::ListItem
2026 | Role::ScrollBar
2027 | Role::Slider
2028 | Role::Menu
2029 | Role::Image
2030 | Role::Unknown
2031 )
2032}
2033
2034pub(crate) fn map_atspi_role(role_name: &str) -> Role {
2036 match role_name.to_lowercase().as_str() {
2037 "application" => Role::Application,
2038 "window" | "frame" => Role::Window,
2039 "dialog" | "file chooser" => Role::Dialog,
2040 "alert" | "notification" => Role::Alert,
2041 "push button" | "push button menu" => Role::Button,
2042 "toggle button" => Role::Switch,
2043 "check box" | "check menu item" => Role::CheckBox,
2044 "radio button" | "radio menu item" => Role::RadioButton,
2045 "entry" | "password text" => Role::TextField,
2046 "spin button" | "spinbutton" => Role::SpinButton,
2047 "text" | "textbox" => Role::TextArea,
2051 "label" | "static" | "caption" => Role::StaticText,
2052 "combo box" | "combobox" => Role::ComboBox,
2053 "list" | "list box" | "listbox" => Role::List,
2055 "list item" => Role::ListItem,
2056 "menu" => Role::Menu,
2057 "menu item" | "tearoff menu item" => Role::MenuItem,
2058 "menu bar" => Role::MenuBar,
2059 "page tab" => Role::Tab,
2060 "page tab list" => Role::TabGroup,
2061 "table" | "tree table" => Role::Table,
2062 "table row" => Role::TableRow,
2063 "table cell" | "table column header" | "table row header" => Role::TableCell,
2064 "tool bar" => Role::Toolbar,
2065 "scroll bar" => Role::ScrollBar,
2066 "slider" => Role::Slider,
2067 "image" | "icon" | "desktop icon" => Role::Image,
2068 "link" => Role::Link,
2069 "panel" | "section" | "form" | "filler" | "viewport" | "scroll pane" => Role::Group,
2070 "progress bar" => Role::ProgressBar,
2071 "tree item" => Role::TreeItem,
2072 "document web" | "document frame" => Role::WebArea,
2073 "heading" => Role::Heading,
2074 "separator" => Role::Separator,
2075 "split pane" => Role::SplitGroup,
2076 "tooltip" | "tool tip" => Role::Tooltip,
2077 "status bar" | "statusbar" => Role::Status,
2078 "landmark" | "navigation" => Role::Navigation,
2079 _ => xa11y_core::unknown_role(role_name),
2080 }
2081}
2082
2083pub(crate) fn map_atspi_role_number(role: u32) -> Role {
2086 match role {
2087 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}")),
2149 }
2150}
2151
2152const GTK_FALLBACK_MAX_DEPTH: u32 = 3;
2156
2157const GTK_FALLBACK_MAX_NODES: usize = 200;
2160
2161fn is_actionable_atspi_role(role: &str) -> bool {
2166 matches!(
2167 role,
2168 "push button"
2169 | "toggle button"
2170 | "check box"
2171 | "radio button"
2172 | "menu item"
2173 | "check menu item"
2174 | "radio menu item"
2175 | "link"
2176 | "page tab"
2177 | "tab"
2178 | "list item"
2179 | "tree item"
2180 )
2181}
2182
2183fn is_never_descend_atspi_role(role: &str) -> bool {
2188 matches!(
2189 role,
2190 "label" | "separator" | "image" | "icon" | "static" | "caption"
2191 )
2192}
2193
2194fn map_atspi_action_name(action_name: &str) -> Option<String> {
2208 let lower = action_name.to_lowercase();
2217 let collapsed: String = lower.chars().filter(|c| !matches!(c, '_' | ' ')).collect();
2218 let canonical = match collapsed.as_str() {
2219 "click" | "activate" | "press" | "invoke" | "dodefault" => "press",
2220 "toggle" | "check" | "uncheck" => "toggle",
2221 "expand" | "open" => "expand",
2222 "collapse" | "close" => "collapse",
2223 "select" => "select",
2224 "menu" | "showmenu" | "showcontextmenu" | "contextmenu" | "popup" => "show_menu",
2225 "increment" => "increment",
2226 "decrement" => "decrement",
2227 _ => return None,
2228 };
2229 Some(canonical.to_string())
2230}
2231
2232#[cfg(test)]
2233mod tests {
2234 use super::*;
2235
2236 #[test]
2237 fn test_role_mapping() {
2238 assert_eq!(map_atspi_role("push button"), Role::Button);
2239 assert_eq!(map_atspi_role("toggle button"), Role::Switch);
2240 assert_eq!(map_atspi_role("check box"), Role::CheckBox);
2241 assert_eq!(map_atspi_role("entry"), Role::TextField);
2242 assert_eq!(map_atspi_role("label"), Role::StaticText);
2243 assert_eq!(map_atspi_role("window"), Role::Window);
2244 assert_eq!(map_atspi_role("frame"), Role::Window);
2245 assert_eq!(map_atspi_role("dialog"), Role::Dialog);
2246 assert_eq!(map_atspi_role("combo box"), Role::ComboBox);
2247 assert_eq!(map_atspi_role("slider"), Role::Slider);
2248 assert_eq!(map_atspi_role("panel"), Role::Group);
2249 assert_eq!(map_atspi_role("unknown_thing"), Role::Unknown);
2250 }
2251
2252 #[test]
2253 fn test_numeric_role_mapping() {
2254 assert_eq!(map_atspi_role_number(62), Role::Switch);
2257 assert_eq!(map_atspi_role_number(43), Role::Button); assert_eq!(map_atspi_role_number(7), Role::CheckBox);
2260 assert_eq!(map_atspi_role_number(67), Role::Unknown); }
2262
2263 #[test]
2264 fn test_action_name_mapping() {
2265 assert_eq!(map_atspi_action_name("click"), Some("press".to_string()));
2266 assert_eq!(map_atspi_action_name("activate"), Some("press".to_string()));
2267 assert_eq!(map_atspi_action_name("press"), Some("press".to_string()));
2268 assert_eq!(map_atspi_action_name("invoke"), Some("press".to_string()));
2269 assert_eq!(
2271 map_atspi_action_name("doDefault"),
2272 Some("press".to_string())
2273 );
2274 assert_eq!(
2275 map_atspi_action_name("do_default"),
2276 Some("press".to_string())
2277 );
2278 assert_eq!(map_atspi_action_name("toggle"), Some("toggle".to_string()));
2279 assert_eq!(map_atspi_action_name("check"), Some("toggle".to_string()));
2280 assert_eq!(map_atspi_action_name("uncheck"), Some("toggle".to_string()));
2281 assert_eq!(map_atspi_action_name("expand"), Some("expand".to_string()));
2282 assert_eq!(map_atspi_action_name("open"), Some("expand".to_string()));
2283 assert_eq!(
2284 map_atspi_action_name("collapse"),
2285 Some("collapse".to_string())
2286 );
2287 assert_eq!(map_atspi_action_name("close"), Some("collapse".to_string()));
2288 assert_eq!(map_atspi_action_name("select"), Some("select".to_string()));
2289 assert_eq!(map_atspi_action_name("menu"), Some("show_menu".to_string()));
2290 assert_eq!(
2291 map_atspi_action_name("showmenu"),
2292 Some("show_menu".to_string())
2293 );
2294 assert_eq!(
2295 map_atspi_action_name("popup"),
2296 Some("show_menu".to_string())
2297 );
2298 assert_eq!(
2299 map_atspi_action_name("show menu"),
2300 Some("show_menu".to_string())
2301 );
2302 assert_eq!(
2305 map_atspi_action_name("showContextMenu"),
2306 Some("show_menu".to_string())
2307 );
2308 assert_eq!(
2309 map_atspi_action_name("show_context_menu"),
2310 Some("show_menu".to_string())
2311 );
2312 assert_eq!(
2313 map_atspi_action_name("increment"),
2314 Some("increment".to_string())
2315 );
2316 assert_eq!(
2317 map_atspi_action_name("decrement"),
2318 Some("decrement".to_string())
2319 );
2320 assert_eq!(map_atspi_action_name("foobar"), None);
2321 }
2322
2323 #[test]
2326 fn test_action_name_aliases_roundtrip() {
2327 let atspi_names = [
2328 "click",
2329 "activate",
2330 "press",
2331 "invoke",
2332 "doDefault",
2333 "do_default",
2334 "toggle",
2335 "check",
2336 "uncheck",
2337 "expand",
2338 "open",
2339 "collapse",
2340 "close",
2341 "select",
2342 "menu",
2343 "showmenu",
2344 "showContextMenu",
2345 "show_context_menu",
2346 "popup",
2347 "show menu",
2348 "increment",
2349 "decrement",
2350 ];
2351 for name in atspi_names {
2352 let canonical = map_atspi_action_name(name).unwrap_or_else(|| {
2353 panic!("AT-SPI2 name {:?} should map to a canonical name", name)
2354 });
2355 let back = map_atspi_action_name(&canonical)
2357 .unwrap_or_else(|| panic!("canonical {:?} should map back to itself", canonical));
2358 assert_eq!(
2359 canonical, back,
2360 "AT-SPI2 {:?} -> {:?} -> {:?} (expected {:?})",
2361 name, canonical, back, canonical
2362 );
2363 }
2364 }
2365
2366 #[test]
2368 fn test_action_name_case_insensitive() {
2369 assert_eq!(map_atspi_action_name("Click"), Some("press".to_string()));
2370 assert_eq!(map_atspi_action_name("TOGGLE"), Some("toggle".to_string()));
2371 assert_eq!(
2372 map_atspi_action_name("Increment"),
2373 Some("increment".to_string())
2374 );
2375 }
2376
2377 #[test]
2382 fn test_gtk_fallback_actionable_roles() {
2383 for role in [
2384 "push button",
2385 "toggle button",
2386 "check box",
2387 "radio button",
2388 "menu item",
2389 "link",
2390 "tab",
2391 "list item",
2392 "tree item",
2393 ] {
2394 assert!(
2395 is_actionable_atspi_role(role),
2396 "{role:?} should be actionable"
2397 );
2398 }
2399 }
2400
2401 #[test]
2406 fn test_gtk_fallback_non_actionable_roles() {
2407 for role in [
2408 "label",
2409 "panel",
2410 "filler",
2411 "section",
2412 "group",
2413 "image",
2414 "separator",
2415 "static",
2416 "frame",
2417 "window",
2418 ] {
2419 assert!(
2420 !is_actionable_atspi_role(role),
2421 "{role:?} must not be treated as actionable"
2422 );
2423 }
2424 }
2425
2426 #[test]
2430 fn test_gtk_fallback_never_descend_roles() {
2431 for role in ["label", "separator", "image", "icon", "static", "caption"] {
2432 assert!(
2433 is_never_descend_atspi_role(role),
2434 "{role:?} must block BFS descent"
2435 );
2436 }
2437 for role in ["panel", "filler", "section", "group", "frame"] {
2440 assert!(
2441 !is_never_descend_atspi_role(role),
2442 "container role {role:?} must remain descendable"
2443 );
2444 }
2445 }
2446}