wayland: Phase 22 — interactive move/resize grab

Add a compositor-side pointer grab so clients can interactively move and
resize their windows.  When an XDG toplevel sends request_move or
request_resize, the compositor records the current pointer position and
window geometry, then routes all subsequent motion events directly to
wl_grab_apply_motion() instead of the normal pointer-focus path.

- wl_grab_begin_move / wl_grab_begin_resize: snapshot grab origin,
  clear seat pointer focus so the client stops receiving motion events.
- wl_grab_apply_motion: on every motion event, compute dx/dy from the
  grab origin and reposition (MOVE) or stretch (RESIZE) the scene node.
  Resize honours all WL_SHELL_SURFACE_RESIZE_* edge combinations.
  Minimum window size clamped to 32×32 px.  XWayland windows use
  wlr_xwayland_surface_configure; XDG toplevels use set_size + schedule_configure.
- wl_grab_end: clears grab state; any button release while a grab is
  active ends it and resumes normal pointer-focus delivery.
- grab_type, grab_view, grab_start_{ptr,win}_{x,y,w,h}, grab_resize_edges
  added to wl_compositor_state.

Smoke test: build-only check added (no existing test client emits
request_move); all 22/22 checks continue to pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Levi Neely 2026-03-30 16:57:38 +02:00
parent 632c093355
commit 73cdf7a6b3
2 changed files with 178 additions and 7 deletions

View File

