Skip to content

Commit 70b534b

Browse files
authored
feat: Let parents declare actions supported on their children (#593)
1 parent 62f193a commit 70b534b

File tree

9 files changed

+164
-68
lines changed

9 files changed

+164
-68
lines changed

common/src/lib.rs

Lines changed: 97 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -915,6 +915,7 @@ struct Properties {
915915
pub struct Node {
916916
role: Role,
917917
actions: u32,
918+
child_actions: u32,
918919
flags: u32,
919920
properties: Properties,
920921
}
@@ -1600,6 +1601,31 @@ impl Node {
16001601
pub fn clear_actions(&mut self) {
16011602
self.actions = 0;
16021603
}
1604+
1605+
/// Return whether the specified action is in the set supported on this node's
1606+
/// direct children in the filtered tree.
1607+
#[inline]
1608+
pub fn child_supports_action(&self, action: Action) -> bool {
1609+
(self.child_actions & action.mask()) != 0
1610+
}
1611+
/// Add the specified action to the set supported on this node's direct
1612+
/// children in the filtered tree.
1613+
#[inline]
1614+
pub fn add_child_action(&mut self, action: Action) {
1615+
self.child_actions |= action.mask();
1616+
}
1617+
/// Remove the specified action from the set supported on this node's direct
1618+
/// children in the filtered tree.
1619+
#[inline]
1620+
pub fn remove_child_action(&mut self, action: Action) {
1621+
self.child_actions &= !(action.mask());
1622+
}
1623+
/// Clear the set of actions supported on this node's direct children in the
1624+
/// filtered tree.
1625+
#[inline]
1626+
pub fn clear_child_actions(&mut self) {
1627+
self.child_actions = 0;
1628+
}
16031629
}
16041630

16051631
flag_methods! {
@@ -2141,6 +2167,11 @@ impl fmt::Debug for Node {
21412167
fmt.field("actions", &supported_actions);
21422168
}
21432169

2170+
let child_supported_actions = action_mask_to_action_vec(self.child_actions);
2171+
if !child_supported_actions.is_empty() {
2172+
fmt.field("child_actions", &child_supported_actions);
2173+
}
2174+
21442175
self.debug_flag_properties(&mut fmt);
21452176
self.debug_node_id_vec_properties(&mut fmt);
21462177
self.debug_node_id_properties(&mut fmt);
@@ -2820,31 +2851,38 @@ mod tests {
28202851
assert_eq!(node.role(), Role::CheckBox);
28212852
}
28222853

2854+
macro_rules! assert_absent_action {
2855+
($node:ident, $action:ident) => {
2856+
assert!(!$node.supports_action(Action::$action));
2857+
assert!(!$node.child_supports_action(Action::$action));
2858+
};
2859+
}
2860+
28232861
#[test]
28242862
fn new_node_should_not_support_anyaction() {
28252863
let node = Node::new(Role::Unknown);
2826-
assert!(!node.supports_action(Action::Click));
2827-
assert!(!node.supports_action(Action::Focus));
2828-
assert!(!node.supports_action(Action::Blur));
2829-
assert!(!node.supports_action(Action::Collapse));
2830-
assert!(!node.supports_action(Action::Expand));
2831-
assert!(!node.supports_action(Action::CustomAction));
2832-
assert!(!node.supports_action(Action::Decrement));
2833-
assert!(!node.supports_action(Action::Increment));
2834-
assert!(!node.supports_action(Action::HideTooltip));
2835-
assert!(!node.supports_action(Action::ShowTooltip));
2836-
assert!(!node.supports_action(Action::ReplaceSelectedText));
2837-
assert!(!node.supports_action(Action::ScrollDown));
2838-
assert!(!node.supports_action(Action::ScrollLeft));
2839-
assert!(!node.supports_action(Action::ScrollRight));
2840-
assert!(!node.supports_action(Action::ScrollUp));
2841-
assert!(!node.supports_action(Action::ScrollIntoView));
2842-
assert!(!node.supports_action(Action::ScrollToPoint));
2843-
assert!(!node.supports_action(Action::SetScrollOffset));
2844-
assert!(!node.supports_action(Action::SetTextSelection));
2845-
assert!(!node.supports_action(Action::SetSequentialFocusNavigationStartingPoint));
2846-
assert!(!node.supports_action(Action::SetValue));
2847-
assert!(!node.supports_action(Action::ShowContextMenu));
2864+
assert_absent_action!(node, Click);
2865+
assert_absent_action!(node, Focus);
2866+
assert_absent_action!(node, Blur);
2867+
assert_absent_action!(node, Collapse);
2868+
assert_absent_action!(node, Expand);
2869+
assert_absent_action!(node, CustomAction);
2870+
assert_absent_action!(node, Decrement);
2871+
assert_absent_action!(node, Increment);
2872+
assert_absent_action!(node, HideTooltip);
2873+
assert_absent_action!(node, ShowTooltip);
2874+
assert_absent_action!(node, ReplaceSelectedText);
2875+
assert_absent_action!(node, ScrollDown);
2876+
assert_absent_action!(node, ScrollLeft);
2877+
assert_absent_action!(node, ScrollRight);
2878+
assert_absent_action!(node, ScrollUp);
2879+
assert_absent_action!(node, ScrollIntoView);
2880+
assert_absent_action!(node, ScrollToPoint);
2881+
assert_absent_action!(node, SetScrollOffset);
2882+
assert_absent_action!(node, SetTextSelection);
2883+
assert_absent_action!(node, SetSequentialFocusNavigationStartingPoint);
2884+
assert_absent_action!(node, SetValue);
2885+
assert_absent_action!(node, ShowContextMenu);
28482886
}
28492887

28502888
#[test]
@@ -2856,6 +2894,15 @@ mod tests {
28562894
assert!(node.supports_action(Action::Blur));
28572895
}
28582896

2897+
#[test]
2898+
fn node_add_child_action_should_add_the_action() {
2899+
let mut node = Node::new(Role::Unknown);
2900+
node.add_child_action(Action::Focus);
2901+
assert!(node.child_supports_action(Action::Focus));
2902+
node.add_child_action(Action::Blur);
2903+
assert!(node.child_supports_action(Action::Blur));
2904+
}
2905+
28592906
#[test]
28602907
fn node_add_action_should_do_nothing_if_the_action_is_already_supported() {
28612908
let mut node = Node::new(Role::Unknown);
@@ -2864,6 +2911,14 @@ mod tests {
28642911
assert!(node.supports_action(Action::Focus));
28652912
}
28662913

2914+
#[test]
2915+
fn node_add_child_action_should_do_nothing_if_the_action_is_already_supported() {
2916+
let mut node = Node::new(Role::Unknown);
2917+
node.add_child_action(Action::Focus);
2918+
node.add_child_action(Action::Focus);
2919+
assert!(node.child_supports_action(Action::Focus));
2920+
}
2921+
28672922
#[test]
28682923
fn node_remove_action_should_remove_the_action() {
28692924
let mut node = Node::new(Role::Unknown);
@@ -2872,6 +2927,14 @@ mod tests {
28722927
assert!(!node.supports_action(Action::Blur));
28732928
}
28742929

2930+
#[test]
2931+
fn node_remove_child_action_should_remove_the_action() {
2932+
let mut node = Node::new(Role::Unknown);
2933+
node.add_child_action(Action::Blur);
2934+
node.remove_child_action(Action::Blur);
2935+
assert!(!node.child_supports_action(Action::Blur));
2936+
}
2937+
28752938
#[test]
28762939
fn node_clear_actions_should_remove_all_actions() {
28772940
let mut node = Node::new(Role::Unknown);
@@ -2882,11 +2945,22 @@ mod tests {
28822945
assert!(!node.supports_action(Action::Blur));
28832946
}
28842947

2948+
#[test]
2949+
fn node_clear_child_actions_should_remove_all_actions() {
2950+
let mut node = Node::new(Role::Unknown);
2951+
node.add_child_action(Action::Focus);
2952+
node.add_child_action(Action::Blur);
2953+
node.clear_child_actions();
2954+
assert!(!node.child_supports_action(Action::Focus));
2955+
assert!(!node.child_supports_action(Action::Blur));
2956+
}
2957+
28852958
#[test]
28862959
fn node_should_have_debug_repr() {
28872960
let mut node = Node::new(Role::Unknown);
28882961
node.add_action(Action::Click);
28892962
node.add_action(Action::Focus);
2963+
node.add_child_action(Action::ScrollIntoView);
28902964
node.set_hidden();
28912965
node.set_multiselectable();
28922966
node.set_children([NodeId(0), NodeId(1)]);
@@ -2898,7 +2972,7 @@ mod tests {
28982972

28992973
assert_eq!(
29002974
&format!("{node:?}"),
2901-
r#"Node { role: Unknown, actions: [Click, Focus], is_hidden: true, is_multiselectable: true, children: [#0, #1], active_descendant: #2, custom_actions: [CustomAction { id: 0, description: "test action" }] }"#
2975+
r#"Node { role: Unknown, actions: [Click, Focus], child_actions: [ScrollIntoView], is_hidden: true, is_multiselectable: true, children: [#0, #1], active_descendant: #2, custom_actions: [CustomAction { id: 0, description: "test action" }] }"#
29022976
);
29032977
}
29042978

consumer/src/node.rs

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,8 @@ impl<'a> Node<'a> {
5454
self.tree_state.focus == self.id()
5555
}
5656

57-
pub fn is_focusable(&self) -> bool {
58-
self.supports_action(Action::Focus) || self.is_focused_in_tree()
57+
pub fn is_focusable(&self, parent_filter: &impl Fn(&Node) -> FilterResult) -> bool {
58+
self.supports_action(Action::Focus, parent_filter) || self.is_focused_in_tree()
5959
}
6060

6161
pub fn is_root(&self) -> bool {
@@ -452,8 +452,8 @@ impl<'a> Node<'a> {
452452
// and we assume that it will based on the role, the attempted action
453453
// does nothing. This stance is a departure from Chromium.
454454

455-
pub fn is_clickable(&self) -> bool {
456-
self.supports_action(Action::Click)
455+
pub fn is_clickable(&self, parent_filter: &impl Fn(&Node) -> FilterResult) -> bool {
456+
self.supports_action(Action::Click, parent_filter)
457457
}
458458

459459
pub fn is_selectable(&self) -> bool {
@@ -491,7 +491,7 @@ impl<'a> Node<'a> {
491491
self.data().is_expanded().is_some()
492492
}
493493

494-
pub fn is_invocable(&self) -> bool {
494+
pub fn is_invocable(&self, parent_filter: &impl Fn(&Node) -> FilterResult) -> bool {
495495
// A control is "invocable" if it initiates an action when activated but
496496
// does not maintain any state. A control that maintains state
497497
// when activated would be considered a toggle or expand-collapse
@@ -500,24 +500,34 @@ impl<'a> Node<'a> {
500500
// such as when clicking a text input, the control is not considered
501501
// "invocable", as the "invoke" action would be a redundant synonym
502502
// for the "set focus" action. The same logic applies to selection.
503-
self.is_clickable()
503+
self.is_clickable(parent_filter)
504504
&& !self.is_text_input()
505505
&& !matches!(self.role(), Role::Document | Role::Terminal)
506506
&& !self.supports_toggle()
507507
&& !self.supports_expand_collapse()
508508
&& self.is_selected().is_none()
509509
}
510510

511-
pub fn supports_action(&self, action: Action) -> bool {
512-
self.data().supports_action(action)
511+
pub fn supports_action(
512+
&self,
513+
action: Action,
514+
parent_filter: &impl Fn(&Node) -> FilterResult,
515+
) -> bool {
516+
if self.data().supports_action(action) {
517+
return true;
518+
}
519+
if let Some(parent) = self.filtered_parent(parent_filter) {
520+
return parent.data().child_supports_action(action);
521+
}
522+
false
513523
}
514524

515-
pub fn supports_increment(&self) -> bool {
516-
self.supports_action(Action::Increment)
525+
pub fn supports_increment(&self, parent_filter: &impl Fn(&Node) -> FilterResult) -> bool {
526+
self.supports_action(Action::Increment, parent_filter)
517527
}
518528

519-
pub fn supports_decrement(&self) -> bool {
520-
self.supports_action(Action::Decrement)
529+
pub fn supports_decrement(&self, parent_filter: &impl Fn(&Node) -> FilterResult) -> bool {
530+
self.supports_action(Action::Decrement, parent_filter)
521531
}
522532
}
523533

platforms/android/src/adapter.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -387,7 +387,10 @@ impl Adapter {
387387
ACTION_CLICK => ActionRequest {
388388
action: {
389389
let node = tree_state.node_by_id(target).unwrap();
390-
if node.is_focusable() && !node.is_focused() && !node.is_clickable() {
390+
if node.is_focusable(&filter)
391+
&& !node.is_focused()
392+
&& !node.is_clickable(&filter)
393+
{
391394
Action::Focus
392395
} else {
393396
Action::Click
@@ -422,12 +425,12 @@ impl Adapter {
422425
}
423426
}
424427
} else if action == ACTION_SCROLL_BACKWARD {
425-
if node.supports_action(Action::ScrollUp) {
428+
if node.supports_action(Action::ScrollUp, &filter) {
426429
Action::ScrollUp
427430
} else {
428431
Action::ScrollLeft
429432
}
430-
} else if node.supports_action(Action::ScrollDown) {
433+
} else if node.supports_action(Action::ScrollDown, &filter) {
431434
Action::ScrollDown
432435
} else {
433436
Action::ScrollRight
@@ -492,7 +495,7 @@ impl Adapter {
492495
// TalkBack expects the text selection change to take effect
493496
// immediately, so we optimistically update the node.
494497
// But don't be *too* optimistic.
495-
if !node.supports_action(Action::SetTextSelection) {
498+
if !node.supports_action(Action::SetTextSelection, &filter) {
496499
return None;
497500
}
498501
let (anchor, focus, extra) = selection_factory(&node)?;

platforms/android/src/node.rs

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ impl NodeWrapper<'_> {
3636
}
3737

3838
fn is_focusable(&self) -> bool {
39-
self.0.is_focusable() && self.0.role() != Role::ScrollView
39+
self.0.is_focusable(&filter) && self.0.role() != Role::ScrollView
4040
}
4141

4242
fn is_focused(&self) -> bool {
@@ -60,10 +60,10 @@ impl NodeWrapper<'_> {
6060
}
6161

6262
fn is_scrollable(&self) -> bool {
63-
self.0.supports_action(Action::ScrollDown)
64-
|| self.0.supports_action(Action::ScrollLeft)
65-
|| self.0.supports_action(Action::ScrollRight)
66-
|| self.0.supports_action(Action::ScrollUp)
63+
self.0.supports_action(Action::ScrollDown, &filter)
64+
|| self.0.supports_action(Action::ScrollLeft, &filter)
65+
|| self.0.supports_action(Action::ScrollRight, &filter)
66+
|| self.0.supports_action(Action::ScrollUp, &filter)
6767
}
6868

6969
fn is_selected(&self) -> bool {
@@ -331,7 +331,7 @@ impl NodeWrapper<'_> {
331331
.unwrap();
332332

333333
let can_focus = self.is_focusable() && !self.0.is_focused();
334-
if self.0.is_clickable() || can_focus {
334+
if self.0.is_clickable(&filter) || can_focus {
335335
add_action(env, node_info, ACTION_CLICK);
336336
}
337337
if can_focus {
@@ -353,10 +353,13 @@ impl NodeWrapper<'_> {
353353
)
354354
.unwrap();
355355
}
356-
if self.0.supports_action(Action::ScrollLeft) || self.0.supports_action(Action::ScrollUp) {
356+
if self.0.supports_action(Action::ScrollLeft, &filter)
357+
|| self.0.supports_action(Action::ScrollUp, &filter)
358+
{
357359
add_action(env, node_info, ACTION_SCROLL_BACKWARD);
358360
}
359-
if self.0.supports_action(Action::ScrollRight) || self.0.supports_action(Action::ScrollDown)
361+
if self.0.supports_action(Action::ScrollRight, &filter)
362+
|| self.0.supports_action(Action::ScrollDown, &filter)
360363
{
361364
add_action(env, node_info, ACTION_SCROLL_FORWARD);
362365
}

0 commit comments

Comments
 (0)