Visitar URL original
fix(surface): keep a press on a window from raising a window below it by deepin-wm · Pull Request #1581 · linuxdeepin/treeland · GitHub
Skip to content

fix(surface): keep a press on a window from raising a window below it - #1581

Closed
deepin-wm wants to merge 2 commits into
linuxdeepin:masterfrom
deepin-wm:agent/bot/ef5b241da5b3
Closed

deepin-wm wants to merge 2 commits into
linuxdeepin:masterfrom
deepin-wm:agent/bot/ef5b241da5b3

Conversation

@deepin-wm

@deepin-wm deepin-wm commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

解决的问题

在已激活窗口内按下鼠标时,位于它 Z 轴下方、带 SSD(服务端装饰)标题栏的窗口会被错误地抬到最上层。用户复现的现象是:按上层窗口的内容区(不只是标题栏)也会触发。

问题来源:WM-936

根因

按下的归属判定缺失,表现为两条通道:

  1. 穿透到下层窗口的按下(真正的触发点):按在上层窗口「不属于客户端 surface」的位置(客户端自己裁掉的透明边/圆角、没有 Decoration 的窗口的边角、CSS 阴影环)时,这次按下会落到下层窗口——落到下层客户端就由 CompositorInputFilter::afterHandleEvent() 激活,落到下层标题栏/边框就由对应的 QML 处理器激活,激活即 stackToLast(),于是下层被抬到上面。
  2. 标题栏自己不做归属判定:标题栏对收到的每次按下都直接 activateSurface() / moveRequested() / 弹菜单,没有问过「我是光标下的那个窗口吗」。

更正:早期分析曾认为「被遮挡的标题栏会收到属于上层窗口的按下」(依据 TapHandler 会被投递给光标下所有 item)。经离屏实测不成立:标题栏是 Control,它作为传统鼠标 item 会独占按下,被遮挡的标题栏收不到(用 Rectangle 复刻时才会泄漏,与真实结构不符)。所以真实场景里点/拖上层标题栏是正常的,只有穿透才会触发,与用户反馈一致。

改了什么

文件 改动
src/core/qml/TitleBar.qml 标题栏交互统一到一个 MouseArea(单击激活、双击最大化/还原、超过拖动阈值才移动、右键与触屏长按弹窗口菜单),归属判定每次按下只算一次;三个窗口按钮也先过覆盖判定
src/input/compositorinputfilter.cpp afterHandleEvent() 里,若按下的位置被按下窗口之上的窗口覆盖,则不激活(层级与激活态都不动),按下照旧送给实际收到它的客户端
src/surface/surfacewrapper.{h,cpp} 新增 isTopmostWindowAt() / isTopmostWindowUnderCursor():用 waylib 的 WOutputRenderWindow::paintOrderItemList() 取整棵 scene 的绘制顺序(自下而上),再从最上层往下找第一个覆盖该位置的窗口。z 序、工作区、「显示在所有工作区」的容器由绘制顺序本身保证,不自己维护排序
src/core/qml/Decoration.qml 边框缩放前先过同一个判定

为什么这么改

  • 层级判定不能看 childItems() 插入序:treeland 的视觉层级由 z 决定(窗口角色、XWayland above/below、XdgPopup),插入序只在同 z 内生效。只比插入序会双向出错——漏判 overlay/X11 above 覆盖者(原 bug 仍复现),误判被 stackToLast() 排到末尾的 z=-1 窗口(点它上面的窗口不聚焦)。所以直接用 waylib 已有的绘制顺序,而不是自己再实现一套比较(src/modules/snap-target/snaphandler.cpp 取「光标下最上层」也是这么做的)。
  • 「光标下视觉最上层是不是我」做成一个共享判定,所有「点击即激活」的入口都过一遍。只改 afterHandleEvent() 修不全:穿透的按下会先被下层窗口的标题栏/边框的 QML 处理器接住直接激活,根本走不到 afterHandleEvent()。
  • 子 toplevel(对话框、X11 瞬态窗)视觉上确实盖在别人上面(setSurfaceRole() 给它同样的 z),所以也算覆盖者;popup / input popup 有自己的堆叠和输入 grab,仍被 type 判断排除,行为与改前一致。

提交(2 个)

按「判定/入口」与「标题栏交互」分组,后者依赖前者,两个提交各自可编译。

