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 "active" => Some(s.active.to_string()),
24 "focusable" => Some(s.focusable.to_string()),
25 "selected" => Some(s.selected.to_string()),
26 "editable" => Some(s.editable.to_string()),
27 "modal" => Some(s.modal.to_string()),
28 "required" => Some(s.required.to_string()),
29 "busy" => Some(s.busy.to_string()),
30 "expanded" => s.expanded.map(|b| b.to_string()),
31 "checked" => s.checked.map(|c| {
32 match c {
33 Toggled::On => "on",
34 Toggled::Off => "off",
35 Toggled::Mixed => "mixed",
36 }
37 .to_string()
38 }),
39 _ => None,
40 }
41}
42
43pub struct LinuxProvider {
45 a11y_bus: Connection,
46 handle_cache: Mutex<HashMap<u64, AccessibleRef>>,
48 action_indices: Mutex<HashMap<u64, HashMap<String, i32>>>,
51}
52
53#[derive(Debug, Clone)]
55pub(crate) struct AccessibleRef {
56 pub(crate) bus_name: String,
57 pub(crate) path: String,
58}
59
60impl LinuxProvider {
61 pub fn new() -> Result<Self> {
66 let a11y_bus = Self::connect_a11y_bus()?;
67 Ok(Self {
68 a11y_bus,
69 handle_cache: Mutex::new(HashMap::new()),
70 action_indices: Mutex::new(HashMap::new()),
71 })
72 }
73
74 pub(crate) fn connect_a11y_bus() -> Result<Connection> {
75 if let Ok(session) = Connection::session() {
79 let proxy = Proxy::new(&session, "org.a11y.Bus", "/org/a11y/bus", "org.a11y.Bus")
80 .map_err(|e| Error::Platform {
81 code: -1,
82 message: format!("Failed to create a11y bus proxy: {}", e),
83 })?;
84
85 if let Ok(addr_reply) = proxy.call_method("GetAddress", &()) {
86 if let Ok(address) = addr_reply.body().deserialize::<String>() {
87 if let Ok(addr) = zbus::Address::try_from(address.as_str()) {
88 if let Ok(Ok(conn)) =
89 zbus::blocking::connection::Builder::address(addr).map(|b| b.build())
90 {
91 return Ok(conn);
92 }
93 }
94 }
95 }
96
97 return Ok(session);
99 }
100
101 Connection::session().map_err(|e| Error::Platform {
102 code: -1,
103 message: format!("Failed to connect to D-Bus session bus: {}", e),
104 })
105 }
106
107 fn make_proxy(&self, bus_name: &str, path: &str, interface: &str) -> Result<Proxy<'_>> {
108 zbus::blocking::proxy::Builder::<Proxy>::new(&self.a11y_bus)
111 .destination(bus_name.to_owned())
112 .map_err(|e| Error::Platform {
113 code: -1,
114 message: format!("Failed to set proxy destination: {}", e),
115 })?
116 .path(path.to_owned())
117 .map_err(|e| Error::Platform {
118 code: -1,
119 message: format!("Failed to set proxy path: {}", e),
120 })?
121 .interface(interface.to_owned())
122 .map_err(|e| Error::Platform {
123 code: -1,
124 message: format!("Failed to set proxy interface: {}", e),
125 })?
126 .cache_properties(zbus::proxy::CacheProperties::No)
127 .build()
128 .map_err(|e| Error::Platform {
129 code: -1,
130 message: format!("Failed to create proxy: {}", e),
131 })
132 }
133
134 fn has_interface(&self, aref: &AccessibleRef, iface: &str) -> bool {
137 let proxy = match self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Accessible") {
138 Ok(p) => p,
139 Err(_) => return false,
140 };
141 let reply = match proxy.call_method("GetInterfaces", &()) {
142 Ok(r) => r,
143 Err(_) => return false,
144 };
145 let interfaces: Vec<String> = match reply.body().deserialize() {
146 Ok(v) => v,
147 Err(_) => return false,
148 };
149 interfaces.iter().any(|i| i.contains(iface))
150 }
151
152 fn get_role_number(&self, aref: &AccessibleRef) -> Result<u32> {
154 let proxy = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Accessible")?;
155 let reply = proxy
156 .call_method("GetRole", &())
157 .map_err(|e| Error::Platform {
158 code: -1,
159 message: format!("GetRole failed: {}", e),
160 })?;
161 reply
162 .body()
163 .deserialize::<u32>()
164 .map_err(|e| Error::Platform {
165 code: -1,
166 message: format!("GetRole deserialize failed: {}", e),
167 })
168 }
169
170 fn get_role_name(&self, aref: &AccessibleRef) -> Result<String> {
172 let proxy = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Accessible")?;
173 let reply = proxy
174 .call_method("GetRoleName", &())
175 .map_err(|e| Error::Platform {
176 code: -1,
177 message: format!("GetRoleName failed: {}", e),
178 })?;
179 reply
180 .body()
181 .deserialize::<String>()
182 .map_err(|e| Error::Platform {
183 code: -1,
184 message: format!("GetRoleName deserialize failed: {}", e),
185 })
186 }
187
188 fn check_chromium_a11y_enabled(
205 &self,
206 parent: &AccessibleRef,
207 role_hint: Option<Role>,
208 ) -> Result<()> {
209 let app_root = AccessibleRef {
210 bus_name: parent.bus_name.clone(),
211 path: "/org/a11y/atspi/accessible/root".to_string(),
212 };
213 let toolkit = match self
214 .make_proxy(
215 &app_root.bus_name,
216 &app_root.path,
217 "org.a11y.atspi.Application",
218 )
219 .ok()
220 .and_then(|proxy| proxy.get_property::<String>("ToolkitName").ok())
221 {
222 Some(t) => t,
223 None => return Ok(()),
224 };
225 if !toolkit.eq_ignore_ascii_case("Chromium") {
226 return Ok(());
227 }
228 let role = role_hint.unwrap_or_else(|| self.resolve_role(parent));
229 if role != Role::Window {
230 return Ok(());
231 }
232 let app_name = self.get_name(&app_root).unwrap_or_default();
233 Err(Error::AccessibilityNotEnabled {
234 app: app_name,
235 instructions: "Chromium/Electron app exposes an empty accessibility tree on Linux. \
236 Relaunch with `--force-renderer-accessibility` (or set the env var \
237 `ACCESSIBILITY_ENABLED=1`) so the renderer accessibility bridge is initialised."
238 .to_string(),
239 })
240 }
241
242 fn get_name(&self, aref: &AccessibleRef) -> Result<String> {
244 let proxy = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Accessible")?;
245 proxy
246 .get_property::<String>("Name")
247 .map_err(|e| Error::Platform {
248 code: -1,
249 message: format!("Get Name property failed: {}", e),
250 })
251 }
252
253 fn get_description(&self, aref: &AccessibleRef) -> Result<String> {
255 let proxy = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Accessible")?;
256 proxy
257 .get_property::<String>("Description")
258 .map_err(|e| Error::Platform {
259 code: -1,
260 message: format!("Get Description property failed: {}", e),
261 })
262 }
263
264 fn get_atspi_children(&self, aref: &AccessibleRef) -> Result<Vec<AccessibleRef>> {
268 let proxy = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Accessible")?;
269 let reply = proxy
270 .call_method("GetChildren", &())
271 .map_err(|e| Error::Platform {
272 code: -1,
273 message: format!("GetChildren failed: {}", e),
274 })?;
275 let children: Vec<(String, zbus::zvariant::OwnedObjectPath)> =
276 reply.body().deserialize().map_err(|e| Error::Platform {
277 code: -1,
278 message: format!("GetChildren deserialize failed: {}", e),
279 })?;
280 Ok(children
281 .into_iter()
282 .map(|(bus_name, path)| AccessibleRef {
283 bus_name,
284 path: path.to_string(),
285 })
286 .collect())
287 }
288
289 fn get_state(&self, aref: &AccessibleRef) -> Result<Vec<u32>> {
291 let proxy = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Accessible")?;
292 let reply = proxy
293 .call_method("GetState", &())
294 .map_err(|e| Error::Platform {
295 code: -1,
296 message: format!("GetState failed: {}", e),
297 })?;
298 reply
299 .body()
300 .deserialize::<Vec<u32>>()
301 .map_err(|e| Error::Platform {
302 code: -1,
303 message: format!("GetState deserialize failed: {}", e),
304 })
305 }
306
307 fn is_multi_line(&self, aref: &AccessibleRef) -> bool {
313 let state_bits = self.get_state(aref).unwrap_or_default();
314 let bits: u64 = if state_bits.len() >= 2 {
315 (state_bits[0] as u64) | ((state_bits[1] as u64) << 32)
316 } else if state_bits.len() == 1 {
317 state_bits[0] as u64
318 } else {
319 0
320 };
321 const MULTI_LINE: u64 = 1 << 17;
323 (bits & MULTI_LINE) != 0
324 }
325
326 fn refine_text_role(&self, coarse: Role, aref: &AccessibleRef) -> Role {
335 match coarse {
336 Role::TextArea | Role::TextField => {
337 if self.is_multi_line(aref) {
338 Role::TextArea
339 } else {
340 Role::TextField
341 }
342 }
343 other => other,
344 }
345 }
346
347 fn get_extents(&self, aref: &AccessibleRef) -> Option<Rect> {
351 if !self.has_interface(aref, "Component") {
352 return None;
353 }
354 let proxy = self
355 .make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Component")
356 .ok()?;
357 let reply = proxy.call_method("GetExtents", &(0u32,)).ok()?;
360 let (x, y, w, h): (i32, i32, i32, i32) = reply.body().deserialize().ok()?;
361 if w <= 0 && h <= 0 {
362 return None;
363 }
364 Some(
369 Rect {
370 x,
371 y,
372 width: w.max(0) as u32,
373 height: h.max(0) as u32,
374 }
375 .to_logical(crate::scale::coordinate_scale()),
376 )
377 }
378
379 fn get_actions(&self, aref: &AccessibleRef) -> (Vec<String>, HashMap<String, i32>) {
385 let mut actions = Vec::new();
386 let mut indices = HashMap::new();
387
388 if let Ok(proxy) = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Action") {
390 let n_actions = proxy
392 .get_property::<i32>("NActions")
393 .or_else(|_| proxy.get_property::<u32>("NActions").map(|n| n as i32))
394 .unwrap_or(0);
395 for i in 0..n_actions {
396 if let Ok(reply) = proxy.call_method("GetName", &(i,)) {
397 if let Ok(name) = reply.body().deserialize::<String>() {
398 if let Some(action_name) = map_atspi_action_name(&name) {
399 if !actions.contains(&action_name) {
400 indices.insert(action_name.clone(), i);
401 actions.push(action_name);
402 }
403 }
404 }
405 }
406 }
407 }
408
409 (actions, indices)
417 }
418
419 fn is_gtk_toolkit(&self, aref: &AccessibleRef) -> bool {
426 let app_root = AccessibleRef {
427 bus_name: aref.bus_name.clone(),
428 path: "/org/a11y/atspi/accessible/root".to_string(),
429 };
430 self.make_proxy(
431 &app_root.bus_name,
432 &app_root.path,
433 "org.a11y.atspi.Application",
434 )
435 .ok()
436 .and_then(|proxy| proxy.get_property::<String>("ToolkitName").ok())
437 .map(|t| t.eq_ignore_ascii_case("GTK"))
438 .unwrap_or(false)
439 }
440
441 fn find_gtk_press_fallback(
454 &self,
455 outer: &AccessibleRef,
456 outer_name: &str,
457 ) -> Option<(AccessibleRef, i32)> {
458 let mut queue: VecDeque<(AccessibleRef, u32)> = VecDeque::new();
459 queue.push_back((outer.clone(), 0));
460 let mut visited: usize = 0;
461 let mut shallowest_depth: Option<u32> = None;
462 let mut hits: Vec<(AccessibleRef, i32)> = Vec::new();
463
464 while let Some((node, depth)) = queue.pop_front() {
465 if let Some(best) = shallowest_depth {
467 if depth > best {
468 continue;
469 }
470 }
471 if visited > GTK_FALLBACK_MAX_NODES {
472 break;
473 }
474
475 let role_name = if depth == 0 {
476 String::new()
477 } else {
478 visited += 1;
479 self.get_role_name(&node).unwrap_or_default().to_lowercase()
480 };
481
482 if depth > 0 && is_actionable_atspi_role(&role_name) {
483 if let Some(idx) = self.gtk_fallback_pick(&node, outer_name) {
484 match shallowest_depth {
485 Some(d) if depth < d => {
486 shallowest_depth = Some(depth);
487 hits.clear();
488 hits.push((node.clone(), idx));
489 }
490 Some(d) if depth == d => hits.push((node.clone(), idx)),
491 Some(_) => {}
492 None => {
493 shallowest_depth = Some(depth);
494 hits.push((node.clone(), idx));
495 }
496 }
497 }
498 }
499
500 let stop_descending = depth >= GTK_FALLBACK_MAX_DEPTH
505 || (depth > 0 && is_never_descend_atspi_role(&role_name));
506 if stop_descending {
507 continue;
508 }
509 if let Ok(children) = self.get_atspi_children(&node) {
510 for c in children {
511 queue.push_back((c, depth + 1));
512 }
513 }
514 }
515
516 if hits.len() == 1 {
517 Some(hits.into_iter().next().unwrap())
518 } else {
519 None
520 }
521 }
522
523 fn gtk_fallback_pick(&self, aref: &AccessibleRef, outer_name: &str) -> Option<i32> {
526 let (_, index_map) = self.get_actions(aref);
527 let idx = *index_map.get("press")?;
528 if !self.is_showing_visible_sensitive(aref) {
529 return None;
530 }
531 if !outer_name.is_empty() {
535 let inner_name = self.get_name(aref).unwrap_or_default();
536 if !inner_name.is_empty() && inner_name != outer_name {
537 return None;
538 }
539 }
540 Some(idx)
541 }
542
543 fn is_showing_visible_sensitive(&self, aref: &AccessibleRef) -> bool {
548 let raw = self.get_state(aref).unwrap_or_default();
549 let bits: u64 = if raw.len() >= 2 {
550 (raw[0] as u64) | ((raw[1] as u64) << 32)
551 } else if raw.len() == 1 {
552 raw[0] as u64
553 } else {
554 0
555 };
556 const SENSITIVE: u64 = 1 << 24;
557 const SHOWING: u64 = 1 << 25;
558 const VISIBLE: u64 = 1 << 30;
559 (bits & (SHOWING | VISIBLE | SENSITIVE)) == (SHOWING | VISIBLE | SENSITIVE)
560 }
561
562 fn get_value(&self, aref: &AccessibleRef) -> Option<String> {
565 let text_value = self.get_text_content(aref);
569 if text_value.is_some() {
570 return text_value;
571 }
572 if let Ok(proxy) = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Value") {
574 if let Ok(val) = proxy.get_property::<f64>("CurrentValue") {
575 return Some(val.to_string());
576 }
577 }
578 None
579 }
580
581 fn get_text_content(&self, aref: &AccessibleRef) -> Option<String> {
583 let proxy = self
584 .make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Text")
585 .ok()?;
586 let char_count: i32 = proxy.get_property("CharacterCount").ok()?;
587 if char_count > 0 {
588 let reply = proxy.call_method("GetText", &(0i32, char_count)).ok()?;
589 let text: String = reply.body().deserialize().ok()?;
590 if !text.is_empty() {
591 return Some(text);
592 }
593 }
594 None
595 }
596
597 fn cache_element(&self, aref: AccessibleRef) -> u64 {
599 let handle = NEXT_HANDLE.fetch_add(1, Ordering::Relaxed);
600 self.handle_cache
601 .lock()
602 .unwrap_or_else(|e| e.into_inner())
603 .insert(handle, aref);
604 handle
605 }
606
607 fn get_cached(&self, handle: u64) -> Result<AccessibleRef> {
609 self.handle_cache
610 .lock()
611 .unwrap_or_else(|e| e.into_inner())
612 .get(&handle)
613 .cloned()
614 .ok_or(Error::ElementStale {
615 selector: format!("handle:{}", handle),
616 })
617 }
618
619 fn build_element_data(&self, aref: &AccessibleRef, pid: Option<u32>) -> ElementData {
624 let role_name = self.get_role_name(aref).unwrap_or_default();
625 let role_num = self.get_role_number(aref).unwrap_or(0);
626 let role = {
627 let by_name = if !role_name.is_empty() {
628 map_atspi_role(&role_name)
629 } else {
630 Role::Unknown
631 };
632 let coarse = if by_name != Role::Unknown {
633 by_name
634 } else {
635 map_atspi_role_number(role_num)
636 };
637 self.refine_text_role(coarse, aref)
638 };
639
640 let (
644 ((mut name, value), description),
645 (
646 (states, bounds),
647 ((actions, action_index_map), (numeric_value, min_value, max_value)),
648 ),
649 ) = rayon::join(
650 || {
651 rayon::join(
652 || {
653 let name = self.get_name(aref).ok().filter(|s| !s.is_empty());
654 let value = if role_has_value(role) {
655 self.get_value(aref)
656 } else {
657 None
658 };
659 (name, value)
660 },
661 || self.get_description(aref).ok().filter(|s| !s.is_empty()),
662 )
663 },
664 || {
665 rayon::join(
666 || {
667 rayon::join(
668 || self.parse_states(aref, role),
669 || {
670 if role != Role::Application {
671 self.get_extents(aref)
672 } else {
673 None
674 }
675 },
676 )
677 },
678 || {
679 rayon::join(
680 || {
681 if role_has_actions(role) {
682 self.get_actions(aref)
683 } else {
684 (vec![], HashMap::new())
685 }
686 },
687 || {
688 if matches!(
689 role,
690 Role::Slider
691 | Role::ProgressBar
692 | Role::ScrollBar
693 | Role::SpinButton
694 ) {
695 if let Ok(proxy) = self.make_proxy(
696 &aref.bus_name,
697 &aref.path,
698 "org.a11y.atspi.Value",
699 ) {
700 (
701 proxy.get_property::<f64>("CurrentValue").ok(),
702 proxy.get_property::<f64>("MinimumValue").ok(),
703 proxy.get_property::<f64>("MaximumValue").ok(),
704 )
705 } else {
706 (None, None, None)
707 }
708 } else {
709 (None, None, None)
710 }
711 },
712 )
713 },
714 )
715 },
716 );
717
718 if name.is_none() && role == Role::StaticText {
721 if let Some(ref v) = value {
722 name = Some(v.clone());
723 }
724 }
725
726 let raw = {
727 let raw_role = if role_name.is_empty() {
728 format!("role_num:{}", role_num)
729 } else {
730 role_name
731 };
732 let mut raw = HashMap::new();
733 raw.insert("atspi_role".into(), serde_json::Value::String(raw_role));
734 raw.insert(
735 "bus_name".into(),
736 serde_json::Value::String(aref.bus_name.clone()),
737 );
738 raw.insert(
739 "object_path".into(),
740 serde_json::Value::String(aref.path.clone()),
741 );
742 if let Some(ref n) = name {
745 raw.insert("atspi_name".into(), serde_json::Value::String(n.clone()));
746 }
747 if let Some(ref v) = value {
748 raw.insert("atspi_value".into(), serde_json::Value::String(v.clone()));
749 }
750 if let Some(ref d) = description {
751 raw.insert(
752 "atspi_description".into(),
753 serde_json::Value::String(d.clone()),
754 );
755 }
756 raw
757 };
758
759 let name = xa11y_core::text::strip_bidi_opt(name);
762 let value = xa11y_core::text::strip_bidi_opt(value);
763 let description = xa11y_core::text::strip_bidi_opt(description);
764
765 let handle = self.cache_element(aref.clone());
766 if !action_index_map.is_empty() {
767 self.action_indices
768 .lock()
769 .unwrap_or_else(|e| e.into_inner())
770 .insert(handle, action_index_map);
771 }
772
773 ElementData {
774 role,
775 name,
776 value,
777 description,
778 bounds,
779 actions,
780 states,
781 numeric_value,
782 min_value,
783 max_value,
784 pid,
785 stable_id: Some(aref.path.clone()),
786 raw,
787 handle,
788 }
789 }
790
791 fn get_atspi_parent(&self, aref: &AccessibleRef) -> Result<Option<AccessibleRef>> {
793 let proxy = self.make_proxy(
795 &aref.bus_name,
796 &aref.path,
797 "org.freedesktop.DBus.Properties",
798 )?;
799 let reply = proxy
800 .call_method("Get", &("org.a11y.atspi.Accessible", "Parent"))
801 .map_err(|e| Error::Platform {
802 code: -1,
803 message: format!("Get Parent property failed: {}", e),
804 })?;
805 let variant: zbus::zvariant::OwnedValue =
807 reply.body().deserialize().map_err(|e| Error::Platform {
808 code: -1,
809 message: format!("Parent deserialize variant failed: {}", e),
810 })?;
811 let (bus, path): (String, zbus::zvariant::OwnedObjectPath) =
812 zbus::zvariant::Value::from(variant).try_into().map_err(
813 |e: zbus::zvariant::Error| Error::Platform {
814 code: -1,
815 message: format!("Parent deserialize struct failed: {}", e),
816 },
817 )?;
818 let path_str = path.as_str();
819 if path_str == "/org/a11y/atspi/null" || bus.is_empty() || path_str.is_empty() {
820 return Ok(None);
821 }
822 if path_str == "/org/a11y/atspi/accessible/root" {
824 return Ok(None);
825 }
826 Ok(Some(AccessibleRef {
827 bus_name: bus,
828 path: path_str.to_string(),
829 }))
830 }
831
832 fn state_bits(&self, aref: &AccessibleRef) -> u64 {
837 let state_bits = self.get_state(aref).unwrap_or_default();
838 if state_bits.len() >= 2 {
839 (state_bits[0] as u64) | ((state_bits[1] as u64) << 32)
840 } else if state_bits.len() == 1 {
841 state_bits[0] as u64
842 } else {
843 0
844 }
845 }
846
847 fn parse_states(&self, aref: &AccessibleRef, role: Role) -> StateSet {
849 let bits = self.state_bits(aref);
850
851 const ACTIVE: u64 = 1 << 1;
853 const BUSY: u64 = 1 << 3;
854 const CHECKED: u64 = 1 << 4;
855 const EDITABLE: u64 = 1 << 7;
856 const ENABLED: u64 = 1 << 8;
857 const EXPANDABLE: u64 = 1 << 9;
858 const EXPANDED: u64 = 1 << 10;
859 const FOCUSABLE: u64 = 1 << 11;
860 const FOCUSED: u64 = 1 << 12;
861 const MODAL: u64 = 1 << 16;
862 const SELECTED: u64 = 1 << 23;
863 const SENSITIVE: u64 = 1 << 24;
864 const SHOWING: u64 = 1 << 25;
865 const VISIBLE: u64 = 1 << 30;
866 const INDETERMINATE: u64 = 1 << 32;
867 const REQUIRED: u64 = 1 << 33;
868
869 let enabled = (bits & ENABLED) != 0 || (bits & SENSITIVE) != 0;
870 let visible = (bits & VISIBLE) != 0 || (bits & SHOWING) != 0;
871
872 let checked = match role {
873 Role::CheckBox | Role::RadioButton | Role::MenuItem | Role::Switch => {
874 if (bits & INDETERMINATE) != 0 {
875 Some(Toggled::Mixed)
876 } else if (bits & CHECKED) != 0 {
877 Some(Toggled::On)
878 } else {
879 Some(Toggled::Off)
880 }
881 }
882 _ => None,
883 };
884
885 let expanded = if (bits & EXPANDABLE) != 0 {
886 Some((bits & EXPANDED) != 0)
887 } else {
888 None
889 };
890
891 let mut states = StateSet::default();
892 states.enabled = enabled;
893 states.visible = visible;
894 states.focused = (bits & FOCUSED) != 0;
895 states.active = (bits & ACTIVE) != 0;
898 states.checked = checked;
899 states.selected = (bits & SELECTED) != 0;
900 states.expanded = expanded;
901 states.editable = (bits & EDITABLE) != 0;
902 states.focusable = (bits & FOCUSABLE) != 0;
903 states.modal = (bits & MODAL) != 0;
904 states.required = (bits & REQUIRED) != 0;
905 states.busy = (bits & BUSY) != 0;
906 states
907 }
908
909 pub(crate) fn find_app_by_pid(&self, pid: u32) -> Result<AccessibleRef> {
914 let registry = AccessibleRef {
915 bus_name: "org.a11y.atspi.Registry".to_string(),
916 path: "/org/a11y/atspi/accessible/root".to_string(),
917 };
918 let children = self.get_atspi_children(®istry)?;
919
920 for child in &children {
921 if child.path == "/org/a11y/atspi/null" {
922 continue;
923 }
924 if let Some(app_pid) = self.get_dbus_pid(&child.bus_name) {
930 if app_pid == pid {
931 return Ok(child.clone());
932 }
933 }
934 if let Ok(proxy) =
936 self.make_proxy(&child.bus_name, &child.path, "org.a11y.atspi.Application")
937 {
938 if let Ok(app_pid) = proxy.get_property::<i32>("Id") {
939 if app_pid as u32 == pid {
940 return Ok(child.clone());
941 }
942 }
943 }
944 }
945
946 Err(Error::Platform {
947 code: -1,
948 message: format!("No application found with PID {}", pid),
949 })
950 }
951
952 fn get_dbus_pid(&self, bus_name: &str) -> Option<u32> {
954 let proxy = self
955 .make_proxy(
956 "org.freedesktop.DBus",
957 "/org/freedesktop/DBus",
958 "org.freedesktop.DBus",
959 )
960 .ok()?;
961 let reply = proxy
962 .call_method("GetConnectionUnixProcessID", &(bus_name,))
963 .ok()?;
964 let pid: u32 = reply.body().deserialize().ok()?;
965 if pid > 0 {
966 Some(pid)
967 } else {
968 None
969 }
970 }
971
972 fn do_atspi_action_by_index(&self, aref: &AccessibleRef, index: i32) -> Result<()> {
974 let proxy = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Action")?;
975 proxy
976 .call_method("DoAction", &(index,))
977 .map_err(|e| Error::Platform {
978 code: -1,
979 message: format!("DoAction({}) failed: {}", index, e),
980 })?;
981 Ok(())
982 }
983
984 fn get_action_index(&self, handle: u64, action: &str) -> Result<i32> {
986 self.action_indices
987 .lock()
988 .unwrap_or_else(|e| e.into_inner())
989 .get(&handle)
990 .and_then(|map| map.get(action).copied())
991 .ok_or_else(|| Error::ActionNotSupported {
992 action: action.to_string(),
993 role: Role::Unknown, })
995 }
996
997 fn get_app_pid(&self, aref: &AccessibleRef) -> Option<u32> {
1003 if let Some(pid) = self.get_dbus_pid(&aref.bus_name) {
1004 return Some(pid);
1005 }
1006
1007 if let Ok(proxy) = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Application")
1014 {
1015 if let Ok(pid) = proxy.get_property::<i32>("Id") {
1016 if pid > 0 && std::path::Path::new(&format!("/proc/{pid}")).exists() {
1017 return Some(pid as u32);
1018 }
1019 }
1020 }
1021
1022 None
1023 }
1024
1025 fn resolve_role(&self, aref: &AccessibleRef) -> Role {
1027 let role_name = self.get_role_name(aref).unwrap_or_default();
1028 let by_name = if !role_name.is_empty() {
1029 map_atspi_role(&role_name)
1030 } else {
1031 Role::Unknown
1032 };
1033 let coarse = if by_name != Role::Unknown {
1034 by_name
1035 } else {
1036 let role_num = self.get_role_number(aref).unwrap_or(0);
1038 map_atspi_role_number(role_num)
1039 };
1040 self.refine_text_role(coarse, aref)
1041 }
1042
1043 fn matches_ref(
1057 &self,
1058 aref: &AccessibleRef,
1059 simple: &xa11y_core::selector::SimpleSelector,
1060 ) -> bool {
1061 let needs_role = simple.role.is_some()
1064 || simple
1065 .filters
1066 .iter()
1067 .any(|f| matches!(f.attr.as_str(), "role" | "checked"));
1068 let role = if needs_role {
1069 Some(self.resolve_role(aref))
1070 } else {
1071 None
1072 };
1073
1074 if let Some(ref role_match) = simple.role {
1075 match role_match {
1076 xa11y_core::selector::RoleMatch::Normalized(expected) => {
1077 if role != Some(*expected) {
1078 return false;
1079 }
1080 }
1081 xa11y_core::selector::RoleMatch::Platform(platform_role) => {
1082 let raw_role = self.get_role_name(aref).unwrap_or_default();
1083 if raw_role != *platform_role {
1084 return false;
1085 }
1086 }
1087 }
1088 }
1089
1090 let mut state_set: Option<StateSet> = None;
1091
1092 for filter in &simple.filters {
1093 let attr = filter.attr.as_str();
1094 let resolved: Option<Option<String>> = match attr {
1095 "role" => Some(role.map(|r| r.to_snake_case().to_string())),
1096 "name" => {
1097 let name = self.get_name(aref).ok().filter(|s| !s.is_empty());
1098 let resolved = if name.is_none() && role == Some(Role::StaticText) {
1101 self.get_value(aref)
1102 } else {
1103 name
1104 };
1105 Some(resolved)
1106 }
1107 "value" => Some(self.get_value(aref)),
1108 "description" => Some(self.get_description(aref).ok().filter(|s| !s.is_empty())),
1109 "enabled" | "visible" | "focused" | "active" | "focusable" | "selected"
1110 | "editable" | "modal" | "required" | "busy" | "expanded" | "checked" => {
1111 let s = state_set.get_or_insert_with(|| {
1112 self.parse_states(aref, role.unwrap_or(Role::Unknown))
1116 });
1117 Some(state_attr_to_string(attr, s))
1118 }
1119 _ => None, };
1121
1122 match resolved {
1123 Some(value) => {
1124 if !xa11y_core::selector::match_op(&filter.op, &filter.value, value.as_deref())
1125 {
1126 return false;
1127 }
1128 }
1129 None => {
1130 let pid = None; let data = self.build_element_data(aref, pid);
1138 return xa11y_core::selector::matches_simple(&data, simple);
1139 }
1140 }
1141 }
1142
1143 true
1144 }
1145
1146 fn collect_matching_refs_group(
1156 &self,
1157 parent: &AccessibleRef,
1158 clauses: &[&xa11y_core::selector::SimpleSelector],
1159 depth: u32,
1160 max_depth: u32,
1161 limit: Option<usize>,
1162 ) -> Result<Vec<(usize, AccessibleRef)>> {
1163 if depth > max_depth {
1164 return Ok(vec![]);
1165 }
1166
1167 let children = self.get_atspi_children(parent)?;
1168
1169 let parent_is_registry = parent.bus_name == "org.a11y.atspi.Registry";
1177 let mut to_search: Vec<AccessibleRef> = Vec::new();
1178 for child in children {
1179 if child.path == "/org/a11y/atspi/null"
1180 || child.bus_name.is_empty()
1181 || child.path.is_empty()
1182 {
1183 continue;
1184 }
1185
1186 if !parent_is_registry {
1187 let child_role = self.get_role_name(&child).unwrap_or_default();
1188 if child_role == "application" {
1189 let grandchildren = self.get_atspi_children(&child).unwrap_or_default();
1190 for gc in grandchildren {
1191 if gc.path == "/org/a11y/atspi/null"
1192 || gc.bus_name.is_empty()
1193 || gc.path.is_empty()
1194 {
1195 continue;
1196 }
1197 let gc_role = self.get_role_name(&gc).unwrap_or_default();
1198 if gc_role == "application" {
1199 continue;
1200 }
1201 to_search.push(gc);
1202 }
1203 continue;
1204 }
1205 }
1206 to_search.push(child);
1207 }
1208
1209 if to_search.is_empty() {
1212 self.check_chromium_a11y_enabled(parent, None)?;
1213 }
1214
1215 type ChildBatch = (Vec<(usize, AccessibleRef)>, Option<Error>);
1230 let per_child: Vec<ChildBatch> = to_search
1231 .par_iter()
1232 .map(|child| {
1233 let mut child_results: Vec<(usize, AccessibleRef)> = Vec::new();
1234 for (idx, simple) in clauses.iter().enumerate() {
1235 if self.matches_ref(child, simple) {
1236 child_results.push((idx, child.clone()));
1237 }
1238 }
1239 match self.collect_matching_refs_group(child, clauses, depth + 1, max_depth, limit)
1240 {
1241 Ok(sub) => {
1242 child_results.extend(sub);
1243 (child_results, None)
1244 }
1245 Err(e @ Error::AccessibilityNotEnabled { .. }) => (Vec::new(), Some(e)),
1246 Err(_) => (child_results, None),
1247 }
1248 })
1249 .collect();
1250
1251 let mut results = Vec::new();
1263 for (batch, maybe_err) in per_child {
1264 if let Some(err) = maybe_err {
1265 return Err(err);
1266 }
1267 for r in batch {
1268 results.push(r);
1269 if let Some(limit) = limit {
1270 if results.len() >= limit {
1271 return Ok(results);
1272 }
1273 }
1274 }
1275 }
1276 Ok(results)
1277 }
1278}
1279
1280impl Provider for LinuxProvider {
1281 fn get_children(&self, element: Option<&ElementData>) -> Result<Vec<ElementData>> {
1282 match element {
1283 None => {
1284 self.list_apps()
1287 }
1288 Some(element_data) => {
1289 let aref = self.get_cached(element_data.handle)?;
1290 let children = self.get_atspi_children(&aref).unwrap_or_default();
1291 let pid = element_data.pid;
1292
1293 let mut to_build: Vec<AccessibleRef> = Vec::new();
1296 for child_ref in &children {
1297 if child_ref.path == "/org/a11y/atspi/null"
1298 || child_ref.bus_name.is_empty()
1299 || child_ref.path.is_empty()
1300 {
1301 continue;
1302 }
1303 let child_role = self.get_role_name(child_ref).unwrap_or_default();
1304 if child_role == "application" {
1305 let grandchildren = self.get_atspi_children(child_ref).unwrap_or_default();
1306 for gc_ref in grandchildren {
1307 if gc_ref.path == "/org/a11y/atspi/null"
1308 || gc_ref.bus_name.is_empty()
1309 || gc_ref.path.is_empty()
1310 {
1311 continue;
1312 }
1313 let gc_role = self.get_role_name(&gc_ref).unwrap_or_default();
1314 if gc_role == "application" {
1315 continue;
1316 }
1317 to_build.push(gc_ref);
1318 }
1319 continue;
1320 }
1321 to_build.push(child_ref.clone());
1322 }
1323
1324 if to_build.is_empty() {
1325 self.check_chromium_a11y_enabled(&aref, Some(element_data.role))?;
1326 }
1327
1328 let results: Vec<ElementData> = to_build
1329 .par_iter()
1330 .map(|r| self.build_element_data(r, pid))
1331 .collect();
1332
1333 Ok(results)
1334 }
1335 }
1336 }
1337
1338 fn find_elements_group(
1339 &self,
1340 root: &ElementData,
1341 group: &xa11y_core::selector::SelectorGroup,
1342 limit: Option<usize>,
1343 max_depth: Option<u32>,
1344 ) -> Result<Vec<ElementData>> {
1345 if group.clauses.is_empty() {
1346 return Ok(vec![]);
1347 }
1348 if group.clauses.iter().any(|c| c.segments.is_empty()) {
1351 return Ok(vec![]);
1352 }
1353
1354 let max_depth_val = max_depth.unwrap_or(xa11y_core::MAX_TREE_DEPTH);
1364
1365 let firsts: Vec<&xa11y_core::selector::SimpleSelector> = group
1366 .clauses
1367 .iter()
1368 .map(|c| &c.segments[0].simple)
1369 .collect();
1370
1371 let start_ref = self.get_cached(root.handle)?;
1372
1373 let phase1_limit = if group.clauses.len() == 1 {
1382 let clause = &group.clauses[0];
1383 let first = firsts[0];
1384 let outer = if clause.segments.len() == 1 {
1385 limit
1386 } else {
1387 None
1388 };
1389 match (outer, first.nth) {
1390 (Some(l), Some(n)) => Some(l.max(n)),
1391 (_, Some(n)) => Some(n),
1392 (l, None) => l,
1393 }
1394 } else {
1395 None
1396 };
1397
1398 let phase1: Vec<(usize, AccessibleRef)> =
1399 self.collect_matching_refs_group(&start_ref, &firsts, 0, max_depth_val, phase1_limit)?;
1400
1401 let pid_from_root = root.pid;
1402
1403 let mut by_clause: Vec<Vec<(usize, AccessibleRef)>> =
1407 (0..group.clauses.len()).map(|_| Vec::new()).collect();
1408 for (walk_pos, (clause_idx, aref)) in phase1.into_iter().enumerate() {
1409 by_clause[clause_idx].push((walk_pos, aref));
1410 }
1411
1412 let mut merged: Vec<(usize, ElementData)> = Vec::new();
1420 for (clause_idx, hits) in by_clause.into_iter().enumerate() {
1421 if hits.is_empty() {
1422 continue;
1423 }
1424 let clause = &group.clauses[clause_idx];
1425 let phase1_data: Vec<(usize, ElementData)> = hits
1427 .par_iter()
1428 .map(|(pos, aref)| {
1429 (*pos, self.build_element_data(aref, pid_from_root))
1433 })
1434 .collect();
1435
1436 if clause.segments.len() == 1 {
1437 let mut candidates = phase1_data;
1439 if let Some(nth) = clause.segments[0].simple.nth {
1440 if nth <= candidates.len() {
1441 let kept = candidates.remove(nth - 1);
1442 candidates.clear();
1443 candidates.push(kept);
1444 } else {
1445 candidates.clear();
1446 }
1447 }
1448 merged.extend(candidates);
1449 continue;
1450 }
1451
1452 for (anchor_pos, head) in phase1_data {
1457 let narrowed = self.narrow_multi_segment(
1458 vec![head],
1459 &clause.segments[1..],
1460 max_depth_val,
1461 None,
1462 )?;
1463 for n in narrowed {
1464 merged.push((anchor_pos, n));
1465 }
1466 }
1467 }
1468
1469 merged.sort_by_key(|(pos, _)| *pos);
1473 let mut seen: HashSet<(String, String)> = HashSet::new();
1474 let mut out: Vec<ElementData> = Vec::with_capacity(merged.len());
1475 for (_, data) in merged {
1476 if let Ok(aref) = self.get_cached(data.handle) {
1480 if !seen.insert((aref.bus_name.clone(), aref.path.clone())) {
1481 continue;
1482 }
1483 }
1484 out.push(data);
1485 }
1486 if let Some(l) = limit {
1487 out.truncate(l);
1488 }
1489 Ok(out)
1490 }
1491
1492 fn get_parent(&self, element: &ElementData) -> Result<Option<ElementData>> {
1493 let aref = self.get_cached(element.handle)?;
1494 match self.get_atspi_parent(&aref)? {
1495 Some(parent_ref) => {
1496 let data = self.build_element_data(&parent_ref, element.pid);
1497 Ok(Some(data))
1498 }
1499 None => Ok(None),
1500 }
1501 }
1502
1503 fn list_apps(&self) -> Result<Vec<ElementData>> {
1510 let registry = AccessibleRef {
1511 bus_name: "org.a11y.atspi.Registry".to_string(),
1512 path: "/org/a11y/atspi/accessible/root".to_string(),
1513 };
1514 let children = self.get_atspi_children(®istry)?;
1515
1516 let valid: Vec<(&AccessibleRef, String)> = children
1518 .iter()
1519 .filter(|c| c.path != "/org/a11y/atspi/null")
1520 .filter_map(|c| {
1521 let name = self.get_name(c).unwrap_or_default();
1522 if name.is_empty() {
1523 None
1524 } else {
1525 Some((c, name))
1526 }
1527 })
1528 .collect();
1529
1530 let results: Vec<ElementData> = valid
1531 .par_iter()
1532 .map(|(child, app_name)| {
1533 let pid = self.get_app_pid(child);
1534 let mut data = self.build_element_data(child, pid);
1535 data.name = Some(app_name.clone());
1536 data
1537 })
1538 .collect();
1539
1540 Ok(results)
1541 }
1542
1543 fn app_by_pid(&self, pid: u32) -> Result<ElementData> {
1555 let registry = AccessibleRef {
1556 bus_name: "org.a11y.atspi.Registry".to_string(),
1557 path: "/org/a11y/atspi/accessible/root".to_string(),
1558 };
1559 let children = self.get_atspi_children(®istry)?;
1560
1561 let mut total = 0usize;
1562 let mut unresolved = 0usize;
1563 for child in children.iter().filter(|c| c.path != "/org/a11y/atspi/null") {
1564 total += 1;
1565 match self.get_app_pid(child) {
1566 Some(p) if p == pid => {
1567 let mut data = self.build_element_data(child, Some(p));
1568 let name = self.get_name(child).unwrap_or_default();
1570 if !name.is_empty() {
1571 data.name = Some(name);
1572 }
1573 return Ok(data);
1574 }
1575 Some(_) => {}
1576 None => unresolved += 1,
1577 }
1578 }
1579 Err(
1580 Error::selector_not_matched(format!("application[pid={pid}]")).diagnose(
1581 xa11y_core::Diagnosis {
1582 last_observed: Some(format!(
1583 "{total} AT-SPI registry entries examined, {unresolved} without a \
1584 resolvable pid"
1585 )),
1586 ..Default::default()
1587 },
1588 ),
1589 )
1590 }
1591
1592 fn focused_app(&self) -> Result<ElementData> {
1605 const ACTIVE: u64 = 1 << 1;
1607
1608 let registry = AccessibleRef {
1609 bus_name: "org.a11y.atspi.Registry".to_string(),
1610 path: "/org/a11y/atspi/accessible/root".to_string(),
1611 };
1612 let apps = self.get_atspi_children(®istry)?;
1613
1614 for app in apps.iter().filter(|c| c.path != "/org/a11y/atspi/null") {
1615 let windows = match self.get_atspi_children(app) {
1616 Ok(w) => w,
1617 Err(_) => continue,
1620 };
1621 let has_active_window = windows
1622 .iter()
1623 .filter(|w| w.path != "/org/a11y/atspi/null")
1624 .any(|w| self.state_bits(w) & ACTIVE != 0);
1625 if has_active_window {
1626 let pid = self.get_app_pid(app);
1627 let mut data = self.build_element_data(app, pid);
1628 let name = self.get_name(app).unwrap_or_default();
1629 if !name.is_empty() {
1630 data.name = Some(name);
1631 }
1632 return Ok(data);
1633 }
1634 }
1635
1636 Err(Error::selector_not_matched("focused application"))
1637 }
1638
1639 fn press(&self, element: &ElementData) -> Result<()> {
1640 let target = self.get_cached(element.handle)?;
1641 if let Ok(index) = self.get_action_index(element.handle, "press") {
1643 return self.do_atspi_action_by_index(&target, index);
1644 }
1645 if element.role == Role::Button && self.is_gtk_toolkit(&target) {
1664 let outer_name = self.get_name(&target).unwrap_or_default();
1665 if let Some((inner, index)) = self.find_gtk_press_fallback(&target, &outer_name) {
1666 return self.do_atspi_action_by_index(&inner, index);
1667 }
1668 }
1669 Err(Error::ActionNotSupported {
1670 action: "press".to_string(),
1671 role: element.role,
1672 })
1673 }
1674
1675 fn focus(&self, element: &ElementData) -> Result<()> {
1676 let target = self.get_cached(element.handle)?;
1677 if let Ok(proxy) =
1681 self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Component")
1682 {
1683 if let Ok(reply) = proxy.call_method("GrabFocus", &()) {
1684 if let Ok(true) = reply.body().deserialize::<bool>() {
1686 return Ok(());
1687 }
1688 }
1690 }
1691 if let Ok(index) = self.get_action_index(element.handle, "focus") {
1692 return self.do_atspi_action_by_index(&target, index);
1693 }
1694 Err(Error::ActionNotSupported {
1695 action: "focus".to_string(),
1696 role: element.role,
1697 })
1698 }
1699
1700 fn blur(&self, element: &ElementData) -> Result<()> {
1701 let target = self.get_cached(element.handle)?;
1702 if let Some(parent_ref) = self.get_atspi_parent(&target)? {
1706 if parent_ref.path != "/org/a11y/atspi/null" {
1707 let p = self.make_proxy(
1708 &parent_ref.bus_name,
1709 &parent_ref.path,
1710 "org.a11y.atspi.Component",
1711 )?;
1712 p.call_method("GrabFocus", &())
1713 .map_err(|e| Error::Platform {
1714 code: -1,
1715 message: format!("Component.GrabFocus on parent failed: {}", e),
1716 })?;
1717 return Ok(());
1718 }
1719 }
1720 Err(Error::ActionNotSupported {
1721 action: "blur".to_string(),
1722 role: element.role,
1723 })
1724 }
1725
1726 fn toggle(&self, element: &ElementData) -> Result<()> {
1727 let target = self.get_cached(element.handle)?;
1728 if let Ok(index) = self.get_action_index(element.handle, "toggle") {
1733 return self.do_atspi_action_by_index(&target, index);
1734 }
1735 if matches!(
1758 element.role,
1759 Role::CheckBox | Role::RadioButton | Role::Switch
1760 ) {
1761 if let Ok(index) = self.get_action_index(element.handle, "press") {
1762 return self.do_atspi_action_by_index(&target, index);
1763 }
1764 }
1765 Err(Error::ActionNotSupported {
1766 action: "toggle".to_string(),
1767 role: element.role,
1768 })
1769 }
1770
1771 fn select(&self, element: &ElementData) -> Result<()> {
1772 let target = self.get_cached(element.handle)?;
1773 let index = self
1774 .get_action_index(element.handle, "select")
1775 .map_err(|_| Error::ActionNotSupported {
1776 action: "select".to_string(),
1777 role: element.role,
1778 })?;
1779 self.do_atspi_action_by_index(&target, index)
1780 }
1781
1782 fn expand(&self, element: &ElementData) -> Result<()> {
1783 let target = self.get_cached(element.handle)?;
1784 let index = self
1785 .get_action_index(element.handle, "expand")
1786 .map_err(|_| Error::ActionNotSupported {
1787 action: "expand".to_string(),
1788 role: element.role,
1789 })?;
1790 self.do_atspi_action_by_index(&target, index)
1791 }
1792
1793 fn collapse(&self, element: &ElementData) -> Result<()> {
1794 let target = self.get_cached(element.handle)?;
1795 let index = self
1796 .get_action_index(element.handle, "collapse")
1797 .map_err(|_| Error::ActionNotSupported {
1798 action: "collapse".to_string(),
1799 role: element.role,
1800 })?;
1801 self.do_atspi_action_by_index(&target, index)
1802 }
1803
1804 fn show_menu(&self, element: &ElementData) -> Result<()> {
1805 let target = self.get_cached(element.handle)?;
1806 let index = self
1807 .get_action_index(element.handle, "show_menu")
1808 .map_err(|_| Error::ActionNotSupported {
1809 action: "show_menu".to_string(),
1810 role: element.role,
1811 })?;
1812 self.do_atspi_action_by_index(&target, index)
1813 }
1814
1815 fn increment(&self, element: &ElementData) -> Result<()> {
1816 let target = self.get_cached(element.handle)?;
1817 if let Ok(index) = self.get_action_index(element.handle, "increment") {
1819 return self.do_atspi_action_by_index(&target, index);
1820 }
1821 let proxy = self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Value")?;
1822 let current: f64 = proxy
1823 .get_property("CurrentValue")
1824 .map_err(|e| Error::Platform {
1825 code: -1,
1826 message: format!("Value.CurrentValue failed: {}", e),
1827 })?;
1828 let step: f64 = proxy.get_property("MinimumIncrement").unwrap_or(1.0);
1829 let step = if step <= 0.0 { 1.0 } else { step };
1830 proxy
1831 .set_property("CurrentValue", current + step)
1832 .map_err(|e| Error::Platform {
1833 code: -1,
1834 message: format!("Value.SetCurrentValue failed: {}", e),
1835 })
1836 }
1837
1838 fn decrement(&self, element: &ElementData) -> Result<()> {
1839 let target = self.get_cached(element.handle)?;
1840 if let Ok(index) = self.get_action_index(element.handle, "decrement") {
1841 return self.do_atspi_action_by_index(&target, index);
1842 }
1843 let proxy = self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Value")?;
1844 let current: f64 = proxy
1845 .get_property("CurrentValue")
1846 .map_err(|e| Error::Platform {
1847 code: -1,
1848 message: format!("Value.CurrentValue failed: {}", e),
1849 })?;
1850 let step: f64 = proxy.get_property("MinimumIncrement").unwrap_or(1.0);
1851 let step = if step <= 0.0 { 1.0 } else { step };
1852 proxy
1853 .set_property("CurrentValue", current - step)
1854 .map_err(|e| Error::Platform {
1855 code: -1,
1856 message: format!("Value.SetCurrentValue failed: {}", e),
1857 })
1858 }
1859
1860 fn scroll_into_view(&self, element: &ElementData) -> Result<()> {
1861 let target = self.get_cached(element.handle)?;
1862 let proxy = self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Component")?;
1863 proxy
1864 .call_method("ScrollTo", &(0u32,))
1865 .map_err(|e| Error::Platform {
1866 code: -1,
1867 message: format!("ScrollTo failed: {}", e),
1868 })?;
1869 Ok(())
1870 }
1871
1872 fn set_value(&self, element: &ElementData, value: &str) -> Result<()> {
1873 let target = self.get_cached(element.handle)?;
1874 let proxy = self
1875 .make_proxy(
1876 &target.bus_name,
1877 &target.path,
1878 "org.a11y.atspi.EditableText",
1879 )
1880 .map_err(|_| Error::TextValueNotSupported)?;
1881 if proxy.call_method("SetTextContents", &(value,)).is_ok() {
1883 return Ok(());
1884 }
1885 let classify_editable_text_error = |op: &str, e: zbus::Error| -> Error {
1891 let msg = e.to_string();
1892 if msg.contains("UnknownMethod") || msg.contains("UnknownInterface") {
1893 Error::TextValueNotSupported
1894 } else {
1895 Error::Platform {
1896 code: -1,
1897 message: format!("EditableText.{} failed: {}", op, msg),
1898 }
1899 }
1900 };
1901 if let Err(e) = proxy.call_method("DeleteText", &(0i32, i32::MAX)) {
1902 return Err(classify_editable_text_error("DeleteText", e));
1903 }
1904 if let Err(e) = proxy.call_method("InsertText", &(0i32, value, value.len() as i32)) {
1905 return Err(classify_editable_text_error("InsertText", e));
1906 }
1907 Ok(())
1908 }
1909
1910 fn set_numeric_value(&self, element: &ElementData, value: f64) -> Result<()> {
1911 let target = self.get_cached(element.handle)?;
1912 let proxy = self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Value")?;
1913 proxy
1914 .set_property("CurrentValue", value)
1915 .map_err(|e| Error::Platform {
1916 code: -1,
1917 message: format!("SetValue failed: {}", e),
1918 })
1919 }
1920
1921 fn type_text(&self, element: &ElementData, text: &str) -> Result<()> {
1922 let target = self.get_cached(element.handle)?;
1923 let text_proxy = self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Text");
1926 let insert_pos = text_proxy
1927 .as_ref()
1928 .ok()
1929 .and_then(|p| p.get_property::<i32>("CaretOffset").ok())
1930 .unwrap_or(-1); let proxy = self
1933 .make_proxy(
1934 &target.bus_name,
1935 &target.path,
1936 "org.a11y.atspi.EditableText",
1937 )
1938 .map_err(|_| Error::TextValueNotSupported)?;
1939 let pos = if insert_pos >= 0 {
1940 insert_pos
1941 } else {
1942 i32::MAX
1943 };
1944 proxy
1945 .call_method("InsertText", &(pos, text, text.len() as i32))
1946 .map_err(|e| Error::Platform {
1947 code: -1,
1948 message: format!("EditableText.InsertText failed: {}", e),
1949 })?;
1950 Ok(())
1951 }
1952
1953 fn set_text_selection(&self, element: &ElementData, start: u32, end: u32) -> Result<()> {
1954 let target = self.get_cached(element.handle)?;
1955 let proxy = self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Text")?;
1956 proxy
1957 .call_method("SetSelection", &(0i32, start as i32, end as i32))
1958 .map_err(|e| Error::Platform {
1959 code: -1,
1960 message: format!("Text.SetSelection failed: {}", e),
1961 })?;
1962 Ok(())
1963 }
1964
1965 fn perform_action(&self, element: &ElementData, action: &str) -> Result<()> {
1966 match action {
1967 "press" => self.press(element),
1968 "focus" => self.focus(element),
1969 "blur" => self.blur(element),
1970 "toggle" => self.toggle(element),
1971 "select" => self.select(element),
1972 "expand" => self.expand(element),
1973 "collapse" => self.collapse(element),
1974 "show_menu" => self.show_menu(element),
1975 "increment" => self.increment(element),
1976 "decrement" => self.decrement(element),
1977 "scroll_into_view" => self.scroll_into_view(element),
1978 _ => Err(Error::ActionNotSupported {
1979 action: action.to_string(),
1980 role: element.role,
1981 }),
1982 }
1983 }
1984
1985 fn subscribe(&self, element: &ElementData) -> Result<Subscription> {
1986 let pid = element.pid.ok_or(Error::Platform {
1987 code: -1,
1988 message: "Element has no PID for subscribe".to_string(),
1989 })?;
1990 let app_name = element.name.clone().unwrap_or_default();
1991 crate::events::subscribe_for_pid(self, pid, app_name)
1992 }
1993}
1994
1995fn role_has_value(role: Role) -> bool {
1998 !matches!(
1999 role,
2000 Role::Application
2001 | Role::Window
2002 | Role::Dialog
2003 | Role::Group
2004 | Role::MenuBar
2005 | Role::Toolbar
2006 | Role::TabGroup
2007 | Role::SplitGroup
2008 | Role::Table
2009 | Role::TableRow
2010 | Role::Separator
2011 )
2012}
2013
2014fn role_has_actions(role: Role) -> bool {
2017 matches!(
2018 role,
2019 Role::Button
2020 | Role::CheckBox
2021 | Role::RadioButton
2022 | Role::MenuItem
2023 | Role::Link
2024 | Role::ComboBox
2025 | Role::TextField
2026 | Role::TextArea
2027 | Role::SpinButton
2028 | Role::Tab
2029 | Role::TreeItem
2030 | Role::ListItem
2031 | Role::ScrollBar
2032 | Role::Slider
2033 | Role::Menu
2034 | Role::Image
2035 | Role::Unknown
2036 )
2037}
2038
2039pub(crate) fn map_atspi_role(role_name: &str) -> Role {
2041 match role_name.to_lowercase().as_str() {
2042 "application" => Role::Application,
2043 "window" | "frame" => Role::Window,
2044 "dialog" | "file chooser" => Role::Dialog,
2045 "alert" | "notification" => Role::Alert,
2046 "push button" | "push button menu" => Role::Button,
2047 "toggle button" => Role::Switch,
2048 "check box" | "check menu item" => Role::CheckBox,
2049 "radio button" | "radio menu item" => Role::RadioButton,
2050 "entry" | "password text" => Role::TextField,
2051 "spin button" | "spinbutton" => Role::SpinButton,
2052 "text" | "textbox" => Role::TextArea,
2056 "label" | "static" | "caption" => Role::StaticText,
2057 "combo box" | "combobox" => Role::ComboBox,
2058 "list" | "list box" | "listbox" => Role::List,
2060 "list item" => Role::ListItem,
2061 "menu" => Role::Menu,
2062 "menu item" | "tearoff menu item" => Role::MenuItem,
2063 "menu bar" => Role::MenuBar,
2064 "page tab" => Role::Tab,
2065 "page tab list" => Role::TabGroup,
2066 "table" | "tree table" => Role::Table,
2067 "table row" => Role::TableRow,
2068 "table cell" | "table column header" | "table row header" => Role::TableCell,
2069 "tool bar" => Role::Toolbar,
2070 "scroll bar" => Role::ScrollBar,
2071 "slider" => Role::Slider,
2072 "image" | "icon" | "desktop icon" => Role::Image,
2073 "link" => Role::Link,
2074 "panel" | "section" | "form" | "filler" | "viewport" | "scroll pane" => Role::Group,
2075 "progress bar" => Role::ProgressBar,
2076 "tree item" => Role::TreeItem,
2077 "document web" | "document frame" => Role::WebArea,
2078 "heading" => Role::Heading,
2079 "separator" => Role::Separator,
2080 "split pane" => Role::SplitGroup,
2081 "tooltip" | "tool tip" => Role::Tooltip,
2082 "status bar" | "statusbar" => Role::Status,
2083 "landmark" | "navigation" => Role::Navigation,
2084 _ => xa11y_core::unknown_role(role_name),
2085 }
2086}
2087
2088pub(crate) fn map_atspi_role_number(role: u32) -> Role {
2091 match role {
2092 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}")),
2154 }
2155}
2156
2157const GTK_FALLBACK_MAX_DEPTH: u32 = 3;
2161
2162const GTK_FALLBACK_MAX_NODES: usize = 200;
2165
2166fn is_actionable_atspi_role(role: &str) -> bool {
2171 matches!(
2172 role,
2173 "push button"
2174 | "toggle button"
2175 | "check box"
2176 | "radio button"
2177 | "menu item"
2178 | "check menu item"
2179 | "radio menu item"
2180 | "link"
2181 | "page tab"
2182 | "tab"
2183 | "list item"
2184 | "tree item"
2185 )
2186}
2187
2188fn is_never_descend_atspi_role(role: &str) -> bool {
2193 matches!(
2194 role,
2195 "label" | "separator" | "image" | "icon" | "static" | "caption"
2196 )
2197}
2198
2199fn map_atspi_action_name(action_name: &str) -> Option<String> {
2213 let lower = action_name.to_lowercase();
2222 let collapsed: String = lower.chars().filter(|c| !matches!(c, '_' | ' ')).collect();
2223 let canonical = match collapsed.as_str() {
2224 "click" | "activate" | "press" | "invoke" | "dodefault" => "press",
2225 "toggle" | "check" | "uncheck" => "toggle",
2226 "expand" | "open" => "expand",
2227 "collapse" | "close" => "collapse",
2228 "select" => "select",
2229 "menu" | "showmenu" | "showcontextmenu" | "contextmenu" | "popup" => "show_menu",
2230 "increment" => "increment",
2231 "decrement" => "decrement",
2232 _ => return None,
2233 };
2234 Some(canonical.to_string())
2235}
2236
2237#[cfg(test)]
2238mod tests {
2239 use super::*;
2240
2241 #[test]
2242 fn test_role_mapping() {
2243 assert_eq!(map_atspi_role("push button"), Role::Button);
2244 assert_eq!(map_atspi_role("toggle button"), Role::Switch);
2245 assert_eq!(map_atspi_role("check box"), Role::CheckBox);
2246 assert_eq!(map_atspi_role("entry"), Role::TextField);
2247 assert_eq!(map_atspi_role("label"), Role::StaticText);
2248 assert_eq!(map_atspi_role("window"), Role::Window);
2249 assert_eq!(map_atspi_role("frame"), Role::Window);
2250 assert_eq!(map_atspi_role("dialog"), Role::Dialog);
2251 assert_eq!(map_atspi_role("combo box"), Role::ComboBox);
2252 assert_eq!(map_atspi_role("slider"), Role::Slider);
2253 assert_eq!(map_atspi_role("panel"), Role::Group);
2254 assert_eq!(map_atspi_role("unknown_thing"), Role::Unknown);
2255 }
2256
2257 #[test]
2258 fn test_numeric_role_mapping() {
2259 assert_eq!(map_atspi_role_number(62), Role::Switch);
2262 assert_eq!(map_atspi_role_number(43), Role::Button); assert_eq!(map_atspi_role_number(7), Role::CheckBox);
2265 assert_eq!(map_atspi_role_number(67), Role::Unknown); }
2267
2268 #[test]
2269 fn test_action_name_mapping() {
2270 assert_eq!(map_atspi_action_name("click"), Some("press".to_string()));
2271 assert_eq!(map_atspi_action_name("activate"), Some("press".to_string()));
2272 assert_eq!(map_atspi_action_name("press"), Some("press".to_string()));
2273 assert_eq!(map_atspi_action_name("invoke"), Some("press".to_string()));
2274 assert_eq!(
2276 map_atspi_action_name("doDefault"),
2277 Some("press".to_string())
2278 );
2279 assert_eq!(
2280 map_atspi_action_name("do_default"),
2281 Some("press".to_string())
2282 );
2283 assert_eq!(map_atspi_action_name("toggle"), Some("toggle".to_string()));
2284 assert_eq!(map_atspi_action_name("check"), Some("toggle".to_string()));
2285 assert_eq!(map_atspi_action_name("uncheck"), Some("toggle".to_string()));
2286 assert_eq!(map_atspi_action_name("expand"), Some("expand".to_string()));
2287 assert_eq!(map_atspi_action_name("open"), Some("expand".to_string()));
2288 assert_eq!(
2289 map_atspi_action_name("collapse"),
2290 Some("collapse".to_string())
2291 );
2292 assert_eq!(map_atspi_action_name("close"), Some("collapse".to_string()));
2293 assert_eq!(map_atspi_action_name("select"), Some("select".to_string()));
2294 assert_eq!(map_atspi_action_name("menu"), Some("show_menu".to_string()));
2295 assert_eq!(
2296 map_atspi_action_name("showmenu"),
2297 Some("show_menu".to_string())
2298 );
2299 assert_eq!(
2300 map_atspi_action_name("popup"),
2301 Some("show_menu".to_string())
2302 );
2303 assert_eq!(
2304 map_atspi_action_name("show menu"),
2305 Some("show_menu".to_string())
2306 );
2307 assert_eq!(
2310 map_atspi_action_name("showContextMenu"),
2311 Some("show_menu".to_string())
2312 );
2313 assert_eq!(
2314 map_atspi_action_name("show_context_menu"),
2315 Some("show_menu".to_string())
2316 );
2317 assert_eq!(
2318 map_atspi_action_name("increment"),
2319 Some("increment".to_string())
2320 );
2321 assert_eq!(
2322 map_atspi_action_name("decrement"),
2323 Some("decrement".to_string())
2324 );
2325 assert_eq!(map_atspi_action_name("foobar"), None);
2326 }
2327
2328 #[test]
2331 fn test_action_name_aliases_roundtrip() {
2332 let atspi_names = [
2333 "click",
2334 "activate",
2335 "press",
2336 "invoke",
2337 "doDefault",
2338 "do_default",
2339 "toggle",
2340 "check",
2341 "uncheck",
2342 "expand",
2343 "open",
2344 "collapse",
2345 "close",
2346 "select",
2347 "menu",
2348 "showmenu",
2349 "showContextMenu",
2350 "show_context_menu",
2351 "popup",
2352 "show menu",
2353 "increment",
2354 "decrement",
2355 ];
2356 for name in atspi_names {
2357 let canonical = map_atspi_action_name(name).unwrap_or_else(|| {
2358 panic!("AT-SPI2 name {:?} should map to a canonical name", name)
2359 });
2360 let back = map_atspi_action_name(&canonical)
2362 .unwrap_or_else(|| panic!("canonical {:?} should map back to itself", canonical));
2363 assert_eq!(
2364 canonical, back,
2365 "AT-SPI2 {:?} -> {:?} -> {:?} (expected {:?})",
2366 name, canonical, back, canonical
2367 );
2368 }
2369 }
2370
2371 #[test]
2373 fn test_action_name_case_insensitive() {
2374 assert_eq!(map_atspi_action_name("Click"), Some("press".to_string()));
2375 assert_eq!(map_atspi_action_name("TOGGLE"), Some("toggle".to_string()));
2376 assert_eq!(
2377 map_atspi_action_name("Increment"),
2378 Some("increment".to_string())
2379 );
2380 }
2381
2382 #[test]
2387 fn test_gtk_fallback_actionable_roles() {
2388 for role in [
2389 "push button",
2390 "toggle button",
2391 "check box",
2392 "radio button",
2393 "menu item",
2394 "link",
2395 "tab",
2396 "list item",
2397 "tree item",
2398 ] {
2399 assert!(
2400 is_actionable_atspi_role(role),
2401 "{role:?} should be actionable"
2402 );
2403 }
2404 }
2405
2406 #[test]
2411 fn test_gtk_fallback_non_actionable_roles() {
2412 for role in [
2413 "label",
2414 "panel",
2415 "filler",
2416 "section",
2417 "group",
2418 "image",
2419 "separator",
2420 "static",
2421 "frame",
2422 "window",
2423 ] {
2424 assert!(
2425 !is_actionable_atspi_role(role),
2426 "{role:?} must not be treated as actionable"
2427 );
2428 }
2429 }
2430
2431 #[test]
2435 fn test_gtk_fallback_never_descend_roles() {
2436 for role in ["label", "separator", "image", "icon", "static", "caption"] {
2437 assert!(
2438 is_never_descend_atspi_role(role),
2439 "{role:?} must block BFS descent"
2440 );
2441 }
2442 for role in ["panel", "filler", "section", "group", "frame"] {
2445 assert!(
2446 !is_never_descend_atspi_role(role),
2447 "container role {role:?} must remain descendable"
2448 );
2449 }
2450 }
2451}