plugin/wheel: tighten the wheel debouncing code

The previous approach had a fixed threshold of 60 (half a detent)
below which scroll events were ignored. Reduce this threshold to have a
threshold of one device-specific delta. That threshold adjusts over time
to the device's individual minimum delta so after a few scroll event it
should settle on the lowest value possible.

The result is that fine-grained scrolling is possible on those devices
and only the very first scroll event is held back/swallowed, two events
in the same direction release scrolling.

Part-of: <https://gitlab.freedesktop.org/libinput/libinput/-/merge_requests/1258>
This commit is contained in:
Peter Hutterer 2025-07-03 12:01:32 +10:00 committed by Marge Bot
parent b347cc8691
commit ca6b82841c
2 changed files with 81 additions and 28 deletions

View file

@ -36,7 +36,7 @@
#include "libinput-plugin.h"
#include "libinput-util.h"
#define ACC_V120_THRESHOLD 60
#define ACC_V120_THRESHOLD 59
#define WHEEL_SCROLL_TIMEOUT ms2us(500)
enum wheel_state {
@ -72,6 +72,7 @@ struct plugin_device {
struct libinput_plugin_timer *scroll_timer;
enum wheel_direction dir;
bool ignore_small_hi_res_movements;
int min_movement;
struct ratelimit hires_warning_limit;
};
@ -293,8 +294,8 @@ wheel_handle_state_accumulating_scroll(struct plugin_device *pd,
{
wheel_remove_scroll_events(frame);
if (abs(pd->hi_res.x) >= ACC_V120_THRESHOLD ||
abs(pd->hi_res.y) >= ACC_V120_THRESHOLD) {
if (abs(pd->hi_res.x) > pd->min_movement ||
abs(pd->hi_res.y) > pd->min_movement) {
wheel_handle_event(pd, WHEEL_EVENT_SCROLL_ACCUMULATED, time);
wheel_queue_scroll_events(pd, frame);
}
@ -348,12 +349,14 @@ wheel_process_relative(struct plugin_device *pd, struct evdev_event *e, uint64_t
case EVDEV_REL_WHEEL_HI_RES:
pd->hi_res.y += e->value;
pd->hi_res_event_received = true;
pd->min_movement = min(pd->min_movement, abs(e->value));
wheel_handle_direction_change(pd, e, time);
wheel_handle_event(pd, WHEEL_EVENT_SCROLL, time);
break;
case EVDEV_REL_HWHEEL_HI_RES:
pd->hi_res.x += e->value;
pd->hi_res_event_received = true;
pd->min_movement = min(pd->min_movement, abs(e->value));
wheel_handle_direction_change(pd, e, time);
wheel_handle_event(pd, WHEEL_EVENT_SCROLL, time);
break;
@ -412,6 +415,7 @@ wheel_plugin_device_create(struct libinput_plugin *libinput_plugin,
pd->state = WHEEL_STATE_NONE;
pd->dir = WHEEL_DIR_UNKNOW;
pd->ignore_small_hi_res_movements = !evdev_device_is_virtual(evdev);
pd->min_movement = ACC_V120_THRESHOLD;
ratelimit_init(&pd->hires_warning_limit, s2us(24 * 60 * 60), 1);
if (pd->ignore_small_hi_res_movements) {

View file

@ -843,6 +843,7 @@ START_TEST(pointer_scroll_wheel_inhibit_small_deltas)
{
struct litest_device *dev = litest_current_device();
struct libinput *li = dev->libinput;
uint32_t delta = litest_test_param_get_u32(test_env->params, "hires-delta");
if (!libevdev_has_event_code(dev->evdev, EV_REL, REL_WHEEL_HI_RES) ||
!libevdev_has_event_code(dev->evdev, EV_REL, REL_HWHEEL_HI_RES))
@ -850,49 +851,94 @@ START_TEST(pointer_scroll_wheel_inhibit_small_deltas)
litest_drain_events(dev->libinput);
/* Scroll deltas below the threshold (60) must be ignored */
litest_event(dev, EV_REL, REL_WHEEL_HI_RES, 15);
litest_event(dev, EV_SYN, SYN_REPORT, 0);
litest_event(dev, EV_REL, REL_WHEEL_HI_RES, 15);
/* A single delta (below the hardcoded threshold 60) is ignored */
litest_event(dev, EV_REL, REL_WHEEL_HI_RES, delta);
litest_event(dev, EV_SYN, SYN_REPORT, 0);
litest_dispatch(li);
litest_assert_empty_queue(li);
/* The accumulated scroll is 30, add 30 to trigger scroll */
litest_event(dev, EV_REL, REL_WHEEL_HI_RES, 30);
/* Once we get two events in the same direction trigger scroll */
litest_event(dev, EV_REL, REL_WHEEL_HI_RES, delta);
litest_event(dev, EV_SYN, SYN_REPORT, 0);
litest_dispatch(li);
test_high_and_low_wheel_events_value(dev, REL_WHEEL_HI_RES, -60);
/* Once the threshold is reached, small scroll deltas are reported */
litest_event(dev, EV_REL, REL_WHEEL_HI_RES, 5);
test_high_and_low_wheel_events_value(dev, REL_WHEEL_HI_RES, -2 * delta);
/* Once the threshold is reached, every scroll deltas are reported */
litest_event(dev, EV_REL, REL_WHEEL_HI_RES, delta);
litest_event(dev, EV_SYN, SYN_REPORT, 0);
litest_dispatch(li);
test_high_and_low_wheel_events_value(dev, REL_WHEEL_HI_RES, -5);
test_high_and_low_wheel_events_value(dev, REL_WHEEL_HI_RES, -delta);
/* When the scroll timeout is triggered, ignore small deltas again */
litest_timeout_wheel_scroll(li);
litest_event(dev, EV_REL, REL_WHEEL_HI_RES, -15);
litest_event(dev, EV_SYN, SYN_REPORT, 0);
litest_event(dev, EV_REL, REL_WHEEL_HI_RES, -15);
litest_event(dev, EV_REL, REL_WHEEL_HI_RES, -delta);
litest_event(dev, EV_SYN, SYN_REPORT, 0);
litest_dispatch(li);
litest_assert_empty_queue(li);
litest_event(dev, EV_REL, REL_HWHEEL_HI_RES, 15);
litest_event(dev, EV_SYN, SYN_REPORT, 0);
litest_event(dev, EV_REL, REL_HWHEEL_HI_RES, 15);
litest_event(dev, EV_REL, REL_HWHEEL_HI_RES, delta);
litest_event(dev, EV_SYN, SYN_REPORT, 0);
litest_dispatch(li);
litest_assert_empty_queue(li);
}
END_TEST
START_TEST(pointer_scroll_wheel_inhibit_small_deltas_reduce_delta)
{
struct litest_device *dev = litest_current_device();
struct libinput *li = dev->libinput;
if (!libevdev_has_event_code(dev->evdev, EV_REL, REL_WHEEL_HI_RES) ||
!libevdev_has_event_code(dev->evdev, EV_REL, REL_HWHEEL_HI_RES))
return LITEST_NOT_APPLICABLE;
litest_drain_events(dev->libinput);
/* A single delta (below the hardcoded threshold 60) is ignored */
litest_event(dev, EV_REL, REL_WHEEL_HI_RES, 50);
litest_event(dev, EV_SYN, SYN_REPORT, 0);
litest_dispatch(li);
litest_assert_empty_queue(li);
/* A second smaller delta changes the internal threshold */
litest_event(dev, EV_REL, REL_WHEEL_HI_RES, 5);
litest_event(dev, EV_SYN, SYN_REPORT, 0);
litest_dispatch(li);
test_high_and_low_wheel_events_value(dev, REL_WHEEL_HI_RES, -55);
litest_timeout_wheel_scroll(li);
/* Internal threshold is now 5 so two deltas of 5 trigger */
litest_log_group("Internal threshold is now 5 so two deltas of 5 trigger") {
litest_event(dev, EV_REL, REL_WHEEL_HI_RES, 5);
litest_event(dev, EV_SYN, SYN_REPORT, 0);
litest_dispatch(li);
litest_assert_empty_queue(li);
litest_event(dev, EV_REL, REL_WHEEL_HI_RES, 5);
litest_event(dev, EV_SYN, SYN_REPORT, 0);
litest_dispatch(li);
test_high_and_low_wheel_events_value(dev, REL_WHEEL_HI_RES, -10);
}
litest_timeout_wheel_scroll(li);
litest_log_group("Internal threshold is now 5 so one delta of 10 trigger") {
litest_event(dev, EV_REL, REL_WHEEL_HI_RES, 10);
litest_event(dev, EV_SYN, SYN_REPORT, 0);
litest_dispatch(li);
test_high_and_low_wheel_events_value(dev, REL_WHEEL_HI_RES, -10);
}
}
END_TEST
START_TEST(pointer_scroll_wheel_inhibit_dir_change)
{
struct litest_device *dev = litest_current_device();
struct libinput *li = dev->libinput;
uint32_t delta = litest_test_param_get_u32(test_env->params, "hires-delta");
if (!libevdev_has_event_code(dev->evdev, EV_REL, REL_WHEEL_HI_RES))
return LITEST_NOT_APPLICABLE;
@ -900,28 +946,28 @@ START_TEST(pointer_scroll_wheel_inhibit_dir_change)
litest_drain_events(dev->libinput);
/* Scroll one detent and a bit */
litest_event(dev, EV_REL, REL_WHEEL_HI_RES, 150);
litest_event(dev, EV_REL, REL_WHEEL_HI_RES, 120 + delta);
litest_event(dev, EV_SYN, SYN_REPORT, 0);
litest_dispatch(li);
test_high_and_low_wheel_events_value(dev, REL_WHEEL_HI_RES, -150);
test_high_and_low_wheel_events_value(dev, REL_WHEEL_HI_RES, -120 - delta);
/* Scroll below the threshold in the oposite direction should be ignored */
litest_event(dev, EV_REL, REL_WHEEL_HI_RES, -30);
litest_event(dev, EV_REL, REL_WHEEL_HI_RES, -delta);
litest_event(dev, EV_SYN, SYN_REPORT, 0);
litest_dispatch(li);
litest_assert_empty_queue(li);
/* But should be triggered if the scroll continues in the same direction */
litest_event(dev, EV_REL, REL_WHEEL_HI_RES, -120);
litest_event(dev, EV_REL, REL_WHEEL_HI_RES, -2 * delta);
litest_event(dev, EV_SYN, SYN_REPORT, 0);
litest_dispatch(li);
test_high_and_low_wheel_events_value(dev, REL_WHEEL_HI_RES, 150);
test_high_and_low_wheel_events_value(dev, REL_WHEEL_HI_RES, 3 * delta);
/* Scroll above the threshold in the same dir should be triggered */
litest_event(dev, EV_REL, REL_WHEEL_HI_RES, 80);
litest_event(dev, EV_REL, REL_WHEEL_HI_RES, 2 * delta);
litest_event(dev, EV_SYN, SYN_REPORT, 0);
litest_dispatch(li);
test_high_and_low_wheel_events_value(dev, REL_WHEEL_HI_RES, -80);
test_high_and_low_wheel_events_value(dev, REL_WHEEL_HI_RES, -2 * delta);
}
END_TEST
@ -3646,8 +3692,11 @@ TEST_COLLECTION(pointer)
litest_named_i32(LIBINPUT_POINTER_AXIS_SCROLL_HORIZONTAL, "horizontal")) {
litest_add_parametrized(pointer_scroll_wheel_hires_send_only_lores, LITEST_WHEEL, LITEST_TABLET, params);
}
litest_add(pointer_scroll_wheel_inhibit_small_deltas, LITEST_WHEEL, LITEST_TABLET);
litest_add(pointer_scroll_wheel_inhibit_dir_change, LITEST_WHEEL, LITEST_TABLET);
litest_with_parameters(params, "hires-delta", 'u', 3, 5, 15, 20) {
litest_add_parametrized(pointer_scroll_wheel_inhibit_small_deltas, LITEST_WHEEL, LITEST_TABLET, params);
litest_add_parametrized(pointer_scroll_wheel_inhibit_dir_change, LITEST_WHEEL, LITEST_TABLET, params);
}
litest_add(pointer_scroll_wheel_inhibit_small_deltas_reduce_delta, LITEST_WHEEL, LITEST_TABLET);
litest_add_for_device(pointer_scroll_wheel_no_inhibit_small_deltas_when_virtual, LITEST_MOUSE_VIRTUAL);
litest_add_for_device(pointer_scroll_wheel_lenovo_scrollpoint, LITEST_LENOVO_SCROLLPOINT);
litest_add(pointer_scroll_button, LITEST_RELATIVE|LITEST_BUTTON, LITEST_ANY);