From f3f8e8ef6c82517ffa00de7dff8ed4cc82f17bc3 Mon Sep 17 00:00:00 2001 From: Peter Hutterer Date: Mon, 25 Aug 2025 10:34:28 +1000 Subject: [PATCH] plugin: ensure prox out for a forced proximity tool if the tool changes A device may send axis events while the tool is out of proximity, causing our plugin to force a proximity in for the pen. If the tool then sends a proximity event for a different tool we ended up with two tools in proximity. The sequence in #1171 shows this: - evdev: - [ 1, 499608, 3, 27, 0] # EV_ABS / ABS_TILT_Y 0 (+30) - [ 1, 499608, 0, 0, 0] # ------------ SYN_REPORT (0) ---------- +0ms - evdev: - [ 2, 199637, 1, 321, 1] # EV_KEY / BTN_TOOL_RUBBER 1 - [ 2, 199637, 4, 4, 30] # EV_MSC / MSC_SCAN 30 (obfuscated) - [ 2, 199637, 1, 330, 1] # EV_KEY / BTN_TOUCH 1 - [ 2, 199637, 3, 0, 910] # EV_ABS / ABS_X 910 (+246) - [ 2, 199637, 3, 1, 8736] # EV_ABS / ABS_Y 8736 (-105) - [ 2, 199637, 3, 27, -25] # EV_ABS / ABS_TILT_Y -25 (-25) - [ 2, 199637, 0, 0, 0] # ------------ SYN_REPORT (0) ---------- +700ms Fix this by remembering that we forced the tool out of proximity so if we see tool events for another tool we force the pen out of proximity again. This will have some interplay with the other tablet plugins but hopefully none that affect real-world devices, e.g. forcing a proximity out means the proximity out timer plugin gets disabled. Since devices behave in unexpected manners anyway let's see if it affects a real-world device. Closes #1171 Part-of: --- src/libinput-plugin-tablet-forced-tool.c | 41 +++++++++-- test/test-tablet.c | 86 ++++++++++++++++++++++++ 2 files changed, 120 insertions(+), 7 deletions(-) diff --git a/src/libinput-plugin-tablet-forced-tool.c b/src/libinput-plugin-tablet-forced-tool.c index aa19bd59..1cc5f136 100644 --- a/src/libinput-plugin-tablet-forced-tool.c +++ b/src/libinput-plugin-tablet-forced-tool.c @@ -53,6 +53,7 @@ struct plugin_device { struct list link; struct libinput_device *device; bitmask_t tool_state; + bool pen_forced_into_proximity; }; struct plugin_data { @@ -89,6 +90,18 @@ plugin_destroy(struct libinput_plugin *libinput_plugin) plugin_data_destroy(plugin); } +static void +forced_tool_plugin_force_pen_out(struct libinput_plugin *libinput_plugin, + struct libinput_device *device, + struct evdev_frame *frame) +{ + _unref_(evdev_frame) *prox_out_frame = evdev_frame_new(2); + evdev_frame_append_one(prox_out_frame, evdev_usage_from(EVDEV_BTN_TOOL_PEN), 0); + evdev_frame_set_time(prox_out_frame, evdev_frame_get_time(frame)); + + libinput_plugin_prepend_evdev_frame(libinput_plugin, device, prox_out_frame); +} + static void forced_tool_plugin_device_handle_frame(struct libinput_plugin *libinput_plugin, struct plugin_device *device, @@ -103,21 +116,34 @@ forced_tool_plugin_device_handle_frame(struct libinput_plugin *libinput_plugin, struct evdev_event *event = &events[i]; switch (evdev_usage_enum(event->usage)) { case EVDEV_BTN_TOOL_PEN: + if (event->value == 1) { + bitmask_set_bit(&device->tool_state, BTN_TOOL_PEN); + } else { + bitmask_clear_bit(&device->tool_state, BTN_TOOL_PEN); + device->pen_forced_into_proximity = false; + } + return; /* Nothing to do */ case EVDEV_BTN_TOOL_RUBBER: case EVDEV_BTN_TOOL_BRUSH: case EVDEV_BTN_TOOL_PENCIL: case EVDEV_BTN_TOOL_AIRBRUSH: case EVDEV_BTN_TOOL_MOUSE: - case EVDEV_BTN_TOOL_LENS: + case EVDEV_BTN_TOOL_LENS: { + int code = evdev_event_code(event) - BTN_TOOL_PEN; if (event->value == 1) { - bitmask_set_bit(&device->tool_state, - evdev_event_code(event) - BTN_TOOL_PEN); + bitmask_set_bit(&device->tool_state, code); + if (device->pen_forced_into_proximity) { + forced_tool_plugin_force_pen_out( + libinput_plugin, + device->device, + frame); + device->pen_forced_into_proximity = false; + } } else { - bitmask_clear_bit(&device->tool_state, - evdev_event_code(event) - - BTN_TOOL_PEN); + bitmask_clear_bit(&device->tool_state, code); } - return; /* Nothing to do */ + return; /* Keep the frame as-is */ + } case EVDEV_ABS_X: case EVDEV_ABS_Y: case EVDEV_ABS_Z: /* rotation */ @@ -155,6 +181,7 @@ forced_tool_plugin_device_handle_frame(struct libinput_plugin *libinput_plugin, evdev_frame_append_one(frame, evdev_usage_from(EVDEV_BTN_TOOL_PEN), 1); /* libinput's event frame will have space */ + device->pen_forced_into_proximity = true; } static void diff --git a/test/test-tablet.c b/test/test-tablet.c index edca1e0f..a68002f6 100644 --- a/test/test-tablet.c +++ b/test/test-tablet.c @@ -1667,6 +1667,91 @@ START_TEST(proximity_out_disables_forced_after_forced) } END_TEST +START_TEST(proximity_forced_in_with_eraser_swap) +{ + struct litest_device *dev = litest_current_device(); + struct libinput *li = dev->libinput; + struct axis_replacement axes[] = { + { ABS_DISTANCE, 10 }, + { ABS_PRESSURE, 0 }, + { -1, -1 }, + }; + + /* Test doesn't work with a double-tool device */ + if (dev->which == LITEST_ELAN_TABLET) + return LITEST_NOT_APPLICABLE; + + if (!libevdev_has_event_code(dev->evdev, EV_KEY, BTN_TOOL_RUBBER)) + return LITEST_NOT_APPLICABLE; + + /* https://gitlab.freedesktop.org/libinput/libinput/-/issues/1171: + * Device sends correct proximity for pen and eraser so both prox timer + * and double-tool are unregistered. Then it sends a single ABS_X event + * before an eraser proximity in - ensure that our forced proximity + * out is immediately removed. + */ + + /* A correct proximity out sequence from the device should disable + the forced proximity out, even when we had a forced prox-out */ + litest_tablet_proximity_in(dev, 10, 10, axes); + litest_tablet_proximity_out(dev); + litest_drain_events(li); + litest_timeout_tablet_proxout(li); + + /* correct eraser prox out disables forced tool plugin */ + litest_tablet_set_tool_type(dev, BTN_TOOL_RUBBER); + litest_tablet_proximity_in(dev, 12, 12, axes); + litest_tablet_proximity_out(dev); + litest_drain_events(li); + litest_timeout_tablet_proxout(li); + + litest_dispatch(li); + + litest_log_group("Expecting ABS_X to trigger a proximity event") { + /* All our tablets have this within their range so hardcoding 100 should + * be fine here. If this breaks, scale it to absinfo */ + auto abs = libevdev_get_abs_info(dev->evdev, ABS_X); + int x = absinfo_range(abs) / 3 + abs->minimum; + litest_event(dev, EV_ABS, ABS_X, x); + litest_event(dev, EV_SYN, SYN_REPORT, 0); + + litest_dispatch(li); + + _destroy_(libinput_event) *pen_in = libinput_get_event(li); + auto tev = litest_is_proximity_event( + pen_in, + LIBINPUT_TABLET_TOOL_PROXIMITY_STATE_IN); + auto tool = libinput_event_tablet_tool_get_tool(tev); + litest_assert_enum_eq(libinput_tablet_tool_get_type(tool), + LIBINPUT_TABLET_TOOL_TYPE_PEN); + } + + litest_log_group( + "Expecting eraser prox in to trigger prox out for pen and prox in for eraser") { + litest_tablet_proximity_in(dev, 15, 15, axes); + litest_dispatch(li); + + _destroy_(libinput_event) *pen_out = libinput_get_event(li); + auto tev = litest_is_proximity_event( + pen_out, + LIBINPUT_TABLET_TOOL_PROXIMITY_STATE_OUT); + auto tool = libinput_event_tablet_tool_get_tool(tev); + litest_assert_enum_eq(libinput_tablet_tool_get_type(tool), + LIBINPUT_TABLET_TOOL_TYPE_PEN); + + _destroy_(libinput_event) *eraser_in = libinput_get_event(li); + tev = litest_is_proximity_event( + eraser_in, + 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_ERASER); + } + + litest_assert_empty_queue(li); +} +END_TEST + START_TEST(proximity_out_on_delete) { _litest_context_destroy_ struct libinput *li = litest_create_context(); @@ -7634,6 +7719,7 @@ TEST_COLLECTION(tablet_proximity) litest_add(proximity_out_not_during_buttonpress, LITEST_TABLET | LITEST_DISTANCE, LITEST_ANY); litest_add(proximity_out_disables_forced, LITEST_TABLET, LITEST_FORCED_PROXOUT|LITEST_TOTEM); litest_add(proximity_out_disables_forced_after_forced, LITEST_TABLET, LITEST_FORCED_PROXOUT|LITEST_TOTEM); + litest_add(proximity_forced_in_with_eraser_swap, LITEST_TABLET, LITEST_FORCED_PROXOUT|LITEST_TOTEM); /* clang-format on */ }