diff --git a/meson.build b/meson.build index 0dd736e3..cb8f10c1 100644 --- a/meson.build +++ b/meson.build @@ -376,6 +376,7 @@ src_libinput = src_libfilter + [ 'src/libinput.c', 'src/libinput-plugin.c', 'src/libinput-plugin-button-debounce.c', + 'src/libinput-plugin-mouse-wheel.c', 'src/libinput-plugin-tablet-double-tool.c', 'src/libinput-plugin-tablet-eraser-button.c', 'src/libinput-plugin-tablet-forced-tool.c', @@ -395,7 +396,6 @@ src_libinput = src_libfilter + [ 'src/evdev-tablet.c', 'src/evdev-tablet-pad.c', 'src/evdev-tablet-pad-leds.c', - 'src/evdev-wheel.c', 'src/path-seat.c', 'src/udev-seat.c', 'src/timer.c', diff --git a/src/evdev-fallback.c b/src/evdev-fallback.c index aaea4a15..1cc6d386 100644 --- a/src/evdev-fallback.c +++ b/src/evdev-fallback.c @@ -207,6 +207,100 @@ fallback_flush_relative_motion(struct fallback_dispatch *dispatch, 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 fallback_flush_absolute_motion(struct fallback_dispatch *dispatch, struct evdev_device *device, @@ -754,6 +848,20 @@ fallback_reject_relative(struct evdev_device *device, 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 fallback_process_relative(struct fallback_dispatch *dispatch, struct evdev_device *device, @@ -771,11 +879,25 @@ fallback_process_relative(struct fallback_dispatch *dispatch, dispatch->rel.y += e->value; dispatch->pending_event |= EVDEV_RELATIVE_MOTION; 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: break; } - - fallback_wheel_process_relative(dispatch, e, time); } static inline void @@ -941,7 +1063,7 @@ fallback_handle_state(struct fallback_dispatch *dispatch, if (need_touch_frame) touch_notify_frame(&device->base, time); - fallback_wheel_handle_state(dispatch, device, time); + fallback_flush_wheels(dispatch, device, time); /* Buttons and keys */ 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 evdev_paired_keyboard *kbd; - libinput_timer_cancel(&dispatch->wheel.scroll_timer); libinput_timer_cancel(&dispatch->debounce.timer); libinput_timer_cancel(&dispatch->debounce.timer_short); 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); - libinput_timer_destroy(&dispatch->wheel.scroll_timer); libinput_timer_destroy(&dispatch->arbitration.arbitration_timer); libinput_timer_destroy(&dispatch->debounce.timer); libinput_timer_destroy(&dispatch->debounce.timer_short); @@ -1718,7 +1838,6 @@ fallback_dispatch_create(struct libinput_device *libinput_device) want_config); } - fallback_init_wheel(dispatch, device); fallback_init_arbitration(dispatch, device); return &dispatch->base; diff --git a/src/evdev-fallback.h b/src/evdev-fallback.h index dab6f11b..252f6c36 100644 --- a/src/evdev-fallback.h +++ b/src/evdev-fallback.h @@ -60,20 +60,6 @@ enum palm_state { 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 { bool dirty; enum mt_slot_state state; @@ -112,13 +98,8 @@ struct fallback_dispatch { struct device_coords rel; struct { - enum wheel_state state; struct device_coords lo_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; struct { @@ -239,17 +220,4 @@ fallback_notify_physical_button(struct fallback_dispatch *dispatch, evdev_usage_t button, 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 diff --git a/src/evdev-wheel.c b/src/evdev-wheel.c deleted file mode 100644 index ad5fb267..00000000 --- a/src/evdev-wheel.c +++ /dev/null @@ -1,445 +0,0 @@ -/* - * Copyright © 2010 Intel Corporation - * Copyright © 2013 Jonas Ådahl - * Copyright © 2013-2017 Red Hat, Inc. - * Copyright © 2017 James Ye - * 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); - } -} diff --git a/src/libinput-plugin-mouse-wheel.c b/src/libinput-plugin-mouse-wheel.c new file mode 100644 index 00000000..064c9fae --- /dev/null +++ b/src/libinput-plugin-mouse-wheel.c @@ -0,0 +1,569 @@ +/* + * Copyright © 2010 Intel Corporation + * Copyright © 2013 Jonas Ådahl + * Copyright © 2013-2017 Red Hat, Inc. + * Copyright © 2017 James Ye + * 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 + +#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; +} diff --git a/src/libinput-plugin-mouse-wheel.h b/src/libinput-plugin-mouse-wheel.h new file mode 100644 index 00000000..14038d81 --- /dev/null +++ b/src/libinput-plugin-mouse-wheel.h @@ -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); diff --git a/src/libinput-plugin.c b/src/libinput-plugin.c index 1e4b8ae3..ee721e2f 100644 --- a/src/libinput-plugin.c +++ b/src/libinput-plugin.c @@ -37,6 +37,7 @@ #include "libinput-util.h" #include "libinput-private.h" #include "libinput-plugin-button-debounce.h" +#include "libinput-plugin-mouse-wheel.h" #include "libinput-plugin-tablet-double-tool.h" #include "libinput-plugin-tablet-eraser-button.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_eraser_button(libinput); libinput_debounce_plugin(libinput); + libinput_mouse_plugin_wheel(libinput); /* Our own event dispatch is implemented as mini-plugin, * guarantee this one to always be last (and after any