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