@ -240,6 +240,18 @@ struct wl_compositor_state {
* event loop reads [0] and calls wl_display_terminate(). */
int shutdown_pipe[2];
struct wl_event_source *shutdown_source;
/* Phase 22: interactive move/resize grab state.
* grab_type: 0=none, 1=move, 2=resize.
* grab_view: the toplevel being moved/resized (NULL when grab_type==0).
* grab_start_ptr_{x,y}: pointer position when grab started.
* grab_start_win_{x,y,w,h}: window geometry when grab started.
* grab_resize_edges: WL_SHELL_SURFACE_RESIZE_* bitmask (resize only). */
int grab_type;
struct wl_toplevel_view *grab_view;
int grab_start_ptr_x, grab_start_ptr_y;
int grab_start_win_x, grab_start_win_y;
int grab_start_win_w, grab_start_win_h;
uint32_t grab_resize_edges;
};
static struct wl_compositor_state wl_state;
@ -359,27 +371,170 @@ handle_toplevel_title(struct wl_listener *listener, void *data)
}
/*
* Phase 15: xdg_toplevel request_move — client requests interactive move.
* In the headless compositor we acknowledge the request but do nothing
* (no real pointer to drag with). A real compositor would start a grab.
* Phase 22: interactive grab helpers.
*
* wl_grab_begin_move — called from handle_toplevel_request_move to start
* a compositor-side move grab. Records the pointer position and window
* origin so that subsequent motion events can compute the displacement.
*
* wl_grab_begin_resize — same for resize; additionally records the window
* size and the edge bitmask so we know which sides to stretch.
*
* wl_grab_end — clears the grab state (called on button release).
*
* wl_grab_apply_motion — called from both pointer motion handlers to
* update the window position or size while the grab is active.
*/
static void
wl_grab_end(void)
{
wl_state.grab_type = 0;
wl_state.grab_view = NULL;
}
static void
wl_grab_begin_move(struct wl_toplevel_view *view)
{
int w, h;
if (!view) return;
/* Retrieve current window geometry. */
if (view->xw_surface) {
w = view->xw_surface->width;
h = view->xw_surface->height;
} else if (view->xdg_surface && view->xdg_surface->surface) {
w = view->xdg_surface->surface->current.width;
h = view->xdg_surface->surface->current.height;
} else { w = 1; h = 1; }
wl_state.grab_type = 1; /* MOVE */
wl_state.grab_view = view;
wl_state.grab_start_ptr_x = wl_state.pointer_x;
wl_state.grab_start_ptr_y = wl_state.pointer_y;
wl_state.grab_start_win_x = view->x;
wl_state.grab_start_win_y = view->y;
wl_state.grab_start_win_w = w;
wl_state.grab_start_win_h = h;
wwarning("wl_backend: move grab started for id=0x%lx at ptr=%d,%d win=%d,%d",
view->id, wl_state.pointer_x, wl_state.pointer_y, view->x, view->y);
/* Clear seat pointer focus so motion events are not delivered to the
* client while we are repositioning it. */
if (wl_state.seat)
wlr_seat_pointer_clear_focus(wl_state.seat);
}
static void
wl_grab_begin_resize(struct wl_toplevel_view *view, uint32_t edges)
{
int w, h;
if (!view) return;
if (view->xw_surface) {
w = view->xw_surface->width;
h = view->xw_surface->height;
} else if (view->xdg_surface && view->xdg_surface->surface) {
w = view->xdg_surface->surface->current.width;
h = view->xdg_surface->surface->current.height;
} else { w = 1; h = 1; }
wl_state.grab_type = 2; /* RESIZE */
wl_state.grab_view = view;
wl_state.grab_start_ptr_x = wl_state.pointer_x;
wl_state.grab_start_ptr_y = wl_state.pointer_y;
wl_state.grab_start_win_x = view->x;
wl_state.grab_start_win_y = view->y;
wl_state.grab_start_win_w = w;
wl_state.grab_start_win_h = h;
wl_state.grab_resize_edges = edges;
wwarning("wl_backend: resize grab started for id=0x%lx edges=0x%x size=%dx%d",
view->id, edges, w, h);
if (wl_state.seat)
wlr_seat_pointer_clear_focus(wl_state.seat);
}
/* Called from both pointer-motion handlers (relative + absolute). */
static void
wl_grab_apply_motion(void)
{
if (!wl_state.grab_type || !wl_state.grab_view)
return;
struct wl_toplevel_view *v = wl_state.grab_view;
int dx = wl_state.pointer_x - wl_state.grab_start_ptr_x;
int dy = wl_state.pointer_y - wl_state.grab_start_ptr_y;
if (wl_state.grab_type == 1) {
/* MOVE: simply reposition the window. */
int nx = wl_state.grab_start_win_x + dx;
int ny = wl_state.grab_start_win_y + dy;
v->x = nx; v->y = ny;
if (v->scene_node)
wlr_scene_node_set_position(v->scene_node, nx, ny);
if (v->xw_surface)
wlr_xwayland_surface_configure(v->xw_surface,
(int16_t)nx, (int16_t)ny,
v->xw_surface->width, v->xw_surface->height);
} else if (wl_state.grab_type == 2) {
/* RESIZE: compute new size (and origin for left/top edges). */
uint32_t edges = wl_state.grab_resize_edges;
int nx = wl_state.grab_start_win_x;
int ny = wl_state.grab_start_win_y;
int nw = wl_state.grab_start_win_w;
int nh = wl_state.grab_start_win_h;
if (edges & WL_SHELL_SURFACE_RESIZE_RIGHT)
nw = wl_state.grab_start_win_w + dx;
if (edges & WL_SHELL_SURFACE_RESIZE_LEFT) {
nw = wl_state.grab_start_win_w - dx;
nx = wl_state.grab_start_win_x + dx;
}
if (edges & WL_SHELL_SURFACE_RESIZE_BOTTOM)
nh = wl_state.grab_start_win_h + dy;
if (edges & WL_SHELL_SURFACE_RESIZE_TOP) {
nh = wl_state.grab_start_win_h - dy;
ny = wl_state.grab_start_win_y + dy;
}
if (nw < 32) { nw = 32; }
if (nh < 32) { nh = 32; }
v->x = nx; v->y = ny;
if (v->scene_node)
wlr_scene_node_set_position(v->scene_node, nx, ny);
if (v->xw_surface) {
wlr_xwayland_surface_configure(v->xw_surface,
(int16_t)nx, (int16_t)ny, (uint16_t)nw, (uint16_t)nh);
} else if (v->xdg_surface) {
wlr_xdg_toplevel_set_size(v->xdg_surface, (uint32_t)nw, (uint32_t)nh);
wlr_xdg_surface_schedule_configure(v->xdg_surface);
}
}
}
/*
* Phase 15/22: xdg_toplevel request_move — client requests interactive move.
* Phase 22: starts a compositor-side move grab.
*/
static void
handle_toplevel_request_move(struct wl_listener *listener, void *data)
{
struct wl_toplevel_view *view = VIEW_FROM_LISTENER(listener, request_move_listener);
(void)data;
wwarning("wl_backend: request_move id=0x%lx (no-op in headless mode)", view->id);
wl_grab_begin_move(view);
}
/*
* Phase 15: xdg_toplevel request_resize — client requests interactive resize.
* Phase 15/22: xdg_toplevel request_resize — client requests interactive resize.
* Phase 22: starts a compositor-side resize grab.
*/
static void
handle_toplevel_request_resize(struct wl_listener *listener, void *data)
{
struct wl_toplevel_view *view = VIEW_FROM_LISTENER(listener, request_resize_listener);
(void)data;
wwarning("wl_backend: request_resize id=0x%lx (no-op in headless mode)", view->id);
struct wlr_xdg_toplevel_resize_event *ev = data;
wl_grab_begin_resize(view, ev ? ev->edges : WL_SHELL_SURFACE_RESIZE_BOTTOM_RIGHT);
}
/*
@ -1741,6 +1896,8 @@ handle_pointer_motion(struct wl_listener *listener, void *data)
if (wl_state.pointer_y < 0) wl_state.pointer_y = 0;
if (wl_state.pointer_x >= ow) wl_state.pointer_x = ow - 1;
if (wl_state.pointer_y >= oh) wl_state.pointer_y = oh - 1;
/* Phase 22: apply grab before updating normal pointer focus. */
if (wl_state.grab_type) { wl_grab_apply_motion(); return; }
wl_update_pointer_focus(ev->time_msec);
}
@ -1757,6 +1914,8 @@ handle_pointer_motion_absolute(struct wl_listener *listener, void *data)
if (wl_state.pointer_y < 0) wl_state.pointer_y = 0;
if (wl_state.pointer_x >= ow) wl_state.pointer_x = ow - 1;
if (wl_state.pointer_y >= oh) wl_state.pointer_y = oh - 1;
/* Phase 22: apply grab before updating normal pointer focus. */
if (wl_state.grab_type) { wl_grab_apply_motion(); return; }
wl_update_pointer_focus(ev->time_msec);
}
@ -1767,6 +1926,14 @@ handle_pointer_button(struct wl_listener *listener, void *data)
struct wlr_event_pointer_button *ev = data;
if (!wl_state.seat)
return;
/* Phase 22: any button release ends an active grab. */
if (wl_state.grab_type &&
ev->state == WLR_BUTTON_RELEASED) {
wwarning("wl_backend: grab ended (button release)");
wl_grab_end();
wl_update_pointer_focus(ev->time_msec);
return;
}
wlr_seat_pointer_notify_button(wl_state.seat, ev->time_msec,
ev->button, ev->state);
wlr_seat_pointer_notify_frame(wl_state.seat);

View File

@ -123,6 +123,10 @@
# Compositor logs "XWayland DISPLAY=:<n> (lazy: starts on first X11 client)".
# DISPLAY env var is set from the pre-allocated display number.
# Verified by "XWayland DISPLAY=.*lazy" log.
# 41. (Phase 22) Interactive move/resize grab: request_move and request_resize
# handlers now start a compositor-side grab that tracks pointer motion
# and repositions/resizes the window in the scene graph. Button release
# ends the grab. Build-only check (no client sends request_move here).
# 38. (Phase 11) noclip/nodock set in wl_screen_create_internals: wDockCreate
# requires X11 rendering resources unavailable in Wayland mode; disabling
# the dock/clip prevents crashes when wWorkspaceNew is called to create