wayland: Phase 23 — cursor image rendering via wlr_cursor + xcursor theme

Wire up wlr_cursor and wlr_xcursor_manager so the cursor is visible when
running as a nested Wayland compositor.

- wlr_cursor created in wl_display_open and attached to the output layout.
- wlr_xcursor_manager created with NULL (system default) theme at 24 px
  base size; wlr_xcursor_manager_load called at scale 1.0 at open time.
- In handle_backend_new_output, after the output joins the layout:
    - Load xcursor at the output's actual scale factor.
    - Set "left_ptr" as the initial cursor image.
    - Warp the hardware cursor to the current virtual pointer position.
  In nested mode this causes wlroots to send wl_pointer.set_cursor to
  the host compositor, making the cursor visible in the window.
- All pointer motion handlers (relative, absolute) and wl_pointer_warp /
  wl_pointer_warp_to now call wlr_cursor_warp after updating pointer_x/y,
  keeping the hardware cursor in sync with the virtual pointer position.
  handle_pointer_motion_absolute uses wlr_cursor_warp_absolute and reads
  back cursor->x/y for the pixel position (correct in all output layouts).
- Grab cursor shapes: wl_grab_begin_move sets "fleur"; wl_grab_begin_resize
  selects an edge-appropriate name (nw-resize, se-resize, etc.) via a new
  resize_edges_to_cursor() helper; wl_grab_end restores "left_ptr".
- wl_display_close destroys xcursor_mgr then cursor before display teardown.

Smoke test: new "cursor check" verifies "cursor created" + "cursor image
set to 'left_ptr'" log lines (23/23 checks pass).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Levi Neely 2026-03-30 17:03:36 +02:00
parent 73cdf7a6b3
commit aee57de37d
2 changed files with 133 additions and 8 deletions

View File

