ravenrs_core/access.rs
1//! A platform-neutral accessibility tree: what screen readers and other
2//! assistive technology see. Widgets describe themselves with a [`Role`] and
3//! state; platform layers translate the [`AccessTree`] to the operating
4//! system's API (AccessKit on desktop, ARIA in the browser).
5
6use crate::Rect;
7
8/// What kind of control or content a node is, as assistive technology names it.
9#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
10#[non_exhaustive]
11#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
12pub enum Role {
13 /// The application window, the root of the tree.
14 Window,
15 /// A container with no meaning of its own.
16 Group,
17 /// Static text.
18 Label,
19 /// A heading; see [`AccessNode::level`].
20 Heading,
21 /// Activates an action.
22 Button,
23 /// Navigates somewhere.
24 Link,
25 /// A two- or three-state check box.
26 CheckBox,
27 /// An on/off switch.
28 Switch,
29 /// A set of radio buttons.
30 RadioGroup,
31 /// One choice of a radio group.
32 RadioButton,
33 /// Editable single-line text.
34 TextInput,
35 /// Editable multi-line text.
36 MultilineTextInput,
37 /// A value picked along a track.
38 Slider,
39 /// A number changed with increment and decrement buttons.
40 SpinButton,
41 /// A field that opens a list of choices.
42 ComboBox,
43 /// A list of selectable options.
44 ListBox,
45 /// One option of a list box.
46 ListBoxOption,
47 /// A popup menu.
48 Menu,
49 /// A horizontal bar of menus.
50 MenuBar,
51 /// One command in a menu.
52 MenuItem,
53 /// A row of tabs.
54 TabList,
55 /// One tab.
56 Tab,
57 /// The content shown for the selected tab.
58 TabPanel,
59 /// A window over the app; see [`AccessNode::modal`].
60 Dialog,
61 /// An urgent message.
62 Alert,
63 /// A status message (toasts).
64 Status,
65 /// A rich tooltip.
66 Tooltip,
67 /// A row of controls.
68 Toolbar,
69 /// A data grid.
70 Grid,
71 /// A static table.
72 Table,
73 /// A row of a grid or table.
74 Row,
75 /// A cell of a grid or table.
76 Cell,
77 /// A column header of a grid or table.
78 ColumnHeader,
79 /// A list of items.
80 List,
81 /// One item of a list.
82 ListItem,
83 /// An image; its name is the alternative text.
84 Image,
85 /// Progress of a task.
86 ProgressIndicator,
87 /// A visual divider.
88 Separator,
89 /// A region that scrolls.
90 ScrollView,
91 /// A date grid.
92 Calendar,
93 /// The band that ends a page: copyright, links, version (a landmark).
94 ContentInfo,
95 /// A set of links or buttons that move around the app (a landmark).
96 Navigation,
97 /// A list of items that can open to show more items.
98 Tree,
99 /// One item of a tree; see [`AccessNode::level`].
100 TreeItem,
101}
102
103#[cfg_attr(feature = "live", ravenrs_live::expose)]
104impl Role {
105 /// Roles whose accessible name comes from the text inside them when no
106 /// explicit name is given, as in ARIA's "name from content".
107 pub fn takes_name_from_content(self) -> bool {
108 matches!(
109 self,
110 Self::Button
111 | Self::Link
112 | Self::CheckBox
113 | Self::Switch
114 | Self::RadioButton
115 | Self::Heading
116 | Self::ListBoxOption
117 | Self::MenuItem
118 | Self::Tab
119 | Self::Cell
120 | Self::ColumnHeader
121 | Self::ListItem
122 | Self::TreeItem
123 | Self::Tooltip
124 | Self::Label
125 )
126 }
127}
128
129/// State of a check box, switch or toggle button.
130#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
131#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
132pub enum Toggled {
133 /// Off or unchecked.
134 False,
135 /// On or checked.
136 True,
137 /// Partly checked (a parent of mixed children).
138 Mixed,
139}
140
141impl From<bool> for Toggled {
142 fn from(on: bool) -> Self {
143 if on { Self::True } else { Self::False }
144 }
145}
146
147/// A numeric value with its range (sliders, spin buttons, progress).
148#[derive(Clone, Copy, Debug, PartialEq)]
149#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
150pub struct NumericValue {
151 /// The current value.
152 pub value: f64,
153 /// The smallest value.
154 pub min: f64,
155 /// The largest value.
156 pub max: f64,
157 /// The amount one arrow key changes the value by, if any.
158 pub step: Option<f64>,
159}
160
161/// What assistive technology can ask a node to do.
162#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
163#[non_exhaustive]
164#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
165pub enum AccessAction {
166 /// Activate it, as a click or Enter would.
167 Click,
168 /// Move keyboard focus to it.
169 Focus,
170 /// Raise its value one step.
171 Increment,
172 /// Lower its value one step.
173 Decrement,
174}
175
176/// One node of the accessibility tree.
177#[derive(Clone, Debug, PartialEq)]
178#[non_exhaustive]
179#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
180pub struct AccessNode {
181 /// Identity, stable across frames for the same widget.
182 pub id: u64,
183 /// What the node is.
184 pub role: Role,
185 /// The accessible name: a label, or the text inside for roles that take
186 /// their name from content.
187 pub name: Option<String>,
188 /// Extra description, read after the name.
189 pub description: Option<String>,
190 /// A text value (text fields, combo boxes, sliders with formatted values).
191 pub value: Option<String>,
192 /// A numeric value with its range.
193 pub numeric: Option<NumericValue>,
194 /// Checked state.
195 pub toggled: Option<Toggled>,
196 /// Selected state (tabs, options, rows, days).
197 pub selected: Option<bool>,
198 /// Expanded state of something that opens (menus, combo boxes, disclosures).
199 pub expanded: Option<bool>,
200 /// The control does not respond to input.
201 pub disabled: bool,
202 /// The value can be read and selected, but not changed.
203 pub read_only: bool,
204 /// Keyboard focus can be on it.
205 pub focusable: bool,
206 /// While it is open, the rest of the app is inert (dialogs).
207 pub modal: bool,
208 /// Heading level, 1 to 6.
209 pub level: Option<u32>,
210 /// The option highlighted inside a focused text field that drives a list
211 /// (combobox pattern): screen readers announce it as if it had focus.
212 pub active_descendant: Option<u64>,
213 /// Absolute frame in logical pixels.
214 pub bounds: Rect,
215 /// Child node ids in reading order.
216 pub children: Vec<u64>,
217 /// Actions assistive technology may request.
218 pub actions: Vec<AccessAction>,
219}
220
221#[cfg_attr(feature = "live", ravenrs_live::expose)]
222impl AccessNode {
223 /// A node with no name, state, children or actions.
224 pub fn new(id: u64, role: Role, bounds: Rect) -> Self {
225 Self {
226 id,
227 role,
228 name: None,
229 description: None,
230 value: None,
231 numeric: None,
232 toggled: None,
233 selected: None,
234 expanded: None,
235 disabled: false,
236 read_only: false,
237 focusable: false,
238 modal: false,
239 level: None,
240 active_descendant: None,
241 bounds,
242 children: Vec::new(),
243 actions: Vec::new(),
244 }
245 }
246}
247
248/// The whole accessibility tree of one window, as of the latest frame.
249#[derive(Clone, Debug, Default, PartialEq)]
250#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
251pub struct AccessTree {
252 /// Id of the root node (role [`Role::Window`]).
253 pub root: u64,
254 /// Every node, root first, parents before children.
255 pub nodes: Vec<AccessNode>,
256 /// The node with keyboard focus, if any.
257 pub focus: Option<u64>,
258}
259
260#[cfg_attr(feature = "live", ravenrs_live::expose)]
261impl AccessTree {
262 /// The node with this id.
263 pub fn get(&self, id: u64) -> Option<&AccessNode> {
264 self.nodes.iter().find(|n| n.id == id)
265 }
266
267 /// The first node with this role and name, for tests and tools.
268 pub fn find(&self, role: Role, name: &str) -> Option<&AccessNode> {
269 self.nodes.iter().find(|n| n.role == role && n.name.as_deref() == Some(name))
270 }
271
272 /// The node with keyboard focus.
273 pub fn focused(&self) -> Option<&AccessNode> {
274 self.focus.and_then(|id| self.get(id))
275 }
276}