fix: normalize libinput wheel scale so standalone overlay cycling works

One wheel notch in standalone did nothing with the overlay up: libinput
reports discrete ticks scaled by WLR_POINTER_AXIS_DISCRETE_STEP (x120),
so a single notch ran cycle_tab 120 times -- an even number of wraps on
two tabs, netting zero visible movement while still swallowing the
event. The standalone axis handler now divides by the macro before
calling overlay_handle_axis; the client forward path stays raw since
the wlroots seat divides it back itself. Nested was never affected
(its discrete values already arrive in wire units).

Proven by test_standalone_overlay_wheel_libinput_scale, which drives
one notch through the real standalone handler via a new wltest
--virtual-axis mode and asserts exactly one step (it fails with 120
steps without the fix). wltest also grows the missing
axis_source/stop/discrete pointer handlers, without which libwayland
aborts any test client forwarded a scroll event, plus axis lines in
--pointer-out.
This commit is contained in:
Andrew Hamilton 2026-09-17 13:33:31 +10:00
parent 0b9012cd6f
commit 3a6fdd4fd4
3 changed files with 109 additions and 1 deletions

9
kick.c
View file

@ -10694,7 +10694,14 @@ static void standalone_ptr_axis_event(struct wl_listener *listener, void *data)
(void)listener;
struct wlr_pointer_axis_event *ev = data;
if (!server.seat) return;
if (overlay_handle_axis(ev->orientation, ev->delta, ev->delta_discrete))
/* libinput scales discrete steps by WLR_POINTER_AXIS_DISCRETE_STEP
(±120 per notch; the seat divides it back off for clients), while
overlay_handle_axis speaks Wayland wire units (±1). Normalize only
for the overlay call -- the client forward below must stay raw.
Sub-notch fractions truncate to 0 and fall through to the smooth
accumulator inside the helper. */
if (overlay_handle_axis(ev->orientation, ev->delta,
ev->delta_discrete / WLR_POINTER_AXIS_DISCRETE_STEP))
return;
wlr_seat_pointer_notify_axis(server.seat, ev->time_msec, ev->orientation,
ev->delta, ev->delta_discrete, ev->source,

View file

@ -5130,6 +5130,41 @@ def test_overlay_wheel_swallowed_by_client(tmp_path):
s.mods(0)
def test_standalone_overlay_wheel_libinput_scale():
"""A libinput-scale discrete tick through the real standalone axis
handler steps exactly one tab -- the ×120 must be normalized, or one
notch cycles 120 times (net zero on two tabs) and the wheel looks
dead. Uses a virtual pointer, which feeds the same
standalone_ptr_axis_event libinput does (wlroots scales the wire
discrete itself, so discrete=1 arrives like one real notch)."""
with _run_standalone(
{}, "%s '#ff0000'" % WL, "%s '#0000ff'" % WL, wait="any") as s:
s.wait_surface_mapped(0)
s.wait_surface_mapped(1)
start = s.active()
other = 0 if start != 0 else 1
s.mods(ALT)
try:
time.sleep(0.7)
n0 = len(re.findall(r"\[wheel\] cycle", s.read_log()))
_oneshot_virtual_axis(s.socket, 0, 15, 1)
deadline = time.monotonic() + 10.0
while time.monotonic() < deadline:
if len(re.findall(r"\[wheel\] cycle", s.read_log())) > n0:
break
time.sleep(0.05)
else:
pytest.fail("libinput-scale wheel tick never stepped a tab")
n1 = len(re.findall(r"\[wheel\] cycle", s.read_log()))
assert n1 == n0 + 1, \
f"one notch stepped {n1 - n0}x, want exactly 1"
s.wait_active(other, timeout=10.0)
finally:
s.mods(0)
s.wait_for(r"\[mru\] commit app=%d" % other, timeout=10.0)
assert s.active() == other
def test_synth_feed_drives_host_listeners(tmp_path):
"""Synthesized host pointer/touch events go through the real host
wl_pointer/wl_touch listeners instead of bypassing them for the shared
@ -5864,6 +5899,23 @@ def _oneshot_virtual_abs(sock, x, y, click=False):
pass
def _oneshot_virtual_axis(sock, axis, value, discrete):
"""Run a one-shot --virtual-axis (scroll) against a live socket."""
p = subprocess.Popen(
[WL, "--virtual-axis", str(axis), str(value), str(discrete)],
env=dict(os.environ, WAYLAND_DISPLAY=sock),
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL, start_new_session=True,
preexec_fn=_preexec_death(os.getpid()))
try:
p.wait(timeout=10.0)
finally:
try:
p.kill()
except Exception:
pass
def test_standalone_headless_virtual_pointer_motion_click(tmp_path):
"""A virtual pointer moves the cursor and clicks: the focused window
receives pointer motion and button events through the seat."""

View file

@ -2869,6 +2869,40 @@ static double virtual_dx, virtual_dy;
static double virtual_abs_x = -1.0, virtual_abs_y = -1.0;
static bool virtual_click;
/* --virtual-axis <axis> <value> <discrete>: send one scroll event through
a virtual pointer (axis 0 = vertical). Values are client wire units:
wlroots scales discrete by WLR_POINTER_AXIS_DISCRETE_STEP itself, so
discrete=1 arrives at the compositor exactly like one libinput wheel
notch. Exits after flushing. */
static bool virtual_axis_set;
static uint32_t virtual_axis;
static double virtual_axis_value;
static int32_t virtual_axis_discrete;
static void virtual_axis_run(void) {
display = wl_display_connect(NULL);
if (!display) {
fprintf(stderr, "wltest: virtual-axis: cannot connect\n");
exit(1);
}
struct wl_registry *reg = wl_display_get_registry(display);
wl_registry_add_listener(reg, &registry_listener, NULL);
wl_display_roundtrip(display);
if (!virtual_ptr_mgr || !seat) {
fprintf(stderr, "wltest: virtual-axis: manager or seat missing\n");
exit(1);
}
struct zwlr_virtual_pointer_v1 *vptr =
zwlr_virtual_pointer_manager_v1_create_virtual_pointer(
virtual_ptr_mgr, seat);
zwlr_virtual_pointer_v1_axis(vptr, 1000, virtual_axis,
wl_fixed_from_double(virtual_axis_value));
zwlr_virtual_pointer_v1_axis_discrete(vptr, 1000, virtual_axis,
wl_fixed_from_double(virtual_axis_value), virtual_axis_discrete);
zwlr_virtual_pointer_v1_frame(vptr);
wl_display_roundtrip(display);
}
static void virtual_motion_run(void) {
display = wl_display_connect(NULL);
if (!display) {
@ -4091,6 +4125,16 @@ int main(int argc, char **argv) {
virtual_abs_x = atof(argv[i + 1]);
virtual_abs_y = atof(argv[i + 2]);
i += 3;
} else if (strcmp(argv[i], "--virtual-axis") == 0) {
if (i + 3 >= argc) {
fprintf(stderr, "wltest: --virtual-axis needs axis value discrete\n");
return 1;
}
virtual_axis = (uint32_t)strtoul(argv[i + 1], NULL, 0);
virtual_axis_value = atof(argv[i + 2]);
virtual_axis_discrete = (int32_t)strtol(argv[i + 3], NULL, 0);
virtual_axis_set = true;
i += 4;
} else if (strcmp(argv[i], "--virtual-modifiers") == 0) {
if (i + 1 >= argc) {
fprintf(stderr, "wltest: --virtual-modifiers needs a mask\n");
@ -4256,6 +4300,11 @@ int main(int argc, char **argv) {
virtual_motion_run();
return 0;
}
/* Virtual pointer scroll: --virtual-axis sends one axis event. */
if (virtual_axis_set) {
virtual_axis_run();
return 0;
}
/* Output-power mode: blank/unblank the output, then exit. */
if (power_arg) {
power_run();