提交 内容
bc2f08476 fix(surface): do not activate a window that a window above covers isTopmostWindowAt() / isTopmostWindowUnderCursor() 判定,以及 afterHandleEvent()、Decoration.qml 缩放边框两个入口
9ecd5f264 fix(surface): give a title bar press only to the window under it 标题栏交互重写为单个 MouseArea + 按下时的归属判定(含按钮、触屏手势与「拖动不弹菜单」)

验证情况

  • ⚠️ 实机回归仍待人工验证,本 PR 尚未转 ready。代码层面的验证:qmlcachegen 编译通过;标题栏手势用 QWindowSystemInterface 按 WSeat 同款 source 做离屏事件回放(真实鼠标按住/拖动/单击、触摸合成长按);确定性 lint 0 findings。真机反馈目前只覆盖一项——拖动标题栏会弹出窗口菜单,已修复(提交 2 里的设备判定)。
  • 评审提出的两点已处理:MouseEvent.source 自 Qt 6.6 起弃用,比较改成显式等值(万一某次 Qt 构建里该属性消失,退化方向是「触屏长按不弹菜单」这种一眼可见的问题,而不是原 bug 静默回来);注释里写明「真实指针事件以 NotSynthesized 送达 QML」这个来自 WSeat(waylib/src/server/kernel/wseat.cpp)的前提。
  • 每个 commit 的 message 里都写了 Log 与 Influence,请按 Influence 逐条跑:
  1. 两个 SSD 窗口部分重叠,点上层窗口的标题栏/边框/内容区 → 下层窗口保持在原层级、不被激活。
  2. 点下层窗口露出来的部分 → 它仍被激活并提升到最上层。
  3. overlay / X11 above(_NET_WM_STATE_ABOVE)窗口盖住一个刚激活过的窗口 → 按下后者可见部分仍正常激活提升。
  4. X11 below(_NET_WM_STATE_BELOW)窗口被激活过之后,点它上面的普通窗口 → 正常聚焦。
  5. 显示在所有工作区的窗口盖住普通窗口时,重复第 1 条。
  6. 最大化的 CSD 窗口在上、SSD 窗口在下 → 点上层窗口的透明边 → 下层不被抬起、不被激活。
  7. CSD 模态对话框压住另一个窗口的标题栏 → 点对话框的透明圆角 → 下面那个窗口不被抬起、不被激活。
  8. 回归:标题栏单击/双击/拖动/右键/触屏长按、三个标题栏按钮、边框缩放、桌面/面板/dock 点击。

已知边界(未在本 PR 处理)

  • 覆盖判定把「吸附蒙层(snap mask)」排除在外:它是边缘平铺时 dde-shell 画的全屏 overlay,treeland 侧不校验它的输入区域,依赖客户端给它设空输入区域;若客户端给它留了输入,按下会落在蒙层自身,不会造成「整屏点不动」。
  • 覆盖矩形用 QRectF(0, 0, other->size()),不含 XdgShadow 环带:方向是「漏判」,误差在 shadowBlurRadius 量级,看起来像阴影压住的位置仍可能抬起下层。
  • 标题栏按钮用「当前光标位置」判定(按钮回调拿不到按下点);圆角不计入矩形判定。
  • isTopmostWindowAt() 会遍历整棵 scene,只允许用在按下/单击路径,不要放进 hover/motion 这类每帧路径。

相关

Summary by Sourcery

Ensure window presses only activate, move, resize, or operate on the visually topmost window at the pointer location.

Bug Fixes:

  • Prevent presses delivered through uncovered areas of an upper window from activating or raising a lower window.
  • Apply consistent topmost-window checks to title-bar actions, resize borders, and window buttons.

Enhancements:

  • Unify title-bar mouse, drag, context-menu, double-click, and touch long-press handling while preserving correct press ownership.

@deepin-ci-robot

Copy link
Copy Markdown

Skipping CI for Draft Pull Request.
If you want CI signal for your change, please convert it to an actual PR.
You can still manually trigger a test run with /test all

@deepin-ci-robot

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: deepin-wm

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@sourcery-ai

sourcery-ai Bot commented Nov 5, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

API

Add the shared topmost-window hit-test API and implement it using the render window’s paint order, including the cursor-position helper used by window chrome.

  • src/surface/surfacewrapper.cpp L20, L953-1005
  • src/surface/surfacewrapper.h L204-211

