evdev: replace the debounce handler with a plugin

Same functionality but run as a plugin on the evdev stream instead.

Part-of: <https://gitlab.freedesktop.org/libinput/libinput/-/merge_requests/1230>
This commit is contained in:
Peter Hutterer 2025-06-16 16:16:13 +10:00
parent 7d8fac2868
commit 2c6fa261a3
6 changed files with 867 additions and 613 deletions

View file

@ -375,13 +375,13 @@ install_headers('src/libinput.h')
src_libinput = src_libfilter + [
'src/libinput.c',
'src/libinput-plugin.c',
'src/libinput-plugin-button-debounce.c',
'src/libinput-plugin-tablet-double-tool.c',
'src/libinput-plugin-tablet-eraser-button.c',
'src/libinput-plugin-tablet-forced-tool.c',
'src/libinput-plugin-tablet-proximity-timer.c',
'src/libinput-private-config.c',
'src/evdev.c',
'src/evdev-debounce.c',
'src/evdev-fallback.c',
'src/evdev-plugin.c',
'src/evdev-totem.c',

View file

@ -1,605 +0,0 @@
/*
* Copyright © 2017 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 "evdev-fallback.h"
/* Debounce cases to handle
P ... button press
R ... button release
---| timeout duration
'normal' .... event sent when it happens
'filtered' .. event is not sent (but may be sent later)
'delayed' ... event is sent with wall-clock delay
1) P---| R P normal, R normal
2) R---| P R normal, P normal
3) P---R--| P P normal, R filtered, delayed, P normal
4) R---P--| R R normal, P filtered, delayed, R normal
4.1) P---| R--P--| P normal, R filtered
5) P--R-P-| R P normal, R filtered, P filtered, R normal
6) R--P-R-| P R normal, P filtered, R filtered, P normal
7) P--R--|
---P-| P normal, R filtered, P filtered
8) R--P--|
---R-| R normal, P filtered, R filtered
1, 2 are the normal click cases without debouncing taking effect
3, 4 are fast clicks where the second event is delivered with a delay
5, 6 are contact bounces, fast
7, 8 are contact bounces, slow
4.1 is a special case with the same event sequence as 4 but we want to
filter the *release* event out, it's a button losing contact while being
held down.
7 and 8 are cases where the first event happens within the first timeout
but the second event is outside that timeout (but within the timeout of
the second event). These cases are handled by restarting the timer on every
event that could be part of a bouncing sequence, which makes these cases
indistinguishable from 5 and 6.
*/
enum debounce_event {
DEBOUNCE_EVENT_PRESS = 50,
DEBOUNCE_EVENT_RELEASE,
DEBOUNCE_EVENT_TIMEOUT,
DEBOUNCE_EVENT_TIMEOUT_SHORT,
DEBOUNCE_EVENT_OTHERBUTTON,
};
static inline const char *
debounce_state_to_str(enum debounce_state state)
{
switch(state) {
CASE_RETURN_STRING(DEBOUNCE_STATE_IS_UP);
CASE_RETURN_STRING(DEBOUNCE_STATE_IS_DOWN);
CASE_RETURN_STRING(DEBOUNCE_STATE_IS_DOWN_WAITING);
CASE_RETURN_STRING(DEBOUNCE_STATE_IS_UP_DELAYING);
CASE_RETURN_STRING(DEBOUNCE_STATE_IS_UP_DELAYING_SPURIOUS);
CASE_RETURN_STRING(DEBOUNCE_STATE_IS_UP_DETECTING_SPURIOUS);
CASE_RETURN_STRING(DEBOUNCE_STATE_IS_DOWN_DETECTING_SPURIOUS);
CASE_RETURN_STRING(DEBOUNCE_STATE_IS_UP_WAITING);
CASE_RETURN_STRING(DEBOUNCE_STATE_IS_DOWN_DELAYING);
CASE_RETURN_STRING(DEBOUNCE_STATE_DISABLED);
}
return NULL;
}
static inline const char*
debounce_event_to_str(enum debounce_event event)
{
switch(event) {
CASE_RETURN_STRING(DEBOUNCE_EVENT_PRESS);
CASE_RETURN_STRING(DEBOUNCE_EVENT_RELEASE);
CASE_RETURN_STRING(DEBOUNCE_EVENT_TIMEOUT);
CASE_RETURN_STRING(DEBOUNCE_EVENT_TIMEOUT_SHORT);
CASE_RETURN_STRING(DEBOUNCE_EVENT_OTHERBUTTON);
}
return NULL;
}
static inline void
log_debounce_bug(struct fallback_dispatch *fallback, enum debounce_event event)
{
evdev_log_bug_libinput(fallback->device,
"invalid debounce event %s in state %s\n",
debounce_event_to_str(event),
debounce_state_to_str(fallback->debounce.state));
}
static inline void
debounce_set_state(struct fallback_dispatch *fallback,
enum debounce_state new_state)
{
assert(new_state >= DEBOUNCE_STATE_IS_UP &&
new_state <= DEBOUNCE_STATE_IS_DOWN_DELAYING);
fallback->debounce.state = new_state;
}
static inline void
debounce_set_timer(struct fallback_dispatch *fallback,
uint64_t time)
{
const int DEBOUNCE_TIMEOUT_BOUNCE = ms2us(25);
libinput_timer_set(&fallback->debounce.timer,
time + DEBOUNCE_TIMEOUT_BOUNCE);
}
static inline void
debounce_set_timer_short(struct fallback_dispatch *fallback,
uint64_t time)
{
const int DEBOUNCE_TIMEOUT_SPURIOUS = ms2us(12);
libinput_timer_set(&fallback->debounce.timer_short,
time + DEBOUNCE_TIMEOUT_SPURIOUS);
}
static inline void
debounce_cancel_timer(struct fallback_dispatch *fallback)
{
libinput_timer_cancel(&fallback->debounce.timer);
}
static inline void
debounce_cancel_timer_short(struct fallback_dispatch *fallback)
{
libinput_timer_cancel(&fallback->debounce.timer_short);
}
static inline void
debounce_enable_spurious(struct fallback_dispatch *fallback)
{
if (fallback->debounce.spurious_enabled)
evdev_log_bug_libinput(fallback->device,
"tried to enable spurious debouncing twice\n");
fallback->debounce.spurious_enabled = true;
evdev_log_info(fallback->device,
"Enabling spurious button debouncing, "
"see %s/button-debouncing.html for details\n",
HTTP_DOC_LINK);
}
static void
debounce_notify_button(struct fallback_dispatch *fallback,
enum libinput_button_state state)
{
struct evdev_device *device = fallback->device;
evdev_usage_t usage = fallback->debounce.button_usage;
uint64_t time = fallback->debounce.button_time;
usage = evdev_to_left_handed(device, usage);
fallback_notify_physical_button(fallback, device, time, usage, state);
}
static void
debounce_is_up_handle_event(struct fallback_dispatch *fallback, enum debounce_event event, uint64_t time)
{
switch (event) {
case DEBOUNCE_EVENT_PRESS:
fallback->debounce.button_time = time;
debounce_set_timer(fallback, time);
debounce_set_state(fallback, DEBOUNCE_STATE_IS_DOWN_WAITING);
debounce_notify_button(fallback,
LIBINPUT_BUTTON_STATE_PRESSED);
break;
case DEBOUNCE_EVENT_RELEASE:
case DEBOUNCE_EVENT_TIMEOUT:
case DEBOUNCE_EVENT_TIMEOUT_SHORT:
log_debounce_bug(fallback, event);
break;
case DEBOUNCE_EVENT_OTHERBUTTON:
break;
}
}
static void
debounce_is_down_handle_event(struct fallback_dispatch *fallback, enum debounce_event event, uint64_t time)
{
switch (event) {
case DEBOUNCE_EVENT_PRESS:
log_debounce_bug(fallback, event);
break;
case DEBOUNCE_EVENT_RELEASE:
fallback->debounce.button_time = time;
debounce_set_timer(fallback, time);
debounce_set_timer_short(fallback, time);
if (fallback->debounce.spurious_enabled) {
debounce_set_state(fallback, DEBOUNCE_STATE_IS_UP_DELAYING_SPURIOUS);
} else {
debounce_set_state(fallback, DEBOUNCE_STATE_IS_UP_DETECTING_SPURIOUS);
debounce_notify_button(fallback,
LIBINPUT_BUTTON_STATE_RELEASED);
}
break;
case DEBOUNCE_EVENT_TIMEOUT:
case DEBOUNCE_EVENT_TIMEOUT_SHORT:
log_debounce_bug(fallback, event);
break;
case DEBOUNCE_EVENT_OTHERBUTTON:
break;
}
}
static void
debounce_is_down_waiting_handle_event(struct fallback_dispatch *fallback, enum debounce_event event, uint64_t time)
{
switch (event) {
case DEBOUNCE_EVENT_PRESS:
log_debounce_bug(fallback, event);
break;
case DEBOUNCE_EVENT_RELEASE:
debounce_set_timer(fallback, time);
debounce_set_state(fallback, DEBOUNCE_STATE_IS_UP_DELAYING);
/* Note: In the debouncing RPR case, we use the last
* release's time stamp */
fallback->debounce.button_time = time;
break;
case DEBOUNCE_EVENT_TIMEOUT:
debounce_set_state(fallback, DEBOUNCE_STATE_IS_DOWN);
break;
case DEBOUNCE_EVENT_TIMEOUT_SHORT:
log_debounce_bug(fallback, event);
break;
case DEBOUNCE_EVENT_OTHERBUTTON:
debounce_set_state(fallback, DEBOUNCE_STATE_IS_DOWN);
break;
}
}
static void
debounce_is_up_delaying_handle_event(struct fallback_dispatch *fallback, enum debounce_event event, uint64_t time)
{
switch (event) {
case DEBOUNCE_EVENT_PRESS:
debounce_set_timer(fallback, time);
debounce_set_state(fallback, DEBOUNCE_STATE_IS_DOWN_WAITING);
break;
case DEBOUNCE_EVENT_RELEASE:
case DEBOUNCE_EVENT_TIMEOUT_SHORT:
log_debounce_bug(fallback, event);
break;
case DEBOUNCE_EVENT_TIMEOUT:
case DEBOUNCE_EVENT_OTHERBUTTON:
debounce_set_state(fallback, DEBOUNCE_STATE_IS_UP);
debounce_notify_button(fallback,
LIBINPUT_BUTTON_STATE_RELEASED);
break;
}
}
static void
debounce_is_up_delaying_spurious_handle_event(struct fallback_dispatch *fallback, enum debounce_event event, uint64_t time)
{
switch (event) {
case DEBOUNCE_EVENT_PRESS:
debounce_set_state(fallback, DEBOUNCE_STATE_IS_DOWN);
debounce_cancel_timer(fallback);
debounce_cancel_timer_short(fallback);
break;
case DEBOUNCE_EVENT_RELEASE:
case DEBOUNCE_EVENT_TIMEOUT:
log_debounce_bug(fallback, event);
break;
case DEBOUNCE_EVENT_TIMEOUT_SHORT:
debounce_set_state(fallback, DEBOUNCE_STATE_IS_UP_WAITING);
debounce_notify_button(fallback,
LIBINPUT_BUTTON_STATE_RELEASED);
break;
case DEBOUNCE_EVENT_OTHERBUTTON:
debounce_set_state(fallback, DEBOUNCE_STATE_IS_UP);
debounce_notify_button(fallback,
LIBINPUT_BUTTON_STATE_RELEASED);
break;
}
}
static void
debounce_is_up_detecting_spurious_handle_event(struct fallback_dispatch *fallback, enum debounce_event event, uint64_t time)
{
switch (event) {
case DEBOUNCE_EVENT_PRESS:
debounce_set_timer(fallback, time);
debounce_set_timer_short(fallback, time);
/* Note: in a bouncing PRP case, we use the last press
* event time */
fallback->debounce.button_time = time;
debounce_set_state(fallback, DEBOUNCE_STATE_IS_DOWN_DETECTING_SPURIOUS);
break;
case DEBOUNCE_EVENT_RELEASE:
log_debounce_bug(fallback, event);
break;
case DEBOUNCE_EVENT_TIMEOUT:
debounce_set_state(fallback, DEBOUNCE_STATE_IS_UP);
break;
case DEBOUNCE_EVENT_TIMEOUT_SHORT:
debounce_set_state(fallback, DEBOUNCE_STATE_IS_UP_WAITING);
break;
case DEBOUNCE_EVENT_OTHERBUTTON:
debounce_set_state(fallback, DEBOUNCE_STATE_IS_UP);
break;
}
}
static void
debounce_is_down_detecting_spurious_handle_event(struct fallback_dispatch *fallback, enum debounce_event event, uint64_t time)
{
switch (event) {
case DEBOUNCE_EVENT_PRESS:
log_debounce_bug(fallback, event);
break;
case DEBOUNCE_EVENT_RELEASE:
debounce_set_timer(fallback, time);
debounce_set_timer_short(fallback, time);
debounce_set_state(fallback, DEBOUNCE_STATE_IS_UP_DETECTING_SPURIOUS);
break;
case DEBOUNCE_EVENT_TIMEOUT_SHORT:
debounce_cancel_timer(fallback);
debounce_set_state(fallback, DEBOUNCE_STATE_IS_DOWN);
debounce_enable_spurious(fallback);
debounce_notify_button(fallback,
LIBINPUT_BUTTON_STATE_PRESSED);
break;
case DEBOUNCE_EVENT_TIMEOUT:
case DEBOUNCE_EVENT_OTHERBUTTON:
debounce_set_state(fallback, DEBOUNCE_STATE_IS_DOWN);
debounce_notify_button(fallback,
LIBINPUT_BUTTON_STATE_PRESSED);
break;
}
}
static void
debounce_is_up_waiting_handle_event(struct fallback_dispatch *fallback, enum debounce_event event, uint64_t time)
{
switch (event) {
case DEBOUNCE_EVENT_PRESS:
debounce_set_timer(fallback, time);
/* Note: in a debouncing PRP case, we use the last press'
* time */
fallback->debounce.button_time = time;
debounce_set_state(fallback, DEBOUNCE_STATE_IS_DOWN_DELAYING);
break;
case DEBOUNCE_EVENT_RELEASE:
case DEBOUNCE_EVENT_TIMEOUT_SHORT:
log_debounce_bug(fallback, event);
break;
case DEBOUNCE_EVENT_TIMEOUT:
case DEBOUNCE_EVENT_OTHERBUTTON:
debounce_set_state(fallback, DEBOUNCE_STATE_IS_UP);
break;
}
}
static void
debounce_is_down_delaying_handle_event(struct fallback_dispatch *fallback, enum debounce_event event, uint64_t time)
{
switch (event) {
case DEBOUNCE_EVENT_PRESS:
log_debounce_bug(fallback, event);
break;
case DEBOUNCE_EVENT_RELEASE:
debounce_set_timer(fallback, time);
debounce_set_state(fallback, DEBOUNCE_STATE_IS_UP_WAITING);
break;
case DEBOUNCE_EVENT_TIMEOUT_SHORT:
log_debounce_bug(fallback, event);
break;
case DEBOUNCE_EVENT_TIMEOUT:
case DEBOUNCE_EVENT_OTHERBUTTON:
debounce_set_state(fallback, DEBOUNCE_STATE_IS_DOWN);
debounce_notify_button(fallback,
LIBINPUT_BUTTON_STATE_PRESSED);
break;
}
}
static void
debounce_disabled_handle_event(struct fallback_dispatch *fallback,
enum debounce_event event,
uint64_t time)
{
switch (event) {
case DEBOUNCE_EVENT_PRESS:
fallback->debounce.button_time = time;
debounce_notify_button(fallback,
LIBINPUT_BUTTON_STATE_PRESSED);
break;
case DEBOUNCE_EVENT_RELEASE:
fallback->debounce.button_time = time;
debounce_notify_button(fallback,
LIBINPUT_BUTTON_STATE_RELEASED);
break;
case DEBOUNCE_EVENT_TIMEOUT_SHORT:
case DEBOUNCE_EVENT_TIMEOUT:
log_debounce_bug(fallback, event);
break;
case DEBOUNCE_EVENT_OTHERBUTTON:
break;
}
}
static void
debounce_handle_event(struct fallback_dispatch *fallback,
enum debounce_event event,
uint64_t time)
{
enum debounce_state current = fallback->debounce.state;
if (event == DEBOUNCE_EVENT_OTHERBUTTON) {
debounce_cancel_timer(fallback);
debounce_cancel_timer_short(fallback);
}
switch(current) {
case DEBOUNCE_STATE_IS_UP:
debounce_is_up_handle_event(fallback, event, time);
break;
case DEBOUNCE_STATE_IS_DOWN:
debounce_is_down_handle_event(fallback, event, time);
break;
case DEBOUNCE_STATE_IS_DOWN_WAITING:
debounce_is_down_waiting_handle_event(fallback, event, time);
break;
case DEBOUNCE_STATE_IS_UP_DELAYING:
debounce_is_up_delaying_handle_event(fallback, event, time);
break;
case DEBOUNCE_STATE_IS_UP_DELAYING_SPURIOUS:
debounce_is_up_delaying_spurious_handle_event(fallback, event, time);
break;
case DEBOUNCE_STATE_IS_UP_DETECTING_SPURIOUS:
debounce_is_up_detecting_spurious_handle_event(fallback, event, time);
break;
case DEBOUNCE_STATE_IS_DOWN_DETECTING_SPURIOUS:
debounce_is_down_detecting_spurious_handle_event(fallback, event, time);
break;
case DEBOUNCE_STATE_IS_UP_WAITING:
debounce_is_up_waiting_handle_event(fallback, event, time);
break;
case DEBOUNCE_STATE_IS_DOWN_DELAYING:
debounce_is_down_delaying_handle_event(fallback, event, time);
break;
case DEBOUNCE_STATE_DISABLED:
debounce_disabled_handle_event(fallback, event, time);
break;
}
evdev_log_debug(fallback->device,
"debounce state: %s → %s → %s\n",
debounce_state_to_str(current),
debounce_event_to_str(event),
debounce_state_to_str(fallback->debounce.state));
}
void
fallback_debounce_handle_state(struct fallback_dispatch *dispatch,
uint64_t time)
{
evdev_usage_t changed[16] = {0}; /* usage of changed buttons */
size_t nchanged = 0;
bool flushed = false;
for (evdev_usage_t usage = evdev_usage_from(EVDEV_KEY_RESERVED);
evdev_usage_le(usage, EVDEV_KEY_MAX);
usage = evdev_usage_next(usage)) {
if (!evdev_usage_is_button(usage))
continue;
if (hw_key_has_changed(dispatch, usage))
changed[nchanged++] = usage;
/* If you manage to press more than 16 buttons in the same
* frame, we just quietly ignore the rest of them */
if (nchanged == ARRAY_LENGTH(changed))
break;
}
/* If we have more than one button this frame or a different button,
* flush the state machine with otherbutton */
if (nchanged > 1 ||
evdev_usage_cmp(changed[0], dispatch->debounce.button_usage) != 0) {
debounce_handle_event(dispatch,
DEBOUNCE_EVENT_OTHERBUTTON,
time);
flushed = true;
}
/* The state machine has some pre-conditions:
* - the IS_DOWN and IS_UP states are neutral entry states without
* any timeouts
* - a OTHERBUTTON event always flushes the state to IS_DOWN or
* IS_UP
*/
for (size_t i = 0; i < nchanged; i++) {
bool is_down = hw_is_key_down(dispatch, changed[i]);
if (flushed &&
dispatch->debounce.state != DEBOUNCE_STATE_DISABLED) {
debounce_set_state(dispatch,
!is_down ?
DEBOUNCE_STATE_IS_DOWN :
DEBOUNCE_STATE_IS_UP);
flushed = false;
}
dispatch->debounce.button_usage = changed[i];
debounce_handle_event(dispatch,
is_down ?
DEBOUNCE_EVENT_PRESS :
DEBOUNCE_EVENT_RELEASE,
time);
/* if we have more than one event, we flush the state
* machine immediately after the event itself */
if (nchanged > 1) {
debounce_handle_event(dispatch,
DEBOUNCE_EVENT_OTHERBUTTON,
time);
flushed = true;
}
}
}
static void
debounce_timeout(uint64_t now, void *data)
{
struct evdev_device *device = data;
struct fallback_dispatch *dispatch =
fallback_dispatch(device->dispatch);
debounce_handle_event(dispatch, DEBOUNCE_EVENT_TIMEOUT, now);
}
static void
debounce_timeout_short(uint64_t now, void *data)
{
struct evdev_device *device = data;
struct fallback_dispatch *dispatch =
fallback_dispatch(device->dispatch);
debounce_handle_event(dispatch, DEBOUNCE_EVENT_TIMEOUT_SHORT, now);
}
void
fallback_init_debounce(struct fallback_dispatch *dispatch)
{
struct evdev_device *device = dispatch->device;
char timer_name[64];
if (evdev_device_has_model_quirk(device, QUIRK_MODEL_BOUNCING_KEYS)) {
dispatch->debounce.state = DEBOUNCE_STATE_DISABLED;
return;
}
dispatch->debounce.state = DEBOUNCE_STATE_IS_UP;
snprintf(timer_name,
sizeof(timer_name),
"%s debounce short",
evdev_device_get_sysname(device));
libinput_timer_init(&dispatch->debounce.timer_short,
evdev_libinput_context(device),
timer_name,
debounce_timeout_short,
device);
snprintf(timer_name,
sizeof(timer_name),
"%s debounce",
evdev_device_get_sysname(device));
libinput_timer_init(&dispatch->debounce.timer,
evdev_libinput_context(device),
timer_name,
debounce_timeout,
device);
}

View file

@ -945,7 +945,6 @@ fallback_handle_state(struct fallback_dispatch *dispatch,
/* Buttons and keys */
if (dispatch->pending_event & EVDEV_KEY) {
bool want_debounce = false;
for (evdev_usage_t usage = evdev_usage_from(EVDEV_KEY_RESERVED);
evdev_usage_le(usage, EVDEV_KEY_MAX);
usage = evdev_usage_next(usage)) {
@ -953,13 +952,19 @@ fallback_handle_state(struct fallback_dispatch *dispatch,
continue;
if (evdev_usage_is_button(usage)) {
want_debounce = true;
break;
}
}
enum libinput_button_state state =
hw_is_key_down(dispatch, usage) ?
LIBINPUT_BUTTON_STATE_PRESSED :
LIBINPUT_BUTTON_STATE_RELEASED;
evdev_usage_t button = evdev_to_left_handed(device, usage);
fallback_notify_physical_button(dispatch,
device,
time,
button,
state);
if (want_debounce)
fallback_debounce_handle_state(dispatch, time);
}
}
hw_key_update_last_state(dispatch);
}
@ -1714,7 +1719,6 @@ fallback_dispatch_create(struct libinput_device *libinput_device)
}
fallback_init_wheel(dispatch, device);
fallback_init_debounce(dispatch);
fallback_init_arbitration(dispatch, device);
return &dispatch->base;

View file

@ -0,0 +1,823 @@
/*
* Copyright © 2017-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 <assert.h>
#include <libevdev/libevdev.h>
#include "util-mem.h"
#include "util-strings.h"
#include "evdev-frame.h"
#include "timer.h"
#include "quirks.h"
#include "libinput-log.h"
#include "libinput-util.h"
#include "libinput-plugin.h"
#include "libinput-plugin-button-debounce.h"
/* Debounce cases to handle
P ... button press
R ... button release
---| timeout duration
'normal' .... event sent when it happens
'filtered' .. event is not sent (but may be sent later)
'delayed' ... event is sent with wall-clock delay
1) P---| R P normal, R normal
2) R---| P R normal, P normal
3) P---R--| P P normal, R filtered, delayed, P normal
4) R---P--| R R normal, P filtered, delayed, R normal
4.1) P---| R--P--| P normal, R filtered
5) P--R-P-| R P normal, R filtered, P filtered, R normal
6) R--P-R-| P R normal, P filtered, R filtered, P normal
7) P--R--|
---P-| P normal, R filtered, P filtered
8) R--P--|
---R-| R normal, P filtered, R filtered
1, 2 are the normal click cases without debouncing taking effect
3, 4 are fast clicks where the second event is delivered with a delay
5, 6 are contact bounces, fast
7, 8 are contact bounces, slow
4.1 is a special case with the same event sequence as 4 but we want to
filter the *release* event out, it's a button losing contact while being
held down.
7 and 8 are cases where the first event happens within the first timeout
but the second event is outside that timeout (but within the timeout of
the second event). These cases are handled by restarting the timer on every
event that could be part of a bouncing sequence, which makes these cases
indistinguishable from 5 and 6.
*/
enum debounce_event {
DEBOUNCE_EVENT_PRESS = 50,
DEBOUNCE_EVENT_RELEASE,
DEBOUNCE_EVENT_TIMEOUT,
DEBOUNCE_EVENT_TIMEOUT_SHORT,
DEBOUNCE_EVENT_OTHERBUTTON,
};
enum debounce_state {
DEBOUNCE_STATE_IS_UP = 100,
DEBOUNCE_STATE_IS_DOWN,
DEBOUNCE_STATE_IS_DOWN_WAITING,
DEBOUNCE_STATE_IS_UP_DELAYING,
DEBOUNCE_STATE_IS_UP_DELAYING_SPURIOUS,
DEBOUNCE_STATE_IS_UP_DETECTING_SPURIOUS,
DEBOUNCE_STATE_IS_DOWN_DETECTING_SPURIOUS,
DEBOUNCE_STATE_IS_UP_WAITING,
DEBOUNCE_STATE_IS_DOWN_DELAYING,
DEBOUNCE_STATE_DISABLED = 999,
};
static inline const char *
debounce_state_to_str(enum debounce_state state)
{
switch(state) {
CASE_RETURN_STRING(DEBOUNCE_STATE_IS_UP);
CASE_RETURN_STRING(DEBOUNCE_STATE_IS_DOWN);
CASE_RETURN_STRING(DEBOUNCE_STATE_IS_DOWN_WAITING);
CASE_RETURN_STRING(DEBOUNCE_STATE_IS_UP_DELAYING);
CASE_RETURN_STRING(DEBOUNCE_STATE_IS_UP_DELAYING_SPURIOUS);
CASE_RETURN_STRING(DEBOUNCE_STATE_IS_UP_DETECTING_SPURIOUS);
CASE_RETURN_STRING(DEBOUNCE_STATE_IS_DOWN_DETECTING_SPURIOUS);
CASE_RETURN_STRING(DEBOUNCE_STATE_IS_UP_WAITING);
CASE_RETURN_STRING(DEBOUNCE_STATE_IS_DOWN_DELAYING);
CASE_RETURN_STRING(DEBOUNCE_STATE_DISABLED);
}
return NULL;
}
static inline const char*
debounce_event_to_str(enum debounce_event event)
{
switch(event) {
CASE_RETURN_STRING(DEBOUNCE_EVENT_PRESS);
CASE_RETURN_STRING(DEBOUNCE_EVENT_RELEASE);
CASE_RETURN_STRING(DEBOUNCE_EVENT_TIMEOUT);
CASE_RETURN_STRING(DEBOUNCE_EVENT_TIMEOUT_SHORT);
CASE_RETURN_STRING(DEBOUNCE_EVENT_OTHERBUTTON);
}
return NULL;
}
struct plugin_device {
struct list link;
struct libinput_device *device;
struct plugin_data *parent;
evdev_usage_t button_usage;
uint64_t button_time;
enum debounce_state state;
bool spurious_enabled;
struct libinput_plugin_timer *timer;
struct libinput_plugin_timer *timer_short;
};
static void
plugin_device_destroy(void *d)
{
struct plugin_device *device = d;
list_remove(&device->link);
libinput_plugin_timer_cancel(device->timer);
libinput_plugin_timer_unref(device->timer);
libinput_plugin_timer_cancel(device->timer_short);
libinput_plugin_timer_unref(device->timer_short);
libinput_device_unref(device->device);
free(device);
}
struct plugin_data {
struct list devices;
struct libinput_plugin *plugin;
};
static void
plugin_data_destroy(void *d)
{
struct plugin_data *data = d;
struct plugin_device *device;
list_for_each_safe(device, &data->devices, link) {
plugin_device_destroy(device);
}
free(data);
}
DEFINE_DESTROY_CLEANUP_FUNC(plugin_data);
static void
plugin_destroy(struct libinput_plugin *libinput_plugin)
{
struct plugin_data *plugin = libinput_plugin_get_user_data(libinput_plugin);
plugin_data_destroy(plugin);
}
static inline void
log_debounce_bug(struct plugin_device *device, enum debounce_event event)
{
plugin_log_bug_libinput(device->parent->plugin,
"invalid debounce event %s in state %s\n",
debounce_event_to_str(event),
debounce_state_to_str(device->state));
}
static inline void
debounce_set_state(struct plugin_device *device,
enum debounce_state new_state)
{
assert(new_state >= DEBOUNCE_STATE_IS_UP &&
new_state <= DEBOUNCE_STATE_IS_DOWN_DELAYING);
device->state = new_state;
}
static inline void
debounce_set_timer(struct plugin_device *device,
uint64_t time)
{
const int DEBOUNCE_TIMEOUT_BOUNCE = ms2us(25);
libinput_plugin_timer_set(device->timer,
time + DEBOUNCE_TIMEOUT_BOUNCE);
}
static inline void
debounce_set_timer_short(struct plugin_device *device,
uint64_t time)
{
const int DEBOUNCE_TIMEOUT_SPURIOUS = ms2us(12);
libinput_plugin_timer_set(device->timer_short,
time + DEBOUNCE_TIMEOUT_SPURIOUS);
}
static inline void
debounce_cancel_timer(struct plugin_device *device)
{
libinput_plugin_timer_cancel(device->timer);
}
static inline void
debounce_cancel_timer_short(struct plugin_device *device)
{
libinput_plugin_timer_cancel(device->timer_short);
}
static inline void
debounce_enable_spurious(struct plugin_device *device)
{
if (device->spurious_enabled)
plugin_log_bug(device->parent->plugin,
"tried to enable spurious debouncing twice\n");
device->spurious_enabled = true;
plugin_log_info(device->parent->plugin,
"%s: enabling spurious button debouncing, "
"see %s/button-debouncing.html for details\n",
libinput_device_get_name(device->device),
HTTP_DOC_LINK);
}
static void
debounce_notify_button(struct plugin_device *device,
struct evdev_frame *frame,
enum libinput_button_state state)
{
const struct evdev_event button = {
.usage = device->button_usage,
.value = state == LIBINPUT_BUTTON_STATE_PRESSED ? 1 : 0,
};
_unref_(evdev_frame) *button_frame = NULL;
if (frame == NULL) {
button_frame = evdev_frame_new(2);
frame = button_frame;
}
evdev_frame_append(frame, &button, 1);
evdev_frame_set_time(frame, device->button_time);
libinput_plugin_prepend_evdev_frame(device->parent->plugin,
device->device,
frame);
}
static void
debounce_is_up_handle_event(struct plugin_device *device,
enum debounce_event event,
struct evdev_frame *frame,
uint64_t time)
{
switch (event) {
case DEBOUNCE_EVENT_PRESS:
device->button_time = time;
debounce_set_timer(device, time);
debounce_set_state(device, DEBOUNCE_STATE_IS_DOWN_WAITING);
debounce_notify_button(device,
frame,
LIBINPUT_BUTTON_STATE_PRESSED);
break;
case DEBOUNCE_EVENT_RELEASE:
case DEBOUNCE_EVENT_TIMEOUT:
case DEBOUNCE_EVENT_TIMEOUT_SHORT:
log_debounce_bug(device, event);
break;
case DEBOUNCE_EVENT_OTHERBUTTON:
break;
}
}
static void
debounce_is_down_handle_event(struct plugin_device *device,
enum debounce_event event,
struct evdev_frame *frame,
uint64_t time)
{
switch (event) {
case DEBOUNCE_EVENT_PRESS:
/* If we lost the kernel button release event (e.g. something
* grabbed the device for a short while) we quietly ignore
* the next down event */
break;
case DEBOUNCE_EVENT_RELEASE:
device->button_time = time;
debounce_set_timer(device, time);
debounce_set_timer_short(device, time);
if (device->spurious_enabled) {
debounce_set_state(device, DEBOUNCE_STATE_IS_UP_DELAYING_SPURIOUS);
} else {
debounce_set_state(device, DEBOUNCE_STATE_IS_UP_DETECTING_SPURIOUS);
debounce_notify_button(device,
frame,
LIBINPUT_BUTTON_STATE_RELEASED);
}
break;
case DEBOUNCE_EVENT_TIMEOUT:
case DEBOUNCE_EVENT_TIMEOUT_SHORT:
log_debounce_bug(device, event);
break;
case DEBOUNCE_EVENT_OTHERBUTTON:
break;
}
}
static void
debounce_is_down_waiting_handle_event(struct plugin_device *device,
enum debounce_event event,
struct evdev_frame *frame,
uint64_t time)
{
switch (event) {
case DEBOUNCE_EVENT_PRESS:
log_debounce_bug(device, event);
break;
case DEBOUNCE_EVENT_RELEASE:
debounce_set_timer(device, time);
debounce_set_state(device, DEBOUNCE_STATE_IS_UP_DELAYING);
/* Note: In the debouncing RPR case, we use the last
* release's time stamp */
device->button_time = time;
break;
case DEBOUNCE_EVENT_TIMEOUT:
debounce_set_state(device, DEBOUNCE_STATE_IS_DOWN);
break;
case DEBOUNCE_EVENT_TIMEOUT_SHORT:
log_debounce_bug(device, event);
break;
case DEBOUNCE_EVENT_OTHERBUTTON:
debounce_set_state(device, DEBOUNCE_STATE_IS_DOWN);
break;
}
}
static void
debounce_is_up_delaying_handle_event(struct plugin_device *device,
enum debounce_event event,
struct evdev_frame *frame,
uint64_t time)
{
switch (event) {
case DEBOUNCE_EVENT_PRESS:
debounce_set_timer(device, time);
debounce_set_state(device, DEBOUNCE_STATE_IS_DOWN_WAITING);
break;
case DEBOUNCE_EVENT_RELEASE:
case DEBOUNCE_EVENT_TIMEOUT_SHORT:
log_debounce_bug(device, event);
break;
case DEBOUNCE_EVENT_TIMEOUT:
case DEBOUNCE_EVENT_OTHERBUTTON:
debounce_set_state(device, DEBOUNCE_STATE_IS_UP);
debounce_notify_button(device,
frame,
LIBINPUT_BUTTON_STATE_RELEASED);
break;
}
}
static void
debounce_is_up_delaying_spurious_handle_event(struct plugin_device *device,
enum debounce_event event,
struct evdev_frame *frame,
uint64_t time)
{
switch (event) {
case DEBOUNCE_EVENT_PRESS:
debounce_set_state(device, DEBOUNCE_STATE_IS_DOWN);
debounce_cancel_timer(device);
debounce_cancel_timer_short(device);
break;
case DEBOUNCE_EVENT_RELEASE:
case DEBOUNCE_EVENT_TIMEOUT:
log_debounce_bug(device, event);
break;
case DEBOUNCE_EVENT_TIMEOUT_SHORT:
debounce_set_state(device, DEBOUNCE_STATE_IS_UP_WAITING);
debounce_notify_button(device,
frame,
LIBINPUT_BUTTON_STATE_RELEASED);
break;
case DEBOUNCE_EVENT_OTHERBUTTON:
debounce_set_state(device, DEBOUNCE_STATE_IS_UP);
debounce_notify_button(device,
frame,
LIBINPUT_BUTTON_STATE_RELEASED);
break;
}
}
static void
debounce_is_up_detecting_spurious_handle_event(struct plugin_device *device,
enum debounce_event event,
struct evdev_frame *frame,
uint64_t time)
{
switch (event) {
case DEBOUNCE_EVENT_PRESS:
debounce_set_timer(device, time);
debounce_set_timer_short(device, time);
/* Note: in a bouncing PRP case, we use the last press
* event time */
device->button_time = time;
debounce_set_state(device, DEBOUNCE_STATE_IS_DOWN_DETECTING_SPURIOUS);
break;
case DEBOUNCE_EVENT_RELEASE:
log_debounce_bug(device, event);
break;
case DEBOUNCE_EVENT_TIMEOUT:
debounce_set_state(device, DEBOUNCE_STATE_IS_UP);
break;
case DEBOUNCE_EVENT_TIMEOUT_SHORT:
debounce_set_state(device, DEBOUNCE_STATE_IS_UP_WAITING);
break;
case DEBOUNCE_EVENT_OTHERBUTTON:
debounce_set_state(device, DEBOUNCE_STATE_IS_UP);
break;
}
}
static void
debounce_is_down_detecting_spurious_handle_event(struct plugin_device *device,
enum debounce_event event,
struct evdev_frame *frame,
uint64_t time)
{
switch (event) {
case DEBOUNCE_EVENT_PRESS:
log_debounce_bug(device, event);
break;
case DEBOUNCE_EVENT_RELEASE:
debounce_set_timer(device, time);
debounce_set_timer_short(device, time);
debounce_set_state(device, DEBOUNCE_STATE_IS_UP_DETECTING_SPURIOUS);
break;
case DEBOUNCE_EVENT_TIMEOUT_SHORT:
debounce_cancel_timer(device);
debounce_set_state(device, DEBOUNCE_STATE_IS_DOWN);
debounce_enable_spurious(device);
debounce_notify_button(device,
frame,
LIBINPUT_BUTTON_STATE_PRESSED);
break;
case DEBOUNCE_EVENT_TIMEOUT:
case DEBOUNCE_EVENT_OTHERBUTTON:
debounce_set_state(device, DEBOUNCE_STATE_IS_DOWN);
debounce_notify_button(device,
frame,
LIBINPUT_BUTTON_STATE_PRESSED);
break;
}
}
static void
debounce_is_up_waiting_handle_event(struct plugin_device *device,
enum debounce_event event,
struct evdev_frame *frame,
uint64_t time)
{
switch (event) {
case DEBOUNCE_EVENT_PRESS:
debounce_set_timer(device, time);
/* Note: in a debouncing PRP case, we use the last press'
* time */
device->button_time = time;
debounce_set_state(device, DEBOUNCE_STATE_IS_DOWN_DELAYING);
break;
case DEBOUNCE_EVENT_RELEASE:
case DEBOUNCE_EVENT_TIMEOUT_SHORT:
log_debounce_bug(device, event);
break;
case DEBOUNCE_EVENT_TIMEOUT:
case DEBOUNCE_EVENT_OTHERBUTTON:
debounce_set_state(device, DEBOUNCE_STATE_IS_UP);
break;
}
}
static void
debounce_is_down_delaying_handle_event(struct plugin_device *device,
enum debounce_event event,
struct evdev_frame *frame,
uint64_t time)
{
switch (event) {
case DEBOUNCE_EVENT_PRESS:
log_debounce_bug(device, event);
break;
case DEBOUNCE_EVENT_RELEASE:
debounce_set_timer(device, time);
debounce_set_state(device, DEBOUNCE_STATE_IS_UP_WAITING);
break;
case DEBOUNCE_EVENT_TIMEOUT_SHORT:
log_debounce_bug(device, event);
break;
case DEBOUNCE_EVENT_TIMEOUT:
case DEBOUNCE_EVENT_OTHERBUTTON:
debounce_set_state(device, DEBOUNCE_STATE_IS_DOWN);
debounce_notify_button(device,
frame,
LIBINPUT_BUTTON_STATE_PRESSED);
break;
}
}
static void
debounce_disabled_handle_event(struct plugin_device *device,
enum debounce_event event,
struct evdev_frame *frame,
uint64_t time)
{
switch (event) {
case DEBOUNCE_EVENT_PRESS:
device->button_time = time;
debounce_notify_button(device,
frame,
LIBINPUT_BUTTON_STATE_PRESSED);
break;
case DEBOUNCE_EVENT_RELEASE:
device->button_time = time;
debounce_notify_button(device,
frame,
LIBINPUT_BUTTON_STATE_RELEASED);
break;
case DEBOUNCE_EVENT_TIMEOUT_SHORT:
case DEBOUNCE_EVENT_TIMEOUT:
log_debounce_bug(device, event);
break;
case DEBOUNCE_EVENT_OTHERBUTTON:
break;
}
}
static void
debounce_handle_event(struct plugin_device *device,
enum debounce_event event,
struct evdev_frame *frame,
uint64_t time)
{
enum debounce_state current = device->state;
if (event == DEBOUNCE_EVENT_OTHERBUTTON) {
debounce_cancel_timer(device);
debounce_cancel_timer_short(device);
}
switch(current) {
case DEBOUNCE_STATE_IS_UP:
debounce_is_up_handle_event(device, event, frame, time);
break;
case DEBOUNCE_STATE_IS_DOWN:
debounce_is_down_handle_event(device, event, frame, time);
break;
case DEBOUNCE_STATE_IS_DOWN_WAITING:
debounce_is_down_waiting_handle_event(device, event, frame, time);
break;
case DEBOUNCE_STATE_IS_UP_DELAYING:
debounce_is_up_delaying_handle_event(device, event, frame, time);
break;
case DEBOUNCE_STATE_IS_UP_DELAYING_SPURIOUS:
debounce_is_up_delaying_spurious_handle_event(device, event, frame, time);
break;
case DEBOUNCE_STATE_IS_UP_DETECTING_SPURIOUS:
debounce_is_up_detecting_spurious_handle_event(device, event, frame, time);
break;
case DEBOUNCE_STATE_IS_DOWN_DETECTING_SPURIOUS:
debounce_is_down_detecting_spurious_handle_event(device, event, frame, time);
break;
case DEBOUNCE_STATE_IS_UP_WAITING:
debounce_is_up_waiting_handle_event(device, event, frame, time);
break;
case DEBOUNCE_STATE_IS_DOWN_DELAYING:
debounce_is_down_delaying_handle_event(device, event, frame, time);
break;
case DEBOUNCE_STATE_DISABLED:
debounce_disabled_handle_event(device, event, frame, time);
break;
}
plugin_log_debug(device->parent->plugin,
"debounce state: %s → %s → %s\n",
debounce_state_to_str(current),
debounce_event_to_str(event),
debounce_state_to_str(device->state));
}
static void
debounce_plugin_handle_frame(struct plugin_device *device,
struct evdev_frame *frame,
uint64_t time)
{
size_t nchanged = 0;
bool flushed = false;
size_t nevents;
struct evdev_event *events = evdev_frame_get_events(frame, &nevents);
/* Strip out all button events from this frame (if any). Then
* append the button events to that stripped frame according
* to our state machine.
*
* We allow for a max of 16 buttons to be appended, if you press more
* than 16 buttons within the same frame good luck to you.
*/
_unref_(evdev_frame) *filtered_frame = evdev_frame_new(nevents + 16);
for (size_t i = 0; i < nevents; i++) {
struct evdev_event *e = &events[i];
if (!evdev_usage_is_button(e->usage)) {
evdev_frame_append(filtered_frame, e, 1);
continue;
}
nchanged++;
/* If we have more than one button this frame or a different button,
* flush the state machine with otherbutton */
if (!flushed &&
(nchanged > 1 ||
evdev_usage_cmp(e->usage, device->button_usage) != 0)) {
debounce_handle_event(device,
DEBOUNCE_EVENT_OTHERBUTTON,
NULL,
time);
flushed = true;
}
}
if (nchanged == 0)
return;
/* The state machine has some pre-conditions:
* - the IS_DOWN and IS_UP states are neutral entry states without
* any timeouts
* - a OTHERBUTTON event always flushes the state to IS_DOWN or
* IS_UP
*/
for (size_t i = 0; i < nevents; i++) {
struct evdev_event *e = &events[i];
bool is_down = !!e->value;
if (!evdev_usage_is_button(e->usage))
continue;
if (flushed &&
device->state != DEBOUNCE_STATE_DISABLED) {
debounce_set_state(device,
!is_down ?
DEBOUNCE_STATE_IS_DOWN :
DEBOUNCE_STATE_IS_UP);
flushed = false;
}
device->button_usage = e->usage;
debounce_handle_event(device,
is_down ?
DEBOUNCE_EVENT_PRESS :
DEBOUNCE_EVENT_RELEASE,
filtered_frame,
time);
/* if we have more than one event, we flush the state
* machine immediately after the event itself */
if (nchanged > 1) {
debounce_handle_event(device,
DEBOUNCE_EVENT_OTHERBUTTON,
filtered_frame,
time);
flushed = true;
}
}
evdev_frame_set(frame,
evdev_frame_get_events(filtered_frame, NULL),
evdev_frame_get_count(filtered_frame));
}
static void
debounce_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;
list_for_each(pd, &plugin->devices, link) {
if (pd->device == device) {
debounce_plugin_handle_frame(pd, frame, frame->time);
break;
}
}
}
static void
debounce_timeout(struct libinput_plugin *plugin, uint64_t now, void *data)
{
struct plugin_device *device = data;
debounce_handle_event(device, DEBOUNCE_EVENT_TIMEOUT, NULL, now);
}
static void
debounce_timeout_short(struct libinput_plugin *plugin, uint64_t now, void *data)
{
struct plugin_device *device = data;
debounce_handle_event(device, DEBOUNCE_EVENT_TIMEOUT_SHORT, NULL, now);
}
static void
debounce_plugin_device_added(struct libinput_plugin *libinput_plugin,
struct libinput_device *device)
{
if (!libinput_device_has_capability(device, LIBINPUT_DEVICE_CAP_POINTER))
return;
_unref_(udev_device) *udev_device = libinput_device_get_udev_device(device);
if (udev_device) {
const char *prop = udev_device_get_property_value(udev_device,
"ID_INPUT_TOUCHPAD");
bool val;
if (parse_boolean_property(prop, &val) && val) {
return;
}
}
_unref_(quirks) *q = libinput_device_get_quirks(device);
bool result = false;
if (q &&
quirks_get_bool(q, QUIRK_MODEL_BOUNCING_KEYS, &result) &&
result) {
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 = zalloc(sizeof(*pd));
pd->device = libinput_device_ref(device);
pd->parent = plugin;
pd->state = DEBOUNCE_STATE_IS_UP;
_autofree_ char *timer1_name = strdup_printf("debounce-%s",
libinput_device_get_sysname(device));
_autofree_ char *timer2_name = strdup_printf("debounce-short-%s",
libinput_device_get_sysname(device));
pd->timer = libinput_plugin_timer_new(libinput_plugin,
timer1_name,
debounce_timeout,
pd);
pd->timer_short = libinput_plugin_timer_new(libinput_plugin,
timer2_name,
debounce_timeout_short,
pd);
list_take_append(&plugin->devices, pd, link);
}
static void
debounce_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 *dev;
list_for_each_safe(dev, &plugin->devices, link) {
if (dev->device == device) {
plugin_device_destroy(dev);
return;
}
}
}
static const struct libinput_plugin_interface interface = {
.run = NULL,
.destroy = plugin_destroy,
.device_new = NULL,
.device_ignored = NULL,
.device_added = debounce_plugin_device_added,
.device_removed = debounce_plugin_device_removed,
.evdev_frame = debounce_plugin_evdev_frame,
};
void
libinput_debounce_plugin(struct libinput *libinput)
{
struct plugin_data *plugin = zalloc(sizeof(*plugin));
list_init(&plugin->devices);
_unref_(libinput_plugin) *p = libinput_plugin_new(libinput,
"button-debounce",
&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_debounce_plugin(struct libinput *libinput);

View file

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