From c48aff86d7264dfefafb690014c8694c104fe768 Mon Sep 17 00:00:00 2001 From: Peter Hutterer Date: Mon, 2 Jun 2025 15:11:08 +1000 Subject: [PATCH] tablet: implement eraser button disabling This adds a new (internal) plugin that is responsible for eraser button disabling. Signed-off-by: Peter Hutterer Part-of: --- meson.build | 1 + src/evdev-tablet.c | 173 ++++++ src/libinput-plugin-tablet-eraser-button.c | 604 +++++++++++++++++++++ src/libinput-plugin-tablet-eraser-button.h | 30 + src/libinput-plugin.c | 2 + src/libinput-private.h | 10 + test/litest.h | 1 + test/test-tablet.c | 285 ++++++++++ tools/shared.c | 6 + 9 files changed, 1112 insertions(+) create mode 100644 src/libinput-plugin-tablet-eraser-button.c create mode 100644 src/libinput-plugin-tablet-eraser-button.h diff --git a/meson.build b/meson.build index 76ed4aad..4fab5b31 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-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', diff --git a/src/evdev-tablet.c b/src/evdev-tablet.c index f66003d4..a61a2465 100644 --- a/src/evdev-tablet.c +++ b/src/evdev-tablet.c @@ -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); } } diff --git a/src/libinput-plugin-tablet-eraser-button.c b/src/libinput-plugin-tablet-eraser-button.c new file mode 100644 index 00000000..22ba50bc --- /dev/null +++ b/src/libinput-plugin-tablet-eraser-button.c @@ -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 +#include + +#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)); +} diff --git a/src/libinput-plugin-tablet-eraser-button.h b/src/libinput-plugin-tablet-eraser-button.h new file mode 100644 index 00000000..b4851253 --- /dev/null +++ b/src/libinput-plugin-tablet-eraser-button.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_tablet_plugin_eraser_button(struct libinput *libinput); diff --git a/src/libinput-plugin.c b/src/libinput-plugin.c index 0001b067..68a1b166 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-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 diff --git a/src/libinput-private.h b/src/libinput-private.h index ab73163b..4fddc655 100644 --- a/src/libinput-private.h +++ b/src/libinput-private.h @@ -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 { diff --git a/test/litest.h b/test/litest.h index a0639858..7edd5dc4 100644 --- a/test/litest.h +++ b/test/litest.h @@ -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; diff --git a/test/test-tablet.c b/test/test-tablet.c index 8427767a..cf50d98e 100644 --- a/test/test-tablet.c +++ b/test/test-tablet.c @@ -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) diff --git a/tools/shared.c b/tools/shared.c index 920e5024..6ae45f6e 100644 --- a/tools/shared.c +++ b/tools/shared.c @@ -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*