use std::{iter::FusedIterator, sync::Arc};
use accesskit::{
Action, Affine, DefaultActionVerb, Live, Node as NodeData, NodeId, Orientation, Point, Rect,
Role, TextSelection, Toggled,
};
use crate::filters::FilterResult;
use crate::iterators::{
FilteredChildren, FollowingFilteredSiblings, FollowingSiblings, LabelledBy,
PrecedingFilteredSiblings, PrecedingSiblings,
};
use crate::tree::State as TreeState;
#[derive(Clone, Copy, PartialEq, Eq)]
pub(crate) struct ParentAndIndex(pub(crate) NodeId, pub(crate) usize);
#[derive(Clone)]
pub(crate) struct NodeState {
pub(crate) parent_and_index: Option<ParentAndIndex>,
pub(crate) data: Arc<NodeData>,
}
#[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(crate) fn data(&self) -> &NodeData {
&self.state.data
}
pub fn is_focused(&self) -> bool {
self.tree_state.focus_id() == Some(self.id())
}
pub fn is_focusable(&self) -> bool {
self.supports_action(Action::Focus)
}
pub fn is_root(&self) -> bool {
self.id() == self.tree_state.root_id()
}
pub fn parent_id(&self) -> Option<NodeId> {
self.state
.parent_and_index
.as_ref()
.map(|ParentAndIndex(id, _)| *id)
}
pub fn parent(&self) -> Option<Node<'a>> {
self.parent_id()
.map(|id| self.tree_state.node_by_id(id).unwrap())
}
pub fn filtered_parent(&self, filter: &impl Fn(&Node) -> FilterResult) -> Option<Node<'a>> {
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<Item = NodeId>
+ ExactSizeIterator<Item = NodeId>
+ FusedIterator<Item = NodeId>
+ '_ {
let data = &self.state.data;
data.children().iter().copied()
}
pub fn children(
&self,
) -> impl DoubleEndedIterator<Item = Node<'a>>
+ ExactSizeIterator<Item = Node<'a>>
+ FusedIterator<Item = Node<'a>>
+ '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<Item = Node<'a>> + FusedIterator<Item = Node<'a>> + 'a {
FilteredChildren::new(*self, filter)
}
pub fn following_sibling_ids(
&self,
) -> impl DoubleEndedIterator<Item = NodeId>
+ ExactSizeIterator<Item = NodeId>
+ FusedIterator<Item = NodeId>
+ 'a {
FollowingSiblings::new(*self)
}
pub fn following_siblings(
&self,
) -> impl DoubleEndedIterator<Item = Node<'a>>
+ ExactSizeIterator<Item = Node<'a>>
+ FusedIterator<Item = Node<'a>>
+ '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<Item = Node<'a>> + FusedIterator<Item = Node<'a>> + 'a {
FollowingFilteredSiblings::new(*self, filter)
}
pub fn preceding_sibling_ids(
&self,
) -> impl DoubleEndedIterator<Item = NodeId>
+ ExactSizeIterator<Item = NodeId>
+ FusedIterator<Item = NodeId>
+ 'a {
PrecedingSiblings::new(*self)
}
pub fn preceding_siblings(
&self,
) -> impl DoubleEndedIterator<Item = Node<'a>>
+ ExactSizeIterator<Item = Node<'a>>
+ FusedIterator<Item = Node<'a>>
+ '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<Item = Node<'a>> + FusedIterator<Item = Node<'a>> + 'a {
PrecedingFilteredSiblings::new(*self, filter)
}
pub fn deepest_first_child(self) -> Option<Node<'a>> {
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<Node<'a>> {
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<Node<'a>> {
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<Node<'a>> {
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
}
pub fn direct_transform(&self) -> Affine {
self.data()
.transform()
.map_or(Affine::IDENTITY, |value| *value)
}
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<Rect> {
self.data().bounds()
}
pub fn has_bounds(&self) -> bool {
self.raw_bounds().is_some()
}
pub fn bounding_box(&self) -> Option<Rect> {
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<Rect> {
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
}
pub fn node_at_point(
&self,
point: Point,
filter: &impl Fn(&Node) -> FilterResult,
) -> Option<Node<'a>> {
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<String> {
self.data().role_description().map(String::from)
}
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<Toggled> {
self.data().toggled()
}
pub fn numeric_value(&self) -> Option<f64> {
self.data().numeric_value()
}
pub fn min_numeric_value(&self) -> Option<f64> {
self.data().min_numeric_value()
}
pub fn max_numeric_value(&self) -> Option<f64> {
self.data().max_numeric_value()
}
pub fn numeric_value_step(&self) -> Option<f64> {
self.data().numeric_value_step()
}
pub fn numeric_value_jump(&self) -> Option<f64> {
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<Orientation> {
self.data().orientation()
}
pub fn default_action_verb(&self) -> Option<DefaultActionVerb> {
self.data().default_action_verb()
}
pub fn is_clickable(&self) -> bool {
if let Some(verb) = self.default_action_verb() {
if verb != DefaultActionVerb::ClickAncestor {
return true;
}
}
false
}
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 {
self.is_clickable()
&& !matches!(
self.default_action_verb(),
Some(
DefaultActionVerb::Focus
| DefaultActionVerb::Select
| DefaultActionVerb::Unselect
)
)
&& !self.supports_toggle()
&& !self.supports_expand_collapse()
}
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<Item = Node<'a>> + FusedIterator<Item = Node<'a>> + 'a {
let explicit = &self.state.data.labelled_by();
if explicit.is_empty()
&& matches!(self.role(), Role::Button | Role::DefaultButton | Role::Link)
{
LabelledBy::FromDescendants(FilteredChildren::new(*self, &descendant_label_filter))
} else {
LabelledBy::Explicit {
ids: explicit.iter(),
tree_state: self.tree_state,
}
}
}
pub fn name(&self) -> Option<String> {
if let Some(name) = &self.data().name() {
Some(name.to_string())
} else {
let names = self
.labelled_by()
.filter_map(|node| node.name())
.collect::<Vec<String>>();
(!names.is_empty()).then(move || names.join(" "))
}
}
pub fn description(&self) -> Option<String> {
self.data()
.description()
.map(|description| description.to_string())
}
pub fn placeholder(&self) -> Option<String> {
self.data()
.placeholder()
.map(|placeholder| placeholder.to_string())
}
pub fn value(&self) -> Option<String> {
if let Some(value) = &self.data().value() {
Some(value.to_string())
} else if self.supports_text_ranges() && !self.is_multiline() {
Some(self.document_range().text())
} else {
None
}
}
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 live(&self) -> Live {
self.data()
.live()
.unwrap_or_else(|| self.parent().map_or(Live::Off, |parent| parent.live()))
}
pub fn is_selected(&self) -> Option<bool> {
self.data().is_selected()
}
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<usize> {
self.relative_index_path(self.tree_state.root_id())
}
pub fn relative_index_path(&self, ancestor_id: NodeId) -> Vec<usize> {
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<Node<'a>> {
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<Node<'a>> {
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
}
}
#[cfg(test)]
mod tests {
use accesskit::{NodeBuilder, NodeId, Point, Rect, Role, Tree, TreeUpdate};
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_name_or_labelled_by() {
let update = TreeUpdate {
nodes: vec![
(NodeId(0), {
let mut builder = NodeBuilder::new(Role::Window);
builder.set_children(vec![NodeId(1)]);
builder.build()
}),
(NodeId(1), NodeBuilder::new(Role::Button).build()),
],
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().name());
}
#[test]
fn name_from_labelled_by() {
const LABEL_1: &str = "Check email every";
const LABEL_2: &str = "minutes";
let update = TreeUpdate {
nodes: vec![
(NodeId(0), {
let mut builder = NodeBuilder::new(Role::Window);
builder.set_children(vec![NodeId(1), NodeId(2), NodeId(3), NodeId(4)]);
builder.build()
}),
(NodeId(1), {
let mut builder = NodeBuilder::new(Role::CheckBox);
builder.set_labelled_by(vec![NodeId(2), NodeId(4)]);
builder.build()
}),
(NodeId(2), {
let mut builder = NodeBuilder::new(Role::Label);
builder.set_name(LABEL_1);
builder.build()
}),
(NodeId(3), {
let mut builder = NodeBuilder::new(Role::TextInput);
builder.push_labelled_by(NodeId(4));
builder.build()
}),
(NodeId(4), {
let mut builder = NodeBuilder::new(Role::Label);
builder.set_name(LABEL_2);
builder.build()
}),
],
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().name()
);
assert_eq!(
Some(LABEL_2.into()),
tree.state().node_by_id(NodeId(3)).unwrap().name()
);
}
#[test]
fn name_from_descendant_label() {
const BUTTON_LABEL: &str = "Play";
const LINK_LABEL: &str = "Watch in browser";
let update = TreeUpdate {
nodes: vec![
(NodeId(0), {
let mut builder = NodeBuilder::new(Role::Window);
builder.set_children(vec![NodeId(1), NodeId(3)]);
builder.build()
}),
(NodeId(1), {
let mut builder = NodeBuilder::new(Role::Button);
builder.push_child(NodeId(2));
builder.build()
}),
(NodeId(2), {
let mut builder = NodeBuilder::new(Role::Image);
builder.set_name(BUTTON_LABEL);
builder.build()
}),
(NodeId(3), {
let mut builder = NodeBuilder::new(Role::Link);
builder.push_child(NodeId(4));
builder.build()
}),
(NodeId(4), {
let mut builder = NodeBuilder::new(Role::GenericContainer);
builder.push_child(NodeId(5));
builder.build()
}),
(NodeId(5), {
let mut builder = NodeBuilder::new(Role::Label);
builder.set_name(LINK_LABEL);
builder.build()
}),
],
tree: Some(Tree::new(NodeId(0))),
focus: NodeId(0),
};
let tree = crate::Tree::new(update, false);
assert_eq!(
Some(BUTTON_LABEL.into()),
tree.state().node_by_id(NodeId(1)).unwrap().name()
);
assert_eq!(
Some(LINK_LABEL.into()),
tree.state().node_by_id(NodeId(3)).unwrap().name()
);
}
}