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

未關閉 適合新手
#848 0 則留言 0 個 reaction 已指派 0 人 在 GitHub 檢視

維護者通常 1 天內回覆

還沒有人認領這個 Issue。

評估

難度
2/5
預估耗時
1-3 小時
新手友好度
78/100
Issue 類型
缺陷
描述清晰度
描述清楚
活躍度
活躍
技術堆疊
rust
領域
accessibility

研究方向

從 accesskit_consumer/src/node.rs 中的 NodeRef::hit_test 和其中現有的 node_at_point 測試開始。為 issue 中描述的被裁剪和部分可見的子節點情況新增測試涵蓋,並涵蓋父節點不裁剪子節點的情況。完成標準是這些被裁剪的後代不會被命中,而不裁剪子節點的父節點下的溢出內容仍會被命中。

由索引模型根據 Issue 內容生成。

描述

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.

主要語言
Rust
星號
1.6k
分支
135
平均合併
10 小時 53 分鐘
30 天內合併 PR
11

環境準備

  • 沒有 Dockerfile 或 Docker Compose 檔案
  • 沒有 Pull Request 範本
  • 閱讀貢獻指南

從這裡開始

  1. 先讀完整個 Issue,再讀專案的貢獻指南。
  2. 在 Issue 下留言說明你要接手 —— 這能避免兩個人做同樣的事。
  3. Fork 儲存庫,在一個分支上完成修改。
  4. 送出 Pull Request,並在描述裡引用這個 Issue 編號。

AccessKit/accesskit 的其他 Issue

查看 AccessKit/accesskit 的全部 Issue

相似的 Issue

更多 Rust Issue

把新 issue 寄到你的電子郵件信箱

精選適合新手參與的 GitHub issue 摘要。