tessera_ui/
accessibility.rs

1//! # Accessibility Support
2//!
3//! This module provides accessibility infrastructure for Tessera UI using AccessKit.
4//! It enables screen readers and other assistive technologies to interact with Tessera applications.
5//!
6//! ## Architecture
7//!
8//! - **Stable IDs**: Each component can have a stable accessibility ID that persists across frames
9//! - **Semantic Metadata**: Components provide semantic information (role, label, state, actions)
10//! - **Decentralized**: Component libraries decide their own semantics; the core only provides infrastructure
11//!
12//! ## Usage
13//!
14//! Components use the accessibility API through the input handler context:
15//!
16//! ```
17//! use accesskit::{Action, Role};
18//! use tessera_ui::tessera;
19//!
20//! #[tessera]
21//! fn my_button(label: String) {
22//!     input_handler(Box::new(move |input| {
23//!         // Set accessibility information
24//!         input.accessibility()
25//!             .role(Role::Button)
26//!             .label(label.clone())
27//!             .action(Action::Click);
28//!         
29//!         // Set action handler
30//!         input.set_accessibility_action_handler(|action| {
31//!             if action == Action::Click {
32//!                 // Handle click from assistive technology
33//!             }
34//!         });
35//!     }));
36//! }
37//! ```
38
39mod tree_builder;
40
41use accesskit::{Action, NodeId as AccessKitNodeId, Role, Toggled};
42
43pub(crate) use tree_builder::{build_tree_update, dispatch_action};
44
45/// A stable identifier for accessibility nodes.
46///
47/// This ID is generated based on the component's position in the tree and optional user-provided keys.
48/// It remains stable across frames as long as the UI structure doesn't change.
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
50pub struct AccessibilityId(pub u64);
51
52impl AccessibilityId {
53    /// Creates a new accessibility ID from a u64.
54    pub fn new(id: u64) -> Self {
55        Self(id)
56    }
57
58    /// Converts to AccessKit's NodeId.
59    pub fn to_accesskit_id(self) -> AccessKitNodeId {
60        AccessKitNodeId(self.0)
61    }
62
63    /// Creates from AccessKit's NodeId.
64    pub fn from_accesskit_id(id: AccessKitNodeId) -> Self {
65        Self(id.0)
66    }
67
68    /// Generates a stable ID from an indextree NodeId.
69    ///
70    /// indextree uses an arena-based implementation where NodeIds contain:
71    /// - A 1-based index into the arena
72    /// - A stamp for detecting node reuse
73    ///
74    /// In Tessera's immediate-mode model, the component tree is cleared and rebuilt each frame,
75    /// so there's no node reuse within a frame. This makes the index stable for the current tree state,
76    /// which is exactly what AccessKit requires (IDs only need to be stable within the current tree).
77    ///
78    /// # Stability Guarantee
79    ///
80    /// The ID is stable within a single frame as long as the UI structure doesn't change.
81    /// This matches AccessKit's requirement perfectly.
82    pub fn from_component_node_id(node_id: indextree::NodeId) -> Self {
83        // NodeId implements Into<usize>, giving us the 1-based index
84        let index: usize = node_id.into();
85        Self(index as u64)
86    }
87}
88
89/// Semantic information for an accessibility node.
90///
91/// This structure contains all the metadata that assistive technologies need
92/// to understand and interact with a UI component.
93#[derive(Debug, Clone, Default)]
94pub struct AccessibilityNode {
95    /// The role of this node (button, text input, etc.)
96    pub role: Option<Role>,
97    /// A human-readable label for this node
98    pub label: Option<String>,
99    /// A detailed description of this node
100    pub description: Option<String>,
101    /// The current value (for text inputs, sliders, etc.)
102    pub value: Option<String>,
103    /// Numeric value (for sliders, progress bars, etc.)
104    pub numeric_value: Option<f64>,
105    /// Minimum numeric value
106    pub min_numeric_value: Option<f64>,
107    /// Maximum numeric value
108    pub max_numeric_value: Option<f64>,
109    /// Whether this node can receive focus
110    pub focusable: bool,
111    /// Whether this node is currently focused
112    pub focused: bool,
113    /// Toggled/checked state (for checkboxes, switches, radio buttons)
114    pub toggled: Option<Toggled>,
115    /// Whether this node is disabled
116    pub disabled: bool,
117    /// Whether this node is hidden from accessibility
118    pub hidden: bool,
119    /// Supported actions
120    pub actions: Vec<Action>,
121    /// Custom accessibility key provided by the component
122    pub key: Option<String>,
123}
124
125impl AccessibilityNode {
126    /// Creates a new empty accessibility node.
127    pub fn new() -> Self {
128        Self::default()
129    }
130
131    /// Sets the role of this node.
132    pub fn with_role(mut self, role: Role) -> Self {
133        self.role = Some(role);
134        self
135    }
136
137    /// Sets the label of this node.
138    pub fn with_label(mut self, label: impl Into<String>) -> Self {
139        self.label = Some(label.into());
140        self
141    }
142
143    /// Sets the description of this node.
144    pub fn with_description(mut self, description: impl Into<String>) -> Self {
145        self.description = Some(description.into());
146        self
147    }
148
149    /// Sets the value of this node.
150    pub fn with_value(mut self, value: impl Into<String>) -> Self {
151        self.value = Some(value.into());
152        self
153    }
154
155    /// Sets the numeric value of this node.
156    pub fn with_numeric_value(mut self, value: f64) -> Self {
157        self.numeric_value = Some(value);
158        self
159    }
160
161    /// Sets the numeric range of this node.
162    pub fn with_numeric_range(mut self, min: f64, max: f64) -> Self {
163        self.min_numeric_value = Some(min);
164        self.max_numeric_value = Some(max);
165        self
166    }
167
168    /// Marks this node as focusable.
169    pub fn focusable(mut self) -> Self {
170        self.focusable = true;
171        self
172    }
173
174    /// Marks this node as focused.
175    pub fn focused(mut self) -> Self {
176        self.focused = true;
177        self
178    }
179
180    /// Sets the toggled/checked state of this node.
181    pub fn with_toggled(mut self, toggled: Toggled) -> Self {
182        self.toggled = Some(toggled);
183        self
184    }
185
186    /// Marks this node as disabled.
187    pub fn disabled(mut self) -> Self {
188        self.disabled = true;
189        self
190    }
191
192    /// Marks this node as hidden from accessibility.
193    pub fn hidden(mut self) -> Self {
194        self.hidden = true;
195        self
196    }
197
198    /// Adds an action that this node supports.
199    pub fn with_action(mut self, action: Action) -> Self {
200        self.actions.push(action);
201        self
202    }
203
204    /// Adds multiple actions that this node supports.
205    pub fn with_actions(mut self, actions: impl IntoIterator<Item = Action>) -> Self {
206        self.actions.extend(actions);
207        self
208    }
209
210    /// Sets a custom accessibility key for stable ID generation.
211    pub fn with_key(mut self, key: impl Into<String>) -> Self {
212        self.key = Some(key.into());
213        self
214    }
215}
216
217/// Handler for accessibility actions.
218///
219/// When an assistive technology requests an action (like clicking a button),
220/// this handler is invoked.
221pub type AccessibilityActionHandler = Box<dyn Fn(Action) + Send + Sync>;