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