@ -111,6 +111,8 @@
#include <wlr/backend/multi.h> /* Phase 18: multi-backend */
#include <wlr/types/wlr_scene.h> /* Phase 18: scene graph compositing */
#include <wlr/render/allocator.h> /* Phase 18: buffer allocator */
#include <wlr/types/wlr_cursor.h> /* Phase 23: hardware cursor tracking */
#include <wlr/types/wlr_xcursor_manager.h> /* Phase 23: xcursor theme */
#include <wlr/util/box.h>
/* Generated xdg-shell protocol implementation (Wayland glue code) */
#include "protocols/xdg-shell-protocol.c"
@ -252,6 +254,9 @@ struct wl_compositor_state {
int grab_start_win_x, grab_start_win_y;
int grab_start_win_w, grab_start_win_h;
uint32_t grab_resize_edges;
/* Phase 23: hardware cursor and xcursor theme. */
struct wlr_cursor *cursor;
struct wlr_xcursor_manager *xcursor_mgr;
};
static struct wl_compositor_state wl_state;
@ -370,6 +375,38 @@ handle_toplevel_title(struct wl_listener *listener, void *data)
wWindowUpdateName(wwin, new_title);
}
/*
* Phase 23: set the cursor image by name from the loaded xcursor theme.
* No-op if cursor or xcursor_mgr was not initialised.
*/
static void
wl_set_cursor_image(const char *name)
{
if (!wl_state.cursor || !wl_state.xcursor_mgr)
return;
wlr_xcursor_manager_set_cursor_image(wl_state.xcursor_mgr, name, wl_state.cursor);
}
/*
* Phase 23: map WL_SHELL_SURFACE_RESIZE_* edge bitmask to an xcursor name.
*/
static const char *
resize_edges_to_cursor(uint32_t edges)
{
switch (edges & (WL_SHELL_SURFACE_RESIZE_TOP | WL_SHELL_SURFACE_RESIZE_BOTTOM |
WL_SHELL_SURFACE_RESIZE_LEFT | WL_SHELL_SURFACE_RESIZE_RIGHT)) {
case WL_SHELL_SURFACE_RESIZE_TOP: return "n-resize";
case WL_SHELL_SURFACE_RESIZE_BOTTOM: return "s-resize";
case WL_SHELL_SURFACE_RESIZE_LEFT: return "w-resize";
case WL_SHELL_SURFACE_RESIZE_RIGHT: return "e-resize";
case WL_SHELL_SURFACE_RESIZE_TOP_LEFT: return "nw-resize";
case WL_SHELL_SURFACE_RESIZE_TOP_RIGHT: return "ne-resize";
case WL_SHELL_SURFACE_RESIZE_BOTTOM_LEFT: return "sw-resize";
case WL_SHELL_SURFACE_RESIZE_BOTTOM_RIGHT: return "se-resize";
default: return "se-resize";
}
}
/*
* Phase 22: interactive grab helpers.
*
@ -390,6 +427,8 @@ wl_grab_end(void)
{
wl_state.grab_type = 0;
wl_state.grab_view = NULL;
/* Phase 23: restore default arrow cursor when grab ends. */
wl_set_cursor_image("left_ptr");
}
static void
@ -421,6 +460,8 @@ wl_grab_begin_move(struct wl_toplevel_view *view)
* client while we are repositioning it. */
if (wl_state.seat)
wlr_seat_pointer_clear_focus(wl_state.seat);
/* Phase 23: show a move cursor during the grab. */
wl_set_cursor_image("fleur");
}
static void
@ -450,6 +491,8 @@ wl_grab_begin_resize(struct wl_toplevel_view *view, uint32_t edges)
view->id, edges, w, h);
if (wl_state.seat)
wlr_seat_pointer_clear_focus(wl_state.seat);
/* Phase 23: show an appropriate resize cursor during the grab. */
wl_set_cursor_image(resize_edges_to_cursor(edges));
}
/* Called from both pointer-motion handlers (relative + absolute). */
@ -1161,6 +1204,23 @@ handle_backend_new_output(struct wl_listener *listener, void *data)
if (wl_state.output_layout)
wlr_output_layout_add_auto(wl_state.output_layout, output);
/* Phase 23: apply cursor image now that this output is in the layout.
* wlr_xcursor_manager_set_cursor_image iterates the output layout and
* sets the hardware cursor on each output; for the nested backend this
* sends wl_pointer.set_cursor to the host compositor, making the cursor
* visible. Also warp to the current virtual pointer position so the
* cursor appears at the right location from the first frame. */
if (wl_state.cursor && wl_state.xcursor_mgr) {
wlr_xcursor_manager_load(wl_state.xcursor_mgr, output->scale);
wlr_xcursor_manager_set_cursor_image(wl_state.xcursor_mgr,
"left_ptr", wl_state.cursor);
wlr_cursor_warp(wl_state.cursor, NULL,
(double)wl_state.pointer_x,
(double)wl_state.pointer_y);
wwarning("wl_backend: cursor image set to 'left_ptr' on output %s",
output->name);
}
/* Set window title when running as a nested compositor. */
if (wl_state.is_nested && wl_state.wl_backend_inner &&
wlr_backend_is_wl(wl_state.wl_backend_inner))
@ -1289,6 +1349,24 @@ wl_display_open(const char *name)
* can add the output to the layout when it fires. */
wl_state.output_layout = wlr_output_layout_create();
/* Phase 23: create hardware cursor tracker and load xcursor theme.
* wlr_cursor tracks the logical cursor position across the output
* layout and communicates cursor image changes to each output.
* In nested Wayland mode this calls wl_pointer.set_cursor on the host
* compositor so the cursor is visible in the window.
* Theme and image are applied in handle_backend_new_output after each
* output is added to the layout, ensuring the scale factor is known. */
wl_state.cursor = wlr_cursor_create();
if (wl_state.cursor) {
wlr_cursor_attach_output_layout(wl_state.cursor, wl_state.output_layout);
wl_state.xcursor_mgr = wlr_xcursor_manager_create(NULL, 24);
if (wl_state.xcursor_mgr)
wlr_xcursor_manager_load(wl_state.xcursor_mgr, 1.0f);
wwarning("wl_backend: cursor created, xcursor theme loaded");
} else {
wwarning("wl_backend: wlr_cursor_create failed; cursor will be invisible");
}
/* Phase 18: scene graph — MUST be created before wlr_backend_start so
* that handle_backend_new_output can create the scene_output immediately
* when the output fires (for nested Wayland, new_output fires during
@ -1647,6 +1725,16 @@ wl_display_close(void)
WL_LISTENER_REMOVE(wl_state.real_pointer_frame_listener);
#undef WL_LISTENER_REMOVE
/* Phase 23: destroy cursor and xcursor manager before display teardown. */
if (wl_state.xcursor_mgr) {
wlr_xcursor_manager_destroy(wl_state.xcursor_mgr);
wl_state.xcursor_mgr = NULL;
}
if (wl_state.cursor) {
wlr_cursor_destroy(wl_state.cursor);
wl_state.cursor = NULL;
}
/* Skip wl_display_destroy() and wl_display_destroy_clients().
*
* wl_display_destroy() fires the display's destroy signal which triggers
@ -1896,6 +1984,10 @@ 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 23: update hardware cursor position. */
if (wl_state.cursor)
wlr_cursor_warp(wl_state.cursor, NULL,
(double)wl_state.pointer_x, (double)wl_state.pointer_y);
/* 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);
@ -1906,14 +1998,23 @@ handle_pointer_motion_absolute(struct wl_listener *listener, void *data)
{
(void)listener;
struct wlr_event_pointer_motion_absolute *ev = data;
int ow = wl_state.output ? (int)wl_state.output->width : 1920;
int oh = wl_state.output ? (int)wl_state.output->height : 1080;
wl_state.pointer_x = (int)(ev->x * (double)ow);
wl_state.pointer_y = (int)(ev->y * (double)oh);
if (wl_state.pointer_x < 0) wl_state.pointer_x = 0;
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 23: use wlr_cursor_warp_absolute so wlroots maps the 0..1
* device-normalized coordinates to layout pixels and updates the
* hardware cursor in the nested window automatically. */
if (wl_state.cursor) {
wlr_cursor_warp_absolute(wl_state.cursor, ev->device, ev->x, ev->y);
wl_state.pointer_x = (int)wl_state.cursor->x;
wl_state.pointer_y = (int)wl_state.cursor->y;
} else {
int ow = wl_state.output ? (int)wl_state.output->width : 1920;
int oh = wl_state.output ? (int)wl_state.output->height : 1080;
wl_state.pointer_x = (int)(ev->x * (double)ow);
wl_state.pointer_y = (int)(ev->y * (double)oh);
if (wl_state.pointer_x < 0) wl_state.pointer_x = 0;
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);
@ -2060,6 +2161,10 @@ wl_pointer_warp(int dx, int dy)
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 23: update hardware cursor position. */
if (wl_state.cursor)
wlr_cursor_warp(wl_state.cursor, NULL,
(double)wl_state.pointer_x, (double)wl_state.pointer_y);
/* Phase 13: update pointer focus after relative warp */
wl_update_pointer_focus(0);
}
@ -2076,6 +2181,9 @@ wl_pointer_warp_to(int x, int y)
if (y >= oh) y = oh - 1;
wl_state.pointer_x = x;
wl_state.pointer_y = y;
/* Phase 23: update hardware cursor position. */
if (wl_state.cursor)
wlr_cursor_warp(wl_state.cursor, NULL, (double)x, (double)y);
/* Phase 13: update pointer focus after absolute warp */
wl_update_pointer_focus(0);
}

