← Otto Developer GuideWindow Move
How Otto drags a window: xdg_toplevel.move, which a client-decorated window
sends when you grab its title bar, and the equivalent path for the titlebars
Otto draws itself.
The shape of the solution is Wayland’s standard one: the compositor installs a grab on the seat. While a grab is active, input events stop going to clients and go to the grab’s own handler instead. That is why dragging a window over another window doesn’t make the other window react — nothing downstream ever sees those events.
Entry points
Otto::move_request_xdg in src/shell/xdg.rs handles the client request. It
checks that the serial belongs to a live pointer or touch grab, that the
surface still maps to a window, and that the grab’s focus belongs to the same
client. Clients can and do send stale requests.
Otto defaults to server-side decorations, so most title bars are Otto’s own
surface-less input target and no client request is involved. Those go through
Otto::move_request_ssd instead, which skips the client-focus check and hands
straight to the same pointer path.
Pointer drags
Otto::begin_pointer_move installs a PointerMoveSurfaceGrab
(src/shell/grabs.rs) on the seat’s pointer with Focus::Clear, so no client
has pointer focus for the duration. Each motion event goes to
PointerMoveSurfaceGrab::motion, which computes the delta from the drag origin
and calls workspaces.map_window to reposition the window. The window’s view
layer is moved to match, in the owning output’s physical pixels, so
compositor-side UI stays in sync.
A maximized or tiled window is not restored at button-down but into the
drag: only once the pointer has travelled DRAG_THRESHOLD (6.0 logical
pixels). Restoring at the press would make a plain click on the title bar
unmaximize the window, and would eat the first press of the double click that
zooms it. Otto::restore_window_for_drag keeps the point the user grabbed
under the cursor and returns the window’s new origin, which the grab then
measures from.
A leaf of a workspace’s tiling tree leaves the tree on the same threshold
instead of being restored to a floating rect; the two are mutually exclusive.
While detached the layer is scaled about its origin, so the drag is measured
from the shrunk offset (shell::scaled_drag_origin) or the window would slide
out from under the pointer.
While Ctrl is held, the zone under the pointer is previewed through
workspaces.tiling_overlay. Ctrl is re-checked at release, because
active_zone is only refreshed on motion.
Touch drags
A TouchMoveSurfaceGrab (src/shell/grabs.rs) does the same through
workspaces.map_window, ignoring any event whose slot is not the one that
started the grab, so a second finger cannot hijack the drag. It keeps the last
touch location, because a touch-up carries none and the tiling drop needs one.
On this path a maximized window is unmaximized in move_request_xdg itself,
before the grab is installed. The grab point’s position within the maximized
window is kept as a ratio and reapplied to the restored size, so the window
stays under the finger.
Release
The pointer grab ends when the last button is released, the touch grab when the
starting slot lifts. A window dragged out of a tiling tree is dropped into the
slot the overlay was showing before the grab is unset — unset cancels a
detach that is still open, which would undo the insert. Otherwise a previewed
snap zone is applied, or the window’s popups are repositioned, since they are
not re-unconstrained during the drag. Under XWayland, the window’s new root
position is sent to the client either way: an X11 client places its own menus
from the position the server last reported.