Hit testing ignores clips_children, so descendants clipped out by an ancestor are still hit
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 78/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Active
- Tech stack
- rust
- Domain
- accessibility
Research direction
Start with NodeRef::hit_test in accesskit_consumer/src/node.rs and the existing node_at_point test there. Add coverage for the clipped-out and partly visible child cases described in the issue, plus the case where the parent does not clip children. Done means those clipped descendants are not hit while overflow under a non-clipping parent still is.
Written by the indexing model from the issue text.
Description
What happens
NodeRef::hit_test (accesskit_consumer/src/node.rs, used by node_at_point) tests each node's raw bounds and visits every child. It never consults clips_children. A descendant that its parent clips out of view is still returned for points where it would be if nothing were clipped. Often that's on top of UI that is actually visible there.
clips_children is used today only by common_filter (filters.rs, from #574). That filter excludes a child of a clipping parent when the child lies entirely outside the parent, unless a neighbouring sibling is inside, so that off-screen items stay reachable for scrolling. Three cases still hit-test wrongly:
- Off-screen neighbours: a child kept by that rule (the one just before or after the visible ones) is hit at its off-screen position.
- Partly visible children: the clipped-off part of a partly visible child is hit.
- Overflowing descendants: a partly visible child's descendants that extend past the clipping ancestor are hit. The filter only looks at the immediate parent.
What should happen
A point outside a node that clips its children shouldn't hit any of that node's descendants.
Platforms
This affects every platform. The macOS, Windows, Unix (AT-SPI), iOS and Android adapters all resolve hit tests through node_at_point.
How to reproduce
The tree: a header button above a clipping scroll view, which holds one child scrolled up under the header and one visible child. Sketched in the style of the filters.rs tests:
let update = TreeUpdate {
nodes: vec![
(NodeId(0), {
let mut node = Node::new(Role::Window);
node.set_bounds(Rect::new(0.0, 0.0, 200.0, 300.0));
node.set_children(vec![NodeId(1), NodeId(2)]);
node
}),
// Header, drawn over the top of the scroll view's content area.
(NodeId(1), {
let mut node = Node::new(Role::Button);
node.set_bounds(Rect::new(0.0, 0.0, 200.0, 50.0));
node
}),
(NodeId(2), {
let mut node = Node::new(Role::ScrollView);
node.set_clips_children();
node.set_bounds(Rect::new(0.0, 50.0, 200.0, 300.0));
node.set_children(vec![NodeId(3), NodeId(4)]);
node
}),
// Scrolled above the viewport, under the header. Kept by
// common_filter because its next sibling is visible.
(NodeId(3), {
let mut node = Node::new(Role::Button);
node.set_bounds(Rect::new(0.0, 10.0, 200.0, 40.0));
node
}),
(NodeId(4), {
let mut node = Node::new(Role::Button);
node.set_bounds(Rect::new(0.0, 60.0, 200.0, 90.0));
node
}),
],
tree: Some(TreeInfo::new(NodeId(0))),
tree_id: TreeId::ROOT,
focus: NodeId(0),
};
let tree = crate::Tree::new(update, false);
let hit = tree
.state()
.root()
.node_at_point(Point::new(100.0, 25.0), &common_filter)
.map(|node| node.id());
// Expected: the header, NodeId(1). Today: NodeId(3), which is clipped out.
A second case: give node 3 bounds of 0..200 × 30..80 (partly visible), and hit (100, 40). That point is above the viewport, so the header should win. Today node 3 does.
We hit this in a GPUI app. With a panel scrolled, VoiceOver and other accessibility clients pressing a header button activated a hidden list item beneath it. We worked around it in the toolkit by clipping each node's bounds before handing them to AccessKit. But toolkits that already set clips_children shouldn't have to do that.
Proposed plan
- The change: in
hit_test'sVisitframe, when a node hasclips_children()and has bounds, push its children only if the point (already in that node's coordinate space at that stage) lies inside its bounds. The node's ownTestframe is unchanged. - Why this is enough: applied recursively, this means descendants are only hit within the intersection of their clipping ancestors. That covers all three cases above, without changing the filter or which nodes are reachable for scrolling into view.
- Chromium comparison:
BrowserAccessibility::ApproximateHitTest(ui/accessibility/platform/browser_accessibility.cc) only descends into a child whoseGetClippedScreenBoundsRect()contains the point, so it already hit-tests against ancestor-clipped bounds. The change would bring AccessKit in line. - Platform parity: all adapters call
node_at_point, so a consumer-only change applies everywhere. No adapter changes are needed. - Tests: the two cases above as unit tests next to the existing
node_at_pointtest innode.rs. Plus a regression check that a node whose parent doesn't setclips_childrenstill hits outside the parent's bounds, as overflow does today.
I'm happy to implement this if the approach works for you.
- Dominant language
- Rust
- Stars
- 1.6k
- Forks
- 135
- Avg merge
- 10h 53m
- Merged PRs (30d)
- 11
Getting set up
- No Dockerfile or Docker Compose file
- No pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from AccessKit/accesskit
-
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Maintainers usually reply within 1 day
-
Document sub-treesOpen
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
Maintainers usually reply within 1 day
-
Difficulty 5/5 Over a week Newbie friendliness 15/100
Maintainers usually reply within 1 day
All issues in AccessKit/accesskit
Similar issues
-
Difficulty 1/5 Under an hour Newbie friendliness 62/100
-
bug UI
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
wardian-app/Wardian#1746 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 85/100
Maintainers usually reply within 2 days
-
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
jlcodes99/cockpit-tools#3125 ·
Maintainers usually reply within 2 days
-
bug user-sprint
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
Vondor/KassenSturz#967 ·
Maintainers usually reply within 1 day