Uses of the API

Use the shared hit test to skip activation when a press delivered to a window is covered by another window.

  • src/input/compositorinputfilter.cpp L304-310

UI

Route title-bar gestures through one press owner and gate activation, moving, menus, maximize, and window-button actions on the press belonging to this window.

  • src/core/qml/TitleBar.qml L43-101, L144-147 (+2 small edits)

Require the window under the cursor to be topmost before starting a resize from its decoration border.

  • src/core/qml/Decoration.qml L62

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

1. A press on the active window could raise and focus a window stacked below
   it, which showed up with server-side-decoration title bars.
2. Cause: the press landed on a part of the window on top that owns no client
   surface (a compositor-drawn title bar, the window border, a transparent
   margin), so it was delivered to the window below, and both the client press
   path and the compositor chrome activated whatever surface received it.
3. Fix: SurfaceWrapper::isTopmostWindowAt() tells whether a window painted above
   this one covers the position. The paint order comes from waylib's
   WOutputRenderWindow::paintOrderItemList(), so z-order, workspaces and
   show-on-all-workspace containers are honoured by construction; child windows
   (dialogs, X11 transients) cover like any other window. afterHandleEvent() and
   the resize border skip the activation when the answer is no.
4. Snap masks are not such a window: the edge-tiling overlay dde-shell draws is
   full-screen but is expected to keep an empty input region, so counting it
   would make every window look covered. Layer shells and popups keep their own
   stacking and are filtered by type as before.

Log: 按下当前激活窗口(含标题栏、边框、透明边距)不再把下方窗口抬起,也不再因此抢走焦点。

Influence:
1. 两个带 SSD 标题栏的窗口部分重叠,点击上层窗口的标题栏与内容区,确认下层窗口不被抬起、也不被激活。
2. 点击下层窗口露出来的部分,确认它仍被激活并提升到最上层。
3. overlay / X11 above 窗口盖住刚激活过的窗口,按下后者可见部分,确认仍正常激活提升。
4. X11 below(_NET_WM_STATE_BELOW)窗口被激活过之后,点它上面的窗口,确认正常聚焦。
5. 显示在所有工作区的窗口、以及 CSD 对话框压住另一个窗口标题栏时,重复第 1 条。
6. 边缘平铺触发吸附蒙层后,确认窗口仍能被点击激活(蒙层不参与覆盖判定)。
7. 点击桌面、面板、dock,以及缩放边框、移动窗口,确认行为不变。

Multica Issue: WM-936
1. A press can reach this window's title bar while the cursor is over the window
   on top: when that window owns no input at the point (a transparent margin or
   the window border) the press falls through, and the title bar below then
   activates its own window, which restacks it above the window that was
   pressed.
2. Cause: the title bar acted on every press it received, without asking whether
   it is the window under the cursor.
3. Fix: one MouseArea owns the title bar presses (click activates, double click
   toggles maximize, dragging past the drag threshold moves the window, right
   click and touch long press open the window menu), and the ownership test runs
   once per press. A TapHandler/DragHandler variant gated its drag on the cursor
   position at drag time and was reverted after the pointer wedged on real
   hardware; that cause is still unknown and is not addressed here.

Log: 上层窗口没有输入的位置落到下层标题栏时,标题栏不再激活、移动本窗口或弹出窗口菜单。

Influence:
1. 上层窗口点击/拖动它的 SSD 标题栏,下层窗口不被抬起、不被激活。
2. 上层窗口是没有 Decoration 的状态(最大化/全屏/平铺)或 CSD 窗口时,按它透明的边角,
   落到下层标题栏的位置,确认下层不被激活、不被抬起。
3. 标题栏单击激活、双击最大化/还原、拖动移动、右键菜单、触屏长按、三个窗口按钮均不受影响。
4. 拖动标题栏移动窗口时,过程中与松手后都不出现窗口菜单。