View File

@ -123,6 +123,16 @@
# 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.
# 42. (Phase 23) Cursor image rendering: wlr_cursor + wlr_xcursor_manager
# created and attached to output_layout; xcursor theme (default, 24px)
# loaded at scale 1.0; "left_ptr" image set on each output after it joins
# the layout. In nested Wayland mode this sends wl_pointer.set_cursor to
# the host compositor making the cursor visible. Grab begin/end switch to
# "fleur" / edge-resize cursors and restore "left_ptr" on release.
# wlr_cursor_warp called from all motion handlers and pointer_warp_to/
# warp_to so the hardware cursor tracks the virtual pointer position.
# Verified by "cursor created, xcursor theme loaded" +
# "cursor image set to 'left_ptr'" log lines.
# 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
@ -272,6 +282,13 @@ grep -q 'dock/clip disabled in Wayland mode' /tmp/wl-smoke-stderr \
|| die "compositor did not log dock/clip disabled (noclip/nodock not set in wl_screen_create_internals)"
echo " nodock check: dock/clip disabled in Wayland mode (noclip=1 nodock=1)"
# -- 4f3. Phase 23: cursor created and xcursor theme loaded ---------------
grep -q 'cursor created, xcursor theme loaded' /tmp/wl-smoke-stderr \
|| die "compositor did not log cursor creation (wlr_cursor_create or xcursor_manager_create failed)"
grep -q "cursor image set to 'left_ptr'" /tmp/wl-smoke-stderr \
|| die "compositor did not log cursor image set (wlr_xcursor_manager_set_cursor_image not called)"
echo " cursor check: wlr_cursor created, xcursor 'left_ptr' set on output"
# -- 4g. Phase 17: XWayland display number allocated (lazy start) --------
#
# wl_display_open() logs "XWayland DISPLAY=:<n> (lazy: starts on first