From 0a3ecbea247a3fb075070756b73426fa630b32da Mon Sep 17 00:00:00 2001 From: Peter Hutterer Date: Wed, 4 Mar 2026 11:19:09 +1000 Subject: [PATCH 1/3] tablet: fix missing linebreak after an error message Part-of: --- src/evdev-tablet.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/evdev-tablet.c b/src/evdev-tablet.c index 21c3c0ab..317e35ee 100644 --- a/src/evdev-tablet.c +++ b/src/evdev-tablet.c @@ -1318,7 +1318,7 @@ eraser_button_set_button(struct libinput_tablet_tool *tool, uint32_t button) break; default: log_bug_libinput(libinput_device_get_context(tool->last_device), - "Unsupported eraser button 0x%x", + "Unsupported eraser button 0x%x\n", button); return LIBINPUT_CONFIG_STATUS_INVALID; } From cdcb8273658d2bb8f4851cbf8657d6abcf97740d Mon Sep 17 00:00:00 2001 From: Peter Hutterer Date: Wed, 4 Mar 2026 11:25:45 +1000 Subject: [PATCH 2/3] Fix the evdev_usage_is_button check for the BTN_STYLUS group Part-of: --- src/evdev-frame.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/evdev-frame.h b/src/evdev-frame.h index 15849dd3..e544aaa4 100644 --- a/src/evdev-frame.h +++ b/src/evdev-frame.h @@ -332,9 +332,9 @@ evdev_usage_is_button(evdev_usage_t usage) case EVDEV_BTN_TOOL_FINGER: case EVDEV_BTN_TOUCH: return false; - case BTN_STYLUS: - case BTN_STYLUS2: - case BTN_STYLUS3: + case EVDEV_BTN_STYLUS: + case EVDEV_BTN_STYLUS2: + case EVDEV_BTN_STYLUS3: return true; case EVDEV_BTN_MISC ... EVDEV_BTN_DIGI - 1: case EVDEV_BTN_WHEEL ... EVDEV_BTN_GEAR_UP: From 7726350420340b607aa474f89095ad13a660a5e7 Mon Sep 17 00:00:00 2001 From: Peter Hutterer Date: Wed, 4 Mar 2026 11:44:14 +1000 Subject: [PATCH 3/3] tablet: allow for the eraser button to be any button The previous restriction was BTN_STYLUS* or any button the pen advertises. This is too restrictive - it works well enough for any pen with less than 3 buttons (BTN_STYLUS3 is always available on those) but otherwise it cannot work. A 3-button pen may not advertise any other buttons, leaving us with the eraser button being a duplicate button. And events cannot be distinquished between eraser button or real button. Open up the configuration to effectively any BTN_ event code. Part-of: --- src/evdev-tablet.c | 12 --- src/libinput.c | 13 +-- src/libinput.h | 9 +- test/test-tablet.c | 199 ++++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 203 insertions(+), 30 deletions(-) diff --git a/src/evdev-tablet.c b/src/evdev-tablet.c index 317e35ee..c6e6b7b6 100644 --- a/src/evdev-tablet.c +++ b/src/evdev-tablet.c @@ -1311,18 +1311,6 @@ eraser_button_get_default_mode(struct libinput_tablet_tool *tool) static enum libinput_config_status eraser_button_set_button(struct libinput_tablet_tool *tool, uint32_t button) { - 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\n", - button); - return LIBINPUT_CONFIG_STATUS_INVALID; - } - tool->eraser_button.want_button = button; eraser_button_toggle(tool); diff --git a/src/libinput.c b/src/libinput.c index 768c8e3e..630e286f 100644 --- a/src/libinput.c +++ b/src/libinput.c @@ -5216,16 +5216,9 @@ libinput_tablet_tool_config_eraser_button_set_button(struct libinput_tablet_tool if (!libinput_tablet_tool_config_eraser_button_get_modes(tool)) return LIBINPUT_CONFIG_STATUS_UNSUPPORTED; - switch (button) { - case BTN_STYLUS: - case BTN_STYLUS2: - case BTN_STYLUS3: - break; - default: - if (!libinput_tablet_tool_has_button(tool, button)) - return LIBINPUT_CONFIG_STATUS_INVALID; - break; - } + evdev_usage_t usage = evdev_usage_from_code(EV_KEY, button); + if (!evdev_usage_is_button(usage)) + return LIBINPUT_CONFIG_STATUS_INVALID; return tool->config.eraser_button.set_button(tool, button); } diff --git a/src/libinput.h b/src/libinput.h index ff206fba..70a87e1b 100644 --- a/src/libinput.h +++ b/src/libinput.h @@ -7371,14 +7371,9 @@ libinput_tablet_tool_config_eraser_button_get_default_mode( * the eraser mode to @ref LIBINPUT_CONFIG_ERASER_BUTTON_BUTTON via * libinput_tablet_tool_config_eraser_button_set_mode(). * - * The buttons BTN_STYLUS, BTN_STYLUS2 and BTN_STYLUS2 are always - * allowed, even if libinput_tablet_tool_has_button() returns zero - * for the button. Otherwise, the button must be one that - * libinput_tablet_tool_has_button() returns a nonzero value for. - * * @param tool The libinput tool - * @param button The button, usually one of BTN_STYLUS, BTN_STYLUS2 or - * BTN_STYLUS3 + * @param button The button code. Must be a valid button (e.g. BTN_STYLUS) + * excluding fake buttons (e.g. BTN_TOOL_*) and keys (KEY_*) * * @return A config status code * diff --git a/test/test-tablet.c b/test/test-tablet.c index d8577f04..a6584b5e 100644 --- a/test/test-tablet.c +++ b/test/test-tablet.c @@ -7883,6 +7883,189 @@ START_TEST(tablet_eraser_button_disabled) } END_TEST +START_TEST(tablet_eraser_button_different_buttons) +{ + struct litest_device *dev = litest_current_device(); + struct libinput *li = dev->libinput; + struct axis_replacement axes[] = { + { ABS_DISTANCE, 10 }, + { ABS_PRESSURE, 0 }, + { -1, -1 }, + }; + _unref_(libinput_tablet_tool) *pen = NULL; + + uint32_t eraser_button_mapping = + litest_test_param_get_i32(test_env->params, "eraser-button-mapping"); + + if (!libevdev_has_event_code(dev->evdev, EV_KEY, BTN_TOOL_RUBBER)) + 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); + auto tev = litest_is_proximity_event( + ev, + LIBINPUT_TABLET_TOOL_PROXIMITY_STATE_IN); + pen = libinput_event_tablet_tool_get_tool(tev); + litest_assert_enum_eq(libinput_tablet_tool_get_type(pen), + LIBINPUT_TABLET_TOOL_TYPE_PEN); + pen = libinput_tablet_tool_ref(pen); + } + + if (!libinput_tablet_tool_config_eraser_button_get_modes(pen)) + return LITEST_NOT_APPLICABLE; + + auto status = libinput_tablet_tool_config_eraser_button_set_mode( + pen, + LIBINPUT_CONFIG_ERASER_BUTTON_BUTTON); + litest_assert_enum_eq(status, LIBINPUT_CONFIG_STATUS_SUCCESS); + status = libinput_tablet_tool_config_eraser_button_set_button( + pen, + eraser_button_mapping); + 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); + } + + litest_mark_test_start(); + + litest_tablet_proximity_in(dev, 10, 10, axes); + litest_drain_events(li); + + /* Make sure the button still works as-is */ + if (libinput_tablet_tool_has_button(pen, eraser_button_mapping)) { + litest_log_group("Testing button on pen") { + litest_event(dev, EV_KEY, eraser_button_mapping, 1); + litest_event(dev, EV_SYN, SYN_REPORT, 0); + litest_dispatch(li); + litest_event(dev, EV_KEY, eraser_button_mapping, 0); + litest_event(dev, EV_SYN, SYN_REPORT, 0); + litest_dispatch(li); + litest_assert_tablet_button_event( + li, + eraser_button_mapping, + LIBINPUT_BUTTON_STATE_PRESSED); + litest_assert_tablet_button_event( + li, + eraser_button_mapping, + LIBINPUT_BUTTON_STATE_RELEASED); + } + } + + litest_dispatch(li); + + litest_log_group("Prox out for the pen ...") { + litest_with_event_frame(dev) { + litest_tablet_set_tool_type(dev, BTN_TOOL_PEN); + 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); + 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") { + _destroy_(libinput_event) *ev = libinput_get_event(li); + auto 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), + eraser_button_mapping); + litest_assert_ptr_eq(libinput_event_tablet_tool_get_tool(tev), pen); + } + + litest_log_group("Prox out for the eraser...") { + litest_with_event_frame(dev) { + 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); + 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") { + _destroy_(libinput_event) *ev = libinput_get_event(li); + auto tev = + litest_is_tablet_event(ev, LIBINPUT_EVENT_TABLET_TOOL_BUTTON); + litest_assert_int_eq(libinput_event_tablet_tool_get_button(tev), + eraser_button_mapping); + litest_assert_ptr_eq(libinput_event_tablet_tool_get_tool(tev), pen); + } +} +END_TEST + +START_TEST(tablet_eraser_button_invalid_buttons) +{ + struct litest_device *dev = litest_current_device(); + struct libinput *li = dev->libinput; + struct axis_replacement axes[] = { + { ABS_DISTANCE, 10 }, + { ABS_PRESSURE, 0 }, + { -1, -1 }, + }; + + uint32_t eraser_button_mapping = + litest_test_param_get_i32(test_env->params, "eraser-button-mapping"); + + if (!libevdev_has_event_code(dev->evdev, EV_KEY, BTN_TOOL_RUBBER)) + return LITEST_NOT_APPLICABLE; + + litest_drain_events(li); + litest_tablet_proximity_in(dev, 20, 20, axes); + litest_dispatch(li); + + _destroy_(libinput_event) *ev = libinput_get_event(li); + auto tev = + litest_is_proximity_event(ev, LIBINPUT_TABLET_TOOL_PROXIMITY_STATE_IN); + auto tool = libinput_event_tablet_tool_get_tool(tev); + + if (!libinput_tablet_tool_config_eraser_button_get_modes(tool)) + return LITEST_NOT_APPLICABLE; + + auto status = libinput_tablet_tool_config_eraser_button_set_button( + tool, + eraser_button_mapping); + litest_assert_enum_eq(status, LIBINPUT_CONFIG_STATUS_INVALID); +} +END_TEST + START_TEST(tablet_eraser_button_config_after_device_removal) { _litest_context_destroy_ struct libinput *li = litest_create_context(); @@ -8099,7 +8282,21 @@ TEST_COLLECTION(tablet_eraser) "with-motion-events", 'b') { litest_add_parametrized(tablet_eraser_button_disabled, LITEST_TABLET, LITEST_TOTEM|LITEST_FORCED_PROXOUT, params); } - + litest_with_parameters(params, + "eraser-button-mapping", 'I', 4, + litest_named_i32(BTN_STYLUS), + litest_named_i32(BTN_STYLUS3), + litest_named_i32(BTN_LEFT), + litest_named_i32(BTN_BACK)){ + litest_add_parametrized(tablet_eraser_button_different_buttons, LITEST_TABLET, LITEST_TOTEM|LITEST_FORCED_PROXOUT, params); + } + litest_with_parameters(params, + "eraser-button-mapping", 'I', 3, + litest_named_i32(BTN_TOUCH), + litest_named_i32(BTN_TOOL_FINGER), + litest_named_i32(KEY_A)) { + litest_add_parametrized(tablet_eraser_button_invalid_buttons, LITEST_TABLET, LITEST_TOTEM|LITEST_FORCED_PROXOUT, params); + } litest_add_no_device(tablet_eraser_button_config_after_device_removal); /* clang-format on */ }