1use std::collections::{HashMap, HashSet, VecDeque};
4use std::sync::atomic::{AtomicU64, Ordering};
5use std::sync::Mutex;
6
7use rayon::prelude::*;
8use xa11y_core::selector::{Combinator, SelectorSegment};
9use xa11y_core::{
10 ElementData, Error, Provider, Rect, Result, Role, Selector, StateSet, Subscription, Toggled,
11};
12use zbus::blocking::{Connection, Proxy};
13
14static NEXT_HANDLE: AtomicU64 = AtomicU64::new(1);
16
17fn state_attr_to_string(name: &str, s: &StateSet) -> Option<String> {
20 match name {
21 "enabled" => Some(s.enabled.to_string()),
22 "visible" => Some(s.visible.to_string()),
23 "focused" => Some(s.focused.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 get_extents(&self, aref: &AccessibleRef) -> Option<Rect> {
330 if !self.has_interface(aref, "Component") {
331 return None;
332 }
333 let proxy = self
334 .make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Component")
335 .ok()?;
336 let reply = proxy.call_method("GetExtents", &(0u32,)).ok()?;
339 let (x, y, w, h): (i32, i32, i32, i32) = reply.body().deserialize().ok()?;
340 if w <= 0 && h <= 0 {
341 return None;
342 }
343 Some(Rect {
344 x,
345 y,
346 width: w.max(0) as u32,
347 height: h.max(0) as u32,
348 })
349 }
350
351 fn get_actions(&self, aref: &AccessibleRef) -> (Vec<String>, HashMap<String, i32>) {
357 let mut actions = Vec::new();
358 let mut indices = HashMap::new();
359
360 if let Ok(proxy) = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Action") {
362 let n_actions = proxy
364 .get_property::<i32>("NActions")
365 .or_else(|_| proxy.get_property::<u32>("NActions").map(|n| n as i32))
366 .unwrap_or(0);
367 for i in 0..n_actions {
368 if let Ok(reply) = proxy.call_method("GetName", &(i,)) {
369 if let Ok(name) = reply.body().deserialize::<String>() {
370 if let Some(action_name) = map_atspi_action_name(&name) {
371 if !actions.contains(&action_name) {
372 indices.insert(action_name.clone(), i);
373 actions.push(action_name);
374 }
375 }
376 }
377 }
378 }
379 }
380
381 (actions, indices)
389 }
390
391 fn is_gtk_toolkit(&self, aref: &AccessibleRef) -> bool {
398 let app_root = AccessibleRef {
399 bus_name: aref.bus_name.clone(),
400 path: "/org/a11y/atspi/accessible/root".to_string(),
401 };
402 self.make_proxy(
403 &app_root.bus_name,
404 &app_root.path,
405 "org.a11y.atspi.Application",
406 )
407 .ok()
408 .and_then(|proxy| proxy.get_property::<String>("ToolkitName").ok())
409 .map(|t| t.eq_ignore_ascii_case("GTK"))
410 .unwrap_or(false)
411 }
412
413 fn find_gtk_press_fallback(
426 &self,
427 outer: &AccessibleRef,
428 outer_name: &str,
429 ) -> Option<(AccessibleRef, i32)> {
430 let mut queue: VecDeque<(AccessibleRef, u32)> = VecDeque::new();
431 queue.push_back((outer.clone(), 0));
432 let mut visited: usize = 0;
433 let mut shallowest_depth: Option<u32> = None;
434 let mut hits: Vec<(AccessibleRef, i32)> = Vec::new();
435
436 while let Some((node, depth)) = queue.pop_front() {
437 if let Some(best) = shallowest_depth {
439 if depth > best {
440 continue;
441 }
442 }
443 if visited > GTK_FALLBACK_MAX_NODES {
444 break;
445 }
446
447 let role_name = if depth == 0 {
448 String::new()
449 } else {
450 visited += 1;
451 self.get_role_name(&node).unwrap_or_default().to_lowercase()
452 };
453
454 if depth > 0 && is_actionable_atspi_role(&role_name) {
455 if let Some(idx) = self.gtk_fallback_pick(&node, outer_name) {
456 match shallowest_depth {
457 Some(d) if depth < d => {
458 shallowest_depth = Some(depth);
459 hits.clear();
460 hits.push((node.clone(), idx));
461 }
462 Some(d) if depth == d => hits.push((node.clone(), idx)),
463 Some(_) => {}
464 None => {
465 shallowest_depth = Some(depth);
466 hits.push((node.clone(), idx));
467 }
468 }
469 }
470 }
471
472 let stop_descending = depth >= GTK_FALLBACK_MAX_DEPTH
477 || (depth > 0 && is_never_descend_atspi_role(&role_name));
478 if stop_descending {
479 continue;
480 }
481 if let Ok(children) = self.get_atspi_children(&node) {
482 for c in children {
483 queue.push_back((c, depth + 1));
484 }
485 }
486 }
487
488 if hits.len() == 1 {
489 Some(hits.into_iter().next().unwrap())
490 } else {
491 None
492 }
493 }
494
495 fn gtk_fallback_pick(&self, aref: &AccessibleRef, outer_name: &str) -> Option<i32> {
498 let (_, index_map) = self.get_actions(aref);
499 let idx = *index_map.get("press")?;
500 if !self.is_showing_visible_sensitive(aref) {
501 return None;
502 }
503 if !outer_name.is_empty() {
507 let inner_name = self.get_name(aref).unwrap_or_default();
508 if !inner_name.is_empty() && inner_name != outer_name {
509 return None;
510 }
511 }
512 Some(idx)
513 }
514
515 fn is_showing_visible_sensitive(&self, aref: &AccessibleRef) -> bool {
520 let raw = self.get_state(aref).unwrap_or_default();
521 let bits: u64 = if raw.len() >= 2 {
522 (raw[0] as u64) | ((raw[1] as u64) << 32)
523 } else if raw.len() == 1 {
524 raw[0] as u64
525 } else {
526 0
527 };
528 const SENSITIVE: u64 = 1 << 24;
529 const SHOWING: u64 = 1 << 25;
530 const VISIBLE: u64 = 1 << 30;
531 (bits & (SHOWING | VISIBLE | SENSITIVE)) == (SHOWING | VISIBLE | SENSITIVE)
532 }
533
534 fn get_value(&self, aref: &AccessibleRef) -> Option<String> {
537 let text_value = self.get_text_content(aref);
541 if text_value.is_some() {
542 return text_value;
543 }
544 if let Ok(proxy) = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Value") {
546 if let Ok(val) = proxy.get_property::<f64>("CurrentValue") {
547 return Some(val.to_string());
548 }
549 }
550 None
551 }
552
553 fn get_text_content(&self, aref: &AccessibleRef) -> Option<String> {
555 let proxy = self
556 .make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Text")
557 .ok()?;
558 let char_count: i32 = proxy.get_property("CharacterCount").ok()?;
559 if char_count > 0 {
560 let reply = proxy.call_method("GetText", &(0i32, char_count)).ok()?;
561 let text: String = reply.body().deserialize().ok()?;
562 if !text.is_empty() {
563 return Some(text);
564 }
565 }
566 None
567 }
568
569 fn cache_element(&self, aref: AccessibleRef) -> u64 {
571 let handle = NEXT_HANDLE.fetch_add(1, Ordering::Relaxed);
572 self.handle_cache
573 .lock()
574 .unwrap_or_else(|e| e.into_inner())
575 .insert(handle, aref);
576 handle
577 }
578
579 fn get_cached(&self, handle: u64) -> Result<AccessibleRef> {
581 self.handle_cache
582 .lock()
583 .unwrap_or_else(|e| e.into_inner())
584 .get(&handle)
585 .cloned()
586 .ok_or(Error::ElementStale {
587 selector: format!("handle:{}", handle),
588 })
589 }
590
591 fn build_element_data(&self, aref: &AccessibleRef, pid: Option<u32>) -> ElementData {
596 let role_name = self.get_role_name(aref).unwrap_or_default();
597 let role_num = self.get_role_number(aref).unwrap_or(0);
598 let role = {
599 let by_name = if !role_name.is_empty() {
600 map_atspi_role(&role_name)
601 } else {
602 Role::Unknown
603 };
604 let coarse = if by_name != Role::Unknown {
605 by_name
606 } else {
607 map_atspi_role_number(role_num)
608 };
609 if coarse == Role::TextArea && !self.is_multi_line(aref) {
610 Role::TextField
611 } else {
612 coarse
613 }
614 };
615
616 let (
620 ((mut name, value), description),
621 (
622 (states, bounds),
623 ((actions, action_index_map), (numeric_value, min_value, max_value)),
624 ),
625 ) = rayon::join(
626 || {
627 rayon::join(
628 || {
629 let name = self.get_name(aref).ok().filter(|s| !s.is_empty());
630 let value = if role_has_value(role) {
631 self.get_value(aref)
632 } else {
633 None
634 };
635 (name, value)
636 },
637 || self.get_description(aref).ok().filter(|s| !s.is_empty()),
638 )
639 },
640 || {
641 rayon::join(
642 || {
643 rayon::join(
644 || self.parse_states(aref, role),
645 || {
646 if role != Role::Application {
647 self.get_extents(aref)
648 } else {
649 None
650 }
651 },
652 )
653 },
654 || {
655 rayon::join(
656 || {
657 if role_has_actions(role) {
658 self.get_actions(aref)
659 } else {
660 (vec![], HashMap::new())
661 }
662 },
663 || {
664 if matches!(
665 role,
666 Role::Slider
667 | Role::ProgressBar
668 | Role::ScrollBar
669 | Role::SpinButton
670 ) {
671 if let Ok(proxy) = self.make_proxy(
672 &aref.bus_name,
673 &aref.path,
674 "org.a11y.atspi.Value",
675 ) {
676 (
677 proxy.get_property::<f64>("CurrentValue").ok(),
678 proxy.get_property::<f64>("MinimumValue").ok(),
679 proxy.get_property::<f64>("MaximumValue").ok(),
680 )
681 } else {
682 (None, None, None)
683 }
684 } else {
685 (None, None, None)
686 }
687 },
688 )
689 },
690 )
691 },
692 );
693
694 if name.is_none() && role == Role::StaticText {
697 if let Some(ref v) = value {
698 name = Some(v.clone());
699 }
700 }
701
702 let raw = {
703 let raw_role = if role_name.is_empty() {
704 format!("role_num:{}", role_num)
705 } else {
706 role_name
707 };
708 let mut raw = HashMap::new();
709 raw.insert("atspi_role".into(), serde_json::Value::String(raw_role));
710 raw.insert(
711 "bus_name".into(),
712 serde_json::Value::String(aref.bus_name.clone()),
713 );
714 raw.insert(
715 "object_path".into(),
716 serde_json::Value::String(aref.path.clone()),
717 );
718 if let Some(ref n) = name {
721 raw.insert("atspi_name".into(), serde_json::Value::String(n.clone()));
722 }
723 if let Some(ref v) = value {
724 raw.insert("atspi_value".into(), serde_json::Value::String(v.clone()));
725 }
726 if let Some(ref d) = description {
727 raw.insert(
728 "atspi_description".into(),
729 serde_json::Value::String(d.clone()),
730 );
731 }
732 raw
733 };
734
735 let name = xa11y_core::text::strip_bidi_opt(name);
738 let value = xa11y_core::text::strip_bidi_opt(value);
739 let description = xa11y_core::text::strip_bidi_opt(description);
740
741 let handle = self.cache_element(aref.clone());
742 if !action_index_map.is_empty() {
743 self.action_indices
744 .lock()
745 .unwrap_or_else(|e| e.into_inner())
746 .insert(handle, action_index_map);
747 }
748
749 ElementData {
750 role,
751 name,
752 value,
753 description,
754 bounds,
755 actions,
756 states,
757 numeric_value,
758 min_value,
759 max_value,
760 pid,
761 stable_id: Some(aref.path.clone()),
762 raw,
763 handle,
764 }
765 }
766
767 fn get_atspi_parent(&self, aref: &AccessibleRef) -> Result<Option<AccessibleRef>> {
769 let proxy = self.make_proxy(
771 &aref.bus_name,
772 &aref.path,
773 "org.freedesktop.DBus.Properties",
774 )?;
775 let reply = proxy
776 .call_method("Get", &("org.a11y.atspi.Accessible", "Parent"))
777 .map_err(|e| Error::Platform {
778 code: -1,
779 message: format!("Get Parent property failed: {}", e),
780 })?;
781 let variant: zbus::zvariant::OwnedValue =
783 reply.body().deserialize().map_err(|e| Error::Platform {
784 code: -1,
785 message: format!("Parent deserialize variant failed: {}", e),
786 })?;
787 let (bus, path): (String, zbus::zvariant::OwnedObjectPath) =
788 zbus::zvariant::Value::from(variant).try_into().map_err(
789 |e: zbus::zvariant::Error| Error::Platform {
790 code: -1,
791 message: format!("Parent deserialize struct failed: {}", e),
792 },
793 )?;
794 let path_str = path.as_str();
795 if path_str == "/org/a11y/atspi/null" || bus.is_empty() || path_str.is_empty() {
796 return Ok(None);
797 }
798 if path_str == "/org/a11y/atspi/accessible/root" {
800 return Ok(None);
801 }
802 Ok(Some(AccessibleRef {
803 bus_name: bus,
804 path: path_str.to_string(),
805 }))
806 }
807
808 fn parse_states(&self, aref: &AccessibleRef, role: Role) -> StateSet {
810 let state_bits = self.get_state(aref).unwrap_or_default();
811
812 let bits: u64 = if state_bits.len() >= 2 {
814 (state_bits[0] as u64) | ((state_bits[1] as u64) << 32)
815 } else if state_bits.len() == 1 {
816 state_bits[0] as u64
817 } else {
818 0
819 };
820
821 const BUSY: u64 = 1 << 3;
823 const CHECKED: u64 = 1 << 4;
824 const EDITABLE: u64 = 1 << 7;
825 const ENABLED: u64 = 1 << 8;
826 const EXPANDABLE: u64 = 1 << 9;
827 const EXPANDED: u64 = 1 << 10;
828 const FOCUSABLE: u64 = 1 << 11;
829 const FOCUSED: u64 = 1 << 12;
830 const MODAL: u64 = 1 << 16;
831 const SELECTED: u64 = 1 << 23;
832 const SENSITIVE: u64 = 1 << 24;
833 const SHOWING: u64 = 1 << 25;
834 const VISIBLE: u64 = 1 << 30;
835 const INDETERMINATE: u64 = 1 << 32;
836 const REQUIRED: u64 = 1 << 33;
837
838 let enabled = (bits & ENABLED) != 0 || (bits & SENSITIVE) != 0;
839 let visible = (bits & VISIBLE) != 0 || (bits & SHOWING) != 0;
840
841 let checked = match role {
842 Role::CheckBox | Role::RadioButton | Role::MenuItem | Role::Switch => {
843 if (bits & INDETERMINATE) != 0 {
844 Some(Toggled::Mixed)
845 } else if (bits & CHECKED) != 0 {
846 Some(Toggled::On)
847 } else {
848 Some(Toggled::Off)
849 }
850 }
851 _ => None,
852 };
853
854 let expanded = if (bits & EXPANDABLE) != 0 {
855 Some((bits & EXPANDED) != 0)
856 } else {
857 None
858 };
859
860 StateSet {
861 enabled,
862 visible,
863 focused: (bits & FOCUSED) != 0,
864 checked,
865 selected: (bits & SELECTED) != 0,
866 expanded,
867 editable: (bits & EDITABLE) != 0,
868 focusable: (bits & FOCUSABLE) != 0,
869 modal: (bits & MODAL) != 0,
870 required: (bits & REQUIRED) != 0,
871 busy: (bits & BUSY) != 0,
872 }
873 }
874
875 pub(crate) fn find_app_by_pid(&self, pid: u32) -> Result<AccessibleRef> {
880 let registry = AccessibleRef {
881 bus_name: "org.a11y.atspi.Registry".to_string(),
882 path: "/org/a11y/atspi/accessible/root".to_string(),
883 };
884 let children = self.get_atspi_children(®istry)?;
885
886 for child in &children {
887 if child.path == "/org/a11y/atspi/null" {
888 continue;
889 }
890 if let Some(app_pid) = self.get_dbus_pid(&child.bus_name) {
896 if app_pid == pid {
897 return Ok(child.clone());
898 }
899 }
900 if let Ok(proxy) =
902 self.make_proxy(&child.bus_name, &child.path, "org.a11y.atspi.Application")
903 {
904 if let Ok(app_pid) = proxy.get_property::<i32>("Id") {
905 if app_pid as u32 == pid {
906 return Ok(child.clone());
907 }
908 }
909 }
910 }
911
912 Err(Error::Platform {
913 code: -1,
914 message: format!("No application found with PID {}", pid),
915 })
916 }
917
918 fn get_dbus_pid(&self, bus_name: &str) -> Option<u32> {
920 let proxy = self
921 .make_proxy(
922 "org.freedesktop.DBus",
923 "/org/freedesktop/DBus",
924 "org.freedesktop.DBus",
925 )
926 .ok()?;
927 let reply = proxy
928 .call_method("GetConnectionUnixProcessID", &(bus_name,))
929 .ok()?;
930 let pid: u32 = reply.body().deserialize().ok()?;
931 if pid > 0 {
932 Some(pid)
933 } else {
934 None
935 }
936 }
937
938 fn do_atspi_action_by_index(&self, aref: &AccessibleRef, index: i32) -> Result<()> {
940 let proxy = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Action")?;
941 proxy
942 .call_method("DoAction", &(index,))
943 .map_err(|e| Error::Platform {
944 code: -1,
945 message: format!("DoAction({}) failed: {}", index, e),
946 })?;
947 Ok(())
948 }
949
950 fn get_action_index(&self, handle: u64, action: &str) -> Result<i32> {
952 self.action_indices
953 .lock()
954 .unwrap_or_else(|e| e.into_inner())
955 .get(&handle)
956 .and_then(|map| map.get(action).copied())
957 .ok_or_else(|| Error::ActionNotSupported {
958 action: action.to_string(),
959 role: Role::Unknown, })
961 }
962
963 fn get_app_pid(&self, aref: &AccessibleRef) -> Option<u32> {
969 if let Some(pid) = self.get_dbus_pid(&aref.bus_name) {
970 return Some(pid);
971 }
972
973 if let Ok(proxy) = self.make_proxy(&aref.bus_name, &aref.path, "org.a11y.atspi.Application")
975 {
976 if let Ok(pid) = proxy.get_property::<i32>("Id") {
977 if pid > 0 {
978 return Some(pid as u32);
979 }
980 }
981 }
982
983 None
984 }
985
986 fn resolve_role(&self, aref: &AccessibleRef) -> Role {
988 let role_name = self.get_role_name(aref).unwrap_or_default();
989 let by_name = if !role_name.is_empty() {
990 map_atspi_role(&role_name)
991 } else {
992 Role::Unknown
993 };
994 let coarse = if by_name != Role::Unknown {
995 by_name
996 } else {
997 let role_num = self.get_role_number(aref).unwrap_or(0);
999 map_atspi_role_number(role_num)
1000 };
1001 if coarse == Role::TextArea && !self.is_multi_line(aref) {
1003 Role::TextField
1004 } else {
1005 coarse
1006 }
1007 }
1008
1009 fn matches_ref(
1023 &self,
1024 aref: &AccessibleRef,
1025 simple: &xa11y_core::selector::SimpleSelector,
1026 ) -> bool {
1027 let needs_role = simple.role.is_some()
1030 || simple
1031 .filters
1032 .iter()
1033 .any(|f| matches!(f.attr.as_str(), "role" | "checked"));
1034 let role = if needs_role {
1035 Some(self.resolve_role(aref))
1036 } else {
1037 None
1038 };
1039
1040 if let Some(ref role_match) = simple.role {
1041 match role_match {
1042 xa11y_core::selector::RoleMatch::Normalized(expected) => {
1043 if role != Some(*expected) {
1044 return false;
1045 }
1046 }
1047 xa11y_core::selector::RoleMatch::Platform(platform_role) => {
1048 let raw_role = self.get_role_name(aref).unwrap_or_default();
1049 if raw_role != *platform_role {
1050 return false;
1051 }
1052 }
1053 }
1054 }
1055
1056 let mut state_set: Option<StateSet> = None;
1057
1058 for filter in &simple.filters {
1059 let attr = filter.attr.as_str();
1060 let resolved: Option<Option<String>> = match attr {
1061 "role" => Some(role.map(|r| r.to_snake_case().to_string())),
1062 "name" => {
1063 let name = self.get_name(aref).ok().filter(|s| !s.is_empty());
1064 let resolved = if name.is_none() && role == Some(Role::StaticText) {
1067 self.get_value(aref)
1068 } else {
1069 name
1070 };
1071 Some(resolved)
1072 }
1073 "value" => Some(self.get_value(aref)),
1074 "description" => Some(self.get_description(aref).ok().filter(|s| !s.is_empty())),
1075 "enabled" | "visible" | "focused" | "focusable" | "selected" | "editable"
1076 | "modal" | "required" | "busy" | "expanded" | "checked" => {
1077 let s = state_set.get_or_insert_with(|| {
1078 self.parse_states(aref, role.unwrap_or(Role::Unknown))
1082 });
1083 Some(state_attr_to_string(attr, s))
1084 }
1085 _ => None, };
1087
1088 match resolved {
1089 Some(value) => {
1090 if !xa11y_core::selector::match_op(&filter.op, &filter.value, value.as_deref())
1091 {
1092 return false;
1093 }
1094 }
1095 None => {
1096 let pid = None; let data = self.build_element_data(aref, pid);
1104 return xa11y_core::selector::matches_simple(&data, simple);
1105 }
1106 }
1107 }
1108
1109 true
1110 }
1111
1112 fn collect_matching_refs(
1118 &self,
1119 parent: &AccessibleRef,
1120 simple: &xa11y_core::selector::SimpleSelector,
1121 depth: u32,
1122 max_depth: u32,
1123 limit: Option<usize>,
1124 ) -> Result<Vec<AccessibleRef>> {
1125 if depth > max_depth {
1126 return Ok(vec![]);
1127 }
1128
1129 let children = self.get_atspi_children(parent)?;
1130
1131 let parent_is_registry = parent.bus_name == "org.a11y.atspi.Registry";
1139 let mut to_search: Vec<AccessibleRef> = Vec::new();
1140 for child in children {
1141 if child.path == "/org/a11y/atspi/null"
1142 || child.bus_name.is_empty()
1143 || child.path.is_empty()
1144 {
1145 continue;
1146 }
1147
1148 if !parent_is_registry {
1149 let child_role = self.get_role_name(&child).unwrap_or_default();
1150 if child_role == "application" {
1151 let grandchildren = self.get_atspi_children(&child).unwrap_or_default();
1152 for gc in grandchildren {
1153 if gc.path == "/org/a11y/atspi/null"
1154 || gc.bus_name.is_empty()
1155 || gc.path.is_empty()
1156 {
1157 continue;
1158 }
1159 let gc_role = self.get_role_name(&gc).unwrap_or_default();
1160 if gc_role == "application" {
1161 continue;
1162 }
1163 to_search.push(gc);
1164 }
1165 continue;
1166 }
1167 }
1168 to_search.push(child);
1169 }
1170
1171 if to_search.is_empty() {
1174 self.check_chromium_a11y_enabled(parent, None)?;
1175 }
1176
1177 let per_child: Vec<(Vec<AccessibleRef>, Option<Error>)> = to_search
1186 .par_iter()
1187 .map(|child| {
1188 let mut child_results = Vec::new();
1189 if self.matches_ref(child, simple) {
1190 child_results.push(child.clone());
1191 }
1192 match self.collect_matching_refs(child, simple, depth + 1, max_depth, limit) {
1193 Ok(sub) => {
1194 child_results.extend(sub);
1195 (child_results, None)
1196 }
1197 Err(e @ Error::AccessibilityNotEnabled { .. }) => (Vec::new(), Some(e)),
1198 Err(_) => (child_results, None),
1199 }
1200 })
1201 .collect();
1202
1203 let mut results = Vec::new();
1208 for (batch, maybe_err) in per_child {
1209 if let Some(err) = maybe_err {
1210 return Err(err);
1211 }
1212 for r in batch {
1213 results.push(r);
1214 if let Some(limit) = limit {
1215 if results.len() >= limit {
1216 return Ok(results);
1217 }
1218 }
1219 }
1220 }
1221 Ok(results)
1222 }
1223}
1224
1225impl Provider for LinuxProvider {
1226 fn get_children(&self, element: Option<&ElementData>) -> Result<Vec<ElementData>> {
1227 match element {
1228 None => {
1229 let registry = AccessibleRef {
1231 bus_name: "org.a11y.atspi.Registry".to_string(),
1232 path: "/org/a11y/atspi/accessible/root".to_string(),
1233 };
1234 let children = self.get_atspi_children(®istry)?;
1235
1236 let valid: Vec<(&AccessibleRef, String)> = children
1238 .iter()
1239 .filter(|c| c.path != "/org/a11y/atspi/null")
1240 .filter_map(|c| {
1241 let name = self.get_name(c).unwrap_or_default();
1242 if name.is_empty() {
1243 None
1244 } else {
1245 Some((c, name))
1246 }
1247 })
1248 .collect();
1249
1250 let results: Vec<ElementData> = valid
1251 .par_iter()
1252 .map(|(child, app_name)| {
1253 let pid = self.get_app_pid(child);
1254 let mut data = self.build_element_data(child, pid);
1255 data.name = Some(app_name.clone());
1256 data
1257 })
1258 .collect();
1259
1260 Ok(results)
1261 }
1262 Some(element_data) => {
1263 let aref = self.get_cached(element_data.handle)?;
1264 let children = self.get_atspi_children(&aref).unwrap_or_default();
1265 let pid = element_data.pid;
1266
1267 let mut to_build: Vec<AccessibleRef> = Vec::new();
1270 for child_ref in &children {
1271 if child_ref.path == "/org/a11y/atspi/null"
1272 || child_ref.bus_name.is_empty()
1273 || child_ref.path.is_empty()
1274 {
1275 continue;
1276 }
1277 let child_role = self.get_role_name(child_ref).unwrap_or_default();
1278 if child_role == "application" {
1279 let grandchildren = self.get_atspi_children(child_ref).unwrap_or_default();
1280 for gc_ref in grandchildren {
1281 if gc_ref.path == "/org/a11y/atspi/null"
1282 || gc_ref.bus_name.is_empty()
1283 || gc_ref.path.is_empty()
1284 {
1285 continue;
1286 }
1287 let gc_role = self.get_role_name(&gc_ref).unwrap_or_default();
1288 if gc_role == "application" {
1289 continue;
1290 }
1291 to_build.push(gc_ref);
1292 }
1293 continue;
1294 }
1295 to_build.push(child_ref.clone());
1296 }
1297
1298 if to_build.is_empty() {
1299 self.check_chromium_a11y_enabled(&aref, Some(element_data.role))?;
1300 }
1301
1302 let results: Vec<ElementData> = to_build
1303 .par_iter()
1304 .map(|r| self.build_element_data(r, pid))
1305 .collect();
1306
1307 Ok(results)
1308 }
1309 }
1310 }
1311
1312 fn find_elements(
1313 &self,
1314 root: Option<&ElementData>,
1315 selector: &Selector,
1316 limit: Option<usize>,
1317 max_depth: Option<u32>,
1318 ) -> Result<Vec<ElementData>> {
1319 if selector.segments.is_empty() {
1320 return Ok(vec![]);
1321 }
1322
1323 let max_depth_val = max_depth.unwrap_or(xa11y_core::MAX_TREE_DEPTH);
1324
1325 let first = &selector.segments[0].simple;
1328
1329 let phase1_limit = if selector.segments.len() == 1 {
1330 limit
1331 } else {
1332 None
1333 };
1334 let phase1_limit = match (phase1_limit, first.nth) {
1335 (Some(l), Some(n)) => Some(l.max(n)),
1336 (_, Some(n)) => Some(n),
1337 (l, None) => l,
1338 };
1339
1340 let phase1_depth = if root.is_none()
1342 && matches!(
1343 first.role,
1344 Some(xa11y_core::selector::RoleMatch::Normalized(
1345 Role::Application
1346 ))
1347 ) {
1348 0
1349 } else {
1350 max_depth_val
1351 };
1352
1353 let start_ref = match root {
1354 None => AccessibleRef {
1355 bus_name: "org.a11y.atspi.Registry".to_string(),
1356 path: "/org/a11y/atspi/accessible/root".to_string(),
1357 },
1358 Some(el) => self.get_cached(el.handle)?,
1359 };
1360
1361 let mut matching_refs =
1362 self.collect_matching_refs(&start_ref, first, 0, phase1_depth, phase1_limit)?;
1363
1364 let pid_from_root = root.and_then(|r| r.pid);
1365
1366 if selector.segments.len() == 1 {
1368 if let Some(nth) = first.nth {
1369 if nth <= matching_refs.len() {
1370 let aref = &matching_refs[nth - 1];
1371 let pid = if root.is_none() {
1372 self.get_app_pid(aref)
1373 .or_else(|| self.get_dbus_pid(&aref.bus_name))
1374 } else {
1375 pid_from_root
1376 };
1377 return Ok(vec![self.build_element_data(aref, pid)]);
1378 } else {
1379 return Ok(vec![]);
1380 }
1381 }
1382
1383 if let Some(limit) = limit {
1384 matching_refs.truncate(limit);
1385 }
1386
1387 let is_root_search = root.is_none();
1388 return Ok(matching_refs
1389 .par_iter()
1390 .map(|aref| {
1391 let pid = if is_root_search {
1392 self.get_app_pid(aref)
1393 .or_else(|| self.get_dbus_pid(&aref.bus_name))
1394 } else {
1395 pid_from_root
1396 };
1397 self.build_element_data(aref, pid)
1398 })
1399 .collect());
1400 }
1401
1402 let is_root_search = root.is_none();
1405 let mut candidates: Vec<ElementData> = matching_refs
1406 .par_iter()
1407 .map(|aref| {
1408 let pid = if is_root_search {
1409 self.get_app_pid(aref)
1410 .or_else(|| self.get_dbus_pid(&aref.bus_name))
1411 } else {
1412 pid_from_root
1413 };
1414 self.build_element_data(aref, pid)
1415 })
1416 .collect();
1417
1418 for segment in &selector.segments[1..] {
1419 let mut next_candidates = Vec::new();
1420 for candidate in &candidates {
1421 match segment.combinator {
1422 Combinator::Child => {
1423 let children = self.get_children(Some(candidate))?;
1424 for child in children {
1425 if xa11y_core::selector::matches_simple(&child, &segment.simple) {
1426 next_candidates.push(child);
1427 }
1428 }
1429 }
1430 Combinator::Descendant => {
1431 let sub_selector = Selector {
1432 segments: vec![SelectorSegment {
1433 combinator: Combinator::Root,
1434 simple: segment.simple.clone(),
1435 }],
1436 };
1437 let mut sub_results = xa11y_core::selector::find_elements_in_tree(
1438 |el| self.get_children(el),
1439 Some(candidate),
1440 &sub_selector,
1441 None,
1442 Some(max_depth_val),
1443 )?;
1444 next_candidates.append(&mut sub_results);
1445 }
1446 Combinator::Root => unreachable!(),
1447 }
1448 }
1449 let mut seen = HashSet::new();
1450 next_candidates.retain(|e| seen.insert(e.handle));
1451 candidates = next_candidates;
1452 }
1453
1454 if let Some(nth) = selector.segments.last().and_then(|s| s.simple.nth) {
1456 if nth <= candidates.len() {
1457 candidates = vec![candidates.remove(nth - 1)];
1458 } else {
1459 candidates.clear();
1460 }
1461 }
1462
1463 if let Some(limit) = limit {
1464 candidates.truncate(limit);
1465 }
1466
1467 Ok(candidates)
1468 }
1469
1470 fn get_parent(&self, element: &ElementData) -> Result<Option<ElementData>> {
1471 let aref = self.get_cached(element.handle)?;
1472 match self.get_atspi_parent(&aref)? {
1473 Some(parent_ref) => {
1474 let data = self.build_element_data(&parent_ref, element.pid);
1475 Ok(Some(data))
1476 }
1477 None => Ok(None),
1478 }
1479 }
1480
1481 fn press(&self, element: &ElementData) -> Result<()> {
1482 let target = self.get_cached(element.handle)?;
1483 if let Ok(index) = self.get_action_index(element.handle, "press") {
1485 return self.do_atspi_action_by_index(&target, index);
1486 }
1487 if element.role == Role::Button && self.is_gtk_toolkit(&target) {
1506 let outer_name = self.get_name(&target).unwrap_or_default();
1507 if let Some((inner, index)) = self.find_gtk_press_fallback(&target, &outer_name) {
1508 return self.do_atspi_action_by_index(&inner, index);
1509 }
1510 }
1511 Err(Error::ActionNotSupported {
1512 action: "press".to_string(),
1513 role: element.role,
1514 })
1515 }
1516
1517 fn focus(&self, element: &ElementData) -> Result<()> {
1518 let target = self.get_cached(element.handle)?;
1519 if let Ok(proxy) =
1523 self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Component")
1524 {
1525 if let Ok(reply) = proxy.call_method("GrabFocus", &()) {
1526 if let Ok(true) = reply.body().deserialize::<bool>() {
1528 return Ok(());
1529 }
1530 }
1532 }
1533 if let Ok(index) = self.get_action_index(element.handle, "focus") {
1534 return self.do_atspi_action_by_index(&target, index);
1535 }
1536 Err(Error::ActionNotSupported {
1537 action: "focus".to_string(),
1538 role: element.role,
1539 })
1540 }
1541
1542 fn blur(&self, element: &ElementData) -> Result<()> {
1543 let target = self.get_cached(element.handle)?;
1544 if let Some(parent_ref) = self.get_atspi_parent(&target)? {
1548 if parent_ref.path != "/org/a11y/atspi/null" {
1549 let p = self.make_proxy(
1550 &parent_ref.bus_name,
1551 &parent_ref.path,
1552 "org.a11y.atspi.Component",
1553 )?;
1554 p.call_method("GrabFocus", &())
1555 .map_err(|e| Error::Platform {
1556 code: -1,
1557 message: format!("Component.GrabFocus on parent failed: {}", e),
1558 })?;
1559 return Ok(());
1560 }
1561 }
1562 Err(Error::ActionNotSupported {
1563 action: "blur".to_string(),
1564 role: element.role,
1565 })
1566 }
1567
1568 fn toggle(&self, element: &ElementData) -> Result<()> {
1569 let target = self.get_cached(element.handle)?;
1570 let index = self
1571 .get_action_index(element.handle, "toggle")
1572 .map_err(|_| Error::ActionNotSupported {
1573 action: "toggle".to_string(),
1574 role: element.role,
1575 })?;
1576 self.do_atspi_action_by_index(&target, index)
1577 }
1578
1579 fn select(&self, element: &ElementData) -> Result<()> {
1580 let target = self.get_cached(element.handle)?;
1581 let index = self
1582 .get_action_index(element.handle, "select")
1583 .map_err(|_| Error::ActionNotSupported {
1584 action: "select".to_string(),
1585 role: element.role,
1586 })?;
1587 self.do_atspi_action_by_index(&target, index)
1588 }
1589
1590 fn expand(&self, element: &ElementData) -> Result<()> {
1591 let target = self.get_cached(element.handle)?;
1592 let index = self
1593 .get_action_index(element.handle, "expand")
1594 .map_err(|_| Error::ActionNotSupported {
1595 action: "expand".to_string(),
1596 role: element.role,
1597 })?;
1598 self.do_atspi_action_by_index(&target, index)
1599 }
1600
1601 fn collapse(&self, element: &ElementData) -> Result<()> {
1602 let target = self.get_cached(element.handle)?;
1603 let index = self
1604 .get_action_index(element.handle, "collapse")
1605 .map_err(|_| Error::ActionNotSupported {
1606 action: "collapse".to_string(),
1607 role: element.role,
1608 })?;
1609 self.do_atspi_action_by_index(&target, index)
1610 }
1611
1612 fn show_menu(&self, element: &ElementData) -> Result<()> {
1613 let target = self.get_cached(element.handle)?;
1614 let index = self
1615 .get_action_index(element.handle, "show_menu")
1616 .map_err(|_| Error::ActionNotSupported {
1617 action: "show_menu".to_string(),
1618 role: element.role,
1619 })?;
1620 self.do_atspi_action_by_index(&target, index)
1621 }
1622
1623 fn increment(&self, element: &ElementData) -> Result<()> {
1624 let target = self.get_cached(element.handle)?;
1625 if let Ok(index) = self.get_action_index(element.handle, "increment") {
1627 return self.do_atspi_action_by_index(&target, index);
1628 }
1629 let proxy = self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Value")?;
1630 let current: f64 = proxy
1631 .get_property("CurrentValue")
1632 .map_err(|e| Error::Platform {
1633 code: -1,
1634 message: format!("Value.CurrentValue failed: {}", e),
1635 })?;
1636 let step: f64 = proxy.get_property("MinimumIncrement").unwrap_or(1.0);
1637 let step = if step <= 0.0 { 1.0 } else { step };
1638 proxy
1639 .set_property("CurrentValue", current + step)
1640 .map_err(|e| Error::Platform {
1641 code: -1,
1642 message: format!("Value.SetCurrentValue failed: {}", e),
1643 })
1644 }
1645
1646 fn decrement(&self, element: &ElementData) -> Result<()> {
1647 let target = self.get_cached(element.handle)?;
1648 if let Ok(index) = self.get_action_index(element.handle, "decrement") {
1649 return self.do_atspi_action_by_index(&target, index);
1650 }
1651 let proxy = self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Value")?;
1652 let current: f64 = proxy
1653 .get_property("CurrentValue")
1654 .map_err(|e| Error::Platform {
1655 code: -1,
1656 message: format!("Value.CurrentValue failed: {}", e),
1657 })?;
1658 let step: f64 = proxy.get_property("MinimumIncrement").unwrap_or(1.0);
1659 let step = if step <= 0.0 { 1.0 } else { step };
1660 proxy
1661 .set_property("CurrentValue", current - step)
1662 .map_err(|e| Error::Platform {
1663 code: -1,
1664 message: format!("Value.SetCurrentValue failed: {}", e),
1665 })
1666 }
1667
1668 fn scroll_into_view(&self, element: &ElementData) -> Result<()> {
1669 let target = self.get_cached(element.handle)?;
1670 let proxy = self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Component")?;
1671 proxy
1672 .call_method("ScrollTo", &(0u32,))
1673 .map_err(|e| Error::Platform {
1674 code: -1,
1675 message: format!("ScrollTo failed: {}", e),
1676 })?;
1677 Ok(())
1678 }
1679
1680 fn set_value(&self, element: &ElementData, value: &str) -> Result<()> {
1681 let target = self.get_cached(element.handle)?;
1682 let proxy = self
1683 .make_proxy(
1684 &target.bus_name,
1685 &target.path,
1686 "org.a11y.atspi.EditableText",
1687 )
1688 .map_err(|_| Error::TextValueNotSupported)?;
1689 if proxy.call_method("SetTextContents", &(value,)).is_ok() {
1691 return Ok(());
1692 }
1693 let classify_editable_text_error = |op: &str, e: zbus::Error| -> Error {
1699 let msg = e.to_string();
1700 if msg.contains("UnknownMethod") || msg.contains("UnknownInterface") {
1701 Error::TextValueNotSupported
1702 } else {
1703 Error::Platform {
1704 code: -1,
1705 message: format!("EditableText.{} failed: {}", op, msg),
1706 }
1707 }
1708 };
1709 if let Err(e) = proxy.call_method("DeleteText", &(0i32, i32::MAX)) {
1710 return Err(classify_editable_text_error("DeleteText", e));
1711 }
1712 if let Err(e) = proxy.call_method("InsertText", &(0i32, value, value.len() as i32)) {
1713 return Err(classify_editable_text_error("InsertText", e));
1714 }
1715 Ok(())
1716 }
1717
1718 fn set_numeric_value(&self, element: &ElementData, value: f64) -> Result<()> {
1719 let target = self.get_cached(element.handle)?;
1720 let proxy = self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Value")?;
1721 proxy
1722 .set_property("CurrentValue", value)
1723 .map_err(|e| Error::Platform {
1724 code: -1,
1725 message: format!("SetValue failed: {}", e),
1726 })
1727 }
1728
1729 fn type_text(&self, element: &ElementData, text: &str) -> Result<()> {
1730 let target = self.get_cached(element.handle)?;
1731 let text_proxy = self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Text");
1734 let insert_pos = text_proxy
1735 .as_ref()
1736 .ok()
1737 .and_then(|p| p.get_property::<i32>("CaretOffset").ok())
1738 .unwrap_or(-1); let proxy = self
1741 .make_proxy(
1742 &target.bus_name,
1743 &target.path,
1744 "org.a11y.atspi.EditableText",
1745 )
1746 .map_err(|_| Error::TextValueNotSupported)?;
1747 let pos = if insert_pos >= 0 {
1748 insert_pos
1749 } else {
1750 i32::MAX
1751 };
1752 proxy
1753 .call_method("InsertText", &(pos, text, text.len() as i32))
1754 .map_err(|e| Error::Platform {
1755 code: -1,
1756 message: format!("EditableText.InsertText failed: {}", e),
1757 })?;
1758 Ok(())
1759 }
1760
1761 fn set_text_selection(&self, element: &ElementData, start: u32, end: u32) -> Result<()> {
1762 let target = self.get_cached(element.handle)?;
1763 let proxy = self.make_proxy(&target.bus_name, &target.path, "org.a11y.atspi.Text")?;
1764 proxy
1765 .call_method("SetSelection", &(0i32, start as i32, end as i32))
1766 .map_err(|e| Error::Platform {
1767 code: -1,
1768 message: format!("Text.SetSelection failed: {}", e),
1769 })?;
1770 Ok(())
1771 }
1772
1773 fn perform_action(&self, element: &ElementData, action: &str) -> Result<()> {
1774 match action {
1775 "press" => self.press(element),
1776 "focus" => self.focus(element),
1777 "blur" => self.blur(element),
1778 "toggle" => self.toggle(element),
1779 "select" => self.select(element),
1780 "expand" => self.expand(element),
1781 "collapse" => self.collapse(element),
1782 "show_menu" => self.show_menu(element),
1783 "increment" => self.increment(element),
1784 "decrement" => self.decrement(element),
1785 "scroll_into_view" => self.scroll_into_view(element),
1786 _ => Err(Error::ActionNotSupported {
1787 action: action.to_string(),
1788 role: element.role,
1789 }),
1790 }
1791 }
1792
1793 fn subscribe(&self, element: &ElementData) -> Result<Subscription> {
1794 let pid = element.pid.ok_or(Error::Platform {
1795 code: -1,
1796 message: "Element has no PID for subscribe".to_string(),
1797 })?;
1798 let app_name = element.name.clone().unwrap_or_default();
1799 crate::events::subscribe_for_pid(self, pid, app_name)
1800 }
1801}
1802
1803fn role_has_value(role: Role) -> bool {
1806 !matches!(
1807 role,
1808 Role::Application
1809 | Role::Window
1810 | Role::Dialog
1811 | Role::Group
1812 | Role::MenuBar
1813 | Role::Toolbar
1814 | Role::TabGroup
1815 | Role::SplitGroup
1816 | Role::Table
1817 | Role::TableRow
1818 | Role::Separator
1819 )
1820}
1821
1822fn role_has_actions(role: Role) -> bool {
1825 matches!(
1826 role,
1827 Role::Button
1828 | Role::CheckBox
1829 | Role::RadioButton
1830 | Role::MenuItem
1831 | Role::Link
1832 | Role::ComboBox
1833 | Role::TextField
1834 | Role::TextArea
1835 | Role::SpinButton
1836 | Role::Tab
1837 | Role::TreeItem
1838 | Role::ListItem
1839 | Role::ScrollBar
1840 | Role::Slider
1841 | Role::Menu
1842 | Role::Image
1843 | Role::Unknown
1844 )
1845}
1846
1847pub(crate) fn map_atspi_role(role_name: &str) -> Role {
1849 match role_name.to_lowercase().as_str() {
1850 "application" => Role::Application,
1851 "window" | "frame" => Role::Window,
1852 "dialog" | "file chooser" => Role::Dialog,
1853 "alert" | "notification" => Role::Alert,
1854 "push button" | "push button menu" => Role::Button,
1855 "toggle button" => Role::Switch,
1856 "check box" | "check menu item" => Role::CheckBox,
1857 "radio button" | "radio menu item" => Role::RadioButton,
1858 "entry" | "password text" => Role::TextField,
1859 "spin button" | "spinbutton" => Role::SpinButton,
1860 "text" | "textbox" => Role::TextArea,
1864 "label" | "static" | "caption" => Role::StaticText,
1865 "combo box" | "combobox" => Role::ComboBox,
1866 "list" | "list box" | "listbox" => Role::List,
1868 "list item" => Role::ListItem,
1869 "menu" => Role::Menu,
1870 "menu item" | "tearoff menu item" => Role::MenuItem,
1871 "menu bar" => Role::MenuBar,
1872 "page tab" => Role::Tab,
1873 "page tab list" => Role::TabGroup,
1874 "table" | "tree table" => Role::Table,
1875 "table row" => Role::TableRow,
1876 "table cell" | "table column header" | "table row header" => Role::TableCell,
1877 "tool bar" => Role::Toolbar,
1878 "scroll bar" => Role::ScrollBar,
1879 "slider" => Role::Slider,
1880 "image" | "icon" | "desktop icon" => Role::Image,
1881 "link" => Role::Link,
1882 "panel" | "section" | "form" | "filler" | "viewport" | "scroll pane" => Role::Group,
1883 "progress bar" => Role::ProgressBar,
1884 "tree item" => Role::TreeItem,
1885 "document web" | "document frame" => Role::WebArea,
1886 "heading" => Role::Heading,
1887 "separator" => Role::Separator,
1888 "split pane" => Role::SplitGroup,
1889 "tooltip" | "tool tip" => Role::Tooltip,
1890 "status bar" | "statusbar" => Role::Status,
1891 "landmark" | "navigation" => Role::Navigation,
1892 _ => xa11y_core::unknown_role(role_name),
1893 }
1894}
1895
1896pub(crate) fn map_atspi_role_number(role: u32) -> Role {
1899 match role {
1900 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}")),
1962 }
1963}
1964
1965const GTK_FALLBACK_MAX_DEPTH: u32 = 3;
1969
1970const GTK_FALLBACK_MAX_NODES: usize = 200;
1973
1974fn is_actionable_atspi_role(role: &str) -> bool {
1979 matches!(
1980 role,
1981 "push button"
1982 | "toggle button"
1983 | "check box"
1984 | "radio button"
1985 | "menu item"
1986 | "check menu item"
1987 | "radio menu item"
1988 | "link"
1989 | "page tab"
1990 | "tab"
1991 | "list item"
1992 | "tree item"
1993 )
1994}
1995
1996fn is_never_descend_atspi_role(role: &str) -> bool {
2001 matches!(
2002 role,
2003 "label" | "separator" | "image" | "icon" | "static" | "caption"
2004 )
2005}
2006
2007fn map_atspi_action_name(action_name: &str) -> Option<String> {
2021 let lower = action_name.to_lowercase();
2030 let collapsed: String = lower.chars().filter(|c| !matches!(c, '_' | ' ')).collect();
2031 let canonical = match collapsed.as_str() {
2032 "click" | "activate" | "press" | "invoke" | "dodefault" => "press",
2033 "toggle" | "check" | "uncheck" => "toggle",
2034 "expand" | "open" => "expand",
2035 "collapse" | "close" => "collapse",
2036 "select" => "select",
2037 "menu" | "showmenu" | "showcontextmenu" | "contextmenu" | "popup" => "show_menu",
2038 "increment" => "increment",
2039 "decrement" => "decrement",
2040 _ => return None,
2041 };
2042 Some(canonical.to_string())
2043}
2044
2045#[cfg(test)]
2046mod tests {
2047 use super::*;
2048
2049 #[test]
2050 fn test_role_mapping() {
2051 assert_eq!(map_atspi_role("push button"), Role::Button);
2052 assert_eq!(map_atspi_role("toggle button"), Role::Switch);
2053 assert_eq!(map_atspi_role("check box"), Role::CheckBox);
2054 assert_eq!(map_atspi_role("entry"), Role::TextField);
2055 assert_eq!(map_atspi_role("label"), Role::StaticText);
2056 assert_eq!(map_atspi_role("window"), Role::Window);
2057 assert_eq!(map_atspi_role("frame"), Role::Window);
2058 assert_eq!(map_atspi_role("dialog"), Role::Dialog);
2059 assert_eq!(map_atspi_role("combo box"), Role::ComboBox);
2060 assert_eq!(map_atspi_role("slider"), Role::Slider);
2061 assert_eq!(map_atspi_role("panel"), Role::Group);
2062 assert_eq!(map_atspi_role("unknown_thing"), Role::Unknown);
2063 }
2064
2065 #[test]
2066 fn test_numeric_role_mapping() {
2067 assert_eq!(map_atspi_role_number(62), Role::Switch);
2070 assert_eq!(map_atspi_role_number(43), Role::Button); assert_eq!(map_atspi_role_number(7), Role::CheckBox);
2073 assert_eq!(map_atspi_role_number(67), Role::Unknown); }
2075
2076 #[test]
2077 fn test_action_name_mapping() {
2078 assert_eq!(map_atspi_action_name("click"), Some("press".to_string()));
2079 assert_eq!(map_atspi_action_name("activate"), Some("press".to_string()));
2080 assert_eq!(map_atspi_action_name("press"), Some("press".to_string()));
2081 assert_eq!(map_atspi_action_name("invoke"), Some("press".to_string()));
2082 assert_eq!(
2084 map_atspi_action_name("doDefault"),
2085 Some("press".to_string())
2086 );
2087 assert_eq!(
2088 map_atspi_action_name("do_default"),
2089 Some("press".to_string())
2090 );
2091 assert_eq!(map_atspi_action_name("toggle"), Some("toggle".to_string()));
2092 assert_eq!(map_atspi_action_name("check"), Some("toggle".to_string()));
2093 assert_eq!(map_atspi_action_name("uncheck"), Some("toggle".to_string()));
2094 assert_eq!(map_atspi_action_name("expand"), Some("expand".to_string()));
2095 assert_eq!(map_atspi_action_name("open"), Some("expand".to_string()));
2096 assert_eq!(
2097 map_atspi_action_name("collapse"),
2098 Some("collapse".to_string())
2099 );
2100 assert_eq!(map_atspi_action_name("close"), Some("collapse".to_string()));
2101 assert_eq!(map_atspi_action_name("select"), Some("select".to_string()));
2102 assert_eq!(map_atspi_action_name("menu"), Some("show_menu".to_string()));
2103 assert_eq!(
2104 map_atspi_action_name("showmenu"),
2105 Some("show_menu".to_string())
2106 );
2107 assert_eq!(
2108 map_atspi_action_name("popup"),
2109 Some("show_menu".to_string())
2110 );
2111 assert_eq!(
2112 map_atspi_action_name("show menu"),
2113 Some("show_menu".to_string())
2114 );
2115 assert_eq!(
2118 map_atspi_action_name("showContextMenu"),
2119 Some("show_menu".to_string())
2120 );
2121 assert_eq!(
2122 map_atspi_action_name("show_context_menu"),
2123 Some("show_menu".to_string())
2124 );
2125 assert_eq!(
2126 map_atspi_action_name("increment"),
2127 Some("increment".to_string())
2128 );
2129 assert_eq!(
2130 map_atspi_action_name("decrement"),
2131 Some("decrement".to_string())
2132 );
2133 assert_eq!(map_atspi_action_name("foobar"), None);
2134 }
2135
2136 #[test]
2139 fn test_action_name_aliases_roundtrip() {
2140 let atspi_names = [
2141 "click",
2142 "activate",
2143 "press",
2144 "invoke",
2145 "doDefault",
2146 "do_default",
2147 "toggle",
2148 "check",
2149 "uncheck",
2150 "expand",
2151 "open",
2152 "collapse",
2153 "close",
2154 "select",
2155 "menu",
2156 "showmenu",
2157 "showContextMenu",
2158 "show_context_menu",
2159 "popup",
2160 "show menu",
2161 "increment",
2162 "decrement",
2163 ];
2164 for name in atspi_names {
2165 let canonical = map_atspi_action_name(name).unwrap_or_else(|| {
2166 panic!("AT-SPI2 name {:?} should map to a canonical name", name)
2167 });
2168 let back = map_atspi_action_name(&canonical)
2170 .unwrap_or_else(|| panic!("canonical {:?} should map back to itself", canonical));
2171 assert_eq!(
2172 canonical, back,
2173 "AT-SPI2 {:?} -> {:?} -> {:?} (expected {:?})",
2174 name, canonical, back, canonical
2175 );
2176 }
2177 }
2178
2179 #[test]
2181 fn test_action_name_case_insensitive() {
2182 assert_eq!(map_atspi_action_name("Click"), Some("press".to_string()));
2183 assert_eq!(map_atspi_action_name("TOGGLE"), Some("toggle".to_string()));
2184 assert_eq!(
2185 map_atspi_action_name("Increment"),
2186 Some("increment".to_string())
2187 );
2188 }
2189
2190 #[test]
2195 fn test_gtk_fallback_actionable_roles() {
2196 for role in [
2197 "push button",
2198 "toggle button",
2199 "check box",
2200 "radio button",
2201 "menu item",
2202 "link",
2203 "tab",
2204 "list item",
2205 "tree item",
2206 ] {
2207 assert!(
2208 is_actionable_atspi_role(role),
2209 "{role:?} should be actionable"
2210 );
2211 }
2212 }
2213
2214 #[test]
2219 fn test_gtk_fallback_non_actionable_roles() {
2220 for role in [
2221 "label",
2222 "panel",
2223 "filler",
2224 "section",
2225 "group",
2226 "image",
2227 "separator",
2228 "static",
2229 "frame",
2230 "window",
2231 ] {
2232 assert!(
2233 !is_actionable_atspi_role(role),
2234 "{role:?} must not be treated as actionable"
2235 );
2236 }
2237 }
2238
2239 #[test]
2243 fn test_gtk_fallback_never_descend_roles() {
2244 for role in ["label", "separator", "image", "icon", "static", "caption"] {
2245 assert!(
2246 is_never_descend_atspi_role(role),
2247 "{role:?} must block BFS descent"
2248 );
2249 }
2250 for role in ["panel", "filler", "section", "group", "frame"] {
2253 assert!(
2254 !is_never_descend_atspi_role(role),
2255 "container role {role:?} must remain descendable"
2256 );
2257 }
2258 }
2259}