Hit testing ignores clips_children, so descendants clipped out by an ancestor are still hit

Offen Anfängerfreundlich
#848 0 Kommentare 0 Reaktionen 0 zugewiesene Personen Auf GitHub ansehen

Maintainer antworten meist innerhalb von 1 Tag

Dieses Issue hat noch niemand übernommen.

Bewertung

Schwierigkeit
2/5
Geschätzter Aufwand
1-3 Stunden
Anfängerfreundlichkeit
78/100
Issue-Typ
Bug
Klarheit
Klar beschrieben
Aktivitätsstatus
Aktiv
Tech-Stack
rust
Bereich
accessibility

Rechercherichtung

Beginne mit NodeRef::hit_test in accesskit_consumer/src/node.rs und dem vorhandenen node_at_point-Test dort. Ergänze Tests für die im Issue beschriebenen Fälle mit abgeschnittenen und teilweise sichtbaren untergeordneten Elementen sowie für den Fall, in dem der übergeordnete Knoten untergeordnete Elemente nicht abschneidet. Fertig ist es, wenn diese abgeschnittenen Nachfahren nicht getroffen werden, während Überlauf unter einem nicht abschneidenden übergeordneten Knoten weiterhin getroffen wird.

Vom Indexierungsmodell aus dem Issue-Text verfasst.

Beschreibung

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's Visit frame, when a node has clips_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 own Test frame 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 whose GetClippedScreenBoundsRect() 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_point test in node.rs. Plus a regression check that a node whose parent doesn't set clips_children still hits outside the parent's bounds, as overflow does today.

I'm happy to implement this if the approach works for you.

Vorherrschende Sprache
Rust
Sterne
1.6k
Forks
135
Ø Merge
10 Std. 53 Min.
Gemergte PRs (30 T.)
11

Entwicklungsumgebung

Erste Schritte

  1. Lesen Sie das ganze Issue und danach den Beitragsleitfaden des Projekts.
  2. Schreiben Sie ins Issue, dass Sie es übernehmen — das erspart doppelte Arbeit.
  3. Forken Sie das Repository und arbeiten Sie in einem Branch.
  4. Öffnen Sie einen Pull Request, der die Issue-Nummer nennt.

Mehr aus AccessKit/accesskit

Alle Issues in AccessKit/accesskit

Ähnliche Issues

Weitere Issues zu Rust

Neue Issues direkt in Ihr Postfach

Eine kurze Übersicht über anfängerfreundliche GitHub-Issues.