diff --git a/src/backend/wayland/wl_backend.c b/src/backend/wayland/wl_backend.c index 9dceb18..c1c80e2 100644 --- a/src/backend/wayland/wl_backend.c +++ b/src/backend/wayland/wl_backend.c @@ -111,6 +111,8 @@ #include /* Phase 18: multi-backend */ #include /* Phase 18: scene graph compositing */ #include /* Phase 18: buffer allocator */ +#include /* Phase 23: hardware cursor tracking */ +#include /* Phase 23: xcursor theme */ #include /* 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); } diff --git a/tests/wl-smoke-test.sh b/tests/wl-smoke-test.sh index 2acb0b2..1c0c07d 100755 --- a/tests/wl-smoke-test.sh +++ b/tests/wl-smoke-test.sh @@ -123,6 +123,16 @@ # Compositor logs "XWayland DISPLAY=: (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=: (lazy: starts on first