Skip to main content

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}