Notes / x11chromiumxfwm4kwinwindow-managercsd
Chromium window drag warps the window (xfwm4) or the cursor (KWin) on X11
Dragging a Chromium window by its own title bar on X11 makes the window jump hundreds of pixels (often off-screen) under xfwm4, or makes the mouse cursor jump under KWin. Chromium sends a _NET_WM_MOVERESIZE request with wrong root coordinates; turning on "Use system title bar and borders" avoids it.
Symptoms #
On an X11 desktop, Chromium windows "get lost":
- You start dragging a Chromium window by its tab strip or title area and the window jumps by several hundred pixels, often mostly off-screen (only a thin strip left visible, sometimes behind the panel).
- Clicks land in the wrong place for a while, as if the window were somewhere other than where it is drawn.
- It looks random: a given window does it once, then behaves until some later point.
- After switching the window manager from xfwm4 to KWin, the window stops jumping, but now the mouse cursor jumps 10–350 px at the start of a drag.
Other applications that use the window manager's own title bar are not affected.
Root cause #
Chromium draws its own title bar (client-side decorations, the default). When you drag that
title bar, Chromium does not move the window itself. It asks the window manager to start an
interactive move by sending an EWMH
_NET_WM_MOVERESIZE
client message. That message contains x_root, y_root: the pointer position in root
(screen) coordinates, as the client believes it to be.
Chromium sometimes sends wrong root coordinates. Our capture under KWin:
textSendEvent sender=Chromium type=_NET_WM_MOVERESIZE data=(1413, 203, 8, 0, 0)
WarpPointer sender=KWin dst=(1413, 203)
pointer motion (1606,52) -> (1413,203) 344 px in 1 ms
Direction 8 is _NET_WM_MOVERESIZE_MOVE. The real pointer was at (1606, 52). Chromium
claimed (1413, 203), an error of (193, 151) px. The most likely explanation is that
Chromium works out the root position from its cached window bounds plus the pointer's
offset inside the window, and the cache is stale (for example after the window manager has
moved or placed the window). That fits the "each window does it once" pattern: after the
jump, the cache matches reality again.
The two window managers handle the bad coordinates differently:
| Window manager | What it does with x_root, y_root |
What you see |
|---|---|---|
| xfwm4 4.20 | Uses them as the grab origin for the move: window x = start x + (pointer x − press x), with a wrong start | The window jumps by the error (we measured −316, −332 and −649 px) |
| KWin 6.7 (X11) | Warps the pointer to the given position before starting the move (X11Window::NETMoveResize: "move cursor to the provided position to prevent the window jumping there on first movement") |
The cursor jumps by the error; the window stays put |
So the bug is in the client's request, and each window manager shows it in its own way.
KWin's behaviour is documented in its source. The xfwm4 part was measured from window
geometry before and after each jump. Our early request tracer under xfwm4 did not log
SendEvent, so the bad payload was only captured directly under KWin.
Fix #
Turn off Chromium's client-side title bar so that moves are started by the window manager from the real pointer event:
Chromium → Settings → Appearance → "Use system title bar and borders" → On
This is a per-profile setting, so turn it on in every profile you use. After the change,
the window manager draws the title bar (xprop _NET_FRAME_EXTENTS on the window shows a
non-zero top extent), no _NET_WM_MOVERESIZE is sent for ordinary drags, and the jumps stop.
This was confirmed working on the affected machine.
The same reasoning applies to other client-side-decorated (GTK/libadwaita) apps that show the same symptom: anything that makes the window manager draw the title bar avoids the client-supplied coordinates.
Changing the window manager alone is not a fix. It only changes which thing jumps.
Recovering a window that is already off-screen #
bashxdotool search --name 'Chromium' | while read w; do
xdotool getwindowname "$w"; xdotool getwindowgeometry "$w"
done
xdotool windowmove <window-id> 100 100
How it was found #
The jumps were rare, so it took permanent watchers, run as systemd user units on both X displays, until one was caught with numbers:
- Window geometry watcher: logs every move of top-level windows and flags any window that ends up mostly off-screen.
- Pointer watcher: logs button presses and motion during drags (no keys).
- Request watcher: uses the XRecord extension to log every
SendEventClientMessage any client sends, with the sender's resource-ID base, so you can see who asked for a move and with what coordinates. - A short-lived probe logging every
XWarpPointerrequest and its sender, which is what showed KWin moving the cursor ~150 ms after button press (Chromium's drag threshold).
The request watcher is the useful one. It is short and needs only python-xlib:
python#!/usr/bin/env python3
"""Log SendEvent ClientMessages (e.g. _NET_WM_MOVERESIZE) via XRecord.
Usage: xreqlog.py DISPLAY OUTFILE"""
import struct, sys
from datetime import datetime
from Xlib import display
from Xlib.ext import record
SEND_EVENT, CLIENT_MESSAGE = 25, 33
def main():
d, d2 = display.Display(sys.argv[1]), display.Display(sys.argv[1])
f = open(sys.argv[2], "a", buffering=1)
names = {}
def aname(a):
if a not in names:
try: names[a] = d2.get_atom_name(a)
except Exception: names[a] = hex(a)
return names[a]
def callback(reply):
if reply.category != record.FromClient or reply.client_swapped:
return
data = reply.data
while len(data) >= 4:
n = int.from_bytes(data[2:4], "little") * 4
if n < 4 or n > len(data):
break
req, data = data[:n], data[n:]
if req[0] != SEND_EVENT or len(req) < 44:
continue
ev = req[12:]
if ev[0] != CLIENT_MESSAGE:
continue
win = int.from_bytes(ev[4:8], "little")
msg = int.from_bytes(ev[8:12], "little")
vals = struct.unpack("<5i", ev[12:32]) if ev[1] == 32 else tuple(ev[12:17])
t = datetime.now().isoformat(timespec="milliseconds")
f.write(f"{t} id_base={hex(reply.id_base)} win={hex(win)} "
f"type={aname(msg)} data={vals}\n")
ctx = d.record_create_context(0, [record.AllClients], [{
"core_requests": (0, 127), "core_replies": (0, 0),
"ext_requests": (0, 0, 0, 0), "ext_replies": (0, 0, 0, 0),
"delivered_events": (0, 0), "device_events": (0, 0), "errors": (0, 0),
"client_started": False, "client_died": False}])
try:
d.record_enable_context(ctx, callback)
finally:
d.record_free_context(ctx)
main()
Compare the data=(x, y, 8, …) of a _NET_WM_MOVERESIZE line with where the pointer
actually was at that moment (xdotool getmouselocation, or a pointer logger). If they
differ, you have this bug.
Things that were checked and ruled out along the way: X Shape input regions (none on the Chromium windows), compositor drawing offsets (screenshots matched the X window tree), and Chromium's saved window state (it never contained the off-screen positions).
References #
- EWMH spec,
_NET_WM_MOVERESIZE. - KDE bug 449105, "Dragging a window that was opened maximized moves the mouse cursor to the top left corner of the window" (Wayland; fixed in Plasma 6.1). A related cursor-jump issue, different trigger.
- KWin merge requests 5399 ("Make Window::interactiveMoveOffset() proportional") and 5421 ("x11: Fix interactive move offset").
- Unrelated noise you may see at the same time with xfwm4 4.20 + picom: a flood of
GLib-CRITICAL: g_hash_table_lookupin~/.xsession-errors(xfwm4 issue #855, cosmetic).