Multica Issue: WM-936
@deepin-wm
deepin-wm force-pushed the agent/bot/ef5b241da5b3 branch from a1c0405 to 9ecd5f2 Compare November 9, 2026 10:49
@deepin-wm
deepin-wm marked this pull request as ready for review November 10, 2026 02:42

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 4 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="src/core/qml/TitleBar.qml" line_range="146" />
<code_context>
                     onClicked: {
+                        // A covered title bar can be reached through an area of the
+                        // window above that owns no input; its buttons stay inert then.
+                        if (!surface.isTopmostWindowUnderCursor())
+                            return
                         surface.minimize()
</code_context>
<issue_to_address>
**Covered buttons still activate**

When a press starts on a covered part of a lower window’s title-bar button and ends on an exposed part of that same button, `onClicked` checks only the release-time position and does not record ownership on press, so the lower window’s button action runs even though the press began on the window above; the lower window is minimized, maximized, or closed.

Record whether the button belongs to the topmost window when pressed, and run its action only if it did.

Also at `src/core/qml/TitleBar.qml:165-166`, `src/core/qml/TitleBar.qml:191`.
</issue_to_address>

### Comment 2
<location path="src/core/qml/TitleBar.qml" line_range="146" />
<code_context>
                     onClicked: {
+                        // A covered title bar can be reached through an area of the
+                        // window above that owns no input; its buttons stay inert then.
+                        if (!surface.isTopmostWindowUnderCursor())
+                            return
                         surface.minimize()
</code_context>
<issue_to_address>
**Valid actions use the wrong position**

When a touchscreen press reaches a title-bar button or resize border while the seat cursor remains elsewhere, or a secondary-seat pointer is at a different position from the primary-seat pointer, the `TitleBar.qml` handlers call `isTopmostWindowUnderCursor()`, which checks the primary seat’s cursor instead of the touch position or the invoking seat’s pointer position, so valid actions are suppressed or actions on covered windows are allowed.

Check topmost-window status using the event position and the seat that received the event.

Also at `src/core/qml/TitleBar.qml:165`, `src/core/qml/TitleBar.qml:191`, `src/core/qml/Decoration.qml:62`, `src/surface/surfacewrapper.cpp:995-1003`.
</issue_to_address>

### Comment 3
<location path="src/surface/surfacewrapper.cpp" line_range="987" />
<code_context>
+            continue;
+        if (window->type() != Type::XdgToplevel && window->type() != Type::XWayland)
+            continue;
+        if (window->mapRectToScene(QRectF(QPointF(0, 0), window->size())).contains(scenePos))
+            return false;
+    }
</code_context>
<issue_to_address>
**Shadow presses still raise lower windows**

When the press lands in a visible shadow band outside the upper window’s bounds, `isTopmostWindowAt()` checks only the upper wrapper’s `size()`, so a press in its visible XdgShadow band falls through the coverage test and the lower window is activated and raised.

Include the upper window’s visible shadow extents in the coverage test, or hit-test its rendered shadow shape.
</issue_to_address>

### Comment 4
<location path="src/core/qml/TitleBar.qml" line_range="99-100" />
<code_context>
+        onPressAndHold: function (mouse) {
+            // Touch screen only, and never mid-drag: this timer is not stopped by
+            // movement, while a dragged window keeps the cursor over the same spot.
+            if (pressBelongsToWindow && pressWasTouch && !movingWindow)
+                surface.windowMenuRequested(Qt.point(mouse.x, mouse.y))
+        }
     }
</code_context>
<issue_to_address>
**Touch long press activates window**

When a touch long press opens the menu on an inactive window and the finger is released, `onPressAndHold` opens the window menu, but releasing the same left-button press still reaches `onClicked`, which activates the surface. Long-pressing an inactive window therefore also focuses and raises it when the menu is invoked.

Track that the press-and-hold handler ran and suppress the corresponding click action on release.

