tessera_ui/
accessibility.rs

1//! # Accessibility Support
2//!
3//! This module provides accessibility infrastructure for Tessera UI using
4//! AccessKit. It enables screen readers and other assistive technologies to
5//! interact with Tessera applications.
6//!
7//! ## Usage
8//!
9//! Accessibility metadata is typically attached through semantics and
10//! interaction modifiers in higher-level component crates. Framework internals
11//! may also write accessibility state directly while building node-local
12//! modifier chains.
13
14mod tree_builder;
15
16use accesskit::{Action, NodeId as AccessKitNodeId, Role, Toggled};
17
18use crate::Px;
19
20pub(crate) use tree_builder::{build_tree_update, dispatch_action};
21
22/// A stable identifier for accessibility nodes.
23///
24/// This ID is generated based on the component's position in the tree and
25/// optional user-provided keys. It remains stable across frames as long as the
26/// UI structure doesn't change.
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
28pub struct AccessibilityId(pub u64);
29
30impl AccessibilityId {
31    /// Creates a new accessibility ID from a u64.
32    pub fn new(id: u64) -> Self {
33        Self(id)
34    }
35
36    /// Converts to AccessKit's NodeId.
37    pub fn to_accesskit_id(self) -> AccessKitNodeId {
38        AccessKitNodeId(self.0)
39    }
40
41    /// Creates from AccessKit's NodeId.
42    pub fn from_accesskit_id(id: AccessKitNodeId) -> Self {
43        Self(id.0)
44    }
45
46    /// Generates a stable ID from an indextree NodeId.
47    ///
48    /// indextree uses an arena-based implementation where NodeIds contain:
49    /// - A 1-based index into the arena
50    /// - A stamp for detecting node reuse
51    ///
52    /// In Tessera's immediate-mode model, the component tree is cleared and
53    /// rebuilt each frame, so there's no node reuse within a frame. This
54    /// makes the index stable for the current tree state, which is exactly
55    /// what AccessKit requires (IDs only need to be stable within the current
56    /// tree).
57    ///
58    /// # Stability Guarantee
59    ///
60    /// The ID is stable within a single frame as long as the UI structure
61    /// doesn't change. This matches AccessKit's requirement perfectly.
62    pub fn from_component_node_id(node_id: indextree::NodeId) -> Self {
63        // NodeId implements Into<usize>, giving us the 1-based index
64        let index: usize = node_id.into();
65        Self(index as u64)
66    }
67}
68
69/// Padding applied to semantic bounds without affecting layout.
70#[derive(Debug, Clone, Copy)]
71pub struct AccessibilityPadding {
72    /// Left padding in physical pixels.
73    pub left: Px,
74    /// Top padding in physical pixels.
75    pub top: Px,
76    /// Right padding in physical pixels.
77    pub right: Px,
78    /// Bottom padding in physical pixels.
79    pub bottom: Px,
80}
81
82impl AccessibilityPadding {
83    /// Creates zero padding.
84    pub const fn zero() -> Self {
85        Self {
86            left: Px::ZERO,
87            top: Px::ZERO,
88            right: Px::ZERO,
89            bottom: Px::ZERO,
90        }
91    }
92}
93
94/// Semantic information for an accessibility node.
95///
96/// This structure contains all the metadata that assistive technologies need
97/// to understand and interact with a UI component.
98#[derive(Debug, Clone)]
99pub struct AccessibilityNode {
100    /// The role of this node (button, text input, etc.)
101    pub role: Option<Role>,
102    /// A human-readable label for this node
103    pub label: Option<String>,
104    /// A detailed description of this node
105    pub description: Option<String>,
106    /// The current value (for text inputs, sliders, etc.)
107    pub value: Option<String>,
108    /// Numeric value (for sliders, progress bars, etc.)
109    pub numeric_value: Option<f64>,
110    /// Minimum numeric value
111    pub min_numeric_value: Option<f64>,
112    /// Maximum numeric value
113    pub max_numeric_value: Option<f64>,
114    /// Whether this node can receive focus
115    pub focusable: bool,
116    /// Whether this node is currently focused
117    pub focused: bool,
118    /// Toggled/checked state (for checkboxes, switches, radio buttons)
119    pub toggled: Option<Toggled>,
120    /// Whether this node is disabled
121    pub disabled: bool,
122    /// Whether this node is hidden from accessibility
123    pub hidden: bool,
124    /// Supported actions
125    pub actions: Vec<Action>,
126    /// Custom accessibility key provided by the component
127    pub key: Option<String>,
128    /// Whether to merge child semantics into this node. When false, child
129    /// semantics are ignored, similar to Compose's `clearAndSetSemantics`.
130    pub merge_descendants: bool,
131    /// Optional padding applied to the semantic bounds without affecting
132    /// layout.
133    pub bounds_padding: Option<AccessibilityPadding>,
134    /// Optional state description announced in addition to the label.
135    pub state_description: Option<String>,
136    /// Optional custom role description for custom controls.
137    pub role_description: Option<String>,
138    /// Optional tooltip text exposed as a name override.
139    pub tooltip: Option<String>,
140    /// Live region politeness.
141    pub live: Option<accesskit::Live>,
142    /// Optional heading level (1-based). When set and no role is provided,
143    /// role will default to `Heading`.
144    pub heading_level: Option<u32>,
145    /// Optional scroll x value and range.
146    pub scroll_x: Option<(f64, f64, f64)>,
147    /// Optional scroll y value and range.
148    pub scroll_y: Option<(f64, f64, f64)>,
149    /// Optional numeric step for range-based controls.
150    pub numeric_value_step: Option<f64>,
151    /// Optional numeric jump value for range-based controls.
152    pub numeric_value_jump: Option<f64>,
153    /// Optional collection info: row_count, column_count, hierarchical.
154    pub collection_info: Option<(usize, usize, bool)>,
155    /// Optional collection item info: row_index, row_span, col_index, col_span,
156    /// heading.
157    pub collection_item_info: Option<(usize, usize, usize, usize, bool)>,
158    /// Optional editable text flag.
159    pub is_editable_text: bool,
160}
161
162impl AccessibilityNode {
163    /// Creates a new empty accessibility node.
164    pub fn new() -> Self {
165        Self::default()
166    }
167
168    /// Sets the role of this node.
169    pub fn with_role(mut self, role: Role) -> Self {
170        self.role = Some(role);
171        self
172    }
173
174    /// Sets the label of this node.
175    pub fn with_label(mut self, label: impl Into<String>) -> Self {
176        self.label = Some(label.into());
177        self
178    }
179
180    /// Sets the description of this node.
181    pub fn with_description(mut self, description: impl Into<String>) -> Self {
182        self.description = Some(description.into());
183        self
184    }
185
186    /// Sets the value of this node.
187    pub fn with_value(mut self, value: impl Into<String>) -> Self {
188        self.value = Some(value.into());
189        self
190    }
191
192    /// Sets the numeric value of this node.
193    pub fn with_numeric_value(mut self, value: f64) -> Self {
194        self.numeric_value = Some(value);
195        self
196    }
197
198    /// Sets the numeric range of this node.
199    pub fn with_numeric_range(mut self, min: f64, max: f64) -> Self {
200        self.min_numeric_value = Some(min);
201        self.max_numeric_value = Some(max);
202        self
203    }
204
205    /// Marks this node as focusable.
206    pub fn focusable(mut self) -> Self {
207        self.focusable = true;
208        self
209    }
210
211    /// Marks this node as focused.
212    pub fn focused(mut self) -> Self {
213        self.focused = true;
214        self
215    }
216
217    /// Sets the toggled/checked state of this node.
218    pub fn with_toggled(mut self, toggled: Toggled) -> Self {
219        self.toggled = Some(toggled);
220        self
221    }
222
223    /// Marks this node as disabled.
224    pub fn disabled(mut self) -> Self {
225        self.disabled = true;
226        self
227    }
228
229    /// Marks this node as hidden from accessibility.
230    pub fn hidden(mut self) -> Self {
231        self.hidden = true;
232        self
233    }
234
235    /// Adds an action that this node supports.
236    pub fn with_action(mut self, action: Action) -> Self {
237        self.actions.push(action);
238        self
239    }
240
241    /// Adds multiple actions that this node supports.
242    pub fn with_actions(mut self, actions: impl IntoIterator<Item = Action>) -> Self {
243        self.actions.extend(actions);
244        self
245    }
246
247    /// Sets a custom accessibility key for stable ID generation.
248    pub fn with_key(mut self, key: impl Into<String>) -> Self {
249        self.key = Some(key.into());
250        self
251    }
252}
253
254impl Default for AccessibilityNode {
255    fn default() -> Self {
256        Self {
257            role: None,
258            label: None,
259            description: None,
260            value: None,
261            numeric_value: None,
262            min_numeric_value: None,
263            max_numeric_value: None,
264            focusable: false,
265            focused: false,
266            toggled: None,
267            disabled: false,
268            hidden: false,
269            actions: Vec::new(),
270            key: None,
271            merge_descendants: true,
272            bounds_padding: None,
273            state_description: None,
274            role_description: None,
275            tooltip: None,
276            live: None,
277            heading_level: None,
278            scroll_x: None,
279            scroll_y: None,
280            numeric_value_step: None,
281            numeric_value_jump: None,
282            collection_info: None,
283            collection_item_info: None,
284            is_editable_text: false,
285        }
286    }
287}
288
289/// Handler for accessibility actions.
290///
291/// When an assistive technology requests an action (like clicking a button),
292/// this handler is invoked.
293pub type AccessibilityActionHandler = Box<dyn Fn(Action) + Send + Sync>;