1 | // Copyright 2021 The AccessKit Authors. All rights reserved. |
2 | // Licensed under the Apache License, Version 2.0 (found in |
3 | // the LICENSE-APACHE file) or the MIT license (found in |
4 | // the LICENSE-MIT file), at your option. |
5 | |
6 | // Derived from Chromium's accessibility abstraction. |
7 | // Copyright 2021 The Chromium Authors. All rights reserved. |
8 | // Use of this source code is governed by a BSD-style license that can be |
9 | // found in the LICENSE.chromium file. |
10 | |
11 | use accesskit::{ |
12 | Action, Affine, FrozenNode as NodeData, Live, NodeId, Orientation, Point, Rect, Role, |
13 | TextSelection, Toggled, |
14 | }; |
15 | use alloc::{ |
16 | string::{String, ToString}, |
17 | sync::Arc, |
18 | vec::Vec, |
19 | }; |
20 | use core::iter::FusedIterator; |
21 | |
22 | use crate::filters::FilterResult; |
23 | use crate::iterators::{ |
24 | FilteredChildren, FollowingFilteredSiblings, FollowingSiblings, LabelledBy, |
25 | PrecedingFilteredSiblings, PrecedingSiblings, |
26 | }; |
27 | use crate::tree::State as TreeState; |
28 | |
29 | #[derive (Clone, Copy, PartialEq, Eq)] |
30 | pub(crate) struct ParentAndIndex(pub(crate) NodeId, pub(crate) usize); |
31 | |
32 | #[derive (Clone)] |
33 | pub(crate) struct NodeState { |
34 | pub(crate) parent_and_index: Option<ParentAndIndex>, |
35 | pub(crate) data: Arc<NodeData>, |
36 | } |
37 | |
38 | #[derive (Copy, Clone)] |
39 | pub struct Node<'a> { |
40 | pub tree_state: &'a TreeState, |
41 | pub(crate) id: NodeId, |
42 | pub(crate) state: &'a NodeState, |
43 | } |
44 | |
45 | impl<'a> Node<'a> { |
46 | pub(crate) fn data(&self) -> &NodeData { |
47 | &self.state.data |
48 | } |
49 | |
50 | pub fn is_focused(&self) -> bool { |
51 | self.tree_state.focus_id() == Some(self.id()) |
52 | } |
53 | |
54 | pub fn is_focused_in_tree(&self) -> bool { |
55 | self.tree_state.focus == self.id() |
56 | } |
57 | |
58 | pub fn is_focusable(&self) -> bool { |
59 | self.supports_action(Action::Focus) || self.is_focused_in_tree() |
60 | } |
61 | |
62 | pub fn is_root(&self) -> bool { |
63 | // Don't check for absence of a parent node, in case a non-root node |
64 | // somehow gets detached from the tree. |
65 | self.id() == self.tree_state.root_id() |
66 | } |
67 | |
68 | pub fn parent_id(&self) -> Option<NodeId> { |
69 | self.state |
70 | .parent_and_index |
71 | .as_ref() |
72 | .map(|ParentAndIndex(id, _)| *id) |
73 | } |
74 | |
75 | pub fn parent(&self) -> Option<Node<'a>> { |
76 | self.parent_id() |
77 | .map(|id| self.tree_state.node_by_id(id).unwrap()) |
78 | } |
79 | |
80 | pub fn filtered_parent(&self, filter: &impl Fn(&Node) -> FilterResult) -> Option<Node<'a>> { |
81 | self.parent().and_then(move |parent| { |
82 | if filter(&parent) == FilterResult::Include { |
83 | Some(parent) |
84 | } else { |
85 | parent.filtered_parent(filter) |
86 | } |
87 | }) |
88 | } |
89 | |
90 | pub fn parent_and_index(self) -> Option<(Node<'a>, usize)> { |
91 | self.state |
92 | .parent_and_index |
93 | .as_ref() |
94 | .map(|ParentAndIndex(parent, index)| { |
95 | (self.tree_state.node_by_id(*parent).unwrap(), *index) |
96 | }) |
97 | } |
98 | |
99 | pub fn child_ids( |
100 | &self, |
101 | ) -> impl DoubleEndedIterator<Item = NodeId> |
102 | + ExactSizeIterator<Item = NodeId> |
103 | + FusedIterator<Item = NodeId> |
104 | + '_ { |
105 | let data = &self.state.data; |
106 | data.children().iter().copied() |
107 | } |
108 | |
109 | pub fn children( |
110 | &self, |
111 | ) -> impl DoubleEndedIterator<Item = Node<'a>> |
112 | + ExactSizeIterator<Item = Node<'a>> |
113 | + FusedIterator<Item = Node<'a>> |
114 | + 'a { |
115 | let state = self.tree_state; |
116 | let data = &self.state.data; |
117 | data.children() |
118 | .iter() |
119 | .map(move |id| state.node_by_id(*id).unwrap()) |
120 | } |
121 | |
122 | pub fn filtered_children( |
123 | &self, |
124 | filter: impl Fn(&Node) -> FilterResult + 'a, |
125 | ) -> impl DoubleEndedIterator<Item = Node<'a>> + FusedIterator<Item = Node<'a>> + 'a { |
126 | FilteredChildren::new(*self, filter) |
127 | } |
128 | |
129 | pub fn following_sibling_ids( |
130 | &self, |
131 | ) -> impl DoubleEndedIterator<Item = NodeId> |
132 | + ExactSizeIterator<Item = NodeId> |
133 | + FusedIterator<Item = NodeId> |
134 | + 'a { |
135 | FollowingSiblings::new(*self) |
136 | } |
137 | |
138 | pub fn following_siblings( |
139 | &self, |
140 | ) -> impl DoubleEndedIterator<Item = Node<'a>> |
141 | + ExactSizeIterator<Item = Node<'a>> |
142 | + FusedIterator<Item = Node<'a>> |
143 | + 'a { |
144 | let state = self.tree_state; |
145 | self.following_sibling_ids() |
146 | .map(move |id| state.node_by_id(id).unwrap()) |
147 | } |
148 | |
149 | pub fn following_filtered_siblings( |
150 | &self, |
151 | filter: impl Fn(&Node) -> FilterResult + 'a, |
152 | ) -> impl DoubleEndedIterator<Item = Node<'a>> + FusedIterator<Item = Node<'a>> + 'a { |
153 | FollowingFilteredSiblings::new(*self, filter) |
154 | } |
155 | |
156 | pub fn preceding_sibling_ids( |
157 | &self, |
158 | ) -> impl DoubleEndedIterator<Item = NodeId> |
159 | + ExactSizeIterator<Item = NodeId> |
160 | + FusedIterator<Item = NodeId> |
161 | + 'a { |
162 | PrecedingSiblings::new(*self) |
163 | } |
164 | |
165 | pub fn preceding_siblings( |
166 | &self, |
167 | ) -> impl DoubleEndedIterator<Item = Node<'a>> |
168 | + ExactSizeIterator<Item = Node<'a>> |
169 | + FusedIterator<Item = Node<'a>> |
170 | + 'a { |
171 | let state = self.tree_state; |
172 | self.preceding_sibling_ids() |
173 | .map(move |id| state.node_by_id(id).unwrap()) |
174 | } |
175 | |
176 | pub fn preceding_filtered_siblings( |
177 | &self, |
178 | filter: impl Fn(&Node) -> FilterResult + 'a, |
179 | ) -> impl DoubleEndedIterator<Item = Node<'a>> + FusedIterator<Item = Node<'a>> + 'a { |
180 | PrecedingFilteredSiblings::new(*self, filter) |
181 | } |
182 | |
183 | pub fn deepest_first_child(self) -> Option<Node<'a>> { |
184 | let mut deepest_child = self.children().next()?; |
185 | while let Some(first_child) = deepest_child.children().next() { |
186 | deepest_child = first_child; |
187 | } |
188 | Some(deepest_child) |
189 | } |
190 | |
191 | pub fn deepest_first_filtered_child( |
192 | &self, |
193 | filter: &impl Fn(&Node) -> FilterResult, |
194 | ) -> Option<Node<'a>> { |
195 | let mut deepest_child = self.first_filtered_child(filter)?; |
196 | while let Some(first_child) = deepest_child.first_filtered_child(filter) { |
197 | deepest_child = first_child; |
198 | } |
199 | Some(deepest_child) |
200 | } |
201 | |
202 | pub fn deepest_last_child(self) -> Option<Node<'a>> { |
203 | let mut deepest_child = self.children().next_back()?; |
204 | while let Some(last_child) = deepest_child.children().next_back() { |
205 | deepest_child = last_child; |
206 | } |
207 | Some(deepest_child) |
208 | } |
209 | |
210 | pub fn deepest_last_filtered_child( |
211 | &self, |
212 | filter: &impl Fn(&Node) -> FilterResult, |
213 | ) -> Option<Node<'a>> { |
214 | let mut deepest_child = self.last_filtered_child(filter)?; |
215 | while let Some(last_child) = deepest_child.last_filtered_child(filter) { |
216 | deepest_child = last_child; |
217 | } |
218 | Some(deepest_child) |
219 | } |
220 | |
221 | pub fn is_descendant_of(&self, ancestor: &Node) -> bool { |
222 | if self.id() == ancestor.id() { |
223 | return true; |
224 | } |
225 | if let Some(parent) = self.parent() { |
226 | return parent.is_descendant_of(ancestor); |
227 | } |
228 | false |
229 | } |
230 | |
231 | /// Returns the transform defined directly on this node, or the identity |
232 | /// transform, without taking into account transforms on ancestors. |
233 | pub fn direct_transform(&self) -> Affine { |
234 | self.data() |
235 | .transform() |
236 | .map_or(Affine::IDENTITY, |value| *value) |
237 | } |
238 | |
239 | /// Returns the combined affine transform of this node and its ancestors, |
240 | /// up to and including the root of this node's tree. |
241 | pub fn transform(&self) -> Affine { |
242 | self.parent() |
243 | .map_or(Affine::IDENTITY, |parent| parent.transform()) |
244 | * self.direct_transform() |
245 | } |
246 | |
247 | pub(crate) fn relative_transform(&self, stop_at: &Node) -> Affine { |
248 | let parent_transform = if let Some(parent) = self.parent() { |
249 | if parent.id() == stop_at.id() { |
250 | Affine::IDENTITY |
251 | } else { |
252 | parent.relative_transform(stop_at) |
253 | } |
254 | } else { |
255 | Affine::IDENTITY |
256 | }; |
257 | parent_transform * self.direct_transform() |
258 | } |
259 | |
260 | pub fn raw_bounds(&self) -> Option<Rect> { |
261 | self.data().bounds() |
262 | } |
263 | |
264 | pub fn has_bounds(&self) -> bool { |
265 | self.raw_bounds().is_some() |
266 | } |
267 | |
268 | /// Returns the node's transformed bounding box relative to the tree's |
269 | /// container (e.g. window). |
270 | pub fn bounding_box(&self) -> Option<Rect> { |
271 | self.raw_bounds() |
272 | .as_ref() |
273 | .map(|rect| self.transform().transform_rect_bbox(*rect)) |
274 | } |
275 | |
276 | pub(crate) fn bounding_box_in_coordinate_space(&self, other: &Node) -> Option<Rect> { |
277 | self.raw_bounds() |
278 | .as_ref() |
279 | .map(|rect| self.relative_transform(other).transform_rect_bbox(*rect)) |
280 | } |
281 | |
282 | pub(crate) fn hit_test( |
283 | &self, |
284 | point: Point, |
285 | filter: &impl Fn(&Node) -> FilterResult, |
286 | ) -> Option<(Node<'a>, Point)> { |
287 | let filter_result = filter(self); |
288 | |
289 | if filter_result == FilterResult::ExcludeSubtree { |
290 | return None; |
291 | } |
292 | |
293 | for child in self.children().rev() { |
294 | let point = child.direct_transform().inverse() * point; |
295 | if let Some(result) = child.hit_test(point, filter) { |
296 | return Some(result); |
297 | } |
298 | } |
299 | |
300 | if filter_result == FilterResult::Include { |
301 | if let Some(rect) = &self.raw_bounds() { |
302 | if rect.contains(point) { |
303 | return Some((*self, point)); |
304 | } |
305 | } |
306 | } |
307 | |
308 | None |
309 | } |
310 | |
311 | /// Returns the deepest filtered node, either this node or a descendant, |
312 | /// at the given point in this node's coordinate space. |
313 | pub fn node_at_point( |
314 | &self, |
315 | point: Point, |
316 | filter: &impl Fn(&Node) -> FilterResult, |
317 | ) -> Option<Node<'a>> { |
318 | self.hit_test(point, filter).map(|(node, _)| node) |
319 | } |
320 | |
321 | pub fn id(&self) -> NodeId { |
322 | self.id |
323 | } |
324 | |
325 | pub fn role(&self) -> Role { |
326 | self.data().role() |
327 | } |
328 | |
329 | pub fn role_description(&self) -> Option<String> { |
330 | self.data().role_description().map(String::from) |
331 | } |
332 | |
333 | pub fn has_role_description(&self) -> bool { |
334 | self.data().role_description().is_some() |
335 | } |
336 | |
337 | pub fn is_hidden(&self) -> bool { |
338 | self.data().is_hidden() |
339 | } |
340 | |
341 | pub fn is_disabled(&self) -> bool { |
342 | self.data().is_disabled() |
343 | } |
344 | |
345 | pub fn is_read_only(&self) -> bool { |
346 | let data = self.data(); |
347 | if data.is_read_only() { |
348 | true |
349 | } else { |
350 | self.should_have_read_only_state_by_default() || !self.is_read_only_supported() |
351 | } |
352 | } |
353 | |
354 | pub fn is_read_only_or_disabled(&self) -> bool { |
355 | self.is_read_only() || self.is_disabled() |
356 | } |
357 | |
358 | pub fn toggled(&self) -> Option<Toggled> { |
359 | self.data().toggled() |
360 | } |
361 | |
362 | pub fn numeric_value(&self) -> Option<f64> { |
363 | self.data().numeric_value() |
364 | } |
365 | |
366 | pub fn min_numeric_value(&self) -> Option<f64> { |
367 | self.data().min_numeric_value() |
368 | } |
369 | |
370 | pub fn max_numeric_value(&self) -> Option<f64> { |
371 | self.data().max_numeric_value() |
372 | } |
373 | |
374 | pub fn numeric_value_step(&self) -> Option<f64> { |
375 | self.data().numeric_value_step() |
376 | } |
377 | |
378 | pub fn numeric_value_jump(&self) -> Option<f64> { |
379 | self.data().numeric_value_jump() |
380 | } |
381 | |
382 | pub fn is_text_input(&self) -> bool { |
383 | matches!( |
384 | self.role(), |
385 | Role::TextInput |
386 | | Role::MultilineTextInput |
387 | | Role::SearchInput |
388 | | Role::DateInput |
389 | | Role::DateTimeInput |
390 | | Role::WeekInput |
391 | | Role::MonthInput |
392 | | Role::TimeInput |
393 | | Role::EmailInput |
394 | | Role::NumberInput |
395 | | Role::PasswordInput |
396 | | Role::PhoneNumberInput |
397 | | Role::UrlInput |
398 | | Role::EditableComboBox |
399 | | Role::SpinButton |
400 | ) |
401 | } |
402 | |
403 | pub fn is_multiline(&self) -> bool { |
404 | self.role() == Role::MultilineTextInput |
405 | } |
406 | |
407 | pub fn orientation(&self) -> Option<Orientation> { |
408 | self.data().orientation() |
409 | } |
410 | |
411 | // When probing for supported actions as the next several functions do, |
412 | // it's tempting to check the role. But it's better to not assume anything |
413 | // beyond what the provider has explicitly told us. Rationale: |
414 | // if the provider developer forgot to call `add_action` for an action, |
415 | // an AT (or even AccessKit itself) can fall back to simulating |
416 | // a mouse click. But if the provider doesn't handle an action request |
417 | // and we assume that it will based on the role, the attempted action |
418 | // does nothing. This stance is a departure from Chromium. |
419 | |
420 | pub fn is_clickable(&self) -> bool { |
421 | self.supports_action(Action::Click) |
422 | } |
423 | |
424 | pub fn supports_toggle(&self) -> bool { |
425 | self.toggled().is_some() |
426 | } |
427 | |
428 | pub fn supports_expand_collapse(&self) -> bool { |
429 | self.data().is_expanded().is_some() |
430 | } |
431 | |
432 | pub fn is_invocable(&self) -> bool { |
433 | // A control is "invocable" if it initiates an action when activated but |
434 | // does not maintain any state. A control that maintains state |
435 | // when activated would be considered a toggle or expand-collapse |
436 | // control - these controls are "clickable" but not "invocable". |
437 | // Similarly, if the action only gives the control keyboard focus, |
438 | // such as when clicking a text input, the control is not considered |
439 | // "invocable", as the "invoke" action would be a redundant synonym |
440 | // for the "set focus" action. The same logic applies to selection. |
441 | self.is_clickable() |
442 | && !self.is_text_input() |
443 | && !matches!(self.role(), Role::Document | Role::Terminal) |
444 | && !self.supports_toggle() |
445 | && !self.supports_expand_collapse() |
446 | && self.is_selected().is_none() |
447 | } |
448 | |
449 | // The future of the `Action` enum is undecided, so keep the following |
450 | // function private for now. |
451 | fn supports_action(&self, action: Action) -> bool { |
452 | self.data().supports_action(action) |
453 | } |
454 | |
455 | pub fn supports_increment(&self) -> bool { |
456 | self.supports_action(Action::Increment) |
457 | } |
458 | |
459 | pub fn supports_decrement(&self) -> bool { |
460 | self.supports_action(Action::Decrement) |
461 | } |
462 | } |
463 | |
464 | fn descendant_label_filter(node: &Node) -> FilterResult { |
465 | match node.role() { |
466 | Role::Label | Role::Image => FilterResult::Include, |
467 | Role::GenericContainer => FilterResult::ExcludeNode, |
468 | _ => FilterResult::ExcludeSubtree, |
469 | } |
470 | } |
471 | |
472 | impl<'a> Node<'a> { |
473 | pub fn labelled_by( |
474 | &self, |
475 | ) -> impl DoubleEndedIterator<Item = Node<'a>> + FusedIterator<Item = Node<'a>> + 'a { |
476 | let explicit = &self.state.data.labelled_by(); |
477 | if explicit.is_empty() |
478 | && matches!( |
479 | self.role(), |
480 | Role::Button |
481 | | Role::CheckBox |
482 | | Role::DefaultButton |
483 | | Role::Link |
484 | | Role::MenuItem |
485 | | Role::MenuItemCheckBox |
486 | | Role::MenuItemRadio |
487 | | Role::RadioButton |
488 | ) |
489 | { |
490 | LabelledBy::FromDescendants(FilteredChildren::new(*self, &descendant_label_filter)) |
491 | } else { |
492 | LabelledBy::Explicit { |
493 | ids: explicit.iter(), |
494 | tree_state: self.tree_state, |
495 | } |
496 | } |
497 | } |
498 | |
499 | pub fn label_comes_from_value(&self) -> bool { |
500 | self.role() == Role::Label |
501 | } |
502 | |
503 | pub fn label(&self) -> Option<String> { |
504 | if let Some(label) = &self.data().label() { |
505 | Some(label.to_string()) |
506 | } else { |
507 | let labels = self |
508 | .labelled_by() |
509 | .filter_map(|node| { |
510 | if node.label_comes_from_value() { |
511 | node.value() |
512 | } else { |
513 | node.label() |
514 | } |
515 | }) |
516 | .collect::<Vec<String>>(); |
517 | (!labels.is_empty()).then(move || labels.join(" " )) |
518 | } |
519 | } |
520 | |
521 | pub fn description(&self) -> Option<String> { |
522 | self.data() |
523 | .description() |
524 | .map(|description| description.to_string()) |
525 | } |
526 | |
527 | pub fn placeholder(&self) -> Option<String> { |
528 | self.data() |
529 | .placeholder() |
530 | .map(|placeholder| placeholder.to_string()) |
531 | } |
532 | |
533 | pub fn value(&self) -> Option<String> { |
534 | if let Some(value) = &self.data().value() { |
535 | Some(value.to_string()) |
536 | } else if self.supports_text_ranges() && !self.is_multiline() { |
537 | Some(self.document_range().text()) |
538 | } else { |
539 | None |
540 | } |
541 | } |
542 | |
543 | pub fn has_value(&self) -> bool { |
544 | self.data().value().is_some() || (self.supports_text_ranges() && !self.is_multiline()) |
545 | } |
546 | |
547 | pub fn is_read_only_supported(&self) -> bool { |
548 | self.is_text_input() |
549 | || matches!( |
550 | self.role(), |
551 | Role::CheckBox |
552 | | Role::ColorWell |
553 | | Role::ComboBox |
554 | | Role::Grid |
555 | | Role::ListBox |
556 | | Role::MenuItemCheckBox |
557 | | Role::MenuItemRadio |
558 | | Role::MenuListPopup |
559 | | Role::RadioButton |
560 | | Role::RadioGroup |
561 | | Role::Slider |
562 | | Role::Switch |
563 | | Role::TreeGrid |
564 | ) |
565 | } |
566 | |
567 | pub fn should_have_read_only_state_by_default(&self) -> bool { |
568 | matches!( |
569 | self.role(), |
570 | Role::Article |
571 | | Role::Definition |
572 | | Role::DescriptionList |
573 | | Role::DescriptionListTerm |
574 | | Role::Directory |
575 | | Role::Document |
576 | | Role::GraphicsDocument |
577 | | Role::Image |
578 | | Role::List |
579 | | Role::ListItem |
580 | | Role::PdfRoot |
581 | | Role::ProgressIndicator |
582 | | Role::RootWebArea |
583 | | Role::Term |
584 | | Role::Timer |
585 | | Role::Toolbar |
586 | | Role::Tooltip |
587 | ) |
588 | } |
589 | |
590 | pub fn live(&self) -> Live { |
591 | self.data() |
592 | .live() |
593 | .unwrap_or_else(|| self.parent().map_or(Live::Off, |parent| parent.live())) |
594 | } |
595 | |
596 | pub fn is_selected(&self) -> Option<bool> { |
597 | self.data().is_selected() |
598 | } |
599 | |
600 | pub fn raw_text_selection(&self) -> Option<&TextSelection> { |
601 | self.data().text_selection() |
602 | } |
603 | |
604 | pub fn raw_value(&self) -> Option<&str> { |
605 | self.data().value() |
606 | } |
607 | |
608 | pub fn author_id(&self) -> Option<&str> { |
609 | self.data().author_id() |
610 | } |
611 | |
612 | pub fn class_name(&self) -> Option<&str> { |
613 | self.data().class_name() |
614 | } |
615 | |
616 | pub fn index_path(&self) -> Vec<usize> { |
617 | self.relative_index_path(self.tree_state.root_id()) |
618 | } |
619 | |
620 | pub fn relative_index_path(&self, ancestor_id: NodeId) -> Vec<usize> { |
621 | let mut result = Vec::new(); |
622 | let mut current = *self; |
623 | while current.id() != ancestor_id { |
624 | let (parent, index) = current.parent_and_index().unwrap(); |
625 | result.push(index); |
626 | current = parent; |
627 | } |
628 | result.reverse(); |
629 | result |
630 | } |
631 | |
632 | pub(crate) fn first_filtered_child( |
633 | &self, |
634 | filter: &impl Fn(&Node) -> FilterResult, |
635 | ) -> Option<Node<'a>> { |
636 | for child in self.children() { |
637 | let result = filter(&child); |
638 | if result == FilterResult::Include { |
639 | return Some(child); |
640 | } |
641 | if result == FilterResult::ExcludeNode { |
642 | if let Some(descendant) = child.first_filtered_child(filter) { |
643 | return Some(descendant); |
644 | } |
645 | } |
646 | } |
647 | None |
648 | } |
649 | |
650 | pub(crate) fn last_filtered_child( |
651 | &self, |
652 | filter: &impl Fn(&Node) -> FilterResult, |
653 | ) -> Option<Node<'a>> { |
654 | for child in self.children().rev() { |
655 | let result = filter(&child); |
656 | if result == FilterResult::Include { |
657 | return Some(child); |
658 | } |
659 | if result == FilterResult::ExcludeNode { |
660 | if let Some(descendant) = child.last_filtered_child(filter) { |
661 | return Some(descendant); |
662 | } |
663 | } |
664 | } |
665 | None |
666 | } |
667 | } |
668 | |
669 | #[cfg (test)] |
670 | mod tests { |
671 | use accesskit::{Node, NodeId, Point, Rect, Role, Tree, TreeUpdate}; |
672 | use alloc::vec; |
673 | |
674 | use crate::tests::*; |
675 | |
676 | #[test ] |
677 | fn parent_and_index() { |
678 | let tree = test_tree(); |
679 | assert!(tree.state().root().parent_and_index().is_none()); |
680 | assert_eq!( |
681 | Some((ROOT_ID, 0)), |
682 | tree.state() |
683 | .node_by_id(PARAGRAPH_0_ID) |
684 | .unwrap() |
685 | .parent_and_index() |
686 | .map(|(parent, index)| (parent.id(), index)) |
687 | ); |
688 | assert_eq!( |
689 | Some((PARAGRAPH_0_ID, 0)), |
690 | tree.state() |
691 | .node_by_id(LABEL_0_0_IGNORED_ID) |
692 | .unwrap() |
693 | .parent_and_index() |
694 | .map(|(parent, index)| (parent.id(), index)) |
695 | ); |
696 | assert_eq!( |
697 | Some((ROOT_ID, 1)), |
698 | tree.state() |
699 | .node_by_id(PARAGRAPH_1_IGNORED_ID) |
700 | .unwrap() |
701 | .parent_and_index() |
702 | .map(|(parent, index)| (parent.id(), index)) |
703 | ); |
704 | } |
705 | |
706 | #[test ] |
707 | fn deepest_first_child() { |
708 | let tree = test_tree(); |
709 | assert_eq!( |
710 | LABEL_0_0_IGNORED_ID, |
711 | tree.state().root().deepest_first_child().unwrap().id() |
712 | ); |
713 | assert_eq!( |
714 | LABEL_0_0_IGNORED_ID, |
715 | tree.state() |
716 | .node_by_id(PARAGRAPH_0_ID) |
717 | .unwrap() |
718 | .deepest_first_child() |
719 | .unwrap() |
720 | .id() |
721 | ); |
722 | assert!(tree |
723 | .state() |
724 | .node_by_id(LABEL_0_0_IGNORED_ID) |
725 | .unwrap() |
726 | .deepest_first_child() |
727 | .is_none()); |
728 | } |
729 | |
730 | #[test ] |
731 | fn filtered_parent() { |
732 | let tree = test_tree(); |
733 | assert_eq!( |
734 | ROOT_ID, |
735 | tree.state() |
736 | .node_by_id(LABEL_1_1_ID) |
737 | .unwrap() |
738 | .filtered_parent(&test_tree_filter) |
739 | .unwrap() |
740 | .id() |
741 | ); |
742 | assert!(tree |
743 | .state() |
744 | .root() |
745 | .filtered_parent(&test_tree_filter) |
746 | .is_none()); |
747 | } |
748 | |
749 | #[test ] |
750 | fn deepest_first_filtered_child() { |
751 | let tree = test_tree(); |
752 | assert_eq!( |
753 | PARAGRAPH_0_ID, |
754 | tree.state() |
755 | .root() |
756 | .deepest_first_filtered_child(&test_tree_filter) |
757 | .unwrap() |
758 | .id() |
759 | ); |
760 | assert!(tree |
761 | .state() |
762 | .node_by_id(PARAGRAPH_0_ID) |
763 | .unwrap() |
764 | .deepest_first_filtered_child(&test_tree_filter) |
765 | .is_none()); |
766 | assert!(tree |
767 | .state() |
768 | .node_by_id(LABEL_0_0_IGNORED_ID) |
769 | .unwrap() |
770 | .deepest_first_filtered_child(&test_tree_filter) |
771 | .is_none()); |
772 | } |
773 | |
774 | #[test ] |
775 | fn deepest_last_child() { |
776 | let tree = test_tree(); |
777 | assert_eq!( |
778 | EMPTY_CONTAINER_3_3_IGNORED_ID, |
779 | tree.state().root().deepest_last_child().unwrap().id() |
780 | ); |
781 | assert_eq!( |
782 | EMPTY_CONTAINER_3_3_IGNORED_ID, |
783 | tree.state() |
784 | .node_by_id(PARAGRAPH_3_IGNORED_ID) |
785 | .unwrap() |
786 | .deepest_last_child() |
787 | .unwrap() |
788 | .id() |
789 | ); |
790 | assert!(tree |
791 | .state() |
792 | .node_by_id(BUTTON_3_2_ID) |
793 | .unwrap() |
794 | .deepest_last_child() |
795 | .is_none()); |
796 | } |
797 | |
798 | #[test ] |
799 | fn deepest_last_filtered_child() { |
800 | let tree = test_tree(); |
801 | assert_eq!( |
802 | BUTTON_3_2_ID, |
803 | tree.state() |
804 | .root() |
805 | .deepest_last_filtered_child(&test_tree_filter) |
806 | .unwrap() |
807 | .id() |
808 | ); |
809 | assert_eq!( |
810 | BUTTON_3_2_ID, |
811 | tree.state() |
812 | .node_by_id(PARAGRAPH_3_IGNORED_ID) |
813 | .unwrap() |
814 | .deepest_last_filtered_child(&test_tree_filter) |
815 | .unwrap() |
816 | .id() |
817 | ); |
818 | assert!(tree |
819 | .state() |
820 | .node_by_id(BUTTON_3_2_ID) |
821 | .unwrap() |
822 | .deepest_last_filtered_child(&test_tree_filter) |
823 | .is_none()); |
824 | assert!(tree |
825 | .state() |
826 | .node_by_id(PARAGRAPH_0_ID) |
827 | .unwrap() |
828 | .deepest_last_filtered_child(&test_tree_filter) |
829 | .is_none()); |
830 | } |
831 | |
832 | #[test ] |
833 | fn is_descendant_of() { |
834 | let tree = test_tree(); |
835 | assert!(tree |
836 | .state() |
837 | .node_by_id(PARAGRAPH_0_ID) |
838 | .unwrap() |
839 | .is_descendant_of(&tree.state().root())); |
840 | assert!(tree |
841 | .state() |
842 | .node_by_id(LABEL_0_0_IGNORED_ID) |
843 | .unwrap() |
844 | .is_descendant_of(&tree.state().root())); |
845 | assert!(tree |
846 | .state() |
847 | .node_by_id(LABEL_0_0_IGNORED_ID) |
848 | .unwrap() |
849 | .is_descendant_of(&tree.state().node_by_id(PARAGRAPH_0_ID).unwrap())); |
850 | assert!(!tree |
851 | .state() |
852 | .node_by_id(LABEL_0_0_IGNORED_ID) |
853 | .unwrap() |
854 | .is_descendant_of(&tree.state().node_by_id(PARAGRAPH_2_ID).unwrap())); |
855 | assert!(!tree |
856 | .state() |
857 | .node_by_id(PARAGRAPH_0_ID) |
858 | .unwrap() |
859 | .is_descendant_of(&tree.state().node_by_id(PARAGRAPH_2_ID).unwrap())); |
860 | } |
861 | |
862 | #[test ] |
863 | fn is_root() { |
864 | let tree = test_tree(); |
865 | assert!(tree.state().node_by_id(ROOT_ID).unwrap().is_root()); |
866 | assert!(!tree.state().node_by_id(PARAGRAPH_0_ID).unwrap().is_root()); |
867 | } |
868 | |
869 | #[test ] |
870 | fn bounding_box() { |
871 | let tree = test_tree(); |
872 | assert!(tree |
873 | .state() |
874 | .node_by_id(ROOT_ID) |
875 | .unwrap() |
876 | .bounding_box() |
877 | .is_none()); |
878 | assert_eq!( |
879 | Some(Rect { |
880 | x0: 10.0, |
881 | y0: 40.0, |
882 | x1: 810.0, |
883 | y1: 80.0, |
884 | }), |
885 | tree.state() |
886 | .node_by_id(PARAGRAPH_1_IGNORED_ID) |
887 | .unwrap() |
888 | .bounding_box() |
889 | ); |
890 | assert_eq!( |
891 | Some(Rect { |
892 | x0: 20.0, |
893 | y0: 50.0, |
894 | x1: 100.0, |
895 | y1: 70.0, |
896 | }), |
897 | tree.state() |
898 | .node_by_id(LABEL_1_1_ID) |
899 | .unwrap() |
900 | .bounding_box() |
901 | ); |
902 | } |
903 | |
904 | #[test ] |
905 | fn node_at_point() { |
906 | let tree = test_tree(); |
907 | assert!(tree |
908 | .state() |
909 | .root() |
910 | .node_at_point(Point::new(10.0, 40.0), &test_tree_filter) |
911 | .is_none()); |
912 | assert_eq!( |
913 | Some(LABEL_1_1_ID), |
914 | tree.state() |
915 | .root() |
916 | .node_at_point(Point::new(20.0, 50.0), &test_tree_filter) |
917 | .map(|node| node.id()) |
918 | ); |
919 | assert_eq!( |
920 | Some(LABEL_1_1_ID), |
921 | tree.state() |
922 | .root() |
923 | .node_at_point(Point::new(50.0, 60.0), &test_tree_filter) |
924 | .map(|node| node.id()) |
925 | ); |
926 | assert!(tree |
927 | .state() |
928 | .root() |
929 | .node_at_point(Point::new(100.0, 70.0), &test_tree_filter) |
930 | .is_none()); |
931 | } |
932 | |
933 | #[test ] |
934 | fn no_label_or_labelled_by() { |
935 | let update = TreeUpdate { |
936 | nodes: vec![ |
937 | (NodeId(0), { |
938 | let mut node = Node::new(Role::Window); |
939 | node.set_children(vec![NodeId(1)]); |
940 | node |
941 | }), |
942 | (NodeId(1), Node::new(Role::Button)), |
943 | ], |
944 | tree: Some(Tree::new(NodeId(0))), |
945 | focus: NodeId(0), |
946 | }; |
947 | let tree = crate::Tree::new(update, false); |
948 | assert_eq!(None, tree.state().node_by_id(NodeId(1)).unwrap().label()); |
949 | } |
950 | |
951 | #[test ] |
952 | fn label_from_labelled_by() { |
953 | // The following mock UI probably isn't very localization-friendly, |
954 | // but it's good for this test. |
955 | const LABEL_1: &str = "Check email every" ; |
956 | const LABEL_2: &str = "minutes" ; |
957 | |
958 | let update = TreeUpdate { |
959 | nodes: vec![ |
960 | (NodeId(0), { |
961 | let mut node = Node::new(Role::Window); |
962 | node.set_children(vec![NodeId(1), NodeId(2), NodeId(3), NodeId(4)]); |
963 | node |
964 | }), |
965 | (NodeId(1), { |
966 | let mut node = Node::new(Role::CheckBox); |
967 | node.set_labelled_by(vec![NodeId(2), NodeId(4)]); |
968 | node |
969 | }), |
970 | (NodeId(2), { |
971 | let mut node = Node::new(Role::Label); |
972 | node.set_value(LABEL_1); |
973 | node |
974 | }), |
975 | (NodeId(3), { |
976 | let mut node = Node::new(Role::TextInput); |
977 | node.push_labelled_by(NodeId(4)); |
978 | node |
979 | }), |
980 | (NodeId(4), { |
981 | let mut node = Node::new(Role::Label); |
982 | node.set_value(LABEL_2); |
983 | node |
984 | }), |
985 | ], |
986 | tree: Some(Tree::new(NodeId(0))), |
987 | focus: NodeId(0), |
988 | }; |
989 | let tree = crate::Tree::new(update, false); |
990 | assert_eq!( |
991 | Some([LABEL_1, LABEL_2].join(" " )), |
992 | tree.state().node_by_id(NodeId(1)).unwrap().label() |
993 | ); |
994 | assert_eq!( |
995 | Some(LABEL_2.into()), |
996 | tree.state().node_by_id(NodeId(3)).unwrap().label() |
997 | ); |
998 | } |
999 | |
1000 | #[test ] |
1001 | fn label_from_descendant_label() { |
1002 | const ROOT_ID: NodeId = NodeId(0); |
1003 | const DEFAULT_BUTTON_ID: NodeId = NodeId(1); |
1004 | const DEFAULT_BUTTON_LABEL_ID: NodeId = NodeId(2); |
1005 | const LINK_ID: NodeId = NodeId(3); |
1006 | const LINK_LABEL_CONTAINER_ID: NodeId = NodeId(4); |
1007 | const LINK_LABEL_ID: NodeId = NodeId(5); |
1008 | const CHECKBOX_ID: NodeId = NodeId(6); |
1009 | const CHECKBOX_LABEL_ID: NodeId = NodeId(7); |
1010 | const RADIO_BUTTON_ID: NodeId = NodeId(8); |
1011 | const RADIO_BUTTON_LABEL_ID: NodeId = NodeId(9); |
1012 | const MENU_BUTTON_ID: NodeId = NodeId(10); |
1013 | const MENU_BUTTON_LABEL_ID: NodeId = NodeId(11); |
1014 | const MENU_ID: NodeId = NodeId(12); |
1015 | const MENU_ITEM_ID: NodeId = NodeId(13); |
1016 | const MENU_ITEM_LABEL_ID: NodeId = NodeId(14); |
1017 | const MENU_ITEM_CHECKBOX_ID: NodeId = NodeId(15); |
1018 | const MENU_ITEM_CHECKBOX_LABEL_ID: NodeId = NodeId(16); |
1019 | const MENU_ITEM_RADIO_ID: NodeId = NodeId(17); |
1020 | const MENU_ITEM_RADIO_LABEL_ID: NodeId = NodeId(18); |
1021 | |
1022 | const DEFAULT_BUTTON_LABEL: &str = "Play" ; |
1023 | const LINK_LABEL: &str = "Watch in browser" ; |
1024 | const CHECKBOX_LABEL: &str = "Resume from previous position" ; |
1025 | const RADIO_BUTTON_LABEL: &str = "Normal speed" ; |
1026 | const MENU_BUTTON_LABEL: &str = "More" ; |
1027 | const MENU_ITEM_LABEL: &str = "Share" ; |
1028 | const MENU_ITEM_CHECKBOX_LABEL: &str = "Apply volume processing" ; |
1029 | const MENU_ITEM_RADIO_LABEL: &str = "Maximize loudness for noisy environment" ; |
1030 | |
1031 | let update = TreeUpdate { |
1032 | nodes: vec![ |
1033 | (ROOT_ID, { |
1034 | let mut node = Node::new(Role::Window); |
1035 | node.set_children(vec![ |
1036 | DEFAULT_BUTTON_ID, |
1037 | LINK_ID, |
1038 | CHECKBOX_ID, |
1039 | RADIO_BUTTON_ID, |
1040 | MENU_BUTTON_ID, |
1041 | MENU_ID, |
1042 | ]); |
1043 | node |
1044 | }), |
1045 | (DEFAULT_BUTTON_ID, { |
1046 | let mut node = Node::new(Role::DefaultButton); |
1047 | node.push_child(DEFAULT_BUTTON_LABEL_ID); |
1048 | node |
1049 | }), |
1050 | (DEFAULT_BUTTON_LABEL_ID, { |
1051 | let mut node = Node::new(Role::Image); |
1052 | node.set_label(DEFAULT_BUTTON_LABEL); |
1053 | node |
1054 | }), |
1055 | (LINK_ID, { |
1056 | let mut node = Node::new(Role::Link); |
1057 | node.push_child(LINK_LABEL_CONTAINER_ID); |
1058 | node |
1059 | }), |
1060 | (LINK_LABEL_CONTAINER_ID, { |
1061 | let mut node = Node::new(Role::GenericContainer); |
1062 | node.push_child(LINK_LABEL_ID); |
1063 | node |
1064 | }), |
1065 | (LINK_LABEL_ID, { |
1066 | let mut node = Node::new(Role::Label); |
1067 | node.set_value(LINK_LABEL); |
1068 | node |
1069 | }), |
1070 | (CHECKBOX_ID, { |
1071 | let mut node = Node::new(Role::CheckBox); |
1072 | node.push_child(CHECKBOX_LABEL_ID); |
1073 | node |
1074 | }), |
1075 | (CHECKBOX_LABEL_ID, { |
1076 | let mut node = Node::new(Role::Label); |
1077 | node.set_value(CHECKBOX_LABEL); |
1078 | node |
1079 | }), |
1080 | (RADIO_BUTTON_ID, { |
1081 | let mut node = Node::new(Role::RadioButton); |
1082 | node.push_child(RADIO_BUTTON_LABEL_ID); |
1083 | node |
1084 | }), |
1085 | (RADIO_BUTTON_LABEL_ID, { |
1086 | let mut node = Node::new(Role::Label); |
1087 | node.set_value(RADIO_BUTTON_LABEL); |
1088 | node |
1089 | }), |
1090 | (MENU_BUTTON_ID, { |
1091 | let mut node = Node::new(Role::Button); |
1092 | node.push_child(MENU_BUTTON_LABEL_ID); |
1093 | node |
1094 | }), |
1095 | (MENU_BUTTON_LABEL_ID, { |
1096 | let mut node = Node::new(Role::Label); |
1097 | node.set_value(MENU_BUTTON_LABEL); |
1098 | node |
1099 | }), |
1100 | (MENU_ID, { |
1101 | let mut node = Node::new(Role::Menu); |
1102 | node.set_children([MENU_ITEM_ID, MENU_ITEM_CHECKBOX_ID, MENU_ITEM_RADIO_ID]); |
1103 | node |
1104 | }), |
1105 | (MENU_ITEM_ID, { |
1106 | let mut node = Node::new(Role::MenuItem); |
1107 | node.push_child(MENU_ITEM_LABEL_ID); |
1108 | node |
1109 | }), |
1110 | (MENU_ITEM_LABEL_ID, { |
1111 | let mut node = Node::new(Role::Label); |
1112 | node.set_value(MENU_ITEM_LABEL); |
1113 | node |
1114 | }), |
1115 | (MENU_ITEM_CHECKBOX_ID, { |
1116 | let mut node = Node::new(Role::MenuItemCheckBox); |
1117 | node.push_child(MENU_ITEM_CHECKBOX_LABEL_ID); |
1118 | node |
1119 | }), |
1120 | (MENU_ITEM_CHECKBOX_LABEL_ID, { |
1121 | let mut node = Node::new(Role::Label); |
1122 | node.set_value(MENU_ITEM_CHECKBOX_LABEL); |
1123 | node |
1124 | }), |
1125 | (MENU_ITEM_RADIO_ID, { |
1126 | let mut node = Node::new(Role::MenuItemRadio); |
1127 | node.push_child(MENU_ITEM_RADIO_LABEL_ID); |
1128 | node |
1129 | }), |
1130 | (MENU_ITEM_RADIO_LABEL_ID, { |
1131 | let mut node = Node::new(Role::Label); |
1132 | node.set_value(MENU_ITEM_RADIO_LABEL); |
1133 | node |
1134 | }), |
1135 | ], |
1136 | tree: Some(Tree::new(ROOT_ID)), |
1137 | focus: ROOT_ID, |
1138 | }; |
1139 | let tree = crate::Tree::new(update, false); |
1140 | assert_eq!( |
1141 | Some(DEFAULT_BUTTON_LABEL.into()), |
1142 | tree.state().node_by_id(DEFAULT_BUTTON_ID).unwrap().label() |
1143 | ); |
1144 | assert_eq!( |
1145 | Some(LINK_LABEL.into()), |
1146 | tree.state().node_by_id(LINK_ID).unwrap().label() |
1147 | ); |
1148 | assert_eq!( |
1149 | Some(CHECKBOX_LABEL.into()), |
1150 | tree.state().node_by_id(CHECKBOX_ID).unwrap().label() |
1151 | ); |
1152 | assert_eq!( |
1153 | Some(RADIO_BUTTON_LABEL.into()), |
1154 | tree.state().node_by_id(RADIO_BUTTON_ID).unwrap().label() |
1155 | ); |
1156 | assert_eq!( |
1157 | Some(MENU_BUTTON_LABEL.into()), |
1158 | tree.state().node_by_id(MENU_BUTTON_ID).unwrap().label() |
1159 | ); |
1160 | assert_eq!( |
1161 | Some(MENU_ITEM_LABEL.into()), |
1162 | tree.state().node_by_id(MENU_ITEM_ID).unwrap().label() |
1163 | ); |
1164 | assert_eq!( |
1165 | Some(MENU_ITEM_CHECKBOX_LABEL.into()), |
1166 | tree.state() |
1167 | .node_by_id(MENU_ITEM_CHECKBOX_ID) |
1168 | .unwrap() |
1169 | .label() |
1170 | ); |
1171 | assert_eq!( |
1172 | Some(MENU_ITEM_RADIO_LABEL.into()), |
1173 | tree.state().node_by_id(MENU_ITEM_RADIO_ID).unwrap().label() |
1174 | ); |
1175 | } |
1176 | } |
1177 | |