xa11y_core/node.rs
1use serde::{Deserialize, Serialize};
2
3use crate::action::Action;
4use crate::role::Role;
5
6/// Internal index for a node within a snapshot (sequential DFS order).
7/// This is an array index, not a stable identity — it changes between snapshots.
8/// Internal index type for node positions within a snapshot.
9#[doc(hidden)]
10pub type NodeIndex = u32;
11
12/// A single element in the accessibility tree snapshot.
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct Node {
15 /// Element role
16 pub role: Role,
17
18 /// Human-readable name (title, label)
19 pub name: Option<String>,
20
21 /// Current value (text content, slider position, etc.)
22 pub value: Option<String>,
23
24 /// Supplementary description (tooltip, help text)
25 pub description: Option<String>,
26
27 /// Bounding rectangle in screen pixels
28 pub bounds: Option<Rect>,
29
30 /// Available actions
31 pub actions: Vec<Action>,
32
33 /// Current state flags
34 pub states: StateSet,
35
36 /// Numeric value for range controls (sliders, progress bars, spinners).
37 pub numeric_value: Option<f64>,
38
39 /// Minimum value for range controls.
40 pub min_value: Option<f64>,
41
42 /// Maximum value for range controls.
43 pub max_value: Option<f64>,
44
45 /// Platform-assigned stable identifier for cross-snapshot correlation.
46 /// - macOS: `AXIdentifier`
47 /// - Windows: `AutomationId`
48 /// - Linux: D-Bus `object_path`
49 ///
50 /// Not all elements have one.
51 pub stable_id: Option<String>,
52
53 /// Platform-specific raw data
54 pub raw: RawPlatformData,
55
56 // ── Internal fields ──────────────────────────────────────────────────────
57 // Present in serialized output for FFI consumers (Python, JS, LLMs),
58 // but not part of the Rust public API.
59 /// Sequential DFS index within the snapshot.
60 /// Internal — present in serialized output for FFI consumers,
61 /// but not intended as part of the primary Rust API.
62 #[doc(hidden)]
63 pub index: NodeIndex,
64
65 /// Child node indices (direct children only).
66 #[doc(hidden)]
67 pub children_indices: Vec<NodeIndex>,
68
69 /// Parent node index (None for root).
70 #[doc(hidden)]
71 pub parent_index: Option<NodeIndex>,
72}
73
74impl Node {
75 /// Create a synthetic empty node, used as a placeholder when a wait
76 /// condition is satisfied by the *absence* of a node (e.g. Detached/Hidden).
77 pub fn synthetic_empty() -> Self {
78 Self {
79 role: Role::Unknown,
80 name: None,
81 value: None,
82 description: None,
83 bounds: None,
84 actions: vec![],
85 states: StateSet::default(),
86 numeric_value: None,
87 min_value: None,
88 max_value: None,
89 stable_id: None,
90 raw: RawPlatformData::Synthetic,
91 index: 0,
92 children_indices: vec![],
93 parent_index: None,
94 }
95 }
96}
97
98/// Boolean state flags for a node.
99///
100/// **Semantics for non-applicable states:** When a state doesn't apply to an
101/// element's role, the backend uses the platform's reported value or defaults:
102/// - `enabled`: `true` (elements are enabled unless explicitly disabled)
103/// - `visible`: `true` (elements are visible unless explicitly hidden/offscreen)
104/// - `focused`, `focusable`, `modal`, `selected`, `editable`, `required`, `busy`: `false`
105///
106/// States that are inherently inapplicable use `Option`: `checked` is `None`
107/// for non-checkable elements, `expanded` is `None` for non-expandable elements.
108#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
109pub struct StateSet {
110 pub enabled: bool,
111 pub visible: bool,
112 pub focused: bool,
113 /// None = not checkable
114 pub checked: Option<Toggled>,
115 pub selected: bool,
116 /// None = not expandable
117 pub expanded: Option<bool>,
118 pub editable: bool,
119 /// Whether the element can receive keyboard focus
120 pub focusable: bool,
121 /// Whether the element is a modal dialog
122 pub modal: bool,
123 /// Form field required
124 pub required: bool,
125 /// Async operation in progress
126 pub busy: bool,
127}
128
129impl Default for StateSet {
130 fn default() -> Self {
131 Self {
132 enabled: true,
133 visible: true,
134 focused: false,
135 checked: None,
136 selected: false,
137 expanded: None,
138 editable: false,
139 focusable: false,
140 modal: false,
141 required: false,
142 busy: false,
143 }
144 }
145}
146
147/// Tri-state toggle value.
148#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
149pub enum Toggled {
150 Off,
151 On,
152 /// Indeterminate / tri-state
153 Mixed,
154}
155
156/// Screen-pixel bounding rectangle (origin + size).
157/// `x`/`y` are signed to support negative multi-monitor coordinates.
158/// `width`/`height` are unsigned (always non-negative).
159#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
160pub struct Rect {
161 pub x: i32,
162 pub y: i32,
163 pub width: u32,
164 pub height: u32,
165}
166
167/// Platform-specific raw data attached to every node.
168#[derive(Debug, Clone, Serialize, Deserialize)]
169pub enum RawPlatformData {
170 MacOS {
171 ax_role: String,
172 ax_subrole: Option<String>,
173 ax_identifier: Option<String>,
174 },
175 Windows {
176 control_type_id: i32,
177 automation_id: Option<String>,
178 class_name: Option<String>,
179 },
180 Linux {
181 atspi_role: String,
182 bus_name: String,
183 object_path: String,
184 },
185 /// Placeholder for synthetic nodes with no real platform backing.
186 Synthetic,
187}