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>;