diff --git a/src/backend/wayland/wl_input_fwd.c b/src/backend/wayland/wl_input_fwd.c index 3836cb2..742dc09 100644 --- a/src/backend/wayland/wl_input_fwd.c +++ b/src/backend/wayland/wl_input_fwd.c @@ -1,10 +1,14 @@ #ifdef HAVE_CONFIG_H #include "config.h" #endif +#include +#include #include #include #include +#include #include +#include #include #include "wl_types.h" @@ -20,6 +24,40 @@ void handle_pointer_axis(struct wl_listener *listener, void *data); #define INPUT_LISTENER_REMOVE(l) \ do { if ((l).link.next) { wl_list_remove(&(l).link); wl_list_init(&(l).link); } } while (0) +struct wl_real_pointer_listener { + struct wlr_input_device *dev; + char *output_name; + double local_x, local_y; + struct wl_listener motion; + struct wl_listener motion_abs; + struct wl_listener button; + struct wl_listener frame; + struct wl_listener axis; + struct wl_listener destroy; + struct wl_list link; +}; + +static struct wl_real_pointer_listener * +real_pointer_from_listener(struct wl_listener *listener, size_t offset) +{ + return (struct wl_real_pointer_listener *)((char *)listener - offset); +} + +static void +real_pointer_update_global(struct wl_real_pointer_listener *pl) +{ + (void)pl; + /* Use wlr_cursor as the single source of truth for pointer position. + * wlr_cursor tracks the logical pointer in output-layout space; reading + * it back avoids the double-offset bug where lo->x was added to coords + * that were already in layout space (from wlr_cursor_absolute_to_layout_coords), + * and also fixes the missing lo->y addition for vertical layouts. */ + if (wl_state.cursor) { + wl_state.pointer_x = (int)wl_state.cursor->x; + wl_state.pointer_y = (int)wl_state.cursor->y; + } +} + static void handle_keyboard_destroy(struct wl_listener *listener, void *data) { @@ -32,17 +70,73 @@ handle_keyboard_destroy(struct wl_listener *listener, void *data) } static void -handle_pointer_destroy(struct wl_listener *listener, void *data) +real_pointer_handle_motion(struct wl_listener *listener, void *data) { - (void)listener; (void)data; - INPUT_LISTENER_REMOVE(wl_state.pointer_motion_listener); - INPUT_LISTENER_REMOVE(wl_state.real_pointer_motion_abs_listener); - INPUT_LISTENER_REMOVE(wl_state.real_pointer_button_listener); - INPUT_LISTENER_REMOVE(wl_state.real_pointer_frame_listener); - INPUT_LISTENER_REMOVE(wl_state.real_pointer_axis_listener); - INPUT_LISTENER_REMOVE(wl_state.real_pointer_destroy_listener); - wl_state.real_pointer_dev = NULL; - wl_state.pointer_dev = NULL; + /* Let handle_pointer_motion be the sole authority: it applies the + * delta to wl_state.pointer_x/y, clamps to screen bounds, and warps + * wlr_cursor. We must NOT pre-accumulate the delta into local_x/y + * because handle_pointer_motion would then double-apply it. */ + handle_pointer_motion(listener, data); +} + +static void +real_pointer_handle_motion_absolute(struct wl_listener *listener, void *data) +{ + /* Let handle_pointer_motion_absolute be the sole authority: it calls + * wlr_cursor_absolute_to_layout_coords, sets pointer_x/y, and warps + * the cursor. We must NOT duplicate that work here — the old code + * set local_x/y to layout coords then real_pointer_update_global + * added lo->x again, double-counting the output offset. */ + handle_pointer_motion_absolute(listener, data); +} + +static void +real_pointer_handle_button(struct wl_listener *listener, void *data) +{ + struct wl_real_pointer_listener *pl = + real_pointer_from_listener(listener, offsetof(struct wl_real_pointer_listener, button)); + real_pointer_update_global(pl); + handle_pointer_button(listener, data); +} + +static void +real_pointer_handle_frame(struct wl_listener *listener, void *data) +{ + struct wl_real_pointer_listener *pl = + real_pointer_from_listener(listener, offsetof(struct wl_real_pointer_listener, frame)); + real_pointer_update_global(pl); + handle_pointer_frame(listener, data); +} + +static void +real_pointer_handle_axis(struct wl_listener *listener, void *data) +{ + struct wl_real_pointer_listener *pl = + real_pointer_from_listener(listener, offsetof(struct wl_real_pointer_listener, axis)); + real_pointer_update_global(pl); + handle_pointer_axis(listener, data); +} + +static void +real_pointer_handle_destroy(struct wl_listener *listener, void *data) +{ + (void)data; + struct wl_real_pointer_listener *pl = + real_pointer_from_listener(listener, offsetof(struct wl_real_pointer_listener, destroy)); + INPUT_LISTENER_REMOVE(pl->motion); + INPUT_LISTENER_REMOVE(pl->motion_abs); + INPUT_LISTENER_REMOVE(pl->button); + INPUT_LISTENER_REMOVE(pl->frame); + INPUT_LISTENER_REMOVE(pl->axis); + INPUT_LISTENER_REMOVE(pl->destroy); + if (pl->link.next) + wl_list_remove(&pl->link); + if (wl_state.real_pointer_dev == pl->dev) { + wl_state.real_pointer_dev = NULL; + wl_state.pointer_dev = NULL; + } + free(pl->output_name); + free(pl); } void @@ -54,6 +148,11 @@ handle_backend_new_input(struct wl_listener *listener, void *data) switch (dev->type) { case WLR_INPUT_DEVICE_KEYBOARD: { struct wlr_keyboard *kb = wlr_keyboard_from_input_device(dev); + if (wl_state.real_keyboard_dev) + break; + INPUT_LISTENER_REMOVE(wl_state.real_keyboard_key_listener); + INPUT_LISTENER_REMOVE(wl_state.real_keyboard_modifiers_listener); + INPUT_LISTENER_REMOVE(wl_state.real_keyboard_destroy_listener); if (wl_state.xkb_keymap && !kb->xkb_state) wlr_keyboard_set_keymap(kb, wl_state.xkb_keymap); wl_state.real_keyboard_dev = dev; @@ -74,28 +173,59 @@ handle_backend_new_input(struct wl_listener *listener, void *data) } case WLR_INPUT_DEVICE_POINTER: { struct wlr_pointer *ptr = wlr_pointer_from_input_device(dev); - wl_state.real_pointer_dev = dev; - wl_state.pointer_dev = dev; - wl_state.pointer_motion_listener.notify = handle_pointer_motion; - wl_signal_add(&ptr->events.motion, - &wl_state.pointer_motion_listener); - wl_state.real_pointer_motion_abs_listener.notify = - handle_pointer_motion_absolute; - wl_signal_add(&ptr->events.motion_absolute, - &wl_state.real_pointer_motion_abs_listener); - wl_state.real_pointer_button_listener.notify = handle_pointer_button; - wl_signal_add(&ptr->events.button, - &wl_state.real_pointer_button_listener); - wl_state.real_pointer_frame_listener.notify = handle_pointer_frame; - wl_signal_add(&ptr->events.frame, - &wl_state.real_pointer_frame_listener); - wl_state.real_pointer_axis_listener.notify = handle_pointer_axis; - wl_signal_add(&ptr->events.axis, - &wl_state.real_pointer_axis_listener); - wl_state.real_pointer_destroy_listener.notify = handle_pointer_destroy; - wl_signal_add(&dev->events.destroy, - &wl_state.real_pointer_destroy_listener); - wwarning("wl_backend: real pointer wired"); + struct wl_real_pointer_listener *pl = calloc(1, sizeof(*pl)); + if (!pl) + break; + pl->dev = dev; + if (ptr->output_name) + pl->output_name = strdup(ptr->output_name); + wl_list_init(&pl->link); + pl->motion.notify = real_pointer_handle_motion; + wl_signal_add(&ptr->events.motion, &pl->motion); + pl->motion_abs.notify = real_pointer_handle_motion_absolute; + wl_signal_add(&ptr->events.motion_absolute, &pl->motion_abs); + pl->button.notify = real_pointer_handle_button; + wl_signal_add(&ptr->events.button, &pl->button); + pl->frame.notify = real_pointer_handle_frame; + wl_signal_add(&ptr->events.frame, &pl->frame); + pl->axis.notify = real_pointer_handle_axis; + wl_signal_add(&ptr->events.axis, &pl->axis); + pl->destroy.notify = real_pointer_handle_destroy; + wl_signal_add(&dev->events.destroy, &pl->destroy); + wl_list_insert(&wl_state.real_pointer_listeners, &pl->link); + if (!wl_state.real_pointer_dev) { + wl_state.real_pointer_dev = dev; + wl_state.pointer_dev = dev; + } + + /* Register the device with wlr_cursor so that + * wlr_cursor_absolute_to_layout_coords can map the per-device + * 0..1 absolute range to the correct output's layout region. + * Without this, absolute events from a device bound to WL-2 + * are mapped across the entire layout instead of just that + * output, causing clicks to land on the wrong monitor. */ + if (wl_state.cursor) { + wlr_cursor_attach_input_device(wl_state.cursor, dev); + /* If the device is bound to a specific output (nested + * Wayland: each output window has its own wl_pointer), + * constrain its absolute range to that output. */ + if (ptr->output_name && wl_state.output_layout) { + struct wlr_output_layout_output *lo; + wl_list_for_each(lo, &wl_state.output_layout->outputs, link) { + if (lo->output && lo->output->name && + strcmp(lo->output->name, ptr->output_name) == 0) { + wlr_cursor_map_input_to_output(wl_state.cursor, + dev, lo->output); + wwarning("wl_backend: pointer mapped to output %s", + ptr->output_name); + break; + } + } + } + } + + wwarning("wl_backend: real pointer wired (output=%s)", + ptr->output_name ? ptr->output_name : "(none)"); break; } default: diff --git a/src/backend/wayland/wl_pointer_focus.c b/src/backend/wayland/wl_pointer_focus.c index dbf0956..0025d9f 100644 --- a/src/backend/wayland/wl_pointer_focus.c +++ b/src/backend/wayland/wl_pointer_focus.c @@ -164,36 +164,59 @@ wl_update_pointer_focus(uint32_t time_msec) } } - void + static void +wl_pointer_get_bounds(int *ow, int *oh) +{ + WScreen *scr = wl_state.scr; + if (scr && scr->scr_width > 0 && scr->scr_height > 0) { + *ow = scr->scr_width; + *oh = scr->scr_height; + } else if (wl_state.output) { + *ow = (int)wl_state.output->width; + *oh = (int)wl_state.output->height; + } else { + *ow = 1920; + *oh = 1080; + } +} + +void wl_pointer_warp(int dx, int dy) { - 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 += dx; - wl_state.pointer_y += dy; - 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; - if (wl_state.cursor) - wlr_cursor_warp(wl_state.cursor, NULL, - (double)wl_state.pointer_x, (double)wl_state.pointer_y); + if (wl_state.cursor) { + wlr_cursor_move(wl_state.cursor, NULL, (double)dx, (double)dy); + wl_state.pointer_x = (int)wl_state.cursor->x; + wl_state.pointer_y = (int)wl_state.cursor->y; + } else { + int ow, oh; + wl_pointer_get_bounds(&ow, &oh); + wl_state.pointer_x += dx; + wl_state.pointer_y += dy; + 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; + } wl_update_pointer_focus(0); } void wl_pointer_warp_to(int x, int y) { - int ow = wl_state.output ? (int)wl_state.output->width : 1920; - int oh = wl_state.output ? (int)wl_state.output->height : 1080; - if (x < 0) x = 0; - if (y < 0) y = 0; - if (x >= ow) x = ow - 1; - if (y >= oh) y = oh - 1; - wl_state.pointer_x = x; - wl_state.pointer_y = y; - if (wl_state.cursor) + if (wl_state.cursor) { wlr_cursor_warp(wl_state.cursor, NULL, (double)x, (double)y); + wl_state.pointer_x = (int)wl_state.cursor->x; + wl_state.pointer_y = (int)wl_state.cursor->y; + } else { + int ow, oh; + wl_pointer_get_bounds(&ow, &oh); + if (x < 0) x = 0; + if (y < 0) y = 0; + if (x >= ow) x = ow - 1; + if (y >= oh) y = oh - 1; + wl_state.pointer_x = x; + wl_state.pointer_y = y; + } wl_update_pointer_focus(0); } diff --git a/src/backend/wayland/wl_pointer_motion.c b/src/backend/wayland/wl_pointer_motion.c index 28f1a40..e5a37ad 100644 --- a/src/backend/wayland/wl_pointer_motion.c +++ b/src/backend/wayland/wl_pointer_motion.c @@ -17,23 +17,49 @@ void wl_grab_apply_motion(void); void wl_update_pointer_focus(uint32_t time_msec); WMScreen *wl_get_wmscreen(void); +static void +wl_pointer_get_bounds(int *ow, int *oh) +{ + WScreen *scr = wl_state.scr; + if (scr && scr->scr_width > 0 && scr->scr_height > 0) { + *ow = scr->scr_width; + *oh = scr->scr_height; + } else if (wl_state.output) { + *ow = (int)wl_state.output->width; + *oh = (int)wl_state.output->height; + } else { + *ow = 1920; + *oh = 1080; + } +} void handle_pointer_motion(struct wl_listener *listener, void *data) { (void)listener; struct wlr_pointer_motion_event *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->delta_x; - wl_state.pointer_y += (int)ev->delta_y; - 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; - if (wl_state.cursor) - wlr_cursor_warp(wl_state.cursor, NULL, - (double)wl_state.pointer_x, (double)wl_state.pointer_y); + /* Use wlr_cursor_move to apply the delta. wlr_cursor_move respects + * the output layout geometry and constrains the pointer to the union + * of all outputs, which is the correct bounding region for multi-head. + * The old code manually accumulated delta into pointer_x/y and clamped + * to scr_width×scr_height (a bounding-box rectangle), which is wrong + * for non-rectangular layouts (e.g. monitors at different heights). + * After the move, read back the authoritative position from wlr_cursor. */ + if (wl_state.cursor) { + wlr_cursor_move(wl_state.cursor, &ev->pointer->base, + ev->delta_x, ev->delta_y); + wl_state.pointer_x = (int)wl_state.cursor->x; + wl_state.pointer_y = (int)wl_state.cursor->y; + } else { + int ow, oh; + wl_pointer_get_bounds(&ow, &oh); + wl_state.pointer_x += (int)ev->delta_x; + wl_state.pointer_y += (int)ev->delta_y; + 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 A: enqueue motion event for WINGs. * Use frame_buf_hit so button sub-windows (which have no active scene_buf * buffer and are invisible to wlr_scene_node_at) are resolved correctly. */ @@ -125,22 +151,32 @@ handle_pointer_motion_absolute(struct wl_listener *listener, void *data) { (void)listener; struct wlr_pointer_motion_absolute_event *ev = data; - /* 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. */ + + /* In nested multi-output mode, each host output window has its own + * wl_pointer device with output_name set (e.g. "WL-1", "WL-2"). + * wlr_cursor_absolute_to_layout_coords uses the device→output mapping + * to convert the per-window 0..1 range to the correct output's layout + * coordinates. The old early-return here discarded these events, + * leaving only relative deltas — which have no concept of which host + * window the mouse is in, causing the cursor to drift to the wrong + * output on every window transition. */ + if (wl_state.cursor) { - wlr_cursor_warp_absolute(wl_state.cursor, &ev->pointer->base, ev->x, ev->y); + double lx = 0.0, ly = 0.0; + wlr_cursor_absolute_to_layout_coords( + wl_state.cursor, &ev->pointer->base, ev->x, ev->y, &lx, &ly); + wlr_cursor_warp(wl_state.cursor, NULL, lx, ly); + /* Read back from wlr_cursor — it is the single source of truth + * for pointer position in layout space. */ 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; + int ow = 0, oh = 0; + wl_pointer_get_bounds(&ow, &oh); 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 A: enqueue motion event for WINGs. * Use frame_buf_hit so button sub-windows (which have no active scene_buf