libinput/src/libinput-plugin-tablet-forced-tool.c
Peter Hutterer f3f8e8ef6c 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>
2025-08-31 00:18:59 +00:00

254 lines
6.9 KiB
C

/*
* 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-plugin-tablet-forced-tool.h"
#include "libinput-plugin.h"
#include "libinput-util.h"
/*
* Handling for tools that never set BTN_TOOL_PEN.
*/
enum tools {
PEN = 0,
RUBBER,
BRUSH,
PENCIL,
AIRBRUSH,
MOUSE,
LENS,
};
struct plugin_device {
struct list link;
struct libinput_device *device;
bitmask_t tool_state;
bool pen_forced_into_proximity;
};
struct plugin_data {
struct list devices;
};
static void
plugin_device_destroy(struct plugin_device *device)
{
libinput_device_unref(device->device);
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
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,
struct evdev_frame *frame)
{
size_t nevents;
struct evdev_event *events = evdev_frame_get_events(frame, &nevents);
bool axis_change = 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:
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: {
int code = evdev_event_code(event) - BTN_TOOL_PEN;
if (event->value == 1) {
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, code);
}
return; /* Keep the frame as-is */
}
case EVDEV_ABS_X:
case EVDEV_ABS_Y:
case EVDEV_ABS_Z: /* rotation */
/* not ABS_DISTANCE! */
case EVDEV_ABS_PRESSURE:
case EVDEV_ABS_TILT_X:
case EVDEV_ABS_TILT_Y:
case EVDEV_ABS_WHEEL: /* slider */
/* no early return here, the BTN_TOOL updates
* may come after the ABS_ events */
axis_change = true;
break;
case EVDEV_REL_WHEEL:
/* no early return here, the BTN_TOOL updates
* may come after the REL_ events */
axis_change = true;
break;
default:
break;
}
}
if (!axis_change)
return;
const bitmask_t all_tools =
bitmask_from_bits(PEN, RUBBER, BRUSH, PENCIL, AIRBRUSH, MOUSE, LENS);
if (bitmask_any(device->tool_state, all_tools))
return;
/* We need to force a BTN_TOOL_PEN if we get an axis event (i.e.
* stylus is def. in proximity). We don't do this for pure
* button events because we discard those.
*/
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
forced_tool_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;
list_for_each(pd, &plugin->devices, link) {
if (pd->device == device) {
forced_tool_plugin_device_handle_frame(libinput_plugin,
pd,
frame);
break;
}
}
}
static void
forced_tool_plugin_device_added(struct libinput_plugin *libinput_plugin,
struct libinput_device *device)
{
if (!libinput_device_has_capability(device, LIBINPUT_DEVICE_CAP_TABLET_TOOL))
return;
libinput_plugin_enable_device_event_frame(libinput_plugin, device, true);
struct plugin_data *plugin = libinput_plugin_get_user_data(libinput_plugin);
struct plugin_device *pd = zalloc(sizeof(*pd));
pd->device = libinput_device_ref(device);
list_take_append(&plugin->devices, pd, link);
}
static void
forced_tool_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 const struct libinput_plugin_interface interface = {
.run = NULL,
.destroy = plugin_destroy,
.device_new = NULL,
.device_ignored = NULL,
.device_added = forced_tool_plugin_device_added,
.device_removed = forced_tool_plugin_device_removed,
.evdev_frame = forced_tool_plugin_evdev_frame,
};
void
libinput_tablet_plugin_forced_tool(struct libinput *libinput)
{
_destroy_(plugin_data) *plugin = zalloc(sizeof(*plugin));
list_init(&plugin->devices);
_unref_(libinput_plugin) *p = libinput_plugin_new(libinput,
"tablet-forced-tool",
&interface,
steal(&plugin));
}