freya_core/states/
accessibility.rs

1use std::sync::{
2    Arc,
3    Mutex,
4};
5
6use accesskit::{
7    Action,
8    AriaCurrent,
9    AutoComplete,
10    HasPopup,
11    Invalid,
12    ListStyle,
13    Live,
14    Node,
15    NodeId as AccessibilityId,
16    Orientation,
17    Role,
18    SortDirection,
19    Toggled,
20    VerticalOffset,
21};
22use freya_engine::prelude::Color;
23use freya_native_core::{
24    attributes::AttributeName,
25    exports::shipyard::Component,
26    node::OwnedAttributeValue,
27    node_ref::NodeView,
28    prelude::{
29        AttributeMaskBuilder,
30        Dependancy,
31        NodeMaskBuilder,
32        State,
33    },
34    tags::TagName,
35    NodeId,
36    SendAnyMap,
37};
38use freya_native_core_macro::partial_derive_state;
39
40use crate::{
41    accessibility::{
42        AccessibilityDirtyNodes,
43        AccessibilityFocusStrategy,
44        AccessibilityGenerator,
45    },
46    custom_attributes::CustomAttributeValues,
47    parsing::{
48        Parse,
49        ParseAttribute,
50        ParseError,
51    },
52    values::Focusable,
53};
54
55#[derive(Clone, Debug, PartialEq, Default, Component)]
56pub struct AccessibilityNodeState {
57    pub node_id: NodeId,
58    pub a11y_id: Option<AccessibilityId>,
59    pub a11y_auto_focus: bool,
60    pub a11y_focusable: Focusable,
61    pub builder: Option<Node>,
62}
63
64impl ParseAttribute for AccessibilityNodeState {
65    fn parse_attribute(
66        &mut self,
67        attr: freya_native_core::prelude::OwnedAttributeView<CustomAttributeValues>,
68    ) -> Result<(), ParseError> {
69        match attr.attribute {
70            AttributeName::A11yId => {
71                if let OwnedAttributeValue::Custom(CustomAttributeValues::AccessibilityId(id)) =
72                    attr.value
73                {
74                    self.a11y_id = Some(*id);
75                    // Enable focus on nodes that pass a custom a11y id
76                    if self.a11y_focusable.is_unknown() {
77                        self.a11y_focusable = Focusable::Enabled;
78                    }
79                }
80            }
81            AttributeName::A11yFocusable => {
82                if let OwnedAttributeValue::Text(attr) = attr.value {
83                    self.a11y_focusable = Focusable::parse(attr)?;
84                }
85            }
86            AttributeName::A11yAutoFocus => {
87                if let OwnedAttributeValue::Text(attr) = attr.value {
88                    self.a11y_auto_focus = attr.parse().unwrap_or_default()
89                }
90            }
91            AttributeName::A11yMemberOf => {
92                if let OwnedAttributeValue::Custom(CustomAttributeValues::AccessibilityId(id)) =
93                    attr.value
94                {
95                    if let Some(builder) = self.builder.as_mut() {
96                        builder.set_member_of(*id);
97                    }
98                }
99            }
100            a11y_attr => {
101                if let OwnedAttributeValue::Text(attr) = attr.value {
102                    if let Some(builder) = self.builder.as_mut() {
103                        match a11y_attr {
104                            AttributeName::A11yName => builder.set_class_name(attr.clone()),
105                            AttributeName::A11yDescription => builder.set_description(attr.clone()),
106                            AttributeName::A11yValue => builder.set_value(attr.clone()),
107                            AttributeName::A11yAccessKey => builder.set_access_key(attr.clone()),
108                            AttributeName::A11yAuthorId => builder.set_author_id(attr.clone()),
109                            AttributeName::A11yKeyboardShortcut => {
110                                builder.set_keyboard_shortcut(attr.clone())
111                            }
112                            AttributeName::A11yLanguage => builder.set_language(attr.clone()),
113                            AttributeName::A11yPlaceholder => builder.set_placeholder(attr.clone()),
114                            AttributeName::A11yRoleDescription => {
115                                builder.set_role_description(attr.clone())
116                            }
117                            AttributeName::A11yStateDescription => {
118                                builder.set_state_description(attr.clone())
119                            }
120                            AttributeName::A11yTooltip => builder.set_tooltip(attr.clone()),
121                            AttributeName::A11yUrl => builder.set_url(attr.clone()),
122                            AttributeName::A11yRowIndexText => {
123                                builder.set_row_index_text(attr.clone())
124                            }
125                            AttributeName::A11yColumnIndexText => {
126                                builder.set_column_index_text(attr.clone())
127                            }
128                            AttributeName::A11yScrollX => {
129                                builder.set_scroll_x(attr.parse().map_err(|_| ParseError)?)
130                            }
131                            AttributeName::A11yScrollXMin => {
132                                builder.set_scroll_x_min(attr.parse().map_err(|_| ParseError)?)
133                            }
134                            AttributeName::A11yScrollXMax => {
135                                builder.set_scroll_x_max(attr.parse().map_err(|_| ParseError)?)
136                            }
137                            AttributeName::A11yScrollY => {
138                                builder.set_scroll_y(attr.parse().map_err(|_| ParseError)?)
139                            }
140                            AttributeName::A11yScrollYMin => {
141                                builder.set_scroll_y_min(attr.parse().map_err(|_| ParseError)?)
142                            }
143                            AttributeName::A11yScrollYMax => {
144                                builder.set_scroll_y_max(attr.parse().map_err(|_| ParseError)?)
145                            }
146                            AttributeName::A11yNumericValue => {
147                                builder.set_numeric_value(attr.parse().map_err(|_| ParseError)?)
148                            }
149                            AttributeName::A11yMinNumericValue => {
150                                builder.set_min_numeric_value(attr.parse().map_err(|_| ParseError)?)
151                            }
152                            AttributeName::A11yMaxNumericValue => {
153                                builder.set_max_numeric_value(attr.parse().map_err(|_| ParseError)?)
154                            }
155                            AttributeName::A11yNumericValueStep => builder
156                                .set_numeric_value_step(attr.parse().map_err(|_| ParseError)?),
157                            AttributeName::A11yNumericValueJump => builder
158                                .set_numeric_value_jump(attr.parse().map_err(|_| ParseError)?),
159                            AttributeName::A11yRowCount => {
160                                builder.set_row_count(attr.parse().map_err(|_| ParseError)?)
161                            }
162                            AttributeName::A11yColumnCount => {
163                                builder.set_column_count(attr.parse().map_err(|_| ParseError)?)
164                            }
165                            AttributeName::A11yRowIndex => {
166                                builder.set_row_index(attr.parse().map_err(|_| ParseError)?)
167                            }
168                            AttributeName::A11yColumnIndex => {
169                                builder.set_column_index(attr.parse().map_err(|_| ParseError)?)
170                            }
171                            AttributeName::A11yRowSpan => {
172                                builder.set_row_span(attr.parse().map_err(|_| ParseError)?)
173                            }
174                            AttributeName::A11yColumnSpan => {
175                                builder.set_column_span(attr.parse().map_err(|_| ParseError)?)
176                            }
177                            AttributeName::A11yLevel => {
178                                builder.set_level(attr.parse().map_err(|_| ParseError)?)
179                            }
180                            AttributeName::A11ySizeOfSet => {
181                                builder.set_size_of_set(attr.parse().map_err(|_| ParseError)?)
182                            }
183                            AttributeName::A11yPositionInSet => {
184                                builder.set_position_in_set(attr.parse().map_err(|_| ParseError)?)
185                            }
186                            AttributeName::A11yColorValue => {
187                                let color = Color::parse(attr)?;
188                                builder.set_color_value(
189                                    ((color.a() as u32) << 24)
190                                        | ((color.b() as u32) << 16)
191                                        | (((color.g() as u32) << 8) + (color.r() as u32)),
192                                );
193                            }
194                            AttributeName::A11yExpanded => {
195                                builder.set_expanded(attr.parse::<bool>().map_err(|_| ParseError)?);
196                            }
197                            AttributeName::A11ySelected => {
198                                builder.set_selected(attr.parse::<bool>().map_err(|_| ParseError)?);
199                            }
200                            AttributeName::A11yHidden => {
201                                if attr.parse::<bool>().map_err(|_| ParseError)? {
202                                    builder.set_hidden();
203                                }
204                            }
205                            AttributeName::A11yMultiselectable => {
206                                if attr.parse::<bool>().map_err(|_| ParseError)? {
207                                    builder.set_multiselectable();
208                                }
209                            }
210                            AttributeName::A11yRequired => {
211                                if attr.parse::<bool>().map_err(|_| ParseError)? {
212                                    builder.set_required();
213                                }
214                            }
215                            AttributeName::A11yVisited => {
216                                if attr.parse::<bool>().map_err(|_| ParseError)? {
217                                    builder.set_visited();
218                                }
219                            }
220                            AttributeName::A11yBusy => {
221                                if attr.parse::<bool>().map_err(|_| ParseError)? {
222                                    builder.set_busy();
223                                }
224                            }
225                            AttributeName::A11yLiveAtomic => {
226                                if attr.parse::<bool>().map_err(|_| ParseError)? {
227                                    builder.set_live_atomic();
228                                }
229                            }
230                            AttributeName::A11yModal => {
231                                if attr.parse::<bool>().map_err(|_| ParseError)? {
232                                    builder.set_modal();
233                                }
234                            }
235                            AttributeName::A11yTouchTransparent => {
236                                if attr.parse::<bool>().map_err(|_| ParseError)? {
237                                    builder.set_touch_transparent();
238                                }
239                            }
240                            AttributeName::A11yReadOnly => {
241                                if attr.parse::<bool>().map_err(|_| ParseError)? {
242                                    builder.set_read_only();
243                                }
244                            }
245                            AttributeName::A11yDisabled => {
246                                if attr.parse::<bool>().map_err(|_| ParseError)? {
247                                    builder.set_disabled();
248                                }
249                            }
250                            AttributeName::A11yIsSpellingError => {
251                                if attr.parse::<bool>().map_err(|_| ParseError)? {
252                                    builder.set_is_spelling_error();
253                                }
254                            }
255                            AttributeName::A11yIsGrammarError => {
256                                if attr.parse::<bool>().map_err(|_| ParseError)? {
257                                    builder.set_is_grammar_error();
258                                }
259                            }
260                            AttributeName::A11yIsSearchMatch => {
261                                if attr.parse::<bool>().map_err(|_| ParseError)? {
262                                    builder.set_is_search_match();
263                                }
264                            }
265                            AttributeName::A11yIsSuggestion => {
266                                if attr.parse::<bool>().map_err(|_| ParseError)? {
267                                    builder.set_is_suggestion();
268                                }
269                            }
270                            AttributeName::A11yRole => {
271                                builder.set_role(Role::parse(attr)?);
272                            }
273                            AttributeName::A11yInvalid => {
274                                builder.set_invalid(Invalid::parse(attr)?);
275                            }
276                            AttributeName::A11yToggled => {
277                                builder.set_toggled(Toggled::parse(attr)?);
278                            }
279                            AttributeName::A11yLive => {
280                                builder.set_live(Live::parse(attr)?);
281                            }
282                            AttributeName::A11yDefaultActionVerb => {
283                                builder.add_action(Action::parse(attr)?);
284                            }
285                            AttributeName::A11yOrientation => {
286                                builder.set_orientation(Orientation::parse(attr)?);
287                            }
288                            AttributeName::A11ySortDirection => {
289                                builder.set_sort_direction(SortDirection::parse(attr)?);
290                            }
291                            AttributeName::A11yCurrent => {
292                                builder.set_aria_current(AriaCurrent::parse(attr)?);
293                            }
294                            AttributeName::A11yAutoComplete => {
295                                builder.set_auto_complete(AutoComplete::parse(attr)?);
296                            }
297                            AttributeName::A11yHasPopup => {
298                                builder.set_has_popup(HasPopup::parse(attr)?);
299                            }
300                            AttributeName::A11yListStyle => {
301                                builder.set_list_style(ListStyle::parse(attr)?);
302                            }
303                            AttributeName::A11yVerticalOffset => {
304                                builder.set_vertical_offset(VerticalOffset::parse(attr)?);
305                            }
306                            _ => {}
307                        }
308                    }
309                }
310            }
311        }
312
313        Ok(())
314    }
315}
316
317#[partial_derive_state]
318impl State<CustomAttributeValues> for AccessibilityNodeState {
319    type ParentDependencies = ();
320
321    type ChildDependencies = ();
322
323    type NodeDependencies = ();
324
325    const NODE_MASK: NodeMaskBuilder<'static> = NodeMaskBuilder::new()
326        .with_attrs(AttributeMaskBuilder::Some(&[
327            AttributeName::A11yId,
328            AttributeName::A11yFocusable,
329            AttributeName::A11yAutoFocus,
330            AttributeName::A11yName,
331            AttributeName::A11yDescription,
332            AttributeName::A11yValue,
333            AttributeName::A11yAccessKey,
334            AttributeName::A11yAuthorId,
335            AttributeName::A11yMemberOf,
336            AttributeName::A11yKeyboardShortcut,
337            AttributeName::A11yLanguage,
338            AttributeName::A11yPlaceholder,
339            AttributeName::A11yRoleDescription,
340            AttributeName::A11yStateDescription,
341            AttributeName::A11yTooltip,
342            AttributeName::A11yUrl,
343            AttributeName::A11yRowIndexText,
344            AttributeName::A11yColumnIndexText,
345            AttributeName::A11yScrollX,
346            AttributeName::A11yScrollXMin,
347            AttributeName::A11yScrollXMax,
348            AttributeName::A11yScrollY,
349            AttributeName::A11yScrollYMin,
350            AttributeName::A11yScrollYMax,
351            AttributeName::A11yNumericValue,
352            AttributeName::A11yMinNumericValue,
353            AttributeName::A11yMaxNumericValue,
354            AttributeName::A11yNumericValueStep,
355            AttributeName::A11yNumericValueJump,
356            AttributeName::A11yRowCount,
357            AttributeName::A11yColumnCount,
358            AttributeName::A11yRowIndex,
359            AttributeName::A11yColumnIndex,
360            AttributeName::A11yRowSpan,
361            AttributeName::A11yColumnSpan,
362            AttributeName::A11yLevel,
363            AttributeName::A11ySizeOfSet,
364            AttributeName::A11yPositionInSet,
365            AttributeName::A11yColorValue,
366            AttributeName::A11yExpanded,
367            AttributeName::A11ySelected,
368            AttributeName::A11yHidden,
369            AttributeName::A11yMultiselectable,
370            AttributeName::A11yRequired,
371            AttributeName::A11yVisited,
372            AttributeName::A11yBusy,
373            AttributeName::A11yLiveAtomic,
374            AttributeName::A11yModal,
375            AttributeName::A11yTouchTransparent,
376            AttributeName::A11yReadOnly,
377            AttributeName::A11yDisabled,
378            AttributeName::A11yIsSpellingError,
379            AttributeName::A11yIsGrammarError,
380            AttributeName::A11yIsSearchMatch,
381            AttributeName::A11yIsSuggestion,
382            AttributeName::A11yRole,
383            AttributeName::A11yInvalid,
384            AttributeName::A11yToggled,
385            AttributeName::A11yLive,
386            AttributeName::A11yDefaultActionVerb,
387            AttributeName::A11yOrientation,
388            AttributeName::A11ySortDirection,
389            AttributeName::A11yCurrent,
390            AttributeName::A11yAutoComplete,
391            AttributeName::A11yHasPopup,
392            AttributeName::A11yListStyle,
393            AttributeName::A11yVerticalOffset,
394        ]))
395        .with_tag();
396
397    fn update<'a>(
398        &mut self,
399        node_view: NodeView<CustomAttributeValues>,
400        _node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
401        _parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
402        _children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
403        context: &SendAnyMap,
404    ) -> bool {
405        let root_id = context.get::<NodeId>().unwrap();
406        let accessibility_dirty_nodes = context
407            .get::<Arc<Mutex<AccessibilityDirtyNodes>>>()
408            .unwrap();
409        let accessibility_generator = context.get::<Arc<AccessibilityGenerator>>().unwrap();
410        let mut accessibility = AccessibilityNodeState {
411            node_id: node_view.node_id(),
412            a11y_id: self.a11y_id,
413            builder: node_view.tag().and_then(|tag| {
414                match tag {
415                    TagName::Image => Some(Node::new(Role::Image)),
416                    TagName::Label => Some(Node::new(Role::Label)),
417                    TagName::Paragraph => Some(Node::new(Role::Paragraph)),
418                    TagName::Rect => Some(Node::new(Role::GenericContainer)),
419                    TagName::Svg => Some(Node::new(Role::GraphicsObject)),
420                    TagName::Root => Some(Node::new(Role::Window)),
421                    // TODO: make this InlineTextBox and supply computed text span properties
422                    TagName::Text => None,
423                }
424            }),
425            ..Default::default()
426        };
427
428        if let Some(attributes) = node_view.attributes() {
429            for attr in attributes {
430                accessibility.parse_safe(attr);
431            }
432        }
433
434        let changed = &accessibility != self;
435        let had_id = self.a11y_id.is_some();
436
437        *self = accessibility;
438
439        let is_orphan = node_view.height() == 0 && node_view.node_id() != *root_id;
440
441        if changed && !is_orphan {
442            // Assign an accessibility ID if none was passed but the node has a valid builder
443            //
444            // In our case, builder will be `None` if the node's tag cannot be added to accessibility
445            // tree.
446            if self.a11y_id.is_none() && self.builder.is_some() {
447                let id = AccessibilityId(accessibility_generator.new_id());
448                #[cfg(debug_assertions)]
449                tracing::info!("Assigned {id:?} to {:?}", node_view.node_id());
450
451                self.a11y_id = Some(id);
452            }
453
454            // Add or update this node if it is the Root or if it has an accessibility ID
455            if self.a11y_id.is_some() || node_view.node_id() == *root_id {
456                accessibility_dirty_nodes
457                    .lock()
458                    .unwrap()
459                    .add_or_update(node_view.node_id())
460            }
461
462            if let Some(a11y_id) = self.a11y_id {
463                if !had_id && self.a11y_auto_focus {
464                    #[cfg(debug_assertions)]
465                    tracing::info!("Requested auto focus for {:?}", a11y_id);
466
467                    accessibility_dirty_nodes
468                        .lock()
469                        .unwrap()
470                        .request_focus(AccessibilityFocusStrategy::Node(a11y_id))
471                }
472            }
473        }
474
475        changed
476    }
477}