wayland: fix multi-output pointer coordinate bugs
Three interrelated bugs caused clicks to land in the wrong place when the compositor spanned multiple outputs (e.g. two 1920x1080 monitors side by side in nested Wayland mode): 1. real_pointer_update_global() added lo->x to coordinates that were already in layout space from wlr_cursor_absolute_to_layout_coords, double-counting the output offset. It also omitted lo->y entirely, breaking vertical monitor layouts. Fix: read position from wlr_cursor->x/y which is always authoritative layout-space. 2. real_pointer_handle_motion() accumulated ev->delta into local_x/y then set pointer_x/y, after which handle_pointer_motion() added the same delta again -- doubling every relative motion. Fix: let handle_pointer_motion() be the sole authority; use wlr_cursor_move() which respects output layout geometry. 3. Pointer devices from nested Wayland outputs (WL-1, WL-2) were never registered with wlr_cursor via wlr_cursor_attach_input_device() or mapped to their output via wlr_cursor_map_input_to_output(). Without this mapping, wlr_cursor_absolute_to_layout_coords treated the 0..1 range as spanning the entire layout (0..3840) instead of the device's specific output (e.g. 1920..3840 for WL-2). A previous workaround disabled absolute motion entirely for nested multi-output, which left only relative deltas that had no concept of which host window the mouse was in. Fix: register devices with wlr_cursor and map them to their outputs; remove the early-return. All pointer paths now follow the same pattern: mutate position via wlr_cursor_move()/wlr_cursor_warp(), then read back cursor->x/y into wl_state.pointer_x/y. Manual bounding-box clamping is retained only for the no-cursor fallback (headless/test mode).
This commit is contained in:
parent
b33774db14
commit
ad0871054c
|
|
@ -1,10 +1,14 @@
|
|||
#ifdef HAVE_CONFIG_H
|
||||
#include "config.h"
|
||||
#endif
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <wayland-server-core.h>
|
||||
#include <wlr/types/wlr_keyboard.h>
|
||||
#include <wlr/types/wlr_pointer.h>
|
||||
#include <wlr/types/wlr_cursor.h>
|
||||
#include <wlr/types/wlr_seat.h>
|
||||
#include <wlr/types/wlr_output_layout.h>
|
||||
#include <wlr/util/log.h>
|
||||
#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:
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue