tablet: implement eraser button disabling

This adds a new (internal) plugin that is responsible for eraser button
disabling.

Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
Part-of: <https://gitlab.freedesktop.org/libinput/libinput/-/merge_requests/1218>
This commit is contained in:
Peter Hutterer 2025-06-02 15:11:08 +10:00
parent 42c0bff29b
commit c48aff86d7
9 changed files with 1112 additions and 0 deletions

View file

@ -376,6 +376,7 @@ src_libinput = src_libfilter + [
'src/libinput.c',
'src/libinput-plugin.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',

View file

@ -1264,6 +1264,161 @@ pressure_range_get_default(struct libinput_tablet_tool *tool, double *min, doubl
*max = 1.0;
}
static void
tablet_tool_apply_eraser_button(struct tablet_dispatch *tablet,
struct libinput_tablet_tool *tool)
{
if (bitmask_is_empty(tool->eraser_button.available_modes))
return;
if (tool->eraser_button.mode == tool->eraser_button.want_mode &&
tool->eraser_button.button == tool->eraser_button.want_button)
return;
if (!tablet_has_status(tablet, TABLET_TOOL_OUT_OF_PROXIMITY))
return;
tool->eraser_button.mode = tool->eraser_button.want_mode;
tool->eraser_button.button = tool->eraser_button.want_button;
struct libinput *libinput = tablet_libinput_context(tablet);
libinput_plugin_system_notify_tablet_tool_configured(&libinput->plugin_system,
tool);
}
static bitmask_t
eraser_button_get_modes(struct libinput_tablet_tool *tool)
{
return tool->eraser_button.available_modes;
}
static void
eraser_button_toggle(struct libinput_tablet_tool *tool)
{
struct libinput_device *libinput_device = tool->last_device;
struct evdev_device *device = evdev_device(libinput_device);
struct tablet_dispatch *tablet = tablet_dispatch(device->dispatch);
tablet_tool_apply_eraser_button(tablet, tool);
}
static enum libinput_config_status
eraser_button_set_mode(struct libinput_tablet_tool *tool,
enum libinput_config_eraser_button_mode mode)
{
if (mode != LIBINPUT_CONFIG_ERASER_BUTTON_DEFAULT &&
!bitmask_all(tool->eraser_button.available_modes, bitmask_from_u32(mode)))
return LIBINPUT_CONFIG_STATUS_UNSUPPORTED;
tool->eraser_button.want_mode = mode;
eraser_button_toggle(tool);
return LIBINPUT_CONFIG_STATUS_SUCCESS;
}
static enum libinput_config_eraser_button_mode
eraser_button_get_mode(struct libinput_tablet_tool *tool)
{
return tool->eraser_button.mode;
}
static enum libinput_config_eraser_button_mode
eraser_button_get_default_mode(struct libinput_tablet_tool *tool)
{
return LIBINPUT_CONFIG_ERASER_BUTTON_DEFAULT;
}
static enum libinput_config_status
eraser_button_set_button(struct libinput_tablet_tool *tool, unsigned int button)
{
if (!libinput_tablet_tool_has_button(tool, button))
return LIBINPUT_CONFIG_STATUS_INVALID;
switch (button) {
case BTN_STYLUS:
case BTN_STYLUS2:
case BTN_STYLUS3:
break;
default:
log_bug_libinput(libinput_device_get_context(tool->last_device),
"Unsupported eraser button 0x%x",
button);
return LIBINPUT_CONFIG_STATUS_INVALID;
}
tool->eraser_button.want_button = button;
eraser_button_toggle(tool);
return LIBINPUT_CONFIG_STATUS_SUCCESS;
}
static unsigned int
eraser_button_get_button(struct libinput_tablet_tool *tool)
{
return tool->eraser_button.button;
}
static unsigned int
eraser_button_get_default_button(struct libinput_tablet_tool *tool)
{
/* Which button we want is complicated. Other than Wacom no-one supports
* tool ids so we cannot know if an individual tool supports any of the
* BTN_STYLUS. e.g. any Huion tablet that supports the Huion PW600
* will have BTN_STYLUS3 - regardless if that tool is actually present.
* So we default to BTN_STYLUS3 because there's no placeholder BTN_STYLUS4.
* in the kernel.
*/
if (!libinput_tablet_tool_has_button(tool, BTN_STYLUS))
return BTN_STYLUS;
if (!libinput_tablet_tool_has_button(tool, BTN_STYLUS2))
return BTN_STYLUS2;
return BTN_STYLUS3;
}
static void
tool_init_eraser_button(struct tablet_dispatch *tablet,
struct libinput_tablet_tool *tool,
const WacomStylus *s)
{
/* We provide an eraser button config if:
* - the tool is a pen
* - we don't know about the stylus (that's a good indication the
* stylus doesn't have tool ids which means it'll follow the windows
* pen protocol)
* - the tool does *not* have an eraser on the back end
*
* Because those are the only tools where the eraser button may
* get changed to a real button (by udev-hid-bpf).
*/
if (libinput_tablet_tool_get_type(tool) != LIBINPUT_TABLET_TOOL_TYPE_PEN)
return;
#if HAVE_LIBWACOM
/* libwacom's API is a bit terrible here:
* - has_eraser is true on styli that have a separate eraser, all
* those are INVERT so we can exclude them
* - get_eraser_type() returns something on actual eraser tools
* but we don't have any separate erasers with buttons so
* we only need to exclude INVERT
*/
if (s &&
libwacom_stylus_has_eraser(s) &&
libwacom_stylus_get_eraser_type(s) == WACOM_ERASER_INVERT) {
return;
}
#endif
/* All other pens need eraser button handling because most of the time
* we don't know if they have one (Huion, XP-Pen, ...) */
bitmask_t available_modes = bitmask_from_masks(LIBINPUT_CONFIG_ERASER_BUTTON_BUTTON);
tool->eraser_button.available_modes = available_modes;
tool->eraser_button.want_button = eraser_button_get_default_button(tool);
tool->eraser_button.button = tool->eraser_button.want_button;
}
static struct libinput_tablet_tool *
tablet_new_tool(struct tablet_dispatch *tablet,
enum libinput_tablet_tool_type type,
@ -1295,14 +1450,29 @@ tablet_new_tool(struct tablet_dispatch *tablet,
.pressure.wanted_range.min = 0.0,
.pressure.wanted_range.max = 1.0,
.eraser_button.available_modes = bitmask_new(),
.eraser_button.mode = LIBINPUT_CONFIG_ERASER_BUTTON_DEFAULT,
.eraser_button.want_mode = LIBINPUT_CONFIG_ERASER_BUTTON_DEFAULT,
.eraser_button.button = BTN_STYLUS2,
.eraser_button.want_button = BTN_STYLUS2,
.config.pressure_range.is_available = pressure_range_is_available,
.config.pressure_range.set = pressure_range_set,
.config.pressure_range.get = pressure_range_get,
.config.pressure_range.get_default = pressure_range_get_default,
.config.eraser_button.get_modes = eraser_button_get_modes,
.config.eraser_button.set_mode = eraser_button_set_mode,
.config.eraser_button.get_mode = eraser_button_get_mode,
.config.eraser_button.get_default_mode = eraser_button_get_default_mode,
.config.eraser_button.set_button = eraser_button_set_button,
.config.eraser_button.get_button = eraser_button_get_button,
.config.eraser_button.get_default_button = eraser_button_get_default_button,
};
tool_init_pressure_thresholds(tablet, tool, &tool->pressure.threshold);
tool_set_bits(tablet, tool, s);
tool_init_eraser_button(tablet, tool, s);
return tool;
}
@ -1367,6 +1537,8 @@ tablet_get_tool(struct tablet_dispatch *tablet,
if (last)
libinput_device_unref(last);
tool->last_tablet_id = tablet->tablet_id;
return tool;
}
@ -2147,6 +2319,7 @@ tablet_flush(struct tablet_dispatch *tablet,
tablet_apply_rotation(device);
tablet_change_area(device);
tablet_history_reset(tablet);
tablet_tool_apply_eraser_button(tablet, tool);
}
}

View file

@ -0,0 +1,604 @@
/*
* 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 <assert.h>
#include <libevdev/libevdev.h>
#include "util-mem.h"
#include "util-strings.h"
#include "evdev-frame.h"
#include "libinput-log.h"
#include "libinput-util.h"
#include "libinput-plugin.h"
#include "libinput-plugin-tablet-eraser-button.h"
static int ERASER_BUTTON_DELAY = 30 * 1000; /* µs */
enum frame_filter_state {
DISCARD,
PROCESS,
};
enum eraser_button_state {
ERASER_BUTTON_NEUTRAL,
ERASER_BUTTON_PEN_PENDING_ERASER,
ERASER_BUTTON_BUTTON_HELD_DOWN,
ERASER_BUTTON_BUTTON_RELEASED,
};
enum eraser_button_event {
ERASER_EVENT_PEN_ENTERING_PROX,
ERASER_EVENT_PEN_LEAVING_PROX,
ERASER_EVENT_ERASER_ENTERING_PROX,
ERASER_EVENT_ERASER_LEAVING_PROX,
ERASER_EVENT_TIMEOUT,
};
static const char *
eraser_button_state_str(enum eraser_button_state state)
{
switch(state) {
CASE_RETURN_STRING(ERASER_BUTTON_NEUTRAL);
CASE_RETURN_STRING(ERASER_BUTTON_PEN_PENDING_ERASER);
CASE_RETURN_STRING(ERASER_BUTTON_BUTTON_HELD_DOWN);
CASE_RETURN_STRING(ERASER_BUTTON_BUTTON_RELEASED);
}
abort();
}
static const char *
eraser_button_event_str(enum eraser_button_event event)
{
switch(event) {
CASE_RETURN_STRING(ERASER_EVENT_PEN_ENTERING_PROX);
CASE_RETURN_STRING(ERASER_EVENT_PEN_LEAVING_PROX);
CASE_RETURN_STRING(ERASER_EVENT_ERASER_ENTERING_PROX);
CASE_RETURN_STRING(ERASER_EVENT_ERASER_LEAVING_PROX);
CASE_RETURN_STRING(ERASER_EVENT_TIMEOUT);
}
abort();
}
struct plugin_device {
struct list link;
struct plugin_data *parent;
struct libinput_device *device;
bool pen_in_prox;
bool eraser_in_prox;
struct evdev_frame *last_frame;
enum libinput_config_eraser_button_mode mode;
/* The evdev code of the button to send */
evdev_usage_t button;
struct libinput_plugin_timer *timer;
enum eraser_button_state state;
};
struct plugin_data {
struct libinput_plugin *plugin;
struct list devices;
};
static void
plugin_device_destroy(struct plugin_device *device)
{
libinput_plugin_timer_cancel(device->timer);
libinput_plugin_timer_unref(device->timer);
libinput_device_unref(device->device);
evdev_frame_unref(device->last_frame);
list_remove(&device->link);
free(device);
}
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 void
eraser_button_set_state(struct plugin_device *device,
enum eraser_button_state to)
{
enum eraser_button_state *state = &device->state;
*state = to;
}
static void
eraser_button_set_timer(struct plugin_device *device, uint64_t time)
{
libinput_plugin_timer_set(device->timer, time + ERASER_BUTTON_DELAY);
}
static void
eraser_button_cancel_timer(struct plugin_device *device)
{
libinput_plugin_timer_cancel(device->timer);
}
static void
eraser_button_state_bug(struct plugin_device *device,
enum eraser_button_event event)
{
plugin_log_bug(device->parent->plugin,
"Invalid eraser button event %s in state %s\n",
eraser_button_event_str(event),
eraser_button_state_str(device->state));
}
enum tool_filter {
SKIP_PEN = bit(1),
SKIP_ERASER = bit(2),
PEN_IN_PROX = bit(3),
PEN_OUT_OF_PROX = bit(4),
ERASER_IN_PROX = bit(5),
ERASER_OUT_OF_PROX = bit(6),
BUTTON_DOWN = bit(7),
BUTTON_UP = bit(8),
SKIP_BTN_TOUCH = bit(9),
};
static void
eraser_button_insert_frame(struct plugin_device *device,
struct evdev_frame *frame_in,
enum tool_filter filter,
evdev_usage_t *button)
{
size_t nevents;
const struct evdev_event *events = evdev_frame_get_events(frame_in, &nevents);
/* +2 because we may add BTN_TOOL_PEN and BTN_TOOL_RUBBER */
_unref_(evdev_frame) *frame_out = evdev_frame_new(nevents + 2);
for (size_t i = 0; i < nevents; i++) {
struct evdev_event event = events[i];
switch (evdev_usage_enum(event.usage)) {
case EVDEV_BTN_TOOL_PEN:
case EVDEV_BTN_TOOL_RUBBER:
/* filter */
break;
case EVDEV_BTN_TOUCH:
if (!(filter & SKIP_BTN_TOUCH))
evdev_frame_append(frame_out, &event, 1);
break;
default:
if (button == NULL || evdev_usage_cmp(event.usage, *button))
evdev_frame_append(frame_out, &event, 1);
break;
}
}
if (filter & (PEN_IN_PROX|PEN_OUT_OF_PROX)) {
struct evdev_event event = {
.usage = evdev_usage_from(EVDEV_BTN_TOOL_PEN),
.value = (filter & PEN_IN_PROX) ? 1 : 0,
};
evdev_frame_append(frame_out, &event, 1);
}
if (filter & (ERASER_IN_PROX|ERASER_OUT_OF_PROX)) {
struct evdev_event event = {
.usage = evdev_usage_from(EVDEV_BTN_TOOL_RUBBER),
.value = (filter & ERASER_IN_PROX) ? 1 : 0,
};
evdev_frame_append(frame_out, &event, 1);
}
if (filter & (BUTTON_UP|BUTTON_DOWN)) {
assert (button != NULL);
struct evdev_event event = {
.usage = *button,
.value = (filter & BUTTON_DOWN) ? 1 : 0,
};
evdev_frame_append(frame_out, &event, 1);
}
evdev_frame_set_time(frame_out, evdev_frame_get_time(frame_in));
libinput_plugin_prepend_evdev_frame(device->parent->plugin,
device->device,
frame_out);
}
static enum frame_filter_state
eraser_button_neutral_handle_event(struct plugin_device *device,
struct evdev_frame *frame,
enum eraser_button_event event,
uint64_t time)
{
switch (event) {
case ERASER_EVENT_PEN_ENTERING_PROX:
break;
case ERASER_EVENT_PEN_LEAVING_PROX:
eraser_button_set_timer(device, time);
eraser_button_set_state(device, ERASER_BUTTON_PEN_PENDING_ERASER);
return DISCARD; /* Discard this event, it has garbage data anyway */
case ERASER_EVENT_ERASER_ENTERING_PROX:
/* Change eraser prox in into pen prox in + button down */
eraser_button_insert_frame(device,
frame,
PEN_IN_PROX|SKIP_ERASER|BUTTON_DOWN,
&device->button);
eraser_button_set_state(device, ERASER_BUTTON_BUTTON_HELD_DOWN);
return DISCARD;
case ERASER_EVENT_ERASER_LEAVING_PROX:
eraser_button_state_bug(device, event);
break;
case ERASER_EVENT_TIMEOUT:
break;
}
return PROCESS;
}
static enum frame_filter_state
eraser_button_pending_eraser_handle_event(struct plugin_device *device,
struct evdev_frame *frame,
enum eraser_button_event event,
uint64_t time)
{
switch (event) {
case ERASER_EVENT_PEN_ENTERING_PROX:
eraser_button_cancel_timer(device);
eraser_button_set_state(device, ERASER_BUTTON_NEUTRAL);
/* We just papered over a quick prox out/in here */
break;
case ERASER_EVENT_PEN_LEAVING_PROX:
eraser_button_state_bug(device, event);
break;
case ERASER_EVENT_ERASER_ENTERING_PROX:
eraser_button_cancel_timer(device);
eraser_button_insert_frame(device,
frame,
SKIP_ERASER|SKIP_PEN|BUTTON_DOWN,
&device->button);
eraser_button_set_state(device, ERASER_BUTTON_BUTTON_HELD_DOWN);
return DISCARD;
case ERASER_EVENT_ERASER_LEAVING_PROX:
eraser_button_state_bug(device, event);
break;
case ERASER_EVENT_TIMEOUT:
/* Pen went out of prox and we delayed expecting an eraser to
* come in prox. That didn't happen -> pen prox out */
eraser_button_set_state(device, ERASER_BUTTON_NEUTRAL);
eraser_button_insert_frame(device,
frame,
SKIP_ERASER|PEN_OUT_OF_PROX,
NULL);
break;
}
return PROCESS;
}
static enum frame_filter_state
eraser_button_button_held_handle_event(struct plugin_device *device,
struct evdev_frame *frame,
enum eraser_button_event event,
uint64_t time)
{
switch (event) {
case ERASER_EVENT_PEN_ENTERING_PROX:
case ERASER_EVENT_PEN_LEAVING_PROX:
/* We should've seen an eraser out-of-prox out here */
eraser_button_state_bug(device, event);
break;
case ERASER_EVENT_ERASER_ENTERING_PROX:
eraser_button_state_bug(device, event);
break;
case ERASER_EVENT_ERASER_LEAVING_PROX:
eraser_button_insert_frame(device,
device->last_frame,
SKIP_ERASER|SKIP_PEN|BUTTON_UP,
&device->button);
eraser_button_set_state(device, ERASER_BUTTON_BUTTON_RELEASED);
eraser_button_set_timer(device, time);
return DISCARD; /* Discard the actual frame, it has garbage data anyway */
case ERASER_EVENT_TIMEOUT:
/* Expected to be cancelled in previous state */
eraser_button_state_bug(device, event);
break;
}
return PROCESS;
}
static enum frame_filter_state
eraser_button_button_released_handle_event(struct plugin_device *device,
struct evdev_frame *frame,
enum eraser_button_event event,
uint64_t time)
{
switch (event) {
case ERASER_EVENT_PEN_ENTERING_PROX:
eraser_button_cancel_timer(device);
eraser_button_insert_frame(device,
frame,
SKIP_PEN|SKIP_ERASER,
NULL);
eraser_button_set_state(device, ERASER_BUTTON_NEUTRAL);
return DISCARD;
case ERASER_EVENT_PEN_LEAVING_PROX:
eraser_button_state_bug(device, event);
break;
case ERASER_EVENT_ERASER_ENTERING_PROX:
break;
case ERASER_EVENT_ERASER_LEAVING_PROX:
eraser_button_state_bug(device, event);
break;
case ERASER_EVENT_TIMEOUT:
/* Eraser went out of prox, we expected the pen to come back in prox but
* that didn't happen. We still have the pen simulated in-prox -> pen
* prox out.
* We release the button first, then send the pen out-of-prox
* event sequence. This way the sequence of tip first/button first is
* predictable.
*/
eraser_button_insert_frame(device,
frame,
SKIP_PEN | SKIP_ERASER | BUTTON_UP,
&device->button);
eraser_button_insert_frame(device,
frame,
PEN_OUT_OF_PROX,
NULL);
eraser_button_set_state(device, ERASER_BUTTON_NEUTRAL);
break;
}
return PROCESS;
}
static enum frame_filter_state
eraser_button_handle_state(struct plugin_device *device,
struct evdev_frame *frame,
enum eraser_button_event event,
uint64_t time)
{
enum eraser_button_state state = device->state;
enum frame_filter_state ret = PROCESS;
switch (state) {
case ERASER_BUTTON_NEUTRAL:
ret = eraser_button_neutral_handle_event(device, frame, event, time);
break;
case ERASER_BUTTON_PEN_PENDING_ERASER:
ret = eraser_button_pending_eraser_handle_event(device, frame, event, time);
break;
case ERASER_BUTTON_BUTTON_HELD_DOWN:
ret = eraser_button_button_held_handle_event(device, frame, event, time);
break;
case ERASER_BUTTON_BUTTON_RELEASED:
ret = eraser_button_button_released_handle_event(device, frame, event, time);
break;
}
if (state != device->state) {
plugin_log_debug(device->parent->plugin,
"eraser button: state %s -> %s -> %s\n",
eraser_button_state_str(state),
eraser_button_event_str(event),
eraser_button_state_str(device->state));
}
return ret;
}
/**
* Physical eraser button handling: if the physical eraser button is
* disabled paper over any pen prox out/eraser prox in events and send a button event
* instead.
*/
static void
eraser_button_handle_frame(struct plugin_device *device,
struct evdev_frame *frame,
uint64_t time)
{
if (device->mode == LIBINPUT_CONFIG_ERASER_BUTTON_DEFAULT)
return;
size_t nevents;
struct evdev_event *events = evdev_frame_get_events(frame, &nevents);
bool pen_toggled = false;
bool eraser_toggled = false;
for (size_t i = 0; i < nevents; i++) {
struct evdev_event *event = &events[i];
switch (evdev_usage_enum(event->usage)) {
case EVDEV_BTN_TOOL_PEN:
pen_toggled = true;
device->pen_in_prox = !!event->value;
break;
case EVDEV_BTN_TOOL_RUBBER:
eraser_toggled = true;
device->eraser_in_prox = !!event->value;
break;
default:
break;
}
}
bool eraser_in_prox = device->eraser_in_prox;
bool pen_in_prox = device->pen_in_prox;
enum eraser_button_event eraser_event = eraser_in_prox ? ERASER_EVENT_ERASER_ENTERING_PROX : ERASER_EVENT_ERASER_LEAVING_PROX;
enum eraser_button_event pen_event = pen_in_prox ? ERASER_EVENT_PEN_ENTERING_PROX : ERASER_EVENT_PEN_LEAVING_PROX;
enum frame_filter_state ret = PROCESS;
/* bit awkward because we definitely want whatever goes out of prox to
* be handled first but if one sends discard and the other one process?
* Unclear...
*/
if (eraser_toggled && pen_toggled) {
if (pen_in_prox) {
eraser_button_handle_state(device, frame, eraser_event, time);
ret = eraser_button_handle_state(device, frame, pen_event, time);
} else {
eraser_button_handle_state(device, frame, pen_event, time);
ret = eraser_button_handle_state(device, frame, eraser_event, time);
}
} else if (eraser_toggled) {
ret = eraser_button_handle_state(device, frame, eraser_event, time);
} else if (pen_toggled) {
ret = eraser_button_handle_state(device, frame, pen_event, time);
}
if (ret == PROCESS) {
evdev_frame_reset(device->last_frame);
evdev_frame_append(device->last_frame, events, nevents);
} else if (ret == DISCARD) {
evdev_frame_reset(frame);
}
}
static void
eraser_button_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) {
eraser_button_handle_frame(pd, frame, time);
break;
}
}
}
static void
eraser_button_timer_func(struct libinput_plugin *plugin, uint64_t now, void *d)
{
struct plugin_device *device = d;
if (!device->last_frame) {
plugin_log_bug(device->parent->plugin,
"Eraser button timer fired without a frame in state %s\n",
eraser_button_state_str(device->state)
);
return;
}
eraser_button_handle_state(device, device->last_frame, ERASER_EVENT_TIMEOUT, now);
}
static void
eraser_button_plugin_device_added(struct libinput_plugin *libinput_plugin,
struct libinput_device *device)
{
if (!libinput_device_has_capability(device, LIBINPUT_DEVICE_CAP_TABLET_TOOL))
return;
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->last_frame = evdev_frame_new(64);
pd->timer = libinput_plugin_timer_new(libinput_plugin,
libinput_device_get_sysname(device),
eraser_button_timer_func,
pd);
list_take_append(&plugin->devices, pd, link);
}
static void
eraser_button_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 void
eraser_button_plugin_tool_configured(struct libinput_plugin *libinput_plugin,
struct libinput_tablet_tool *tool)
{
struct plugin_data *plugin = libinput_plugin_get_user_data(libinput_plugin);
struct plugin_device *pd;
list_for_each(pd, &plugin->devices, link) {
/* FIXME: sigh, we need a separate list of tools? */
pd->mode = libinput_tablet_tool_config_eraser_button_get_mode(tool);
uint32_t button = libinput_tablet_tool_config_eraser_button_get_button(tool);
pd->button = evdev_usage_from_code(EV_KEY, button);
}
}
static const struct libinput_plugin_interface interface = {
.run = NULL,
.destroy = plugin_destroy,
.device_new = NULL,
.device_ignored = NULL,
.device_added = eraser_button_plugin_device_added,
.device_removed = eraser_button_plugin_device_removed,
.evdev_frame = eraser_button_plugin_evdev_frame,
.tool_configured = eraser_button_plugin_tool_configured,
};
void
libinput_tablet_plugin_eraser_button(struct libinput *libinput)
{
if (getenv("LIBINPUT_RUNNING_TEST_SUITE"))
ERASER_BUTTON_DELAY = ms2us(150);
_destroy_(plugin_data) *plugin = zalloc(sizeof(*plugin));
list_init(&plugin->devices);
_unref_(libinput_plugin) *p = libinput_plugin_new(libinput,
"tablet-eraser-button",
&interface,
NULL);
plugin->plugin = p;
libinput_plugin_set_user_data(p, steal(&plugin));
}

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_tablet_plugin_eraser_button(struct libinput *libinput);

View file

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

View file

@ -590,10 +590,20 @@ struct libinput_tablet_tool {
struct libinput_tablet_tool_pressure_threshold threshold;
} pressure;
struct {
bitmask_t available_modes;
enum libinput_config_eraser_button_mode mode;
enum libinput_config_eraser_button_mode want_mode;
unsigned int button;
unsigned int want_button;
} eraser_button;
struct {
struct libinput_tablet_tool_config_pressure_range pressure_range;
struct libinput_tablet_tool_config_eraser_button eraser_button;
} config;
unsigned int last_tablet_id; /* tablet_dispatch->tablet_id */
};
struct libinput_tablet_pad_mode_group {

View file

@ -1349,6 +1349,7 @@ _litest_timeout(struct libinput *li, const char *func, int lineno, int millis);
#define litest_timeout_touch_arbitration(li_) litest_timeout(li_, 100)
#define litest_timeout_hysteresis(li_) litest_timeout(li_, 90)
#define litest_timeout_3fg_drag(li_) litest_timeout(li_, 800)
#define litest_timeout_eraser_button(li_) litest_timeout(li_, 50)
struct litest_logcapture {
char **errors;

View file

@ -7202,6 +7202,282 @@ START_TEST(tablet_smoothing)
}
END_TEST
START_TEST(tablet_eraser_button_disabled)
{
struct litest_device *dev = litest_current_device();
struct libinput *li = dev->libinput;
struct axis_replacement axes[] = {
{ ABS_DISTANCE, 10 },
{ ABS_PRESSURE, 0 },
{ -1, -1 }
};
struct axis_replacement tip_down_axes[] = {
{ ABS_DISTANCE, 0 },
{ ABS_PRESSURE, 30 },
{ -1, -1 }
};
struct libinput_event *ev;
struct libinput_event_tablet_tool *tev;
struct libinput_tablet_tool *tool, *pen;
bool with_tip_down = litest_test_param_get_bool(test_env->params, "with-tip-down");
bool configure_while_out_of_prox = litest_test_param_get_bool(test_env->params, "configure-while-out-of-prox");
bool down_when_in_prox = litest_test_param_get_bool(test_env->params, "down-when-in-prox");
bool down_when_out_of_prox = litest_test_param_get_bool(test_env->params, "down-when-out-of-prox");
bool with_motion_events = litest_test_param_get_bool(test_env->params, "with-motion-events");
/* Device forces BTN_TOOL_PEN on tip */
if (with_tip_down && dev->which == LITEST_WACOM_ISDV4_524C_PEN)
return LITEST_NOT_APPLICABLE;
litest_log_group("Prox in/out to disable proximity timer") {
litest_tablet_proximity_in(dev, 25, 25, axes);
litest_tablet_proximity_out(dev);
litest_timeout_tablet_proxout(li);
litest_checkpoint("Eraser prox in/out to force-disable config on broken tablets");
litest_tablet_set_tool_type(dev, BTN_TOOL_RUBBER);
litest_tablet_proximity_in(dev, 25, 25, axes);
litest_tablet_proximity_out(dev);
litest_timeout_tablet_proxout(li);
}
litest_drain_events(li);
litest_log_group("Proximity in for pen") {
litest_tablet_set_tool_type(dev, BTN_TOOL_PEN);
litest_tablet_proximity_in(dev, 20, 20, axes);
litest_dispatch(li);
_destroy_(libinput_event) *ev = libinput_get_event(li);
tev = litest_is_proximity_event(ev, LIBINPUT_TABLET_TOOL_PROXIMITY_STATE_IN);
tool = libinput_event_tablet_tool_get_tool(tev);
litest_assert_enum_eq(libinput_tablet_tool_get_type(tool), LIBINPUT_TABLET_TOOL_TYPE_PEN);
pen = libinput_tablet_tool_ref(tool);
}
if (!libinput_tablet_tool_config_eraser_button_get_modes(tool)) {
libinput_tablet_tool_unref(tool);
return LITEST_NOT_APPLICABLE;
}
unsigned int expected_button = BTN_STYLUS3;
if (!libinput_tablet_tool_has_button(pen, BTN_STYLUS))
expected_button = BTN_STYLUS;
else if (!libinput_tablet_tool_has_button(pen, BTN_STYLUS2))
expected_button = BTN_STYLUS2;
else if (!libinput_tablet_tool_has_button(pen, BTN_STYLUS3))
expected_button = BTN_STYLUS3;
litest_checkpoint("expected button from now on: %s (%d)",
libevdev_event_code_get_name(EV_KEY, expected_button),
expected_button);
if (!configure_while_out_of_prox) {
litest_log_group("Configuring eraser button while in of proximity") {
auto status = libinput_tablet_tool_config_eraser_button_set_mode(tool,
LIBINPUT_CONFIG_ERASER_BUTTON_BUTTON);
if (status == LIBINPUT_CONFIG_STATUS_UNSUPPORTED)
return LITEST_NOT_APPLICABLE;
litest_assert_enum_eq(status, LIBINPUT_CONFIG_STATUS_SUCCESS);
}
}
litest_log_group("Prox out to apply changed settings") {
litest_tablet_proximity_out(dev);
litest_timeout_tablet_proxout(li);
litest_drain_events(li);
}
if (configure_while_out_of_prox) {
litest_log_group("Configuring eraser button while out of proximity") {
auto status = libinput_tablet_tool_config_eraser_button_set_mode(tool,
LIBINPUT_CONFIG_ERASER_BUTTON_BUTTON);
if (status == LIBINPUT_CONFIG_STATUS_UNSUPPORTED)
return LITEST_NOT_APPLICABLE;
litest_assert_enum_eq(status, LIBINPUT_CONFIG_STATUS_SUCCESS);
}
}
litest_mark_test_start();
if (down_when_in_prox) {
litest_log_group("Prox in with eraser, expecting eraser button event") {
litest_tablet_set_tool_type(dev, BTN_TOOL_RUBBER);
litest_tablet_proximity_in(dev, 10, 10, axes);
litest_wait_for_event(li);
ev = libinput_get_event(li);
tev = litest_is_proximity_event(ev, LIBINPUT_TABLET_TOOL_PROXIMITY_STATE_IN);
litest_assert_ptr_eq(libinput_event_tablet_tool_get_tool(tev), pen);
libinput_event_destroy(ev);
litest_drain_events_of_type(li, LIBINPUT_EVENT_TABLET_TOOL_AXIS);
ev = libinput_get_event(li);
tev = litest_is_tablet_event(ev, LIBINPUT_EVENT_TABLET_TOOL_BUTTON);
litest_assert_enum_eq(libinput_event_tablet_tool_get_button_state(tev), LIBINPUT_BUTTON_STATE_PRESSED);
litest_assert_int_eq(libinput_event_tablet_tool_get_button(tev), expected_button);
litest_assert_ptr_eq(libinput_event_tablet_tool_get_tool(tev), pen);
libinput_event_destroy(ev);
}
} else {
litest_tablet_proximity_in(dev, 10, 10, axes);
}
litest_drain_events(li);
if (with_motion_events) {
for (int i = 0; i < 3; i++) {
litest_tablet_motion(dev, 11 + i, 11 + i, axes);
litest_dispatch(li);
}
ev = libinput_get_event(li);
do {
tev = litest_is_tablet_event(ev, LIBINPUT_EVENT_TABLET_TOOL_AXIS);
litest_assert_ptr_eq(libinput_event_tablet_tool_get_tool(tev), pen);
libinput_event_destroy(ev);
ev = libinput_get_event(li);
} while (ev);
}
if (with_tip_down) {
litest_tablet_tip_down(dev, 11, 11, tip_down_axes);
litest_dispatch(li);
litest_assert_tablet_tip_event(li, LIBINPUT_TABLET_TOOL_TIP_DOWN);
if (with_motion_events) {
for (int i = 0; i < 3; i++) {
litest_tablet_motion(dev, 11 + i, 11 + i, tip_down_axes);
litest_dispatch(li);
}
ev = libinput_get_event(li);
do {
tev = litest_is_tablet_event(ev, LIBINPUT_EVENT_TABLET_TOOL_AXIS);
litest_assert_ptr_eq(libinput_event_tablet_tool_get_tool(tev), pen);
libinput_event_destroy(ev);
ev = libinput_get_event(li);
} while (ev);
}
}
/* Make sure the button still works as-is */
if (libinput_tablet_tool_has_button(pen, BTN_STYLUS)) {
litest_log_group("Testing BTN_STYLUS") {
litest_event(dev, EV_KEY, BTN_STYLUS, 1);
litest_event(dev, EV_SYN, SYN_REPORT, 0);
litest_dispatch(li);
litest_event(dev, EV_KEY, BTN_STYLUS, 0);
litest_event(dev, EV_SYN, SYN_REPORT, 0);
litest_dispatch(li);
litest_assert_tablet_button_event(li, BTN_STYLUS, LIBINPUT_BUTTON_STATE_PRESSED);
litest_assert_tablet_button_event(li, BTN_STYLUS, LIBINPUT_BUTTON_STATE_RELEASED);
}
}
litest_dispatch(li);
if (!down_when_in_prox) {
litest_log_group("Prox out for the pen ...") {
litest_with_event_frame(dev) {
litest_tablet_set_tool_type(dev, BTN_TOOL_PEN);
if (with_tip_down)
litest_tablet_tip_up(dev, 11, 11, axes);
litest_tablet_proximity_out(dev);
}
litest_dispatch(li);
}
litest_log_group("...and prox in for the eraser") {
litest_with_event_frame(dev) {
litest_tablet_set_tool_type(dev, BTN_TOOL_RUBBER);
if (with_tip_down) {
litest_tablet_tip_down(dev, 12, 12, tip_down_axes);
litest_tablet_proximity_in(dev, 12, 12, tip_down_axes);
} else {
litest_tablet_proximity_in(dev, 12, 12, axes);
}
}
litest_dispatch(li);
}
litest_drain_events_of_type(li, LIBINPUT_EVENT_TABLET_TOOL_AXIS);
litest_log_group("Expect button event") {
ev = libinput_get_event(li);
tev = litest_is_tablet_event(ev, LIBINPUT_EVENT_TABLET_TOOL_BUTTON);
litest_assert_enum_eq(libinput_event_tablet_tool_get_button_state(tev), LIBINPUT_BUTTON_STATE_PRESSED);
litest_assert_int_eq(libinput_event_tablet_tool_get_button(tev), expected_button);
litest_assert_ptr_eq(libinput_event_tablet_tool_get_tool(tev), pen);
libinput_event_destroy(ev);
}
}
if (!down_when_out_of_prox) {
litest_log_group("Prox out for the eraser...") {
litest_with_event_frame(dev) {
if (with_tip_down)
litest_tablet_tip_up(dev, 11, 11, axes);
litest_tablet_proximity_out(dev);
}
litest_dispatch(li);
}
litest_log_group("...and prox in for the pen") {
litest_with_event_frame(dev) {
litest_tablet_set_tool_type(dev, BTN_TOOL_PEN);
if (with_tip_down) {
litest_tablet_tip_down(dev, 12, 12, tip_down_axes);
litest_tablet_proximity_in(dev, 12, 12, tip_down_axes);
} else {
litest_tablet_proximity_in(dev, 12, 12, axes);
}
}
litest_dispatch(li);
}
litest_drain_events_of_type(li, LIBINPUT_EVENT_TABLET_TOOL_AXIS);
litest_log_group("Expect button event") {
ev = libinput_get_event(li);
tev = litest_is_tablet_event(ev, LIBINPUT_EVENT_TABLET_TOOL_BUTTON);
litest_assert_int_eq(libinput_event_tablet_tool_get_button(tev), expected_button);
litest_assert_ptr_eq(libinput_event_tablet_tool_get_tool(tev), pen);
libinput_event_destroy(ev);
}
}
litest_log_group("Real prox out for the %s", down_when_out_of_prox ? "eraser" : "pen") {
litest_with_event_frame(dev) {
if (with_tip_down)
litest_tablet_tip_up(dev, 12, 12, axes);
litest_tablet_proximity_out(dev);
}
litest_dispatch(li);
litest_timeout_tablet_proxout(li);
}
litest_drain_events_of_type(li, LIBINPUT_EVENT_TABLET_TOOL_AXIS);
if (down_when_out_of_prox) {
litest_log_group("Expect button release") {
ev = libinput_get_event(li);
tev = litest_is_tablet_event(ev, LIBINPUT_EVENT_TABLET_TOOL_BUTTON);
litest_assert_int_eq(libinput_event_tablet_tool_get_button(tev), expected_button);
litest_assert_ptr_eq(libinput_event_tablet_tool_get_tool(tev), pen);
libinput_event_destroy(ev);
}
}
if (with_tip_down)
litest_assert_tablet_tip_event(li, LIBINPUT_TABLET_TOOL_TIP_UP);
litest_log_group("Expect final prox out for the pen") {
ev = libinput_get_event(li);
tev = litest_is_proximity_event(ev, LIBINPUT_TABLET_TOOL_PROXIMITY_STATE_OUT);
tool = libinput_event_tablet_tool_get_tool(tev);
litest_assert_ptr_eq(pen, tool);
libinput_event_destroy(ev);
}
libinput_tablet_tool_unref(pen);
}
END_TEST
TEST_COLLECTION(tablet)
{
litest_add(tool_ref, LITEST_TABLET | LITEST_TOOL_SERIAL, LITEST_ANY);
@ -7355,6 +7631,15 @@ TEST_COLLECTION(tablet)
}
litest_add_for_device(tablet_smoothing, LITEST_WACOM_HID4800_PEN);
litest_with_parameters(params,
"with-tip-down", 'b',
"configure-while-out-of-prox", 'b',
"down-when-in-prox", 'b',
"down-when-out-of-prox", 'b',
"with-motion-events", 'b') {
litest_add_parametrized(tablet_eraser_button_disabled, LITEST_TABLET, LITEST_TOTEM|LITEST_FORCED_PROXOUT, params);
}
}
TEST_COLLECTION(tablet_left_handed)

View file

@ -139,6 +139,9 @@ tools_init_options(struct tools_options *options)
options->area.x2 = 1.0;
options->area.y2 = 1.0;
options->sendevents = LIBINPUT_CONFIG_SEND_EVENTS_ENABLED;
options->eraser_button_mode = LIBINPUT_CONFIG_ERASER_BUTTON_DEFAULT;
options->eraser_button_button = BTN_STYLUS;
options->eraser_button_button = 0;
}
int
@ -680,6 +683,9 @@ tools_tablet_tool_apply_config(struct libinput_tablet_tool *tool,
libinput_tablet_tool_config_pressure_range_set(tool,
options->pressure_range[0],
options->pressure_range[1]);
if (options->eraser_button_button)
libinput_tablet_tool_config_eraser_button_set_button(tool, options->eraser_button_button);
libinput_tablet_tool_config_eraser_button_set_mode(tool, options->eraser_button_mode);
}
static char*