Skip to main content

xa11y_core/
node.rs

1use std::fmt;
2use std::ops::Deref;
3use std::sync::Arc;
4
5use serde::{Deserialize, Serialize};
6
7use crate::action::Action;
8use crate::error::Result;
9use crate::role::Role;
10use crate::tree::Tree;
11
12/// Internal index for a node within a snapshot (sequential DFS order).
13/// This is an array index, not a stable identity — it changes between snapshots.
14/// Internal index type for node positions within a snapshot.
15#[doc(hidden)]
16pub type NodeIndex = u32;
17
18/// The raw data for a single element in an accessibility tree snapshot.
19///
20/// This is the underlying data struct. Most consumers should use [`Node`],
21/// which wraps `NodeData` with snapshot navigation (parent/children).
22/// `NodeData` is used directly by provider implementors building trees.
23#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct NodeData {
25    /// Element role
26    pub role: Role,
27
28    /// Human-readable name (title, label)
29    pub name: Option<String>,
30
31    /// Current value (text content, slider position, etc.)
32    pub value: Option<String>,
33
34    /// Supplementary description (tooltip, help text)
35    pub description: Option<String>,
36
37    /// Bounding rectangle in screen pixels
38    pub bounds: Option<Rect>,
39
40    /// Available actions
41    pub actions: Vec<Action>,
42
43    /// Current state flags
44    pub states: StateSet,
45
46    /// Numeric value for range controls (sliders, progress bars, spinners).
47    pub numeric_value: Option<f64>,
48
49    /// Minimum value for range controls.
50    pub min_value: Option<f64>,
51
52    /// Maximum value for range controls.
53    pub max_value: Option<f64>,
54
55    /// Platform-assigned stable identifier for cross-snapshot correlation.
56    /// - macOS: `AXIdentifier`
57    /// - Windows: `AutomationId`
58    /// - Linux: D-Bus `object_path`
59    ///
60    /// Not all elements have one.
61    pub stable_id: Option<String>,
62
63    /// Process ID of the application that owns this node.
64    pub pid: Option<u32>,
65
66    /// Platform-specific raw data
67    pub raw: RawPlatformData,
68
69    // ── Internal fields ──────────────────────────────────────────────────────
70    // Present in serialized output for FFI consumers (Python, JS, LLMs),
71    // but not part of the Rust public API.
72    /// Sequential DFS index within the snapshot.
73    /// Internal — present in serialized output for FFI consumers,
74    /// but not intended as part of the primary Rust API.
75    #[doc(hidden)]
76    pub index: NodeIndex,
77
78    /// Child node indices (direct children only).
79    #[doc(hidden)]
80    pub children_indices: Vec<NodeIndex>,
81
82    /// Parent node index (None for root).
83    #[doc(hidden)]
84    pub parent_index: Option<NodeIndex>,
85}
86
87/// A node in an accessibility tree snapshot, with navigation.
88///
89/// `Node` dereferences to [`NodeData`], so all properties (`role`, `name`,
90/// `value`, `states`, etc.) are accessible via field access. Navigation
91/// methods (`parent()`, `children()`, `query()`) use the shared snapshot
92/// — no platform refetch occurs.
93///
94/// Nodes are cheap to clone (they share the underlying snapshot via `Arc`).
95#[derive(Clone)]
96pub struct Node {
97    snapshot: Arc<Tree>,
98    index: u32,
99}
100
101impl Deref for Node {
102    type Target = NodeData;
103
104    fn deref(&self) -> &NodeData {
105        self.snapshot
106            .get_data(self.index)
107            .expect("Node index must be valid within its snapshot")
108    }
109}
110
111impl fmt::Debug for Node {
112    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113        // Delegate to the underlying NodeData's Debug
114        fmt::Debug::fmt(&**self, f)
115    }
116}
117
118impl fmt::Display for Node {
119    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
120        fmt::Display::fmt(&self.snapshot, f)
121    }
122}
123
124impl Serialize for Node {
125    fn serialize<S: serde::Serializer>(
126        &self,
127        serializer: S,
128    ) -> std::result::Result<S::Ok, S::Error> {
129        // Serialize as the underlying NodeData
130        (**self).serialize(serializer)
131    }
132}
133
134impl Node {
135    /// Create a Node handle from a snapshot and an index into the snapshot.
136    pub fn new(snapshot: Arc<Tree>, index: u32) -> Self {
137        Self { snapshot, index }
138    }
139
140    /// Get the underlying snapshot (Tree) this node belongs to.
141    ///
142    /// Used by provider crates for action dispatch.
143    pub fn tree(&self) -> &Arc<Tree> {
144        &self.snapshot
145    }
146
147    /// Get the node's index within its snapshot.
148    ///
149    /// Used by provider crates for action dispatch.
150    pub fn node_index(&self) -> u32 {
151        self.index
152    }
153
154    /// Get the parent node, if any (root has no parent).
155    ///
156    /// Uses the snapshot — no platform refetch.
157    pub fn parent(&self) -> Option<Node> {
158        self.parent_index
159            .map(|idx| Node::new(Arc::clone(&self.snapshot), idx))
160    }
161
162    /// Get direct children of this node.
163    ///
164    /// Uses the snapshot — no platform refetch.
165    pub fn children(&self) -> Vec<Node> {
166        self.children_indices
167            .iter()
168            .map(|&idx| Node::new(Arc::clone(&self.snapshot), idx))
169            .collect()
170    }
171
172    /// Get the subtree rooted at this node (including this node).
173    ///
174    /// Uses the snapshot — no platform refetch.
175    pub fn subtree(&self) -> Vec<Node> {
176        self.snapshot
177            .subtree_indices(self.index)
178            .into_iter()
179            .map(|idx| Node::new(Arc::clone(&self.snapshot), idx))
180            .collect()
181    }
182
183    /// Query nodes matching a CSS-like selector string within this snapshot.
184    ///
185    /// Searches the *entire* snapshot (not just this node's subtree).
186    /// Uses the snapshot — no platform refetch.
187    pub fn query(&self, selector_str: &str) -> Result<Vec<Node>> {
188        let indices = self.snapshot.query_indices(selector_str)?;
189        Ok(indices
190            .into_iter()
191            .map(|idx| Node::new(Arc::clone(&self.snapshot), idx))
192            .collect())
193    }
194}
195
196/// Boolean state flags for a node.
197///
198/// **Semantics for non-applicable states:** When a state doesn't apply to an
199/// element's role, the backend uses the platform's reported value or defaults:
200/// - `enabled`: `true` (elements are enabled unless explicitly disabled)
201/// - `visible`: `true` (elements are visible unless explicitly hidden/offscreen)
202/// - `focused`, `focusable`, `modal`, `selected`, `editable`, `required`, `busy`: `false`
203///
204/// States that are inherently inapplicable use `Option`: `checked` is `None`
205/// for non-checkable elements, `expanded` is `None` for non-expandable elements.
206#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
207pub struct StateSet {
208    pub enabled: bool,
209    pub visible: bool,
210    pub focused: bool,
211    /// None = not checkable
212    pub checked: Option<Toggled>,
213    pub selected: bool,
214    /// None = not expandable
215    pub expanded: Option<bool>,
216    pub editable: bool,
217    /// Whether the element can receive keyboard focus
218    pub focusable: bool,
219    /// Whether the element is a modal dialog
220    pub modal: bool,
221    /// Form field required
222    pub required: bool,
223    /// Async operation in progress
224    pub busy: bool,
225}
226
227impl Default for StateSet {
228    fn default() -> Self {
229        Self {
230            enabled: true,
231            visible: true,
232            focused: false,
233            checked: None,
234            selected: false,
235            expanded: None,
236            editable: false,
237            focusable: false,
238            modal: false,
239            required: false,
240            busy: false,
241        }
242    }
243}
244
245/// Tri-state toggle value.
246#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
247pub enum Toggled {
248    Off,
249    On,
250    /// Indeterminate / tri-state
251    Mixed,
252}
253
254/// Screen-pixel bounding rectangle (origin + size).
255/// `x`/`y` are signed to support negative multi-monitor coordinates.
256/// `width`/`height` are unsigned (always non-negative).
257#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
258pub struct Rect {
259    pub x: i32,
260    pub y: i32,
261    pub width: u32,
262    pub height: u32,
263}
264
265/// Platform-specific raw data attached to every node.
266#[derive(Debug, Clone, Serialize, Deserialize)]
267pub enum RawPlatformData {
268    MacOS {
269        ax_role: String,
270        ax_subrole: Option<String>,
271        ax_identifier: Option<String>,
272    },
273    Windows {
274        control_type_id: i32,
275        automation_id: Option<String>,
276        class_name: Option<String>,
277    },
278    Linux {
279        atspi_role: String,
280        bus_name: String,
281        object_path: String,
282    },
283    /// Placeholder for synthetic nodes with no real platform backing.
284    Synthetic,
285}