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: <https://gitlab.freedesktop.org/libinput/libinput/-/merge_requests/1306>
This commit is contained in:
Peter Hutterer 2025-08-25 10:34:28 +10:00 committed by Marge Bot
parent ce1112c263
commit f3f8e8ef6c
2 changed files with 120 additions and 7 deletions

View file

@ -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

View file

@ -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 */
}