Commit b519ea4a authored by Peter Hutterer's avatar Peter Hutterer

tablet: add touch arbitration

So far we've relied on the wacom kernel module to do touch arbitration for us
but that won't be the case in upcoming kernels. Implement touch arbitration in
userspace by pairing the two devices and suspending the touch device whenever
a tool comes into proximity.

In the future more sophisticated arbitration can be done (e.g. only touches
which are close to the pen) but let's burn that bridge when we have to cross
it.

Note that touch arbitration is "device suspend light", i.e. we leave the
device enabled and the fd is active. Tablet interactions are comparatively
short-lived, so closing the fd and asking logind for a new one every time the
pen changes proximity is suboptimal. Instead, we just keep a boolean around
and discard all events while it is set.
Signed-off-by: Peter Hutterer's avatarPeter Hutterer <peter.hutterer@who-t.net>
Reviewed-by: Jason Gerecke's avatarJason Gerecke <jason.gerecke@wacom.com>
parent 1ce99fd6
......@@ -331,4 +331,17 @@ button and ring events on the right. When one of the three mode toggle
buttons on the right is pressed, the right mode switches to that button's
mode but the left mode remains unchanged.
@section tablet-touch-arbitration Tablet touch arbitration
"Touch arbitration" is the terminology used when touch events are suppressed
while the pen is in proximity. Since it is almost impossible to use a stylus
or other tool without triggering touches with the hand holding the tool,
touch arbitration serves to reduce the number of accidental inputs.
The wacom kernel driver currently provides touch arbitration but for other
devices arbitration has to be done in userspace.
libinput uses the @ref libinput_device_group to decide on touch arbitration
and automatically discards touch events whenever a tool is in proximity.
The exact behavior is device-dependent.
*/
......@@ -1165,6 +1165,9 @@ tp_interface_process(struct evdev_dispatch *dispatch,
struct tp_dispatch *tp =
(struct tp_dispatch *)dispatch;
if (tp->ignore_events)
return;
switch (e->type) {
case EV_ABS:
if (tp->has_mt)
......@@ -1679,6 +1682,23 @@ evdev_tag_touchpad(struct evdev_device *device,
}
}
static void
tp_interface_toggle_touch(struct evdev_dispatch *dispatch,
struct evdev_device *device,
bool enable)
{
struct tp_dispatch *tp = (struct tp_dispatch*)dispatch;
bool ignore_events = !enable;
if (ignore_events == tp->ignore_events)
return;
if (ignore_events)
tp_clear_state(tp);
tp->ignore_events = ignore_events;
}
static struct evdev_dispatch_interface tp_interface = {
tp_interface_process,
tp_interface_suspend,
......@@ -1689,6 +1709,7 @@ static struct evdev_dispatch_interface tp_interface = {
tp_interface_device_removed, /* device_suspended, treat as remove */
tp_interface_device_added, /* device_resumed, treat as add */
NULL, /* post_added */
tp_interface_toggle_touch,
};
static void
......
......@@ -231,6 +231,10 @@ struct tp_dispatch {
bool semi_mt;
bool reports_distance; /* does the device support true hovering */
/* true if we're reading events (i.e. not suspended) but we're
* ignoring them */
bool ignore_events;
unsigned int num_slots; /* number of slots */
unsigned int ntouches; /* no slots inc. fakes */
struct tp_touch *touches; /* len == ntouches */
......
......@@ -512,6 +512,7 @@ static struct evdev_dispatch_interface pad_interface = {
NULL, /* device_suspended */
NULL, /* device_resumed */
NULL, /* post_added */
NULL, /* toggle_touch */
};
static void
......
......@@ -1433,6 +1433,39 @@ tablet_flush(struct tablet_dispatch *tablet,
}
}
static inline void
tablet_set_touch_device_enabled(struct evdev_device *touch_device,
bool enable)
{
struct evdev_dispatch *dispatch;
if (touch_device == NULL)
return;
dispatch = touch_device->dispatch;
if (dispatch->interface->toggle_touch)
dispatch->interface->toggle_touch(dispatch,
touch_device,
enable);
}
static inline void
tablet_toggle_touch_device(struct tablet_dispatch *tablet,
struct evdev_device *tablet_device)
{
bool enable_events;
enable_events = tablet_has_status(tablet,
TABLET_TOOL_OUT_OF_RANGE) ||
tablet_has_status(tablet, TABLET_NONE) ||
tablet_has_status(tablet,
TABLET_TOOL_LEAVING_PROXIMITY) ||
tablet_has_status(tablet,
TABLET_TOOL_OUT_OF_PROXIMITY);
tablet_set_touch_device_enabled(tablet->touch_device, enable_events);
}
static inline void
tablet_reset_state(struct tablet_dispatch *tablet)
{
......@@ -1466,6 +1499,7 @@ tablet_process(struct evdev_dispatch *dispatch,
break;
case EV_SYN:
tablet_flush(tablet, device, time);
tablet_toggle_touch_device(tablet, device);
tablet_reset_state(tablet);
break;
default:
......@@ -1477,6 +1511,16 @@ tablet_process(struct evdev_dispatch *dispatch,
}
}
static void
tablet_suspend(struct evdev_dispatch *dispatch,
struct evdev_device *device)
{
struct tablet_dispatch *tablet =
(struct tablet_dispatch *)dispatch;
tablet_set_touch_device_enabled(tablet->touch_device, true);
}
static void
tablet_destroy(struct evdev_dispatch *dispatch)
{
......@@ -1491,6 +1535,35 @@ tablet_destroy(struct evdev_dispatch *dispatch)
free(tablet);
}
static void
tablet_device_added(struct evdev_device *device,
struct evdev_device *added_device)
{
struct tablet_dispatch *tablet =
(struct tablet_dispatch*)device->dispatch;
if (libinput_device_get_device_group(&device->base) !=
libinput_device_get_device_group(&added_device->base))
return;
/* Touch screens or external touchpads only */
if (evdev_device_has_capability(added_device, LIBINPUT_DEVICE_CAP_TOUCH) ||
(evdev_device_has_capability(added_device, LIBINPUT_DEVICE_CAP_POINTER) &&
(added_device->tags & EVDEV_TAG_EXTERNAL_TOUCHPAD)))
tablet->touch_device = added_device;
}
static void
tablet_device_removed(struct evdev_device *device,
struct evdev_device *removed_device)
{
struct tablet_dispatch *tablet =
(struct tablet_dispatch*)device->dispatch;
if (tablet->touch_device == removed_device)
tablet->touch_device = NULL;
}
static void
tablet_check_initial_proximity(struct evdev_device *device,
struct evdev_dispatch *dispatch)
......@@ -1532,14 +1605,15 @@ tablet_check_initial_proximity(struct evdev_device *device,
static struct evdev_dispatch_interface tablet_interface = {
tablet_process,
NULL, /* suspend */
tablet_suspend,
NULL, /* remove */
tablet_destroy,
NULL, /* device_added */
NULL, /* device_removed */
tablet_device_added,
tablet_device_removed,
NULL, /* device_suspended */
NULL, /* device_resumed */
tablet_check_initial_proximity,
NULL, /* toggle_touch */
};
static void
......
......@@ -71,6 +71,9 @@ struct tablet_dispatch {
uint32_t cursor_proximity_threshold;
struct libinput_device_config_calibration calibration;
/* The paired touch device on devices with both pen & touch */
struct evdev_device *touch_device;
};
static inline enum libinput_tablet_tool_axis
......
......@@ -1055,6 +1055,9 @@ fallback_process(struct evdev_dispatch *evdev_dispatch,
struct fallback_dispatch *dispatch = (struct fallback_dispatch*)evdev_dispatch;
enum evdev_event_type sent;
if (dispatch->ignore_events)
return;
switch (event->type) {
case EV_REL:
fallback_process_relative(dispatch, device, event, time);
......@@ -1183,6 +1186,23 @@ fallback_suspend(struct evdev_dispatch *evdev_dispatch,
fallback_return_to_neutral_state(dispatch, device);
}
static void
fallback_toggle_touch(struct evdev_dispatch *evdev_dispatch,
struct evdev_device *device,
bool enable)
{
struct fallback_dispatch *dispatch = (struct fallback_dispatch*)evdev_dispatch;
bool ignore_events = !enable;
if (ignore_events == dispatch->ignore_events)
return;
if (ignore_events)
fallback_return_to_neutral_state(dispatch, device);
dispatch->ignore_events = ignore_events;
}
static void
fallback_destroy(struct evdev_dispatch *evdev_dispatch)
{
......@@ -1243,6 +1263,7 @@ struct evdev_dispatch_interface fallback_interface = {
NULL, /* device_suspended */
NULL, /* device_resumed */
NULL, /* post_added */
fallback_toggle_touch, /* toggle_touch */
};
static uint32_t
......
......@@ -265,6 +265,10 @@ struct evdev_dispatch_interface {
* was sent */
void (*post_added)(struct evdev_device *device,
struct evdev_dispatch *dispatch);
void (*toggle_touch)(struct evdev_dispatch *dispatch,
struct evdev_device *device,
bool enable);
};
struct evdev_dispatch {
......@@ -308,6 +312,10 @@ struct fallback_dispatch {
unsigned long hw_key_mask[NLONGS(KEY_CNT)];
enum evdev_event_type pending_event;
/* true if we're reading events (i.e. not suspended) but we're
ignoring them */
bool ignore_events;
};
struct evdev_device *
......
......@@ -3767,6 +3767,338 @@ START_TEST(relative_calibration)
}
END_TEST
static void
touch_arbitration(struct litest_device *dev,
enum litest_device_type other,
bool is_touchpad)
{
struct litest_device *finger;
struct libinput *li = dev->libinput;
struct axis_replacement axes[] = {
{ ABS_DISTANCE, 10 },
{ ABS_PRESSURE, 0 },
{ -1, -1 }
};
finger = litest_add_device(li, other);
litest_drain_events(li);
litest_tablet_proximity_in(dev, 10, 10, axes);
litest_tablet_motion(dev, 10, 10, axes);
litest_tablet_motion(dev, 20, 40, axes);
litest_drain_events(li);
litest_touch_down(finger, 0, 30, 30);
litest_touch_move_to(finger, 0, 30, 30, 80, 80, 10, 1);
litest_assert_empty_queue(li);
litest_tablet_motion(dev, 10, 10, axes);
litest_tablet_motion(dev, 20, 40, axes);
litest_assert_only_typed_events(li,
LIBINPUT_EVENT_TABLET_TOOL_AXIS);
litest_tablet_proximity_out(dev);
litest_assert_only_typed_events(li,
LIBINPUT_EVENT_TABLET_TOOL_PROXIMITY);
/* finger still down */
litest_touch_move_to(finger, 0, 80, 80, 30, 30, 10, 1);
litest_touch_up(finger, 0);
litest_assert_empty_queue(li);
/* lift finger, expect expect events */
litest_touch_down(finger, 0, 30, 30);
litest_touch_move_to(finger, 0, 30, 30, 80, 80, 10, 1);
litest_touch_up(finger, 0);
libinput_dispatch(li);
if (is_touchpad)
litest_assert_only_typed_events(li,
LIBINPUT_EVENT_POINTER_MOTION);
else
litest_assert_touch_sequence(li);
litest_delete_device(finger);
}
START_TEST(intuos_touch_arbitration)
{
touch_arbitration(litest_current_device(), LITEST_WACOM_FINGER, true);
}
END_TEST
START_TEST(cintiq_touch_arbitration)
{
touch_arbitration(litest_current_device(),
LITEST_WACOM_CINTIQ_13HDT_FINGER,
false);
}
END_TEST
static void
touch_arbitration_stop_touch(struct litest_device *dev,
enum litest_device_type other,
bool is_touchpad)
{
struct litest_device *finger;
struct libinput *li = dev->libinput;
struct axis_replacement axes[] = {
{ ABS_DISTANCE, 10 },
{ ABS_PRESSURE, 0 },
{ -1, -1 }
};
finger = litest_add_device(li, other);
litest_touch_down(finger, 0, 30, 30);
litest_touch_move_to(finger, 0, 30, 30, 80, 80, 10, 1);
litest_tablet_proximity_in(dev, 10, 10, axes);
litest_tablet_motion(dev, 10, 10, axes);
litest_tablet_motion(dev, 20, 40, axes);
litest_drain_events(li);
litest_touch_move_to(finger, 0, 80, 80, 30, 30, 10, 1);
/* start another finger to make sure that one doesn't send events
either */
litest_touch_down(finger, 1, 30, 30);
litest_touch_move_to(finger, 1, 30, 30, 80, 80, 10, 1);
litest_assert_empty_queue(li);
litest_tablet_motion(dev, 10, 10, axes);
litest_tablet_motion(dev, 20, 40, axes);
litest_assert_only_typed_events(li,
LIBINPUT_EVENT_TABLET_TOOL_AXIS);
litest_tablet_proximity_out(dev);
litest_drain_events(li);
/* Finger needs to be lifted for events to happen*/
litest_touch_move_to(finger, 0, 30, 30, 80, 80, 10, 1);
litest_assert_empty_queue(li);
litest_touch_move_to(finger, 1, 80, 80, 30, 30, 10, 1);
litest_assert_empty_queue(li);
litest_touch_up(finger, 0);
litest_touch_move_to(finger, 1, 30, 30, 80, 80, 10, 1);
litest_assert_empty_queue(li);
litest_touch_up(finger, 1);
litest_touch_down(finger, 0, 30, 30);
litest_touch_move_to(finger, 0, 30, 30, 80, 80, 10, 1);
litest_touch_up(finger, 0);
libinput_dispatch(li);
if (is_touchpad)
litest_assert_only_typed_events(li,
LIBINPUT_EVENT_POINTER_MOTION);
else
litest_assert_touch_sequence(li);
litest_delete_device(finger);
litest_assert_only_typed_events(li, LIBINPUT_EVENT_DEVICE_REMOVED);
}
START_TEST(intuos_touch_arbitration_stop_touch)
{
touch_arbitration_stop_touch(litest_current_device(),
LITEST_WACOM_FINGER,
true);
}
END_TEST
START_TEST(cintiq_touch_arbitration_stop_touch)
{
touch_arbitration_stop_touch(litest_current_device(),
LITEST_WACOM_CINTIQ_13HDT_FINGER,
false);
}
END_TEST
static void
touch_arbitration_suspend_touch(struct litest_device *dev,
enum litest_device_type other,
bool is_touchpad)
{
struct litest_device *tablet;
struct libinput *li = dev->libinput;
enum libinput_config_status status;
struct axis_replacement axes[] = {
{ ABS_DISTANCE, 10 },
{ ABS_PRESSURE, 0 },
{ -1, -1 }
};
tablet = litest_add_device(li, other);
/* we can't force a device suspend, but we can at least make sure
the device doesn't send events */
status = libinput_device_config_send_events_set_mode(
dev->libinput_device,
LIBINPUT_CONFIG_SEND_EVENTS_DISABLED);
ck_assert_int_eq(status, LIBINPUT_CONFIG_STATUS_SUCCESS);
litest_drain_events(li);
litest_tablet_proximity_in(tablet, 10, 10, axes);
litest_tablet_motion(tablet, 10, 10, axes);
litest_tablet_motion(tablet, 20, 40, axes);
litest_drain_events(li);
litest_touch_down(dev, 0, 30, 30);
litest_touch_move_to(dev, 0, 30, 30, 80, 80, 10, 1);
litest_touch_up(dev, 0);
litest_assert_empty_queue(li);
/* Remove tablet device to unpair, still disabled though */
litest_delete_device(tablet);
litest_assert_only_typed_events(li, LIBINPUT_EVENT_DEVICE_REMOVED);
litest_touch_down(dev, 0, 30, 30);
litest_touch_move_to(dev, 0, 30, 30, 80, 80, 10, 1);
litest_touch_up(dev, 0);
litest_assert_empty_queue(li);
/* Touch device is still disabled */
litest_touch_down(dev, 0, 30, 30);
litest_touch_move_to(dev, 0, 30, 30, 80, 80, 10, 1);
litest_touch_up(dev, 0);
litest_assert_empty_queue(li);
status = libinput_device_config_send_events_set_mode(
dev->libinput_device,
LIBINPUT_CONFIG_SEND_EVENTS_ENABLED);
ck_assert_int_eq(status, LIBINPUT_CONFIG_STATUS_SUCCESS);
litest_touch_down(dev, 0, 30, 30);
litest_touch_move_to(dev, 0, 30, 30, 80, 80, 10, 1);
litest_touch_up(dev, 0);
libinput_dispatch(li);
if (is_touchpad)
litest_assert_only_typed_events(li,
LIBINPUT_EVENT_POINTER_MOTION);
else
litest_assert_touch_sequence(li);
}
START_TEST(intuos_touch_arbitration_suspend_touch_device)
{
touch_arbitration_suspend_touch(litest_current_device(),
LITEST_WACOM_INTUOS,
true);
}
END_TEST
START_TEST(cintiq_touch_arbitration_suspend_touch_device)
{
touch_arbitration_suspend_touch(litest_current_device(),
LITEST_WACOM_CINTIQ_13HDT_PEN,
false);
}
END_TEST
static void
touch_arbitration_remove_touch(struct litest_device *dev,
enum litest_device_type other,
bool is_touchpad)
{
struct litest_device *finger;
struct libinput *li = dev->libinput;
struct axis_replacement axes[] = {
{ ABS_DISTANCE, 10 },
{ ABS_PRESSURE, 0 },
{ -1, -1 }
};
finger = litest_add_device(li, other);
litest_touch_down(finger, 0, 30, 30);
litest_touch_move_to(finger, 0, 30, 30, 80, 80, 10, 1);
litest_tablet_proximity_in(dev, 10, 10, axes);
litest_drain_events(li);
litest_delete_device(finger);
libinput_dispatch(li);
litest_assert_only_typed_events(li, LIBINPUT_EVENT_DEVICE_REMOVED);
litest_assert_empty_queue(li);
litest_tablet_motion(dev, 10, 10, axes);
litest_tablet_motion(dev, 20, 40, axes);
litest_assert_only_typed_events(li,
LIBINPUT_EVENT_TABLET_TOOL_AXIS);
}
START_TEST(intuos_touch_arbitration_remove_touch)
{
touch_arbitration_remove_touch(litest_current_device(),
LITEST_WACOM_INTUOS,
true);
}
END_TEST
START_TEST(cintiq_touch_arbitration_remove_touch)
{
touch_arbitration_remove_touch(litest_current_device(),
LITEST_WACOM_CINTIQ_13HDT_FINGER,
false);
}
END_TEST
static void
touch_arbitration_remove_tablet(struct litest_device *dev,
enum litest_device_type other,
bool is_touchpad)
{
struct litest_device *tablet;
struct libinput *li = dev->libinput;
struct axis_replacement axes[] = {
{ ABS_DISTANCE, 10 },
{ ABS_PRESSURE, 0 },
{ -1, -1 }
};
tablet = litest_add_device(li, other);
libinput_dispatch(li);
litest_tablet_proximity_in(tablet, 10, 10, axes);
litest_tablet_motion(tablet, 10, 10, axes);
litest_tablet_motion(tablet, 20, 40, axes);
litest_drain_events(li);
litest_touch_down(dev, 0, 30, 30);
litest_touch_move_to(dev, 0, 30, 30, 80, 80, 10, 1);
litest_assert_empty_queue(li);
litest_delete_device(tablet);
litest_assert_only_typed_events(li, LIBINPUT_EVENT_DEVICE_REMOVED);
/* Touch is still down, don't enable */
litest_touch_move_to(dev, 0, 80, 80, 30, 30, 10, 1);
litest_touch_up(dev, 0);
litest_assert_empty_queue(li);
litest_touch_down(dev, 0, 30, 30);
litest_touch_move_to(dev, 0, 30, 30, 80, 80, 10, 1);
litest_touch_up(dev, 0);
libinput_dispatch(li);
if (is_touchpad)
litest_assert_only_typed_events(li, LIBINPUT_EVENT_POINTER_MOTION);
else
litest_assert_touch_sequence(li);
}
START_TEST(intuos_touch_arbitration_remove_tablet)
{
touch_arbitration_remove_tablet(litest_current_device(),
LITEST_WACOM_INTUOS,
true);
}
END_TEST
START_TEST(cintiq_touch_arbitration_remove_tablet)
{
touch_arbitration_remove_tablet(litest_current_device(),
LITEST_WACOM_CINTIQ_13HDT_PEN,
false);
}
END_TEST
void
litest_setup_tests_tablet(void)
{
......@@ -3844,4 +4176,16 @@ litest_setup_tests_tablet(void)
litest_add("tablet:relative", relative_no_delta_prox_in, LITEST_TABLET, LITEST_ANY);
litest_add("tablet:relative", relative_delta, LITEST_TABLET, LITEST_ANY);
litest_add("tablet:relative", relative_calibration, LITEST_TABLET, LITEST_ANY);
litest_add_for_device("tablet:touch-arbitration", intuos_touch_arbitration, LITEST_WACOM_INTUOS);
litest_add_for_device("tablet:touch-arbitration", intuos_touch_arbitration_stop_touch, LITEST_WACOM_INTUOS);
litest_add_for_device("tablet:touch-arbitration", intuos_touch_arbitration_suspend_touch_device, LITEST_WACOM_FINGER);
litest_add_for_device("tablet:touch-arbitration", intuos_touch_arbitration_remove_touch, LITEST_WACOM_INTUOS);
litest_add_for_device("tablet:touch-arbitration", intuos_touch_arbitration_remove_tablet, LITEST_WACOM_FINGER);
litest_add_for_device("tablet:touch-arbitration", cintiq_touch_arbitration, LITEST_WACOM_CINTIQ_13HDT_PEN);
litest_add_for_device("tablet:touch-arbitration", cintiq_touch_arbitration_stop_touch, LITEST_WACOM_CINTIQ_13HDT_PEN);
litest_add_for_device("tablet:touch-arbitration", cintiq_touch_arbitration_suspend_touch_device, LITEST_WACOM_CINTIQ_13HDT_FINGER);
litest_add_for_device("tablet:touch-arbitration", cintiq_touch_arbitration_remove_touch, LITEST_WACOM_CINTIQ_13HDT_PEN);
litest_add_for_device("tablet:touch-arbitration", cintiq_touch_arbitration_remove_tablet, LITEST_WACOM_CINTIQ_13HDT_FINGER);
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment