diff --git a/meson.build b/meson.build index 622203f9..76ed4aad 100644 --- a/meson.build +++ b/meson.build @@ -376,6 +376,8 @@ src_libinput = src_libfilter + [ 'src/libinput.c', 'src/libinput-plugin.c', 'src/libinput-plugin-tablet-double-tool.c', + 'src/libinput-plugin-tablet-forced-tool.c', + 'src/libinput-plugin-tablet-proximity-timer.c', 'src/libinput-private-config.c', 'src/evdev.c', 'src/evdev-debounce.c', diff --git a/src/evdev-tablet.c b/src/evdev-tablet.c index 44057024..b3049f1e 100644 --- a/src/evdev-tablet.c +++ b/src/evdev-tablet.c @@ -40,11 +40,6 @@ enum notify { DO_NOTIFY, }; -/* The tablet sends events every ~2ms , 50ms should be plenty enough to - detect out-of-range. - This value is higher during test suite runs */ -static int FORCED_PROXOUT_TIMEOUT = 50 * 1000; /* µs */ - #define tablet_set_status(tablet_,s_) (tablet_)->status |= (s_) #define tablet_unset_status(tablet_,s_) (tablet_)->status &= ~(s_) #define tablet_has_status(tablet_,s_) (!!((tablet_)->status & (s_))) @@ -1993,34 +1988,6 @@ tablet_send_events(struct tablet_dispatch *tablet, tablet_send_proximity_out(tablet, tool, device, &axes, time); } -/** - * Handling for the proximity out workaround. Some tablets only send - * BTN_TOOL_PEN on the very first event, then leave it set even when the pen - * leaves the detectable range. To libinput this looks like we always have - * the pen in proximity. - * - * To avoid this, we set a timer on BTN_TOOL_PEN in. We expect the tablet to - * continuously send events, and while it's doing so we keep updating the - * timer. Once we go Xms without an event we assume proximity out and inject - * a BTN_TOOL_PEN event into the sequence through the timer func. - * - * We need to remember that we did that, on the first event after the - * timeout we need to emulate a BTN_TOOL_PEN event again to force proximity - * in. - * - * Other tools never send the BTN_TOOL_PEN event. For those tools, we - * piggyback along with the proximity out quirks by injecting - * the event during the first event frame. - */ -static inline void -tablet_proximity_out_quirk_set_timer(struct tablet_dispatch *tablet, - uint64_t time) -{ - if (tablet->quirks.need_to_force_prox_out) - libinput_timer_set(&tablet->quirks.prox_out_timer, - time + FORCED_PROXOUT_TIMEOUT); -} - static void tablet_update_tool_state(struct tablet_dispatch *tablet, struct evdev_device *device, @@ -2030,41 +1997,6 @@ tablet_update_tool_state(struct tablet_dispatch *tablet, uint32_t changed; int state; - /* we were already out of proximity but now got a tool update but - * our tool state is zero - i.e. we got a valid prox out from the - * device. - */ - if (tablet->quirks.proximity_out_forced && - tablet_has_status(tablet, TABLET_TOOL_UPDATED) && - !tablet->tool_state) { - tablet->quirks.need_to_force_prox_out = false; - tablet->quirks.proximity_out_forced = false; - } - /* We need to emulate a BTN_TOOL_PEN if we get an axis event (i.e. - * stylus is def. in proximity) and: - * - we forced a proximity out before, or - * - on the very first event after init, because if we didn't get a - * BTN_TOOL_PEN and the state for the tool was 0, this device will - * never send the event. - * We don't do this for pure button events because we discard those. - * - * But: on some devices the proximity out is delayed by the kernel, - * so we get it after our forced prox-out has triggered. In that - * case we need to just ignore the change. - */ - if (tablet_has_status(tablet, TABLET_AXES_UPDATED)) { - if (tablet->quirks.proximity_out_forced) { - if (!tablet_has_status(tablet, TABLET_TOOL_UPDATED) && - !tablet->tool_state) - tablet->tool_state = bit(LIBINPUT_TABLET_TOOL_TYPE_PEN); - tablet->quirks.proximity_out_forced = false; - } else if (tablet->tool_state == 0 && - tablet->current_tool.type == LIBINPUT_TOOL_NONE) { - tablet->tool_state = bit(LIBINPUT_TABLET_TOOL_TYPE_PEN); - tablet->quirks.proximity_out_forced = false; - } - } - if (tablet->tool_state == tablet->prev_tool_state) return; @@ -2074,24 +2006,6 @@ tablet_update_tool_state(struct tablet_dispatch *tablet, tablet_update_tool(tablet, device, type, state); - /* The proximity timeout is only needed for BTN_TOOL_PEN, devices - * that require it don't do erasers */ - if (type == LIBINPUT_TABLET_TOOL_TYPE_PEN) { - if (state) { - tablet_proximity_out_quirk_set_timer(tablet, time); - } else { - /* If we get a BTN_TOOL_PEN 0 when *not* injecting - * events it means the tablet will give us the right - * events after all and we can disable our - * timer-based proximity out. - */ - if (!tablet->quirks.proximity_out_in_progress) - tablet->quirks.need_to_force_prox_out = false; - - libinput_timer_cancel(&tablet->quirks.prox_out_timer); - } - } - tablet->prev_tool_state = tablet->tool_state; } @@ -2307,47 +2221,6 @@ tablet_reset_state(struct tablet_dispatch *tablet) tablet_set_status(tablet, TABLET_BUTTONS_DOWN); } -static void -tablet_proximity_out_quirk_timer_func(uint64_t now, void *data) -{ - struct tablet_dispatch *tablet = data; - struct evdev_event events[2] = { - { - .usage = evdev_usage_from(EVDEV_BTN_TOOL_PEN), - .value = 0 - }, - { - .usage = evdev_usage_from(EVDEV_SYN_REPORT), - .value = 0, - } - }; - - if (tablet_has_status(tablet, TABLET_TOOL_IN_CONTACT) || - tablet_has_status(tablet, TABLET_BUTTONS_DOWN)) { - tablet_proximity_out_quirk_set_timer(tablet, now); - return; - } - - if (tablet->quirks.last_event_time > now - FORCED_PROXOUT_TIMEOUT) { - tablet_proximity_out_quirk_set_timer(tablet, - tablet->quirks.last_event_time); - return; - } - - evdev_log_debug(tablet->device, "tablet: forcing proximity after timeout\n"); - - tablet->quirks.proximity_out_in_progress = true; - ARRAY_FOR_EACH(events, e) { - tablet->base.interface->process(&tablet->base, - tablet->device, - e, - now); - } - tablet->quirks.proximity_out_in_progress = false; - - tablet->quirks.proximity_out_forced = true; -} - static void tablet_process(struct evdev_dispatch *dispatch, struct evdev_device *device, @@ -2374,7 +2247,6 @@ tablet_process(struct evdev_dispatch *dispatch, tablet_flush(tablet, device, time); tablet_toggle_touch_device(tablet, device, time); tablet_reset_state(tablet); - tablet->quirks.last_event_time = time; break; default: evdev_log_error(device, @@ -2434,9 +2306,6 @@ tablet_destroy(struct evdev_dispatch *dispatch) struct libinput_tablet_tool *tool; struct libinput *li = tablet_libinput_context(tablet); - libinput_timer_cancel(&tablet->quirks.prox_out_timer); - libinput_timer_destroy(&tablet->quirks.prox_out_timer); - list_for_each_safe(tool, &tablet->tool_list, link) { libinput_tablet_tool_unref(tool); } @@ -2545,7 +2414,6 @@ tablet_check_initial_proximity(struct evdev_device *device, struct evdev_dispatch *dispatch) { struct tablet_dispatch *tablet = tablet_dispatch(dispatch); - struct libinput *li = tablet_libinput_context(tablet); int code, state; enum libinput_tablet_tool_type tool; @@ -2569,8 +2437,6 @@ tablet_check_initial_proximity(struct evdev_device *device, return; tablet_update_tool(tablet, device, tool, state); - if (tablet->quirks.need_to_force_prox_out) - tablet_proximity_out_quirk_set_timer(tablet, libinput_now(li)); tablet->current_tool.id = libevdev_get_event_value(device->evdev, @@ -2983,7 +2849,6 @@ tablet_init(struct tablet_dispatch *tablet, if (!libevdev_has_event_code(evdev, EV_KEY, BTN_TOOL_PEN)) { libevdev_enable_event_code(evdev, EV_KEY, BTN_TOOL_PEN, NULL); - tablet->quirks.proximity_out_forced = true; } /* Our rotation code only works with Wacoms, let's wait until @@ -3014,16 +2879,6 @@ tablet_init(struct tablet_dispatch *tablet, tablet_set_status(tablet, TABLET_TOOL_OUT_OF_PROXIMITY); - /* We always enable the proximity out quirk, but disable it once a - device gives us the right event sequence */ - tablet->quirks.need_to_force_prox_out = true; - - libinput_timer_init(&tablet->quirks.prox_out_timer, - li, - "proxout", - tablet_proximity_out_quirk_timer_func, - tablet); - rc = 0; out: #if HAVE_LIBWACOM @@ -3043,10 +2898,6 @@ evdev_tablet_create(struct evdev_device *device) libinput_libwacom_ref(li); - /* Stop false positives caused by the forced proximity code */ - if (getenv("LIBINPUT_RUNNING_TEST_SUITE")) - FORCED_PROXOUT_TIMEOUT = 150 * 1000; /* µs */ - tablet = zalloc(sizeof *tablet); if (tablet_init(tablet, device) != 0) { diff --git a/src/evdev-tablet.h b/src/evdev-tablet.h index e521ccb8..f35fe78e 100644 --- a/src/evdev-tablet.h +++ b/src/evdev-tablet.h @@ -116,16 +116,6 @@ struct tablet_dispatch { bool rotate; bool want_rotate; } rotation; - - struct { - bool need_to_force_prox_out; - struct libinput_timer prox_out_timer; - bool proximity_out_forced; - uint64_t last_event_time; - - /* true while injecting BTN_TOOL_PEN events */ - bool proximity_out_in_progress; - } quirks; }; static inline struct tablet_dispatch* diff --git a/src/libinput-plugin-tablet-forced-tool.c b/src/libinput-plugin-tablet-forced-tool.c new file mode 100644 index 00000000..0636ade3 --- /dev/null +++ b/src/libinput-plugin-tablet-forced-tool.c @@ -0,0 +1,229 @@ +/* + * 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-forced-tool.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; +}; + +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_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: + 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: + if (event->value == 1) { + bitmask_set_bit(&device->tool_state, + evdev_event_code(event) - BTN_TOOL_PEN); + } else { + bitmask_clear_bit(&device->tool_state, + evdev_event_code(event)- BTN_TOOL_PEN); + } + return; /* Nothing to do */ + 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. + */ + const struct evdev_event prox = { + .usage = evdev_usage_from(EVDEV_BTN_TOOL_PEN), + .value = 1, + }; + evdev_frame_append(frame, &prox, 1); /* libinput's event frame will have space */ +} + +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; + + 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)); +} diff --git a/src/libinput-plugin-tablet-forced-tool.h b/src/libinput-plugin-tablet-forced-tool.h new file mode 100644 index 00000000..33bf45bf --- /dev/null +++ b/src/libinput-plugin-tablet-forced-tool.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_forced_tool(struct libinput *libinput); diff --git a/src/libinput-plugin-tablet-proximity-timer.c b/src/libinput-plugin-tablet-proximity-timer.c new file mode 100644 index 00000000..f0f955bf --- /dev/null +++ b/src/libinput-plugin-tablet-proximity-timer.c @@ -0,0 +1,312 @@ +/* + * 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 "timer.h" +#include "libinput-log.h" +#include "libinput-util.h" +#include "libinput-plugin.h" +#include "libinput-plugin-tablet-proximity-timer.h" + +/* The tablet sends events every ~2ms , 50ms should be plenty enough to + detect out-of-range. + This value is higher during test suite runs */ +static int FORCED_PROXOUT_TIMEOUT = 50 * 1000; /* µs */ + +struct plugin_device { + struct list link; + + struct libinput_plugin_timer *prox_out_timer; + bool proximity_out_forced; + uint64_t last_event_time; + + bool pen_state; + bitmask_t button_state; + + struct libinput_device *device; + struct plugin_data *parent; +}; + +static void +plugin_device_destroy(void *d) +{ + struct plugin_device *device = d; + + list_remove(&device->link); + libinput_plugin_timer_cancel(device->prox_out_timer); + libinput_plugin_timer_unref(device->prox_out_timer); + libinput_device_unref(device->device); + + free(device); +} + +struct plugin_data { + struct list devices; + struct libinput_plugin *plugin; +}; + +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 inline void +proximity_timer_plugin_set_timer(struct plugin_device *device, uint64_t time) +{ + libinput_plugin_timer_set(device->prox_out_timer, + time + FORCED_PROXOUT_TIMEOUT); +} + +static void +tablet_proximity_out_quirk_timer_func(struct libinput_plugin *plugin, + uint64_t now, + void *data) +{ + struct plugin_device *device = data; + + if (!bitmask_is_empty(device->button_state)) { + proximity_timer_plugin_set_timer(device, now); + return; + } + + if (device->last_event_time > now - FORCED_PROXOUT_TIMEOUT) { + proximity_timer_plugin_set_timer(device, device->last_event_time); + return; + } + + plugin_log_debug(device->parent->plugin, + "%s: forcing proximity out after timeout\n", + libinput_device_get_name(device->device)); + const struct evdev_event prox_out_event = { + .usage = evdev_usage_from(EVDEV_BTN_TOOL_PEN), + .value = 0, + }; + + _unref_(evdev_frame) *prox_out_frame = evdev_frame_new(2); + evdev_frame_append(prox_out_frame, &prox_out_event, 1); + evdev_frame_set_time(prox_out_frame, now); + + libinput_plugin_prepend_evdev_frame(device->parent->plugin, + device->device, + prox_out_frame); + + device->proximity_out_forced = true; +} + +/* + * Handling for the proximity out workaround. Some tablets only send + * BTN_TOOL_PEN on the very first event, then leave it set even when the pen + * leaves the detectable range. To libinput this looks like we always have + * the pen in proximity. + * + * To avoid this, we set a timer on BTN_TOOL_PEN in. We expect the tablet to + * continuously send events, and while it's doing so we keep updating the + * timer. Once we go Xms without an event we assume proximity out and inject + * a BTN_TOOL_PEN 0 event into the sequence through the timer func. + * + * On the next axis event after a prox out we enforce + * BTN_TOOL_PEN 1 to force proximity in. + */ +static void +proximity_timer_plugin_device_handle_frame(struct libinput_plugin *libinput_plugin, + struct plugin_device *device, + struct evdev_frame *frame) +{ + uint64_t time = evdev_frame_get_time(frame); + /* First event after adding a device - by definition the pen + * + * is in proximity if we get this one */ + if (device->last_event_time == 0) + proximity_timer_plugin_set_timer(device, time); + + device->last_event_time = time; + + bool pen_toggled = false; + + size_t nevents; + struct evdev_event *events = evdev_frame_get_events(frame, &nevents); + for (size_t i = 0; i < nevents; i++) { + struct evdev_event *event = &events[i]; + + /* The proximity timeout is only needed for BTN_TOOL_PEN, devices + * that require it don't do erasers */ + switch (evdev_usage_enum(event->usage)) { + case EVDEV_BTN_STYLUS: + case EVDEV_BTN_STYLUS2: + case EVDEV_BTN_STYLUS3: + case EVDEV_BTN_TOUCH: + if (event->value) + bitmask_set_bit(&device->button_state, + evdev_event_code(event) - BTN_STYLUS3); + else + bitmask_clear_bit(&device->button_state, + evdev_event_code(event) - BTN_STYLUS3); + break; + case EVDEV_BTN_TOOL_PEN: + pen_toggled = true; + device->pen_state = event->value == 1; + break; + case EVDEV_BTN_TOOL_RUBBER: + case EVDEV_BTN_TOOL_BRUSH: + case EVDEV_BTN_TOOL_PENCIL: + case EVDEV_BTN_TOOL_AIRBRUSH: + case EVDEV_BTN_TOOL_FINGER: + case EVDEV_BTN_TOOL_MOUSE: + case EVDEV_BTN_TOOL_LENS: + plugin_device_destroy(device); + return; + default: + break; + } + } + + if (pen_toggled) { + if (device->pen_state) { + proximity_timer_plugin_set_timer(device, time); + } else { + /* If we get a BTN_TOOL_PEN 0 it means the tablet will + * give us the right events after all and we can disable + * our timer-based proximity out. + */ + libinput_plugin_timer_cancel(device->prox_out_timer); + plugin_log_debug(libinput_plugin, + "%s: proximity out timer unloaded\n", + libinput_device_get_name(device->device)); + plugin_device_destroy(device); + return; + } + } else if (device->proximity_out_forced) { + struct evdev_event pen_in_event = { + .usage = evdev_usage_from(EVDEV_BTN_TOOL_PEN), + .value = 1, + }; + plugin_log_debug(libinput_plugin, + "%s: forcing proximity in\n", + libinput_device_get_name(device->device)); + evdev_frame_append(frame, &pen_in_event, 1); /* libinput's event frame will have space */ + device->proximity_out_forced = false; + proximity_timer_plugin_set_timer(device, time); + } +} + +static void +proximity_timer_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) { + proximity_timer_plugin_device_handle_frame(libinput_plugin, pd, frame); + break; + } + } +} + +static void +proximity_timer_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->prox_out_timer = libinput_plugin_timer_new(libinput_plugin, + "proximity-timer", + tablet_proximity_out_quirk_timer_func, + pd); + + list_take_append(&plugin->devices, pd, link); +} + +static void +proximity_timer_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 +plugin_destroy(struct libinput_plugin *libinput_plugin) +{ + struct plugin_data *plugin = libinput_plugin_get_user_data(libinput_plugin); + plugin_data_destroy(plugin); +} + +static const struct libinput_plugin_interface interface = { + .run = NULL, + .destroy = plugin_destroy, + .device_new = NULL, + .device_ignored = NULL, + .device_added = proximity_timer_plugin_device_added, + .device_removed = proximity_timer_plugin_device_removed, + .evdev_frame = proximity_timer_plugin_evdev_frame, +}; + +void +libinput_tablet_plugin_proximity_timer(struct libinput *libinput) +{ + struct plugin_data *plugin = zalloc(sizeof(*plugin)); + list_init(&plugin->devices); + + /* Stop false positives caused by the forced proximity code */ + if (getenv("LIBINPUT_RUNNING_TEST_SUITE")) + FORCED_PROXOUT_TIMEOUT = 150 * 1000; /* µs */ + + _unref_(libinput_plugin) *p = libinput_plugin_new(libinput, + "tablet-proximity-timer", + &interface, + plugin); + plugin->plugin = p; +} diff --git a/src/libinput-plugin-tablet-proximity-timer.h b/src/libinput-plugin-tablet-proximity-timer.h new file mode 100644 index 00000000..4586c429 --- /dev/null +++ b/src/libinput-plugin-tablet-proximity-timer.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_proximity_timer(struct libinput *libinput); diff --git a/src/libinput-plugin.c b/src/libinput-plugin.c index 0b5f39d0..709a35e1 100644 --- a/src/libinput-plugin.c +++ b/src/libinput-plugin.c @@ -37,6 +37,8 @@ #include "libinput-util.h" #include "libinput-private.h" #include "libinput-plugin-tablet-double-tool.h" +#include "libinput-plugin-tablet-forced-tool.h" +#include "libinput-plugin-tablet-proximity-timer.h" #include "evdev-plugin.h" @@ -362,7 +364,9 @@ libinput_plugin_system_load_internal_plugins(struct libinput *libinput, { /* FIXME: this should really be one of the first in the sequence * so plugins don't have to take care of this? */ + libinput_tablet_plugin_forced_tool(libinput); libinput_tablet_plugin_double_tool(libinput); + libinput_tablet_plugin_proximity_timer(libinput); /* Our own event dispatch is implemented as mini-plugin, * guarantee this one to always be last (and after any diff --git a/test/test-tablet.c b/test/test-tablet.c index a912c2c5..8427767a 100644 --- a/test/test-tablet.c +++ b/test/test-tablet.c @@ -1567,6 +1567,7 @@ START_TEST(proximity_out_not_during_contact) }; litest_tablet_proximity_in(dev, 10, 10, axes); + litest_tablet_tip_down(dev, 10, 10, axes); litest_tablet_motion(dev, 12, 12, axes); litest_drain_events(li); @@ -1577,6 +1578,7 @@ START_TEST(proximity_out_not_during_contact) litest_axis_set_value(axes, ABS_PRESSURE, 0); litest_tablet_motion(dev, 14, 14, axes); + litest_tablet_tip_up(dev, 14, 14, axes); litest_drain_events(li); litest_timeout_tablet_proxout(li);