// Copyright 2021 The AccessKit Authors. All rights reserved. // Licensed under the Apache License, Version 2.0 (found in // the LICENSE-APACHE file) or the MIT license (found in // the LICENSE-MIT file), at your option. // Derived from Chromium's accessibility abstraction. // Copyright 2021 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE.chromium file. use accesskit::{ Action, Affine, FrozenNode as NodeData, Live, NodeId, Orientation, Point, Rect, Role, TextSelection, Toggled, }; use alloc::{ string::{String, ToString}, sync::Arc, vec::Vec, }; use core::{fmt, iter::FusedIterator}; use crate::filters::FilterResult; use crate::iterators::{ FilteredChildren, FollowingFilteredSiblings, FollowingSiblings, LabelledBy, PrecedingFilteredSiblings, PrecedingSiblings, }; use crate::tree::State as TreeState; #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub(crate) struct ParentAndIndex(pub(crate) NodeId, pub(crate) usize); #[derive(Clone, Debug)] pub(crate) struct NodeState { pub(crate) parent_and_index: Option, pub(crate) data: Arc, } #[derive(Copy, Clone)] pub struct Node<'a> { pub tree_state: &'a TreeState, pub(crate) id: NodeId, pub(crate) state: &'a NodeState, } impl<'a> Node<'a> { pub fn data(&self) -> &NodeData { &self.state.data } pub fn is_focused(&self) -> bool { self.tree_state.focus_id() == Some(self.id()) } pub fn is_focused_in_tree(&self) -> bool { self.tree_state.focus == self.id() } pub fn is_focusable(&self) -> bool { self.supports_action(Action::Focus) || self.is_focused_in_tree() } pub fn is_root(&self) -> bool { // Don't check for absence of a parent node, in case a non-root node // somehow gets detached from the tree. self.id() == self.tree_state.root_id() } pub fn parent_id(&self) -> Option { self.state .parent_and_index .as_ref() .map(|ParentAndIndex(id, _)| *id) } pub fn parent(&self) -> Option> { self.parent_id() .map(|id| self.tree_state.node_by_id(id).unwrap()) } pub fn filtered_parent(&self, filter: &impl Fn(&Node) -> FilterResult) -> Option> { self.parent().and_then(move |parent| { if filter(&parent) == FilterResult::Include { Some(parent) } else { parent.filtered_parent(filter) } }) } pub fn parent_and_index(self) -> Option<(Node<'a>, usize)> { self.state .parent_and_index .as_ref() .map(|ParentAndIndex(parent, index)| { (self.tree_state.node_by_id(*parent).unwrap(), *index) }) } pub fn child_ids( &self, ) -> impl DoubleEndedIterator + ExactSizeIterator + FusedIterator + '_ { let data = &self.state.data; data.children().iter().copied() } pub fn children( &self, ) -> impl DoubleEndedIterator> + ExactSizeIterator> + FusedIterator> + 'a { let state = self.tree_state; let data = &self.state.data; data.children() .iter() .map(move |id| state.node_by_id(*id).unwrap()) } pub fn filtered_children( &self, filter: impl Fn(&Node) -> FilterResult + 'a, ) -> impl DoubleEndedIterator> + FusedIterator> + 'a { FilteredChildren::new(*self, filter) } pub fn following_sibling_ids( &self, ) -> impl DoubleEndedIterator + ExactSizeIterator + FusedIterator + 'a { FollowingSiblings::new(*self) } pub fn following_siblings( &self, ) -> impl DoubleEndedIterator> + ExactSizeIterator> + FusedIterator> + 'a { let state = self.tree_state; self.following_sibling_ids() .map(move |id| state.node_by_id(id).unwrap()) } pub fn following_filtered_siblings( &self, filter: impl Fn(&Node) -> FilterResult + 'a, ) -> impl DoubleEndedIterator> + FusedIterator> + 'a { FollowingFilteredSiblings::new(*self, filter) } pub fn preceding_sibling_ids( &self, ) -> impl DoubleEndedIterator + ExactSizeIterator + FusedIterator + 'a { PrecedingSiblings::new(*self) } pub fn preceding_siblings( &self, ) -> impl DoubleEndedIterator> + ExactSizeIterator> + FusedIterator> + 'a { let state = self.tree_state; self.preceding_sibling_ids() .map(move |id| state.node_by_id(id).unwrap()) } pub fn preceding_filtered_siblings( &self, filter: impl Fn(&Node) -> FilterResult + 'a, ) -> impl DoubleEndedIterator> + FusedIterator> + 'a { PrecedingFilteredSiblings::new(*self, filter) } pub fn deepest_first_child(self) -> Option> { let mut deepest_child = self.children().next()?; while let Some(first_child) = deepest_child.children().next() { deepest_child = first_child; } Some(deepest_child) } pub fn deepest_first_filtered_child( &self, filter: &impl Fn(&Node) -> FilterResult, ) -> Option> { let mut deepest_child = self.first_filtered_child(filter)?; while let Some(first_child) = deepest_child.first_filtered_child(filter) { deepest_child = first_child; } Some(deepest_child) } pub fn deepest_last_child(self) -> Option> { let mut deepest_child = self.children().next_back()?; while let Some(last_child) = deepest_child.children().next_back() { deepest_child = last_child; } Some(deepest_child) } pub fn deepest_last_filtered_child( &self, filter: &impl Fn(&Node) -> FilterResult, ) -> Option> { let mut deepest_child = self.last_filtered_child(filter)?; while let Some(last_child) = deepest_child.last_filtered_child(filter) { deepest_child = last_child; } Some(deepest_child) } pub fn is_descendant_of(&self, ancestor: &Node) -> bool { if self.id() == ancestor.id() { return true; } if let Some(parent) = self.parent() { return parent.is_descendant_of(ancestor); } false } /// Returns the transform defined directly on this node, or the identity /// transform, without taking into account transforms on ancestors. pub fn direct_transform(&self) -> Affine { self.data() .transform() .map_or(Affine::IDENTITY, |value| *value) } /// Returns the combined affine transform of this node and its ancestors, /// up to and including the root of this node's tree. pub fn transform(&self) -> Affine { self.parent() .map_or(Affine::IDENTITY, |parent| parent.transform()) * self.direct_transform() } pub(crate) fn relative_transform(&self, stop_at: &Node) -> Affine { let parent_transform = if let Some(parent) = self.parent() { if parent.id() == stop_at.id() { Affine::IDENTITY } else { parent.relative_transform(stop_at) } } else { Affine::IDENTITY }; parent_transform * self.direct_transform() } pub fn raw_bounds(&self) -> Option { self.data().bounds() } pub fn has_bounds(&self) -> bool { self.raw_bounds().is_some() } /// Returns the node's transformed bounding box relative to the tree's /// container (e.g. window). pub fn bounding_box(&self) -> Option { self.raw_bounds() .as_ref() .map(|rect| self.transform().transform_rect_bbox(*rect)) } pub(crate) fn bounding_box_in_coordinate_space(&self, other: &Node) -> Option { self.raw_bounds() .as_ref() .map(|rect| self.relative_transform(other).transform_rect_bbox(*rect)) } pub(crate) fn hit_test( &self, point: Point, filter: &impl Fn(&Node) -> FilterResult, ) -> Option<(Node<'a>, Point)> { let filter_result = filter(self); if filter_result == FilterResult::ExcludeSubtree { return None; } for child in self.children().rev() { let point = child.direct_transform().inverse() * point; if let Some(result) = child.hit_test(point, filter) { return Some(result); } } if filter_result == FilterResult::Include { if let Some(rect) = &self.raw_bounds() { if rect.contains(point) { return Some((*self, point)); } } } None } /// Returns the deepest filtered node, either this node or a descendant, /// at the given point in this node's coordinate space. pub fn node_at_point( &self, point: Point, filter: &impl Fn(&Node) -> FilterResult, ) -> Option> { self.hit_test(point, filter).map(|(node, _)| node) } pub fn id(&self) -> NodeId { self.id } pub fn role(&self) -> Role { self.data().role() } pub fn role_description(&self) -> Option<&str> { self.data().role_description() } pub fn has_role_description(&self) -> bool { self.data().role_description().is_some() } pub fn is_hidden(&self) -> bool { self.data().is_hidden() } pub fn is_disabled(&self) -> bool { self.data().is_disabled() } pub fn is_read_only(&self) -> bool { let data = self.data(); if data.is_read_only() { true } else { self.should_have_read_only_state_by_default() || !self.is_read_only_supported() } } pub fn is_read_only_or_disabled(&self) -> bool { self.is_read_only() || self.is_disabled() } pub fn toggled(&self) -> Option { self.data().toggled() } pub fn numeric_value(&self) -> Option { self.data().numeric_value() } pub fn min_numeric_value(&self) -> Option { self.data().min_numeric_value() } pub fn max_numeric_value(&self) -> Option { self.data().max_numeric_value() } pub fn numeric_value_step(&self) -> Option { self.data().numeric_value_step() } pub fn numeric_value_jump(&self) -> Option { self.data().numeric_value_jump() } pub fn is_text_input(&self) -> bool { matches!( self.role(), Role::TextInput | Role::MultilineTextInput | Role::SearchInput | Role::DateInput | Role::DateTimeInput | Role::WeekInput | Role::MonthInput | Role::TimeInput | Role::EmailInput | Role::NumberInput | Role::PasswordInput | Role::PhoneNumberInput | Role::UrlInput | Role::EditableComboBox | Role::SpinButton ) } pub fn is_multiline(&self) -> bool { self.role() == Role::MultilineTextInput } pub fn orientation(&self) -> Option { self.data().orientation().or_else(|| { if self.role() == Role::ListBox { Some(Orientation::Vertical) } else { None } }) } // When probing for supported actions as the next several functions do, // it's tempting to check the role. But it's better to not assume anything // beyond what the provider has explicitly told us. Rationale: // if the provider developer forgot to call `add_action` for an action, // an AT (or even AccessKit itself) can fall back to simulating // a mouse click. But if the provider doesn't handle an action request // and we assume that it will based on the role, the attempted action // does nothing. This stance is a departure from Chromium. pub fn is_clickable(&self) -> bool { self.supports_action(Action::Click) } pub fn is_selectable(&self) -> bool { // It's selectable if it has the attribute, whether it's true or false. self.is_selected().is_some() && !self.is_disabled() } pub fn is_multiselectable(&self) -> bool { self.data().is_multiselectable() } pub fn size_of_set_from_container( &self, filter: &impl Fn(&Node) -> FilterResult, ) -> Option { self.selection_container(filter) .and_then(|c| c.size_of_set()) } pub fn size_of_set(&self) -> Option { // TODO: compute this if it is not provided (#9). self.data().size_of_set() } pub fn position_in_set(&self) -> Option { // TODO: compute this if it is not provided (#9). self.data().position_in_set() } pub fn supports_toggle(&self) -> bool { self.toggled().is_some() } pub fn supports_expand_collapse(&self) -> bool { self.data().is_expanded().is_some() } pub fn is_invocable(&self) -> bool { // A control is "invocable" if it initiates an action when activated but // does not maintain any state. A control that maintains state // when activated would be considered a toggle or expand-collapse // control - these controls are "clickable" but not "invocable". // Similarly, if the action only gives the control keyboard focus, // such as when clicking a text input, the control is not considered // "invocable", as the "invoke" action would be a redundant synonym // for the "set focus" action. The same logic applies to selection. self.is_clickable() && !self.is_text_input() && !matches!(self.role(), Role::Document | Role::Terminal) && !self.supports_toggle() && !self.supports_expand_collapse() && self.is_selected().is_none() } pub fn supports_action(&self, action: Action) -> bool { self.data().supports_action(action) } pub fn supports_increment(&self) -> bool { self.supports_action(Action::Increment) } pub fn supports_decrement(&self) -> bool { self.supports_action(Action::Decrement) } } fn descendant_label_filter(node: &Node) -> FilterResult { match node.role() { Role::Label | Role::Image => FilterResult::Include, Role::GenericContainer => FilterResult::ExcludeNode, _ => FilterResult::ExcludeSubtree, } } impl<'a> Node<'a> { pub fn labelled_by( &self, ) -> impl DoubleEndedIterator> + FusedIterator> + 'a { let explicit = &self.state.data.labelled_by(); if explicit.is_empty() && matches!( self.role(), Role::Button | Role::CheckBox | Role::DefaultButton | Role::Link | Role::MenuItem | Role::MenuItemCheckBox | Role::MenuItemRadio | Role::RadioButton ) { LabelledBy::FromDescendants(FilteredChildren::new(*self, &descendant_label_filter)) } else { LabelledBy::Explicit { ids: explicit.iter(), tree_state: self.tree_state, } } } pub fn label_comes_from_value(&self) -> bool { self.role() == Role::Label } pub fn label(&self) -> Option { let mut result = String::new(); self.write_label(&mut result).unwrap().then_some(result) } fn write_label_direct(&self, mut writer: W) -> Result { if let Some(label) = &self.data().label() { writer.write_str(label)?; Ok(true) } else { Ok(false) } } pub fn write_label(&self, mut writer: W) -> Result { if self.write_label_direct(&mut writer)? { Ok(true) } else { let mut wrote_one = false; for node in self.labelled_by() { let writer = SpacePrefixingWriter { inner: &mut writer, need_prefix: wrote_one, }; let wrote_this_time = if node.label_comes_from_value() { node.write_value(writer) } else { node.write_label_direct(writer) }?; wrote_one = wrote_one || wrote_this_time; } Ok(wrote_one) } } pub fn description(&self) -> Option { self.data() .description() .map(|description| description.to_string()) } pub fn placeholder(&self) -> Option<&str> { self.data().placeholder() } pub fn value(&self) -> Option { let mut result = String::new(); self.write_value(&mut result).unwrap().then_some(result) } pub fn write_value(&self, mut writer: W) -> Result { if let Some(value) = &self.data().value() { writer.write_str(value)?; Ok(true) } else if self.supports_text_ranges() && !self.is_multiline() { self.document_range().write_text(writer)?; Ok(true) } else { Ok(false) } } pub fn has_value(&self) -> bool { self.data().value().is_some() || (self.supports_text_ranges() && !self.is_multiline()) } pub fn is_read_only_supported(&self) -> bool { self.is_text_input() || matches!( self.role(), Role::CheckBox | Role::ColorWell | Role::ComboBox | Role::Grid | Role::ListBox | Role::MenuItemCheckBox | Role::MenuItemRadio | Role::MenuListPopup | Role::RadioButton | Role::RadioGroup | Role::Slider | Role::Switch | Role::TreeGrid ) } pub fn should_have_read_only_state_by_default(&self) -> bool { matches!( self.role(), Role::Article | Role::Definition | Role::DescriptionList | Role::DescriptionListTerm | Role::Directory | Role::Document | Role::GraphicsDocument | Role::Image | Role::List | Role::ListItem | Role::PdfRoot | Role::ProgressIndicator | Role::RootWebArea | Role::Term | Role::Timer | Role::Toolbar | Role::Tooltip ) } pub fn is_required(&self) -> bool { self.data().is_required() } pub fn live(&self) -> Live { self.data() .live() .unwrap_or_else(|| self.parent().map_or(Live::Off, |parent| parent.live())) } pub fn is_selected(&self) -> Option { self.data().is_selected() } pub fn is_item_like(&self) -> bool { matches!( self.role(), Role::Article | Role::Comment | Role::ListItem | Role::MenuItem | Role::MenuItemRadio | Role::Tab | Role::MenuItemCheckBox | Role::TreeItem | Role::ListBoxOption | Role::MenuListOption | Role::RadioButton | Role::DescriptionListTerm | Role::Term ) } pub fn is_container_with_selectable_children(&self) -> bool { matches!( self.role(), Role::ComboBox | Role::EditableComboBox | Role::Grid | Role::ListBox | Role::ListGrid | Role::Menu | Role::MenuBar | Role::MenuListPopup | Role::RadioGroup | Role::TabList | Role::Toolbar | Role::Tree | Role::TreeGrid ) } pub fn raw_text_selection(&self) -> Option<&TextSelection> { self.data().text_selection() } pub fn raw_value(&self) -> Option<&str> { self.data().value() } pub fn author_id(&self) -> Option<&str> { self.data().author_id() } pub fn class_name(&self) -> Option<&str> { self.data().class_name() } pub fn index_path(&self) -> Vec { self.relative_index_path(self.tree_state.root_id()) } pub fn relative_index_path(&self, ancestor_id: NodeId) -> Vec { let mut result = Vec::new(); let mut current = *self; while current.id() != ancestor_id { let (parent, index) = current.parent_and_index().unwrap(); result.push(index); current = parent; } result.reverse(); result } pub(crate) fn first_filtered_child( &self, filter: &impl Fn(&Node) -> FilterResult, ) -> Option> { for child in self.children() { let result = filter(&child); if result == FilterResult::Include { return Some(child); } if result == FilterResult::ExcludeNode { if let Some(descendant) = child.first_filtered_child(filter) { return Some(descendant); } } } None } pub(crate) fn last_filtered_child( &self, filter: &impl Fn(&Node) -> FilterResult, ) -> Option> { for child in self.children().rev() { let result = filter(&child); if result == FilterResult::Include { return Some(child); } if result == FilterResult::ExcludeNode { if let Some(descendant) = child.last_filtered_child(filter) { return Some(descendant); } } } None } pub fn selection_container(&self, filter: &impl Fn(&Node) -> FilterResult) -> Option> { self.filtered_parent(&|parent| match filter(parent) { FilterResult::Include if parent.is_container_with_selectable_children() => { FilterResult::Include } FilterResult::Include => FilterResult::ExcludeNode, filter_result => filter_result, }) } pub fn items( &self, filter: impl Fn(&Node) -> FilterResult + 'a, ) -> impl DoubleEndedIterator> + FusedIterator> + 'a { self.filtered_children(move |child| match filter(child) { FilterResult::Include if child.is_item_like() => FilterResult::Include, FilterResult::Include => FilterResult::ExcludeNode, filter_result => filter_result, }) } } struct SpacePrefixingWriter { inner: W, need_prefix: bool, } impl SpacePrefixingWriter { fn write_prefix_if_needed(&mut self) -> fmt::Result { if self.need_prefix { self.inner.write_char(' ')?; self.need_prefix = false; } Ok(()) } } impl fmt::Write for SpacePrefixingWriter { fn write_str(&mut self, s: &str) -> fmt::Result { self.write_prefix_if_needed()?; self.inner.write_str(s) } fn write_char(&mut self, c: char) -> fmt::Result { self.write_prefix_if_needed()?; self.inner.write_char(c) } } #[cfg(test)] mod tests { use accesskit::{Node, NodeId, Point, Rect, Role, Tree, TreeUpdate}; use alloc::vec; use crate::tests::*; #[test] fn parent_and_index() { let tree = test_tree(); assert!(tree.state().root().parent_and_index().is_none()); assert_eq!( Some((ROOT_ID, 0)), tree.state() .node_by_id(PARAGRAPH_0_ID) .unwrap() .parent_and_index() .map(|(parent, index)| (parent.id(), index)) ); assert_eq!( Some((PARAGRAPH_0_ID, 0)), tree.state() .node_by_id(LABEL_0_0_IGNORED_ID) .unwrap() .parent_and_index() .map(|(parent, index)| (parent.id(), index)) ); assert_eq!( Some((ROOT_ID, 1)), tree.state() .node_by_id(PARAGRAPH_1_IGNORED_ID) .unwrap() .parent_and_index() .map(|(parent, index)| (parent.id(), index)) ); } #[test] fn deepest_first_child() { let tree = test_tree(); assert_eq!( LABEL_0_0_IGNORED_ID, tree.state().root().deepest_first_child().unwrap().id() ); assert_eq!( LABEL_0_0_IGNORED_ID, tree.state() .node_by_id(PARAGRAPH_0_ID) .unwrap() .deepest_first_child() .unwrap() .id() ); assert!(tree .state() .node_by_id(LABEL_0_0_IGNORED_ID) .unwrap() .deepest_first_child() .is_none()); } #[test] fn filtered_parent() { let tree = test_tree(); assert_eq!( ROOT_ID, tree.state() .node_by_id(LABEL_1_1_ID) .unwrap() .filtered_parent(&test_tree_filter) .unwrap() .id() ); assert!(tree .state() .root() .filtered_parent(&test_tree_filter) .is_none()); } #[test] fn deepest_first_filtered_child() { let tree = test_tree(); assert_eq!( PARAGRAPH_0_ID, tree.state() .root() .deepest_first_filtered_child(&test_tree_filter) .unwrap() .id() ); assert!(tree .state() .node_by_id(PARAGRAPH_0_ID) .unwrap() .deepest_first_filtered_child(&test_tree_filter) .is_none()); assert!(tree .state() .node_by_id(LABEL_0_0_IGNORED_ID) .unwrap() .deepest_first_filtered_child(&test_tree_filter) .is_none()); } #[test] fn deepest_last_child() { let tree = test_tree(); assert_eq!( EMPTY_CONTAINER_3_3_IGNORED_ID, tree.state().root().deepest_last_child().unwrap().id() ); assert_eq!( EMPTY_CONTAINER_3_3_IGNORED_ID, tree.state() .node_by_id(PARAGRAPH_3_IGNORED_ID) .unwrap() .deepest_last_child() .unwrap() .id() ); assert!(tree .state() .node_by_id(BUTTON_3_2_ID) .unwrap() .deepest_last_child() .is_none()); } #[test] fn deepest_last_filtered_child() { let tree = test_tree(); assert_eq!( BUTTON_3_2_ID, tree.state() .root() .deepest_last_filtered_child(&test_tree_filter) .unwrap() .id() ); assert_eq!( BUTTON_3_2_ID, tree.state() .node_by_id(PARAGRAPH_3_IGNORED_ID) .unwrap() .deepest_last_filtered_child(&test_tree_filter) .unwrap() .id() ); assert!(tree .state() .node_by_id(BUTTON_3_2_ID) .unwrap() .deepest_last_filtered_child(&test_tree_filter) .is_none()); assert!(tree .state() .node_by_id(PARAGRAPH_0_ID) .unwrap() .deepest_last_filtered_child(&test_tree_filter) .is_none()); } #[test] fn is_descendant_of() { let tree = test_tree(); assert!(tree .state() .node_by_id(PARAGRAPH_0_ID) .unwrap() .is_descendant_of(&tree.state().root())); assert!(tree .state() .node_by_id(LABEL_0_0_IGNORED_ID) .unwrap() .is_descendant_of(&tree.state().root())); assert!(tree .state() .node_by_id(LABEL_0_0_IGNORED_ID) .unwrap() .is_descendant_of(&tree.state().node_by_id(PARAGRAPH_0_ID).unwrap())); assert!(!tree .state() .node_by_id(LABEL_0_0_IGNORED_ID) .unwrap() .is_descendant_of(&tree.state().node_by_id(PARAGRAPH_2_ID).unwrap())); assert!(!tree .state() .node_by_id(PARAGRAPH_0_ID) .unwrap() .is_descendant_of(&tree.state().node_by_id(PARAGRAPH_2_ID).unwrap())); } #[test] fn is_root() { let tree = test_tree(); assert!(tree.state().node_by_id(ROOT_ID).unwrap().is_root()); assert!(!tree.state().node_by_id(PARAGRAPH_0_ID).unwrap().is_root()); } #[test] fn bounding_box() { let tree = test_tree(); assert!(tree .state() .node_by_id(ROOT_ID) .unwrap() .bounding_box() .is_none()); assert_eq!( Some(Rect { x0: 10.0, y0: 40.0, x1: 810.0, y1: 80.0, }), tree.state() .node_by_id(PARAGRAPH_1_IGNORED_ID) .unwrap() .bounding_box() ); assert_eq!( Some(Rect { x0: 20.0, y0: 50.0, x1: 100.0, y1: 70.0, }), tree.state() .node_by_id(LABEL_1_1_ID) .unwrap() .bounding_box() ); } #[test] fn node_at_point() { let tree = test_tree(); assert!(tree .state() .root() .node_at_point(Point::new(10.0, 40.0), &test_tree_filter) .is_none()); assert_eq!( Some(LABEL_1_1_ID), tree.state() .root() .node_at_point(Point::new(20.0, 50.0), &test_tree_filter) .map(|node| node.id()) ); assert_eq!( Some(LABEL_1_1_ID), tree.state() .root() .node_at_point(Point::new(50.0, 60.0), &test_tree_filter) .map(|node| node.id()) ); assert!(tree .state() .root() .node_at_point(Point::new(100.0, 70.0), &test_tree_filter) .is_none()); } #[test] fn no_label_or_labelled_by() { let update = TreeUpdate { nodes: vec![ (NodeId(0), { let mut node = Node::new(Role::Window); node.set_children(vec![NodeId(1)]); node }), (NodeId(1), Node::new(Role::Button)), ], tree: Some(Tree::new(NodeId(0))), focus: NodeId(0), }; let tree = crate::Tree::new(update, false); assert_eq!(None, tree.state().node_by_id(NodeId(1)).unwrap().label()); } #[test] fn label_from_labelled_by() { // The following mock UI probably isn't very localization-friendly, // but it's good for this test. const LABEL_1: &str = "Check email every"; const LABEL_2: &str = "minutes"; let update = TreeUpdate { nodes: vec![ (NodeId(0), { let mut node = Node::new(Role::Window); node.set_children(vec![NodeId(1), NodeId(2), NodeId(3), NodeId(4)]); node }), (NodeId(1), { let mut node = Node::new(Role::CheckBox); node.set_labelled_by(vec![NodeId(2), NodeId(4)]); node }), (NodeId(2), { let mut node = Node::new(Role::Label); node.set_value(LABEL_1); node }), (NodeId(3), { let mut node = Node::new(Role::TextInput); node.push_labelled_by(NodeId(4)); node }), (NodeId(4), { let mut node = Node::new(Role::Label); node.set_value(LABEL_2); node }), ], tree: Some(Tree::new(NodeId(0))), focus: NodeId(0), }; let tree = crate::Tree::new(update, false); assert_eq!( Some([LABEL_1, LABEL_2].join(" ")), tree.state().node_by_id(NodeId(1)).unwrap().label() ); assert_eq!( Some(LABEL_2.into()), tree.state().node_by_id(NodeId(3)).unwrap().label() ); } #[test] fn label_from_descendant_label() { const ROOT_ID: NodeId = NodeId(0); const DEFAULT_BUTTON_ID: NodeId = NodeId(1); const DEFAULT_BUTTON_LABEL_ID: NodeId = NodeId(2); const LINK_ID: NodeId = NodeId(3); const LINK_LABEL_CONTAINER_ID: NodeId = NodeId(4); const LINK_LABEL_ID: NodeId = NodeId(5); const CHECKBOX_ID: NodeId = NodeId(6); const CHECKBOX_LABEL_ID: NodeId = NodeId(7); const RADIO_BUTTON_ID: NodeId = NodeId(8); const RADIO_BUTTON_LABEL_ID: NodeId = NodeId(9); const MENU_BUTTON_ID: NodeId = NodeId(10); const MENU_BUTTON_LABEL_ID: NodeId = NodeId(11); const MENU_ID: NodeId = NodeId(12); const MENU_ITEM_ID: NodeId = NodeId(13); const MENU_ITEM_LABEL_ID: NodeId = NodeId(14); const MENU_ITEM_CHECKBOX_ID: NodeId = NodeId(15); const MENU_ITEM_CHECKBOX_LABEL_ID: NodeId = NodeId(16); const MENU_ITEM_RADIO_ID: NodeId = NodeId(17); const MENU_ITEM_RADIO_LABEL_ID: NodeId = NodeId(18); const DEFAULT_BUTTON_LABEL: &str = "Play"; const LINK_LABEL: &str = "Watch in browser"; const CHECKBOX_LABEL: &str = "Resume from previous position"; const RADIO_BUTTON_LABEL: &str = "Normal speed"; const MENU_BUTTON_LABEL: &str = "More"; const MENU_ITEM_LABEL: &str = "Share"; const MENU_ITEM_CHECKBOX_LABEL: &str = "Apply volume processing"; const MENU_ITEM_RADIO_LABEL: &str = "Maximize loudness for noisy environment"; let update = TreeUpdate { nodes: vec![ (ROOT_ID, { let mut node = Node::new(Role::Window); node.set_children(vec![ DEFAULT_BUTTON_ID, LINK_ID, CHECKBOX_ID, RADIO_BUTTON_ID, MENU_BUTTON_ID, MENU_ID, ]); node }), (DEFAULT_BUTTON_ID, { let mut node = Node::new(Role::DefaultButton); node.push_child(DEFAULT_BUTTON_LABEL_ID); node }), (DEFAULT_BUTTON_LABEL_ID, { let mut node = Node::new(Role::Image); node.set_label(DEFAULT_BUTTON_LABEL); node }), (LINK_ID, { let mut node = Node::new(Role::Link); node.push_child(LINK_LABEL_CONTAINER_ID); node }), (LINK_LABEL_CONTAINER_ID, { let mut node = Node::new(Role::GenericContainer); node.push_child(LINK_LABEL_ID); node }), (LINK_LABEL_ID, { let mut node = Node::new(Role::Label); node.set_value(LINK_LABEL); node }), (CHECKBOX_ID, { let mut node = Node::new(Role::CheckBox); node.push_child(CHECKBOX_LABEL_ID); node }), (CHECKBOX_LABEL_ID, { let mut node = Node::new(Role::Label); node.set_value(CHECKBOX_LABEL); node }), (RADIO_BUTTON_ID, { let mut node = Node::new(Role::RadioButton); node.push_child(RADIO_BUTTON_LABEL_ID); node }), (RADIO_BUTTON_LABEL_ID, { let mut node = Node::new(Role::Label); node.set_value(RADIO_BUTTON_LABEL); node }), (MENU_BUTTON_ID, { let mut node = Node::new(Role::Button); node.push_child(MENU_BUTTON_LABEL_ID); node }), (MENU_BUTTON_LABEL_ID, { let mut node = Node::new(Role::Label); node.set_value(MENU_BUTTON_LABEL); node }), (MENU_ID, { let mut node = Node::new(Role::Menu); node.set_children([MENU_ITEM_ID, MENU_ITEM_CHECKBOX_ID, MENU_ITEM_RADIO_ID]); node }), (MENU_ITEM_ID, { let mut node = Node::new(Role::MenuItem); node.push_child(MENU_ITEM_LABEL_ID); node }), (MENU_ITEM_LABEL_ID, { let mut node = Node::new(Role::Label); node.set_value(MENU_ITEM_LABEL); node }), (MENU_ITEM_CHECKBOX_ID, { let mut node = Node::new(Role::MenuItemCheckBox); node.push_child(MENU_ITEM_CHECKBOX_LABEL_ID); node }), (MENU_ITEM_CHECKBOX_LABEL_ID, { let mut node = Node::new(Role::Label); node.set_value(MENU_ITEM_CHECKBOX_LABEL); node }), (MENU_ITEM_RADIO_ID, { let mut node = Node::new(Role::MenuItemRadio); node.push_child(MENU_ITEM_RADIO_LABEL_ID); node }), (MENU_ITEM_RADIO_LABEL_ID, { let mut node = Node::new(Role::Label); node.set_value(MENU_ITEM_RADIO_LABEL); node }), ], tree: Some(Tree::new(ROOT_ID)), focus: ROOT_ID, }; let tree = crate::Tree::new(update, false); assert_eq!( Some(DEFAULT_BUTTON_LABEL.into()), tree.state().node_by_id(DEFAULT_BUTTON_ID).unwrap().label() ); assert_eq!( Some(LINK_LABEL.into()), tree.state().node_by_id(LINK_ID).unwrap().label() ); assert_eq!( Some(CHECKBOX_LABEL.into()), tree.state().node_by_id(CHECKBOX_ID).unwrap().label() ); assert_eq!( Some(RADIO_BUTTON_LABEL.into()), tree.state().node_by_id(RADIO_BUTTON_ID).unwrap().label() ); assert_eq!( Some(MENU_BUTTON_LABEL.into()), tree.state().node_by_id(MENU_BUTTON_ID).unwrap().label() ); assert_eq!( Some(MENU_ITEM_LABEL.into()), tree.state().node_by_id(MENU_ITEM_ID).unwrap().label() ); assert_eq!( Some(MENU_ITEM_CHECKBOX_LABEL.into()), tree.state() .node_by_id(MENU_ITEM_CHECKBOX_ID) .unwrap() .label() ); assert_eq!( Some(MENU_ITEM_RADIO_LABEL.into()), tree.state().node_by_id(MENU_ITEM_RADIO_ID).unwrap().label() ); } }