diff --git a/common/src/lib.rs b/common/src/lib.rs index b2ef5114..1a8bc6e1 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -1821,6 +1821,9 @@ node_id_vec_property_methods! { } node_id_property_methods! { + /// For a composite widget such as a listbox, tree, or grid, identifies + /// the currently active descendant. Used when focus remains on the container + /// while the active item changes. (ActiveDescendant, active_descendant, set_active_descendant, clear_active_descendant), (ErrorMessage, error_message, set_error_message, clear_error_message), (InPageLinkTarget, in_page_link_target, set_in_page_link_target, clear_in_page_link_target), diff --git a/consumer/src/node.rs b/consumer/src/node.rs index 7429a9cf..f2c91fab 100644 --- a/consumer/src/node.rs +++ b/consumer/src/node.rs @@ -72,7 +72,21 @@ impl<'a> Node<'a> { } pub fn is_focused(&self) -> bool { - self.tree_state.focus_id() == Some(self.id()) + let dominated_by_active_descendant = |node_id| { + self.tree_state + .node_by_id(node_id) + .and_then(|node| node.active_descendant()) + .is_some() + }; + match self.tree_state.focus_id() { + Some(focus_id) if focus_id == self.id() => !dominated_by_active_descendant(focus_id), + Some(focus_id) => self + .tree_state + .node_by_id(focus_id) + .and_then(|focused| focused.active_descendant()) + .is_some_and(|active_descendant| active_descendant.id() == self.id()), + None => false, + } } pub fn is_focused_in_tree(&self) -> bool { @@ -896,6 +910,13 @@ impl<'a> Node<'a> { .map(move |control_id| state.node_by_id(id.with_same_tree(*control_id)).unwrap()) } + pub fn active_descendant(&self) -> Option> { + self.state + .data + .active_descendant() + .and_then(|id| self.tree_state.node_by_id(self.id.with_same_tree(id))) + } + pub fn raw_text_selection(&self) -> Option<&TextSelection> { self.data().text_selection() } @@ -1791,4 +1812,118 @@ mod tests { assert_ne!(id1, id4); } } + + #[test] + fn is_focused_when_node_has_focus() { + const ROOT_ID: NodeId = NodeId(0); + const BUTTON_ID: NodeId = NodeId(1); + + let update = TreeUpdate { + nodes: vec![ + (ROOT_ID, { + let mut node = Node::new(Role::Window); + node.set_children(vec![BUTTON_ID]); + node + }), + (BUTTON_ID, Node::new(Role::Button)), + ], + tree: Some(Tree::new(ROOT_ID)), + tree_id: TreeId::ROOT, + focus: BUTTON_ID, + }; + let tree = crate::Tree::new(update, true); + assert!(tree + .state() + .node_by_id(nid(BUTTON_ID)) + .unwrap() + .is_focused()); + } + + #[test] + fn is_focused_when_node_does_not_have_focus() { + const ROOT_ID: NodeId = NodeId(0); + const BUTTON_ID: NodeId = NodeId(1); + + let update = TreeUpdate { + nodes: vec![ + (ROOT_ID, { + let mut node = Node::new(Role::Window); + node.set_children(vec![BUTTON_ID]); + node + }), + (BUTTON_ID, Node::new(Role::Button)), + ], + tree: Some(Tree::new(ROOT_ID)), + tree_id: TreeId::ROOT, + focus: ROOT_ID, + }; + let tree = crate::Tree::new(update, true); + assert!(!tree + .state() + .node_by_id(nid(BUTTON_ID)) + .unwrap() + .is_focused()); + } + + #[test] + fn is_focused_active_descendant_is_focused() { + const ROOT_ID: NodeId = NodeId(0); + const LISTBOX_ID: NodeId = NodeId(1); + const ITEM_ID: NodeId = NodeId(2); + + let update = TreeUpdate { + nodes: vec![ + (ROOT_ID, { + let mut node = Node::new(Role::Window); + node.set_children(vec![LISTBOX_ID]); + node + }), + (LISTBOX_ID, { + let mut node = Node::new(Role::ListBox); + node.set_children(vec![ITEM_ID]); + node.set_active_descendant(ITEM_ID); + node + }), + (ITEM_ID, Node::new(Role::ListBoxOption)), + ], + tree: Some(Tree::new(ROOT_ID)), + tree_id: TreeId::ROOT, + focus: LISTBOX_ID, + }; + let tree = crate::Tree::new(update, true); + assert!(tree.state().node_by_id(nid(ITEM_ID)).unwrap().is_focused()); + } + + #[test] + fn is_focused_node_with_active_descendant_is_not_focused() { + const ROOT_ID: NodeId = NodeId(0); + const LISTBOX_ID: NodeId = NodeId(1); + const ITEM_ID: NodeId = NodeId(2); + + let update = TreeUpdate { + nodes: vec![ + (ROOT_ID, { + let mut node = Node::new(Role::Window); + node.set_children(vec![LISTBOX_ID]); + node + }), + (LISTBOX_ID, { + let mut node = Node::new(Role::ListBox); + node.set_children(vec![ITEM_ID]); + node.set_active_descendant(ITEM_ID); + node + }), + (ITEM_ID, Node::new(Role::ListBoxOption)), + ], + tree: Some(Tree::new(ROOT_ID)), + tree_id: TreeId::ROOT, + focus: LISTBOX_ID, + }; + let tree = crate::Tree::new(update, true); + assert!(!tree + .state() + .node_by_id(nid(LISTBOX_ID)) + .unwrap() + .is_focused()); + } } diff --git a/consumer/src/tree.rs b/consumer/src/tree.rs index 91d3fab6..252ef623 100644 --- a/consumer/src/tree.rs +++ b/consumer/src/tree.rs @@ -538,7 +538,10 @@ impl State { } pub fn focus(&self) -> Option> { - self.focus_id().map(|id| self.node_by_id(id).unwrap()) + self.focus_id().map(|id| { + let focused = self.node_by_id(id).unwrap(); + focused.active_descendant().unwrap_or(focused) + }) } pub fn active_dialog(&self) -> Option> { @@ -641,9 +644,10 @@ impl Tree { let new_node = self.next_state.node_by_id(*id).unwrap(); handler.node_updated(&old_node, &new_node); } - if self.state.focus_id() != self.next_state.focus_id() { - let old_node = self.state.focus(); - if let Some(old_node) = &old_node { + let old_focus = self.state.focus(); + let new_focus = self.next_state.focus(); + if old_focus.as_ref().map(|n| n.id()) != new_focus.as_ref().map(|n| n.id()) { + if let Some(old_node) = &old_focus { let id = old_node.id(); if !changes.updated_node_ids.contains(&id) && !changes.removed_node_ids.contains(&id) @@ -653,8 +657,7 @@ impl Tree { } } } - let new_node = self.next_state.focus(); - if let Some(new_node) = &new_node { + if let Some(new_node) = &new_focus { let id = new_node.id(); if !changes.added_node_ids.contains(&id) && !changes.updated_node_ids.contains(&id) { @@ -663,7 +666,7 @@ impl Tree { } } } - handler.focus_moved(old_node.as_ref(), new_node.as_ref()); + handler.focus_moved(old_focus.as_ref(), new_focus.as_ref()); } for id in &changes.removed_node_ids { let node = self.state.node_by_id(*id).unwrap(); @@ -2867,4 +2870,112 @@ mod tests { assert!(handler.updated_nodes.contains(&subtree_node_id(1)),); assert!(!handler.updated_nodes.contains(&subtree_node_id(0)),); } + + #[test] + fn focus_returns_focused_node() { + let update = TreeUpdate { + nodes: vec![ + (LocalNodeId(0), { + let mut node = Node::new(Role::Window); + node.set_children(vec![LocalNodeId(1)]); + node + }), + (LocalNodeId(1), Node::new(Role::Button)), + ], + tree: Some(Tree::new(LocalNodeId(0))), + tree_id: TreeId::ROOT, + focus: LocalNodeId(1), + }; + let tree = super::Tree::new(update, true); + assert_eq!(tree.state().focus().unwrap().id(), node_id(1)); + } + + #[test] + fn focus_returns_active_descendant() { + let update = TreeUpdate { + nodes: vec![ + (LocalNodeId(0), { + let mut node = Node::new(Role::Window); + node.set_children(vec![LocalNodeId(1)]); + node + }), + (LocalNodeId(1), { + let mut node = Node::new(Role::ListBox); + node.set_children(vec![LocalNodeId(2)]); + node.set_active_descendant(LocalNodeId(2)); + node + }), + (LocalNodeId(2), Node::new(Role::ListBoxOption)), + ], + tree: Some(Tree::new(LocalNodeId(0))), + tree_id: TreeId::ROOT, + focus: LocalNodeId(1), + }; + let tree = super::Tree::new(update, true); + assert_eq!(tree.state().focus().unwrap().id(), node_id(2)); + } + + #[test] + fn focus_moved_when_active_descendant_changes() { + let update = TreeUpdate { + nodes: vec![ + (LocalNodeId(0), { + let mut node = Node::new(Role::Window); + node.set_children(vec![LocalNodeId(1)]); + node + }), + (LocalNodeId(1), { + let mut node = Node::new(Role::ListBox); + node.set_children(vec![LocalNodeId(2), LocalNodeId(3)]); + node.set_active_descendant(LocalNodeId(2)); + node + }), + (LocalNodeId(2), Node::new(Role::ListBoxOption)), + (LocalNodeId(3), Node::new(Role::ListBoxOption)), + ], + tree: Some(Tree::new(LocalNodeId(0))), + tree_id: TreeId::ROOT, + focus: LocalNodeId(1), + }; + let mut tree = super::Tree::new(update, true); + + struct Handler { + focus_moved_called: bool, + old_focus: Option, + new_focus: Option, + } + impl super::ChangeHandler for Handler { + fn node_added(&mut self, _: &crate::Node) {} + fn node_updated(&mut self, _: &crate::Node, _: &crate::Node) {} + fn focus_moved(&mut self, old: Option<&crate::Node>, new: Option<&crate::Node>) { + self.focus_moved_called = true; + self.old_focus = old.map(|n| n.id()); + self.new_focus = new.map(|n| n.id()); + } + fn node_removed(&mut self, _: &crate::Node) {} + } + + let mut handler = Handler { + focus_moved_called: false, + old_focus: None, + new_focus: None, + }; + + let update = TreeUpdate { + nodes: vec![(LocalNodeId(1), { + let mut node = Node::new(Role::ListBox); + node.set_children(vec![LocalNodeId(2), LocalNodeId(3)]); + node.set_active_descendant(LocalNodeId(3)); + node + })], + tree: None, + tree_id: TreeId::ROOT, + focus: LocalNodeId(1), + }; + tree.update_and_process_changes(update, &mut handler); + + assert!(handler.focus_moved_called); + assert_eq!(handler.old_focus, Some(node_id(2))); + assert_eq!(handler.new_focus, Some(node_id(3))); + } }