Also at `src/core/qml/TitleBar.qml:89`.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread src/core/qml/TitleBar.qml
onClicked: {
// A covered title bar can be reached through an area of the
// window above that owns no input; its buttons stay inert then.
if (!surface.isTopmostWindowUnderCursor())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium · Covered buttons still activate

When a press starts on a covered part of a lower window’s title-bar button and ends on an exposed part of that same button, onClicked checks only the release-time position and does not record ownership on press, so the lower window’s button action runs even though the press began on the window above; the lower window is minimized, maximized, or closed.

Record whether the button belongs to the topmost window when pressed, and run its action only if it did.

Also at src/core/qml/TitleBar.qml:165-166, src/core/qml/TitleBar.qml:191.

Prompt for AI agents
In `src/core/qml/TitleBar.qml` at line 146:

**Covered buttons still activate**

When a press starts on a covered part of a lower window’s title-bar button and ends on an exposed part of that same button, `onClicked` checks only the release-time position and does not record ownership on press, so the lower window’s button action runs even though the press began on the window above; the lower window is minimized, maximized, or closed.

Record whether the button belongs to the topmost window when pressed, and run its action only if it did.

Also at `src/core/qml/TitleBar.qml:165-166`, `src/core/qml/TitleBar.qml:191`.

Comment thread src/core/qml/TitleBar.qml
onClicked: {
// A covered title bar can be reached through an area of the
// window above that owns no input; its buttons stay inert then.
if (!surface.isTopmostWindowUnderCursor())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium · Valid actions use the wrong position

When a touchscreen press reaches a title-bar button or resize border while the seat cursor remains elsewhere, or a secondary-seat pointer is at a different position from the primary-seat pointer, the TitleBar.qml handlers call isTopmostWindowUnderCursor(), which checks the primary seat’s cursor instead of the touch position or the invoking seat’s pointer position, so valid actions are suppressed or actions on covered windows are allowed.

Check topmost-window status using the event position and the seat that received the event.

Also at src/core/qml/TitleBar.qml:165, src/core/qml/TitleBar.qml:191, src/core/qml/Decoration.qml:62, src/surface/surfacewrapper.cpp:995-1003.

Prompt for AI agents
In `src/core/qml/TitleBar.qml` at line 146:

**Valid actions use the wrong position**

When a touchscreen press reaches a title-bar button or resize border while the seat cursor remains elsewhere, or a secondary-seat pointer is at a different position from the primary-seat pointer, the `TitleBar.qml` handlers call `isTopmostWindowUnderCursor()`, which checks the primary seat’s cursor instead of the touch position or the invoking seat’s pointer position, so valid actions are suppressed or actions on covered windows are allowed.

Check topmost-window status using the event position and the seat that received the event.

Also at `src/core/qml/TitleBar.qml:165`, `src/core/qml/TitleBar.qml:191`, `src/core/qml/Decoration.qml:62`, `src/surface/surfacewrapper.cpp:995-1003`.

continue;
if (window->type() != Type::XdgToplevel && window->type() != Type::XWayland)
continue;
if (window->mapRectToScene(QRectF(QPointF(0, 0), window->size())).contains(scenePos))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium · Shadow presses still raise lower windows

When the press lands in a visible shadow band outside the upper window’s bounds, isTopmostWindowAt() checks only the upper wrapper’s size(), so a press in its visible XdgShadow band falls through the coverage test and the lower window is activated and raised.

Include the upper window’s visible shadow extents in the coverage test, or hit-test its rendered shadow shape.

Prompt for AI agents
In `src/surface/surfacewrapper.cpp` at line 987:

**Shadow presses still raise lower windows**

When the press lands in a visible shadow band outside the upper window’s bounds, `isTopmostWindowAt()` checks only the upper wrapper’s `size()`, so a press in its visible XdgShadow band falls through the coverage test and the lower window is activated and raised.

Include the upper window’s visible shadow extents in the coverage test, or hit-test its rendered shadow shape.

Comment thread src/core/qml/TitleBar.qml
Comment on lines +99 to +100
if (pressBelongsToWindow && pressWasTouch && !movingWindow)
surface.windowMenuRequested(Qt.point(mouse.x, mouse.y))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium · Touch long press activates window

When a touch long press opens the menu on an inactive window and the finger is released, onPressAndHold opens the window menu, but releasing the same left-button press still reaches onClicked, which activates the surface. Long-pressing an inactive window therefore also focuses and raises it when the menu is invoked.

Track that the press-and-hold handler ran and suppress the corresponding click action on release.

Also at src/core/qml/TitleBar.qml:89.

Prompt for AI agents
In `src/core/qml/TitleBar.qml` at lines 99-100:

**Touch long press activates window**

When a touch long press opens the menu on an inactive window and the finger is released, `onPressAndHold` opens the window menu, but releasing the same left-button press still reaches `onClicked`, which activates the surface. Long-pressing an inactive window therefore also focuses and raises it when the menu is invoked.

Track that the press-and-hold handler ran and suppress the corresponding click action on release.

Also at `src/core/qml/TitleBar.qml:89`.

@Groveer

Groveer commented Nov 10, 2026

Copy link
Copy Markdown
Contributor

被 #1612 修复

@Groveer Groveer closed this Nov 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants