evdev: Handle scroll wheel with a plugin

Transform the code present in evdev-wheel.c into a internal plugin.

Part-of: <https://gitlab.freedesktop.org/libinput/libinput/-/merge_requests/1235>
This commit is contained in:
José Expósito 2025-06-23 12:35:08 +02:00 committed by Marge Bot
parent ee35e04207
commit d1800a76fe
7 changed files with 727 additions and 484 deletions

View file

@ -376,6 +376,7 @@ src_libinput = src_libfilter + [
'src/libinput.c', 'src/libinput.c',
'src/libinput-plugin.c', 'src/libinput-plugin.c',
'src/libinput-plugin-button-debounce.c', 'src/libinput-plugin-button-debounce.c',
'src/libinput-plugin-mouse-wheel.c',
'src/libinput-plugin-tablet-double-tool.c', 'src/libinput-plugin-tablet-double-tool.c',
'src/libinput-plugin-tablet-eraser-button.c', 'src/libinput-plugin-tablet-eraser-button.c',
'src/libinput-plugin-tablet-forced-tool.c', 'src/libinput-plugin-tablet-forced-tool.c',
@ -395,7 +396,6 @@ src_libinput = src_libfilter + [
'src/evdev-tablet.c', 'src/evdev-tablet.c',
'src/evdev-tablet-pad.c', 'src/evdev-tablet-pad.c',
'src/evdev-tablet-pad-leds.c', 'src/evdev-tablet-pad-leds.c',
'src/evdev-wheel.c',
'src/path-seat.c', 'src/path-seat.c',
'src/udev-seat.c', 'src/udev-seat.c',
'src/timer.c', 'src/timer.c',

View file

@ -207,6 +207,100 @@ fallback_flush_relative_motion(struct fallback_dispatch *dispatch,
pointer_notify_motion(base, time, &accel, &raw); pointer_notify_motion(base, time, &accel, &raw);
} }
static void
fallback_flush_wheels(struct fallback_dispatch *dispatch,
struct evdev_device *device,
uint64_t time)
{
struct normalized_coords wheel_degrees = { 0.0, 0.0 };
struct discrete_coords discrete = { 0.0, 0.0 };
struct wheel_v120 v120 = { 0.0, 0.0 };
if (!libinput_device_has_capability(&device->base, LIBINPUT_DEVICE_CAP_POINTER))
return;
/* This mouse has a trackstick instead of a mouse wheel and sends
* trackstick data via REL_WHEEL. Normalize it like normal x/y coordinates.
*/
if (device->model_flags & EVDEV_MODEL_LENOVO_SCROLLPOINT) {
const struct device_float_coords raw = {
.x = dispatch->wheel.lo_res.x,
.y = dispatch->wheel.lo_res.y * -1,
};
const struct normalized_coords normalized =
filter_dispatch_scroll(device->pointer.filter,
&raw,
device,
time);
evdev_post_scroll(device,
time,
LIBINPUT_POINTER_AXIS_SOURCE_CONTINUOUS,
&normalized);
dispatch->wheel.hi_res.x = 0;
dispatch->wheel.hi_res.y = 0;
dispatch->wheel.lo_res.x = 0;
dispatch->wheel.lo_res.y = 0;
return;
}
if (dispatch->wheel.hi_res.y != 0) {
int value = dispatch->wheel.hi_res.y;
v120.y = -1 * value;
wheel_degrees.y = -1 * value/120.0 * device->scroll.wheel_click_angle.y;
evdev_notify_axis_wheel(
device,
time,
bit(LIBINPUT_POINTER_AXIS_SCROLL_VERTICAL),
&wheel_degrees,
&v120);
dispatch->wheel.hi_res.y = 0;
}
if (dispatch->wheel.lo_res.y != 0) {
int value = dispatch->wheel.lo_res.y;
wheel_degrees.y = -1 * value * device->scroll.wheel_click_angle.y;
discrete.y = -1 * value;
evdev_notify_axis_legacy_wheel(
device,
time,
bit(LIBINPUT_POINTER_AXIS_SCROLL_VERTICAL),
&wheel_degrees,
&discrete);
dispatch->wheel.lo_res.y = 0;
}
if (dispatch->wheel.hi_res.x != 0) {
int value = dispatch->wheel.hi_res.x;
v120.x = value;
wheel_degrees.x = value/120.0 * device->scroll.wheel_click_angle.x;
evdev_notify_axis_wheel(
device,
time,
bit(LIBINPUT_POINTER_AXIS_SCROLL_HORIZONTAL),
&wheel_degrees,
&v120);
dispatch->wheel.hi_res.x = 0;
}
if (dispatch->wheel.lo_res.x != 0) {
int value = dispatch->wheel.lo_res.x;
wheel_degrees.x = value * device->scroll.wheel_click_angle.x;
discrete.x = value;
evdev_notify_axis_legacy_wheel(
device,
time,
bit(LIBINPUT_POINTER_AXIS_SCROLL_HORIZONTAL),
&wheel_degrees,
&discrete);
dispatch->wheel.lo_res.x = 0;
}
}
static void static void
fallback_flush_absolute_motion(struct fallback_dispatch *dispatch, fallback_flush_absolute_motion(struct fallback_dispatch *dispatch,
struct evdev_device *device, struct evdev_device *device,
@ -754,6 +848,20 @@ fallback_reject_relative(struct evdev_device *device,
return false; return false;
} }
static void
fallback_rotate_wheel(struct fallback_dispatch *dispatch,
struct evdev_event *e)
{
/* Special case: if we're upside down (-ish),
* swap the direction of the wheels so that user-down
* means scroll down. This isn't done for any other angle
* since it's not clear what the heuristics should be.*/
if (dispatch->rotation.angle >= 160.0 &&
dispatch->rotation.angle <= 220.0) {
e->value *= -1;
}
}
static inline void static inline void
fallback_process_relative(struct fallback_dispatch *dispatch, fallback_process_relative(struct fallback_dispatch *dispatch,
struct evdev_device *device, struct evdev_device *device,
@ -771,11 +879,25 @@ fallback_process_relative(struct fallback_dispatch *dispatch,
dispatch->rel.y += e->value; dispatch->rel.y += e->value;
dispatch->pending_event |= EVDEV_RELATIVE_MOTION; dispatch->pending_event |= EVDEV_RELATIVE_MOTION;
break; break;
case EVDEV_REL_WHEEL:
fallback_rotate_wheel(dispatch, e);
dispatch->wheel.lo_res.y += e->value;
break;
case EVDEV_REL_HWHEEL:
fallback_rotate_wheel(dispatch, e);
dispatch->wheel.lo_res.x += e->value;
break;
case EVDEV_REL_WHEEL_HI_RES:
fallback_rotate_wheel(dispatch, e);
dispatch->wheel.hi_res.y += e->value;
break;
case EVDEV_REL_HWHEEL_HI_RES:
fallback_rotate_wheel(dispatch, e);
dispatch->wheel.hi_res.x += e->value;
break;
default: default:
break; break;
} }
fallback_wheel_process_relative(dispatch, e, time);
} }
static inline void static inline void
@ -941,7 +1063,7 @@ fallback_handle_state(struct fallback_dispatch *dispatch,
if (need_touch_frame) if (need_touch_frame)
touch_notify_frame(&device->base, time); touch_notify_frame(&device->base, time);
fallback_wheel_handle_state(dispatch, device, time); fallback_flush_wheels(dispatch, device, time);
/* Buttons and keys */ /* Buttons and keys */
if (dispatch->pending_event & EVDEV_KEY) { if (dispatch->pending_event & EVDEV_KEY) {
@ -1125,7 +1247,6 @@ fallback_interface_remove(struct evdev_dispatch *evdev_dispatch)
struct fallback_dispatch *dispatch = fallback_dispatch(evdev_dispatch); struct fallback_dispatch *dispatch = fallback_dispatch(evdev_dispatch);
struct evdev_paired_keyboard *kbd; struct evdev_paired_keyboard *kbd;
libinput_timer_cancel(&dispatch->wheel.scroll_timer);
libinput_timer_cancel(&dispatch->debounce.timer); libinput_timer_cancel(&dispatch->debounce.timer);
libinput_timer_cancel(&dispatch->debounce.timer_short); libinput_timer_cancel(&dispatch->debounce.timer_short);
libinput_timer_cancel(&dispatch->arbitration.arbitration_timer); libinput_timer_cancel(&dispatch->arbitration.arbitration_timer);
@ -1245,7 +1366,6 @@ fallback_interface_destroy(struct evdev_dispatch *evdev_dispatch)
{ {
struct fallback_dispatch *dispatch = fallback_dispatch(evdev_dispatch); struct fallback_dispatch *dispatch = fallback_dispatch(evdev_dispatch);
libinput_timer_destroy(&dispatch->wheel.scroll_timer);
libinput_timer_destroy(&dispatch->arbitration.arbitration_timer); libinput_timer_destroy(&dispatch->arbitration.arbitration_timer);
libinput_timer_destroy(&dispatch->debounce.timer); libinput_timer_destroy(&dispatch->debounce.timer);
libinput_timer_destroy(&dispatch->debounce.timer_short); libinput_timer_destroy(&dispatch->debounce.timer_short);
@ -1718,7 +1838,6 @@ fallback_dispatch_create(struct libinput_device *libinput_device)
want_config); want_config);
} }
fallback_init_wheel(dispatch, device);
fallback_init_arbitration(dispatch, device); fallback_init_arbitration(dispatch, device);
return &dispatch->base; return &dispatch->base;

View file

@ -60,20 +60,6 @@ enum palm_state {
PALM_WAS_PALM, /* this touch sequence was a palm but isn't now */ PALM_WAS_PALM, /* this touch sequence was a palm but isn't now */
}; };
enum wheel_state {
WHEEL_STATE_NONE,
WHEEL_STATE_ACCUMULATING_SCROLL,
WHEEL_STATE_SCROLLING,
};
enum wheel_direction {
WHEEL_DIR_UNKNOW,
WHEEL_DIR_VPOS,
WHEEL_DIR_VNEG,
WHEEL_DIR_HPOS,
WHEEL_DIR_HNEG,
};
struct mt_slot { struct mt_slot {
bool dirty; bool dirty;
enum mt_slot_state state; enum mt_slot_state state;
@ -112,13 +98,8 @@ struct fallback_dispatch {
struct device_coords rel; struct device_coords rel;
struct { struct {
enum wheel_state state;
struct device_coords lo_res; struct device_coords lo_res;
struct device_coords hi_res; struct device_coords hi_res;
bool hi_res_event_received;
struct libinput_timer scroll_timer;
enum wheel_direction dir;
bool ignore_small_hi_res_movements;
} wheel; } wheel;
struct { struct {
@ -239,17 +220,4 @@ fallback_notify_physical_button(struct fallback_dispatch *dispatch,
evdev_usage_t button, evdev_usage_t button,
enum libinput_button_state state); enum libinput_button_state state);
void
fallback_init_wheel(struct fallback_dispatch *dispatch,
struct evdev_device *device);
void
fallback_wheel_process_relative(struct fallback_dispatch *dispatch,
struct evdev_event *e, uint64_t time);
void
fallback_wheel_handle_state(struct fallback_dispatch *dispatch,
struct evdev_device *device,
uint64_t time);
#endif #endif

View file

@ -1,445 +0,0 @@
/*
* Copyright © 2010 Intel Corporation
* Copyright © 2013 Jonas Ådahl
* Copyright © 2013-2017 Red Hat, Inc.
* Copyright © 2017 James Ye <jye836@gmail.com>
* Copyright © 2021 José Expósito
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice (including the next
* paragraph) shall be included in all copies or substantial portions of the
* Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*/
#include "config.h"
#include "evdev-fallback.h"
#include "util-input-event.h"
#define ACC_V120_THRESHOLD 60
#define WHEEL_SCROLL_TIMEOUT ms2us(500)
enum wheel_event {
WHEEL_EVENT_SCROLL_ACCUMULATED,
WHEEL_EVENT_SCROLL,
WHEEL_EVENT_SCROLL_TIMEOUT,
WHEEL_EVENT_SCROLL_DIR_CHANGED,
};
static inline const char *
wheel_state_to_str(enum wheel_state state)
{
switch(state) {
CASE_RETURN_STRING(WHEEL_STATE_NONE);
CASE_RETURN_STRING(WHEEL_STATE_ACCUMULATING_SCROLL);
CASE_RETURN_STRING(WHEEL_STATE_SCROLLING);
}
return NULL;
}
static inline const char*
wheel_event_to_str(enum wheel_event event)
{
switch(event) {
CASE_RETURN_STRING(WHEEL_EVENT_SCROLL_ACCUMULATED);
CASE_RETURN_STRING(WHEEL_EVENT_SCROLL);
CASE_RETURN_STRING(WHEEL_EVENT_SCROLL_TIMEOUT);
CASE_RETURN_STRING(WHEEL_EVENT_SCROLL_DIR_CHANGED);
}
return NULL;
}
static inline void
log_wheel_bug(struct fallback_dispatch *dispatch, enum wheel_event event)
{
evdev_log_bug_libinput(dispatch->device,
"invalid wheel event %s in state %s\n",
wheel_event_to_str(event),
wheel_state_to_str(dispatch->wheel.state));
}
static inline void
wheel_set_scroll_timer(struct fallback_dispatch *dispatch, uint64_t time)
{
libinput_timer_set(&dispatch->wheel.scroll_timer,
time + WHEEL_SCROLL_TIMEOUT);
}
static inline void
wheel_cancel_scroll_timer(struct fallback_dispatch *dispatch)
{
libinput_timer_cancel(&dispatch->wheel.scroll_timer);
}
static void
wheel_handle_event_on_state_none(struct fallback_dispatch *dispatch,
enum wheel_event event,
uint64_t time)
{
switch (event) {
case WHEEL_EVENT_SCROLL:
dispatch->wheel.state =
dispatch->wheel.ignore_small_hi_res_movements ?
WHEEL_STATE_ACCUMULATING_SCROLL :
WHEEL_STATE_SCROLLING;
break;
case WHEEL_EVENT_SCROLL_DIR_CHANGED:
break;
case WHEEL_EVENT_SCROLL_ACCUMULATED:
case WHEEL_EVENT_SCROLL_TIMEOUT:
log_wheel_bug(dispatch, event);
break;
}
}
static void
wheel_handle_event_on_state_accumulating_scroll(struct fallback_dispatch *dispatch,
enum wheel_event event,
uint64_t time)
{
switch (event) {
case WHEEL_EVENT_SCROLL_ACCUMULATED:
dispatch->wheel.state = WHEEL_STATE_SCROLLING;
wheel_set_scroll_timer(dispatch, time);
break;
case WHEEL_EVENT_SCROLL:
/* Ignore scroll while accumulating deltas */
break;
case WHEEL_EVENT_SCROLL_DIR_CHANGED:
dispatch->wheel.state = WHEEL_STATE_NONE;
break;
case WHEEL_EVENT_SCROLL_TIMEOUT:
log_wheel_bug(dispatch, event);
break;
}
}
static void
wheel_handle_event_on_state_scrolling(struct fallback_dispatch *dispatch,
enum wheel_event event,
uint64_t time)
{
switch (event) {
case WHEEL_EVENT_SCROLL:
if (dispatch->wheel.ignore_small_hi_res_movements) {
wheel_cancel_scroll_timer(dispatch);
wheel_set_scroll_timer(dispatch, time);
}
break;
case WHEEL_EVENT_SCROLL_TIMEOUT:
dispatch->wheel.state = WHEEL_STATE_NONE;
break;
case WHEEL_EVENT_SCROLL_DIR_CHANGED:
if (dispatch->wheel.ignore_small_hi_res_movements)
wheel_cancel_scroll_timer(dispatch);
dispatch->wheel.state = WHEEL_STATE_NONE;
break;
case WHEEL_EVENT_SCROLL_ACCUMULATED:
log_wheel_bug(dispatch, event);
break;
}
}
static void
wheel_handle_event(struct fallback_dispatch *dispatch,
enum wheel_event event,
uint64_t time)
{
enum wheel_state oldstate = dispatch->wheel.state;
switch (oldstate) {
case WHEEL_STATE_NONE:
wheel_handle_event_on_state_none(dispatch, event, time);
break;
case WHEEL_STATE_ACCUMULATING_SCROLL:
wheel_handle_event_on_state_accumulating_scroll(dispatch,
event,
time);
break;
case WHEEL_STATE_SCROLLING:
wheel_handle_event_on_state_scrolling(dispatch, event, time);
break;
}
if (oldstate != dispatch->wheel.state) {
evdev_log_debug(dispatch->device,
"wheel: %s → %s → %s\n",
wheel_state_to_str(oldstate),
wheel_event_to_str(event),
wheel_state_to_str(dispatch->wheel.state));
}
}
static void
wheel_flush_scroll(struct fallback_dispatch *dispatch,
struct evdev_device *device,
uint64_t time)
{
struct normalized_coords wheel_degrees = { 0.0, 0.0 };
struct discrete_coords discrete = { 0.0, 0.0 };
struct wheel_v120 v120 = { 0.0, 0.0 };
/* This mouse has a trackstick instead of a mouse wheel and sends
* trackstick data via REL_WHEEL. Normalize it like normal x/y coordinates.
*/
if (device->model_flags & EVDEV_MODEL_LENOVO_SCROLLPOINT) {
const struct device_float_coords raw = {
.x = dispatch->wheel.lo_res.x,
.y = dispatch->wheel.lo_res.y * -1,
};
const struct normalized_coords normalized =
filter_dispatch_scroll(device->pointer.filter,
&raw,
device,
time);
evdev_post_scroll(device,
time,
LIBINPUT_POINTER_AXIS_SOURCE_CONTINUOUS,
&normalized);
dispatch->wheel.hi_res.x = 0;
dispatch->wheel.hi_res.y = 0;
dispatch->wheel.lo_res.x = 0;
dispatch->wheel.lo_res.y = 0;
return;
}
if (dispatch->wheel.hi_res.y != 0) {
int value = dispatch->wheel.hi_res.y;
v120.y = -1 * value;
wheel_degrees.y = -1 * value/120.0 * device->scroll.wheel_click_angle.y;
evdev_notify_axis_wheel(
device,
time,
bit(LIBINPUT_POINTER_AXIS_SCROLL_VERTICAL),
&wheel_degrees,
&v120);
dispatch->wheel.hi_res.y = 0;
}
if (dispatch->wheel.lo_res.y != 0) {
int value = dispatch->wheel.lo_res.y;
wheel_degrees.y = -1 * value * device->scroll.wheel_click_angle.y;
discrete.y = -1 * value;
evdev_notify_axis_legacy_wheel(
device,
time,
bit(LIBINPUT_POINTER_AXIS_SCROLL_VERTICAL),
&wheel_degrees,
&discrete);
dispatch->wheel.lo_res.y = 0;
}
if (dispatch->wheel.hi_res.x != 0) {
int value = dispatch->wheel.hi_res.x;
v120.x = value;
wheel_degrees.x = value/120.0 * device->scroll.wheel_click_angle.x;
evdev_notify_axis_wheel(
device,
time,
bit(LIBINPUT_POINTER_AXIS_SCROLL_HORIZONTAL),
&wheel_degrees,
&v120);
dispatch->wheel.hi_res.x = 0;
}
if (dispatch->wheel.lo_res.x != 0) {
int value = dispatch->wheel.lo_res.x;
wheel_degrees.x = value * device->scroll.wheel_click_angle.x;
discrete.x = value;
evdev_notify_axis_legacy_wheel(
device,
time,
bit(LIBINPUT_POINTER_AXIS_SCROLL_HORIZONTAL),
&wheel_degrees,
&discrete);
dispatch->wheel.lo_res.x = 0;
}
}
static void
wheel_handle_state_none(struct fallback_dispatch *dispatch,
struct evdev_device *device,
uint64_t time)
{
}
static void
wheel_handle_state_accumulating_scroll(struct fallback_dispatch *dispatch,
struct evdev_device *device,
uint64_t time)
{
if (abs(dispatch->wheel.hi_res.x) >= ACC_V120_THRESHOLD ||
abs(dispatch->wheel.hi_res.y) >= ACC_V120_THRESHOLD) {
wheel_handle_event(dispatch,
WHEEL_EVENT_SCROLL_ACCUMULATED,
time);
wheel_flush_scroll(dispatch, device, time);
}
}
static void
wheel_handle_state_scrolling(struct fallback_dispatch *dispatch,
struct evdev_device *device,
uint64_t time)
{
wheel_flush_scroll(dispatch, device, time);
}
static void
wheel_handle_direction_change(struct fallback_dispatch *dispatch,
struct evdev_event *e,
uint64_t time)
{
enum wheel_direction new_dir = WHEEL_DIR_UNKNOW;
switch (evdev_usage_enum(e->usage)) {
case EVDEV_REL_WHEEL_HI_RES:
new_dir = (e->value > 0) ? WHEEL_DIR_VPOS : WHEEL_DIR_VNEG;
break;
case EVDEV_REL_HWHEEL_HI_RES:
new_dir = (e->value > 0) ? WHEEL_DIR_HPOS : WHEEL_DIR_HNEG;
break;
default:
return;
}
if (new_dir != WHEEL_DIR_UNKNOW && new_dir != dispatch->wheel.dir) {
dispatch->wheel.dir = new_dir;
wheel_handle_event(dispatch,
WHEEL_EVENT_SCROLL_DIR_CHANGED,
time);
}
}
static void
fallback_rotate_wheel(struct fallback_dispatch *dispatch,
struct evdev_event *e)
{
/* Special case: if we're upside down (-ish),
* swap the direction of the wheels so that user-down
* means scroll down. This isn't done for any other angle
* since it's not clear what the heuristics should be.*/
if (dispatch->rotation.angle >= 160.0 &&
dispatch->rotation.angle <= 220.0) {
e->value *= -1;
}
}
void
fallback_wheel_process_relative(struct fallback_dispatch *dispatch,
struct evdev_event *e, uint64_t time)
{
switch (evdev_usage_enum(e->usage)) {
case EVDEV_REL_WHEEL:
fallback_rotate_wheel(dispatch, e);
dispatch->wheel.lo_res.y += e->value;
wheel_handle_event(dispatch, WHEEL_EVENT_SCROLL, time);
break;
case EVDEV_REL_HWHEEL:
fallback_rotate_wheel(dispatch, e);
dispatch->wheel.lo_res.x += e->value;
wheel_handle_event(dispatch, WHEEL_EVENT_SCROLL, time);
break;
case EVDEV_REL_WHEEL_HI_RES:
fallback_rotate_wheel(dispatch, e);
dispatch->wheel.hi_res.y += e->value;
dispatch->wheel.hi_res_event_received = true;
wheel_handle_direction_change(dispatch, e, time);
wheel_handle_event(dispatch, WHEEL_EVENT_SCROLL, time);
break;
case EVDEV_REL_HWHEEL_HI_RES:
fallback_rotate_wheel(dispatch, e);
dispatch->wheel.hi_res.x += e->value;
dispatch->wheel.hi_res_event_received = true;
wheel_handle_direction_change(dispatch, e, time);
wheel_handle_event(dispatch, WHEEL_EVENT_SCROLL, time);
break;
default:
break;
}
}
void
fallback_wheel_handle_state(struct fallback_dispatch *dispatch,
struct evdev_device *device,
uint64_t time)
{
if (!libinput_device_has_capability(&device->base, LIBINPUT_DEVICE_CAP_POINTER))
return;
if (!dispatch->wheel.hi_res_event_received &&
(dispatch->wheel.lo_res.x != 0 || dispatch->wheel.lo_res.y != 0)) {
evdev_log_bug_kernel(device,
"device supports high-resolution scroll but only low-resolution events have been received.\n"
"See %s/incorrectly-enabled-hires.html for details\n",
HTTP_DOC_LINK);
dispatch->wheel.hi_res.x = dispatch->wheel.lo_res.x * 120;
dispatch->wheel.hi_res.y = dispatch->wheel.lo_res.y * 120;
}
switch (dispatch->wheel.state) {
case WHEEL_STATE_NONE:
wheel_handle_state_none(dispatch, device, time);
break;
case WHEEL_STATE_ACCUMULATING_SCROLL:
wheel_handle_state_accumulating_scroll(dispatch, device, time);
break;
case WHEEL_STATE_SCROLLING:
wheel_handle_state_scrolling(dispatch, device, time);
break;
}
}
static void
wheel_on_scroll_timer_timeout(uint64_t now, void *data)
{
struct evdev_device *device = data;
struct fallback_dispatch *dispatch =
fallback_dispatch(device->dispatch);
wheel_handle_event(dispatch, WHEEL_EVENT_SCROLL_TIMEOUT, now);
}
void
fallback_init_wheel(struct fallback_dispatch *dispatch,
struct evdev_device *device)
{
char timer_name[64];
dispatch->wheel.state = WHEEL_STATE_NONE;
dispatch->wheel.dir = WHEEL_DIR_UNKNOW;
dispatch->wheel.ignore_small_hi_res_movements =
!evdev_device_is_virtual(dispatch->device);
if (dispatch->wheel.ignore_small_hi_res_movements) {
snprintf(timer_name,
sizeof(timer_name),
"%s wheel scroll",
evdev_device_get_sysname(device));
libinput_timer_init(&dispatch->wheel.scroll_timer,
evdev_libinput_context(device),
timer_name,
wheel_on_scroll_timer_timeout,
device);
}
}

View file

@ -0,0 +1,569 @@
/*
* Copyright © 2010 Intel Corporation
* Copyright © 2013 Jonas Ådahl
* Copyright © 2013-2017 Red Hat, Inc.
* Copyright © 2017 James Ye <jye836@gmail.com>
* Copyright © 2021-2025 José Expósito
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice (including the next
* paragraph) shall be included in all copies or substantial portions of the
* Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*/
#include "config.h"
#include <libevdev/libevdev.h>
#include "evdev.h"
#include "evdev-fallback.h"
#include "libinput-log.h"
#include "libinput-util.h"
#include "libinput-plugin.h"
#include "libinput-plugin-mouse-wheel.h"
#define ACC_V120_THRESHOLD 60
#define WHEEL_SCROLL_TIMEOUT ms2us(500)
enum wheel_state {
WHEEL_STATE_NONE,
WHEEL_STATE_ACCUMULATING_SCROLL,
WHEEL_STATE_SCROLLING,
};
enum wheel_direction {
WHEEL_DIR_UNKNOW,
WHEEL_DIR_VPOS,
WHEEL_DIR_VNEG,
WHEEL_DIR_HPOS,
WHEEL_DIR_HNEG,
};
enum wheel_event {
WHEEL_EVENT_SCROLL_ACCUMULATED,
WHEEL_EVENT_SCROLL,
WHEEL_EVENT_SCROLL_TIMEOUT,
WHEEL_EVENT_SCROLL_DIR_CHANGED,
};
struct plugin_device {
struct list link;
struct plugin_data *parent;
struct libinput_device *device;
enum wheel_state state;
struct device_coords lo_res;
struct device_coords hi_res;
bool hi_res_event_received;
struct libinput_plugin_timer *scroll_timer;
enum wheel_direction dir;
bool ignore_small_hi_res_movements;
};
struct plugin_data {
struct libinput_plugin *plugin;
struct list devices;
};
static inline const char *
wheel_state_to_str(enum wheel_state state)
{
switch(state) {
CASE_RETURN_STRING(WHEEL_STATE_NONE);
CASE_RETURN_STRING(WHEEL_STATE_ACCUMULATING_SCROLL);
CASE_RETURN_STRING(WHEEL_STATE_SCROLLING);
}
return NULL;
}
static inline const char*
wheel_event_to_str(enum wheel_event event)
{
switch(event) {
CASE_RETURN_STRING(WHEEL_EVENT_SCROLL_ACCUMULATED);
CASE_RETURN_STRING(WHEEL_EVENT_SCROLL);
CASE_RETURN_STRING(WHEEL_EVENT_SCROLL_TIMEOUT);
CASE_RETURN_STRING(WHEEL_EVENT_SCROLL_DIR_CHANGED);
}
return NULL;
}
static inline void
log_wheel_bug(struct plugin_device *pd, enum wheel_event event)
{
plugin_log_bug_libinput(pd->parent->plugin,
"invalid wheel event %s in state %s\n",
wheel_event_to_str(event),
wheel_state_to_str(pd->state));
}
static inline void
wheel_set_scroll_timer(struct plugin_device *pd, uint64_t time)
{
if (!pd->scroll_timer)
return;
libinput_plugin_timer_set(pd->scroll_timer,
time + WHEEL_SCROLL_TIMEOUT);
}
static inline void
wheel_cancel_scroll_timer(struct plugin_device *pd)
{
if (!pd->scroll_timer)
return;
libinput_plugin_timer_cancel(pd->scroll_timer);
}
static void
wheel_handle_event_on_state_none(struct plugin_device *pd,
enum wheel_event event,
uint64_t time)
{
switch (event) {
case WHEEL_EVENT_SCROLL:
pd->state =
pd->ignore_small_hi_res_movements ?
WHEEL_STATE_ACCUMULATING_SCROLL :
WHEEL_STATE_SCROLLING;
break;
case WHEEL_EVENT_SCROLL_DIR_CHANGED:
break;
case WHEEL_EVENT_SCROLL_ACCUMULATED:
case WHEEL_EVENT_SCROLL_TIMEOUT:
log_wheel_bug(pd, event);
break;
}
}
static void
wheel_handle_event_on_state_accumulating_scroll(struct plugin_device *pd,
enum wheel_event event,
uint64_t time)
{
switch (event) {
case WHEEL_EVENT_SCROLL_ACCUMULATED:
pd->state = WHEEL_STATE_SCROLLING;
wheel_set_scroll_timer(pd, time);
break;
case WHEEL_EVENT_SCROLL:
/* Ignore scroll while accumulating deltas */
break;
case WHEEL_EVENT_SCROLL_DIR_CHANGED:
pd->state = WHEEL_STATE_NONE;
break;
case WHEEL_EVENT_SCROLL_TIMEOUT:
log_wheel_bug(pd, event);
break;
}
}
static void
wheel_handle_event_on_state_scrolling(struct plugin_device *pd,
enum wheel_event event,
uint64_t time)
{
switch (event) {
case WHEEL_EVENT_SCROLL:
wheel_cancel_scroll_timer(pd);
wheel_set_scroll_timer(pd, time);
break;
case WHEEL_EVENT_SCROLL_TIMEOUT:
pd->state = WHEEL_STATE_NONE;
break;
case WHEEL_EVENT_SCROLL_DIR_CHANGED:
wheel_cancel_scroll_timer(pd);
pd->state = WHEEL_STATE_NONE;
break;
case WHEEL_EVENT_SCROLL_ACCUMULATED:
log_wheel_bug(pd, event);
break;
}
}
static void
wheel_handle_event(struct plugin_device *pd,
enum wheel_event event,
uint64_t time)
{
enum wheel_state oldstate = pd->state;
switch (oldstate) {
case WHEEL_STATE_NONE:
wheel_handle_event_on_state_none(pd, event, time);
break;
case WHEEL_STATE_ACCUMULATING_SCROLL:
wheel_handle_event_on_state_accumulating_scroll(pd,
event,
time);
break;
case WHEEL_STATE_SCROLLING:
wheel_handle_event_on_state_scrolling(pd, event, time);
break;
}
if (oldstate != pd->state) {
plugin_log_debug(pd->parent->plugin,
"wheel: %s → %s → %s\n",
wheel_state_to_str(oldstate),
wheel_event_to_str(event),
wheel_state_to_str(pd->state));
}
}
static void
wheel_remove_scroll_events(struct evdev_frame *frame)
{
size_t nevents;
_unref_(evdev_frame) *copy = evdev_frame_clone(frame);
struct evdev_event *events = evdev_frame_get_events(copy, &nevents);
evdev_frame_reset(frame);
for (size_t i = 0; i < nevents; i++) {
struct evdev_event *e = &events[i];
switch (evdev_usage_enum(e->usage)) {
case EVDEV_REL_WHEEL:
case EVDEV_REL_WHEEL_HI_RES:
case EVDEV_REL_HWHEEL:
case EVDEV_REL_HWHEEL_HI_RES:
/* Do not append scroll events */
break;
default:
evdev_frame_append(frame, e, 1);
break;
}
}
}
static void
wheel_queue_scroll_events(struct plugin_device *pd, struct evdev_frame *frame)
{
if (pd->hi_res.y != 0) {
struct evdev_event e = {
.usage = evdev_usage_from(EVDEV_REL_WHEEL_HI_RES),
.value = pd->hi_res.y,
};
evdev_frame_append(frame, &e, 1);
pd->hi_res.y = 0;
}
if (pd->lo_res.y != 0) {
struct evdev_event e = {
.usage = evdev_usage_from(EVDEV_REL_WHEEL),
.value = pd->lo_res.y,
};
evdev_frame_append(frame, &e, 1);
pd->lo_res.y = 0;
}
if (pd->hi_res.x != 0) {
struct evdev_event e = {
.usage = evdev_usage_from(EVDEV_REL_HWHEEL_HI_RES),
.value = pd->hi_res.x,
};
evdev_frame_append(frame, &e, 1);
pd->hi_res.x = 0;
}
if (pd->lo_res.x != 0) {
struct evdev_event e = {
.usage = evdev_usage_from(EVDEV_REL_HWHEEL),
.value = pd->lo_res.x,
};
evdev_frame_append(frame, &e, 1);
pd->lo_res.x = 0;
}
}
static void
wheel_handle_state_none(struct plugin_device *pd,
struct evdev_frame *frame,
uint64_t time)
{
}
static void
wheel_handle_state_accumulating_scroll(struct plugin_device *pd,
struct evdev_frame *frame,
uint64_t time)
{
wheel_remove_scroll_events(frame);
if (abs(pd->hi_res.x) >= ACC_V120_THRESHOLD ||
abs(pd->hi_res.y) >= ACC_V120_THRESHOLD) {
wheel_handle_event(pd, WHEEL_EVENT_SCROLL_ACCUMULATED, time);
wheel_queue_scroll_events(pd, frame);
}
}
static void
wheel_handle_state_scrolling(struct plugin_device *pd,
struct evdev_frame *frame,
uint64_t time)
{
wheel_remove_scroll_events(frame);
wheel_queue_scroll_events(pd, frame);
}
static void
wheel_handle_direction_change(struct plugin_device *pd,
struct evdev_event *e,
uint64_t time)
{
enum wheel_direction new_dir = WHEEL_DIR_UNKNOW;
switch (evdev_usage_enum(e->usage)) {
case EVDEV_REL_WHEEL_HI_RES:
new_dir = (e->value > 0) ? WHEEL_DIR_VPOS : WHEEL_DIR_VNEG;
break;
case EVDEV_REL_HWHEEL_HI_RES:
new_dir = (e->value > 0) ? WHEEL_DIR_HPOS : WHEEL_DIR_HNEG;
break;
default:
return;
}
if (new_dir != WHEEL_DIR_UNKNOW && new_dir != pd->dir) {
pd->dir = new_dir;
wheel_handle_event(pd, WHEEL_EVENT_SCROLL_DIR_CHANGED, time);
}
}
static void
wheel_process_relative(struct plugin_device *pd,
struct evdev_event *e,
uint64_t time)
{
switch (evdev_usage_enum(e->usage)) {
case EVDEV_REL_WHEEL:
pd->lo_res.y += e->value;
wheel_handle_event(pd, WHEEL_EVENT_SCROLL, time);
break;
case EVDEV_REL_HWHEEL:
pd->lo_res.x += e->value;
wheel_handle_event(pd, WHEEL_EVENT_SCROLL, time);
break;
case EVDEV_REL_WHEEL_HI_RES:
pd->hi_res.y += e->value;
pd->hi_res_event_received = true;
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;
wheel_handle_direction_change(pd, e, time);
wheel_handle_event(pd, WHEEL_EVENT_SCROLL, time);
break;
default:
break;
}
}
static void
wheel_handle_state(struct plugin_device *pd,
struct evdev_frame *frame,
uint64_t time)
{
struct evdev_device *evdev = evdev_device(pd->device);
if (!pd->hi_res_event_received &&
(pd->lo_res.x != 0 || pd->lo_res.y != 0)) {
evdev_log_bug_kernel(evdev,
"device supports high-resolution scroll but only low-resolution events have been received.\n"
"See %s/incorrectly-enabled-hires.html for details\n",
HTTP_DOC_LINK);
pd->hi_res.x = pd->lo_res.x * 120;
pd->hi_res.y = pd->lo_res.y * 120;
}
switch (pd->state) {
case WHEEL_STATE_NONE:
wheel_handle_state_none(pd, frame, time);
break;
case WHEEL_STATE_ACCUMULATING_SCROLL:
wheel_handle_state_accumulating_scroll(pd, frame, time);
break;
case WHEEL_STATE_SCROLLING:
wheel_handle_state_scrolling(pd, frame, time);
break;
}
}
static void
wheel_on_scroll_timer_timeout(struct libinput_plugin *plugin,
uint64_t now,
void *data)
{
struct plugin_device *pd = data;
wheel_handle_event(pd, WHEEL_EVENT_SCROLL_TIMEOUT, now);
}
static struct plugin_device *
wheel_plugin_device_create(struct libinput_plugin *libinput_plugin,
struct plugin_data *plugin,
struct libinput_device *device)
{
struct evdev_device *evdev = evdev_device(device);
struct plugin_device *pd = zalloc(sizeof(*pd));
pd->parent = plugin;
pd->device = libinput_device_ref(device);
pd->state = WHEEL_STATE_NONE;
pd->dir = WHEEL_DIR_UNKNOW;
pd->ignore_small_hi_res_movements = !evdev_device_is_virtual(evdev);
if (pd->ignore_small_hi_res_movements) {
pd->scroll_timer = libinput_plugin_timer_new(libinput_plugin,
libinput_device_get_sysname(device),
wheel_on_scroll_timer_timeout,
pd);
}
return pd;
}
static void
wheel_plugin_device_destroy(struct plugin_device *pd)
{
list_remove(&pd->link);
if (pd->scroll_timer) {
wheel_cancel_scroll_timer(pd);
libinput_plugin_timer_unref(pd->scroll_timer);
}
libinput_device_unref(pd->device);
free(pd);
}
static void
wheel_plugin_destroy(struct libinput_plugin *libinput_plugin)
{
struct plugin_data *data = libinput_plugin_get_user_data(libinput_plugin);
struct plugin_device *pd;
list_for_each_safe(pd, &data->devices, link) {
wheel_plugin_device_destroy(pd);
}
free(data);
}
static void
wheel_plugin_device_added(struct libinput_plugin *libinput_plugin,
struct libinput_device *device)
{
if (!libinput_device_has_capability(device, LIBINPUT_DEVICE_CAP_POINTER))
return;
libinput_plugin_enable_device_event_frame(libinput_plugin, device, true);
struct plugin_data *plugin = libinput_plugin_get_user_data(libinput_plugin);
struct plugin_device *pd = wheel_plugin_device_create(libinput_plugin,
plugin, device);
list_take_append(&plugin->devices, pd, link);
}
static void
wheel_plugin_device_removed(struct libinput_plugin *libinput_plugin,
struct libinput_device *device)
{
struct plugin_data *plugin = libinput_plugin_get_user_data(libinput_plugin);
struct plugin_device *pd;
list_for_each_safe(pd, &plugin->devices, link) {
if (pd->device == device) {
wheel_plugin_device_destroy(pd);
return;
}
}
}
static void
wheel_handle_frame(struct plugin_device *pd,
struct evdev_frame *frame,
uint64_t time)
{
size_t nevents;
struct evdev_event *events = evdev_frame_get_events(frame, &nevents);
for (size_t i = 0; i < nevents; i++) {
struct evdev_event *e = &events[i];
uint16_t type = evdev_event_type(e);
switch (type) {
case EV_REL:
wheel_process_relative(pd, e, time);
break;
case EV_SYN:
wheel_handle_state(pd, frame, time);
break;
}
}
}
static void
wheel_plugin_evdev_frame(struct libinput_plugin *libinput_plugin,
struct libinput_device *device,
struct evdev_frame *frame)
{
struct plugin_data *plugin = libinput_plugin_get_user_data(libinput_plugin);
struct plugin_device *pd;
uint64_t time = evdev_frame_get_time(frame);
list_for_each(pd, &plugin->devices, link) {
if (pd->device == device) {
wheel_handle_frame(pd, frame, time);
break;
}
}
}
static const struct libinput_plugin_interface interface = {
.run = NULL,
.destroy = wheel_plugin_destroy,
.device_new = NULL,
.device_ignored = NULL,
.device_added = wheel_plugin_device_added,
.device_removed = wheel_plugin_device_removed,
.evdev_frame = wheel_plugin_evdev_frame,
};
void
libinput_mouse_plugin_wheel(struct libinput *libinput)
{
struct plugin_data *plugin = zalloc(sizeof(*plugin));
list_init(&plugin->devices);
_unref_(libinput_plugin) *p = libinput_plugin_new(libinput,
"mouse-wheel",
&interface,
plugin);
plugin->plugin = p;
}

View file

@ -0,0 +1,30 @@
/*
* Copyright © 2025 Red Hat, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice (including the next
* paragraph) shall be included in all copies or substantial portions of the
* Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*/
#include "config.h"
#include "libinput.h"
#include "libinput-plugin.h"
void
libinput_mouse_plugin_wheel(struct libinput *libinput);

View file

@ -37,6 +37,7 @@
#include "libinput-util.h" #include "libinput-util.h"
#include "libinput-private.h" #include "libinput-private.h"
#include "libinput-plugin-button-debounce.h" #include "libinput-plugin-button-debounce.h"
#include "libinput-plugin-mouse-wheel.h"
#include "libinput-plugin-tablet-double-tool.h" #include "libinput-plugin-tablet-double-tool.h"
#include "libinput-plugin-tablet-eraser-button.h" #include "libinput-plugin-tablet-eraser-button.h"
#include "libinput-plugin-tablet-forced-tool.h" #include "libinput-plugin-tablet-forced-tool.h"
@ -394,6 +395,7 @@ libinput_plugin_system_load_internal_plugins(struct libinput *libinput,
libinput_tablet_plugin_proximity_timer(libinput); libinput_tablet_plugin_proximity_timer(libinput);
libinput_tablet_plugin_eraser_button(libinput); libinput_tablet_plugin_eraser_button(libinput);
libinput_debounce_plugin(libinput); libinput_debounce_plugin(libinput);
libinput_mouse_plugin_wheel(libinput);
/* Our own event dispatch is implemented as mini-plugin, /* Our own event dispatch is implemented as mini-plugin,
* guarantee this one to always be last (and after any * guarantee this one to always be last (and after any