Skip to main content

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}