[PATCH libinput 3/3] touchpad: use motion speed to ignore accidental 2fg touches

Peter Hutterer peter.hutterer at who-t.net
Fri Sep 1 06:09:18 UTC 2017


Calculate the speed of the touch and compare it against a fixed speed limit.
If a touch exceeds the speed when a second touch is set down, that second
touch is marked as a thumb and ignored (unless it's right next to the other
finger, then it's likely a 2fg scroll).

The speed calculation is simple but has to lag behind by one sample - we reset
the motion history whenever a new finger is set down (to avoid pointer jumps)
so we need to know if the finger was moving fast *before* this happens. Plus,
with the pointer jumps we're more likely to get false positives if we
calculate the speed on actual finger down.

This is the simplest version for now, the speed varies greatly between
movements and should probably be averaged across the last 3-or-so samples.

https://bugs.freedesktop.org/show_bug.cgi?id=99703

Signed-off-by: Peter Hutterer <peter.hutterer at who-t.net>
---
 src/evdev-mt-touchpad.c | 125 +++++++++++++++++++++++++++++++++++++++++++++++-
 src/evdev-mt-touchpad.h |   5 ++
 test/litest.h           |   9 ++++
 test/test-touchpad.c    |  86 ++++++++++++++++++++++++++++++++-
 4 files changed, 221 insertions(+), 4 deletions(-)

diff --git a/src/evdev-mt-touchpad.c b/src/evdev-mt-touchpad.c
index fed25824..3fcc1ac5 100644
--- a/src/evdev-mt-touchpad.c
+++ b/src/evdev-mt-touchpad.c
@@ -35,6 +35,7 @@
 #define DEFAULT_KEYBOARD_ACTIVITY_TIMEOUT_2 ms2us(500)
 #define THUMB_MOVE_TIMEOUT ms2us(300)
 #define FAKE_FINGER_OVERFLOW (1 << 7)
+#define THUMB_IGNORE_SPEED_THRESHOLD 20 /* mm/s */
 
 static inline struct tp_history_point*
 tp_motion_history_offset(struct tp_touch *t, int offset)
@@ -83,6 +84,41 @@ tp_filter_motion_unaccelerated(struct tp_dispatch *tp,
 }
 
 static inline void
+tp_calculate_motion_speed(struct tp_dispatch *tp, struct tp_touch *t)
+{
+	const struct tp_history_point *last;
+	struct device_coords delta;
+	struct phys_coords mm;
+	double distance;
+	double speed;
+
+	/* This doesn't kick in until we have at least 4 events in the
+	 * motion history. As a side-effect, this automatically handles the
+	 * 2fg scroll where a finger is down and moving fast before the
+	 * other finger comes down for the scroll.
+	 *
+	 * We do *not* reset the speed to 0 here though. The motion history
+	 * is reset whenever a new finger is down, so we'd be resetting the
+	 * speed and failing.
+	 */
+	if (t->history.count < 4)
+		return;
+
+	/* TODO: we probably need a speed history here so we can average
+	 * across a few events */
+	last = tp_motion_history_offset(t, 1);
+	delta.x = abs(t->point.x - last->point.x);
+	delta.y = abs(t->point.y - last->point.y);
+	mm = evdev_device_unit_delta_to_mm(tp->device, &delta);
+
+	distance = length_in_mm(mm);
+	speed = distance/(t->time - last->time); /* mm/us */
+	speed *= 1000000; /* mm/s */
+
+	t->speed.last_speed = speed;
+}
+
+static inline void
 tp_motion_history_push(struct tp_touch *t)
 {
 	int motion_index = (t->history.index + 1) % TOUCHPAD_HISTORY_LENGTH;
@@ -219,6 +255,8 @@ tp_new_touch(struct tp_dispatch *tp, struct tp_touch *t, uint64_t time)
 	t->state = TOUCH_HOVERING;
 	t->pinned.is_pinned = false;
 	t->time = time;
+	t->speed.last_speed = 0;
+	t->speed.exceeded_count = 0;
 	tp->queued |= TOUCHPAD_EVENT_MOTION;
 }
 
@@ -1278,11 +1316,54 @@ tp_detect_jumps(const struct tp_dispatch *tp, struct tp_touch *t)
 }
 
 static void
+tp_detect_thumb_while_moving(struct tp_dispatch *tp)
+{
+	struct tp_touch *t;
+	struct tp_touch *first = NULL,
+			*second = NULL;
+	struct device_coords distance;
+	struct phys_coords mm;
+
+	tp_for_each_touch(tp, t) {
+		if (t->state != TOUCH_BEGIN)
+			first = t;
+		else
+			second = t;
+
+		if (first && second)
+			break;
+	}
+
+	assert(first);
+	assert(second);
+
+	if (tp->scroll.method == LIBINPUT_CONFIG_SCROLL_2FG) {
+		/* If the second finger comes down next to the other one, we
+		 * assume this is a scroll motion.
+		 */
+		distance.x = abs(first->point.x - second->point.x);
+		distance.y = abs(first->point.y - second->point.y);
+		mm = evdev_device_unit_delta_to_mm(tp->device, &distance);
+
+		if (mm.x <= 25 && mm.y <= 15)
+			return;
+	}
+
+	/* Finger are too far apart or 2fg scrolling is disabled, mark
+	 * second finger as thumb */
+	evdev_log_debug(tp->device,
+			"touch is speed-based thumb\n");
+	second->thumb.state = THUMB_STATE_YES;
+}
+
+static void
 tp_process_state(struct tp_dispatch *tp, uint64_t time)
 {
 	struct tp_touch *t;
 	bool restart_filter = false;
 	bool want_motion_reset;
+	bool have_new_touch = false;
+	unsigned int speed_exceeded_count = 0;
 
 	tp_process_fake_touches(tp, time);
 	tp_unhover_touches(tp, time);
@@ -1299,8 +1380,15 @@ tp_process_state(struct tp_dispatch *tp, uint64_t time)
 			t->quirks.reset_motion_history = false;
 		}
 
-		if (!t->dirty)
+		if (!t->dirty) {
+			/* A non-dirty touch must be below the speed limit */
+			if (t->speed.exceeded_count > 0)
+				t->speed.exceeded_count--;
+
+			speed_exceeded_count = max(speed_exceeded_count,
+						   t->speed.exceeded_count);
 			continue;
+		}
 
 		if (tp_detect_jumps(tp, t)) {
 			if (!tp->semi_mt)
@@ -1317,12 +1405,45 @@ tp_process_state(struct tp_dispatch *tp, uint64_t time)
 		tp_motion_hysteresis(tp, t);
 		tp_motion_history_push(t);
 
+		/* Touch speed handling: if we'are above the threshold,
+		 * count each event that we're over the threshold up to 10
+		 * events. Count down when we are below the speed.
+		 *
+		 * Take the touch with the highest speed excess, if it is
+		 * above a certain threshold (5, see below), assume a
+		 * dropped finger is a thumb.
+		 *
+		 * Yes, this relies on the touchpad to keep sending us
+		 * events even if the finger doesn't move, otherwise we
+		 * never count down. Let's see how far we get with that.
+		 */
+		if (t->speed.last_speed > THUMB_IGNORE_SPEED_THRESHOLD) {
+			if (t->speed.exceeded_count < 10)
+				t->speed.exceeded_count++;
+		} else if (t->speed.exceeded_count > 0) {
+				t->speed.exceeded_count--;
+		}
+
+		speed_exceeded_count = max(speed_exceeded_count,
+					   t->speed.exceeded_count);
+
+		tp_calculate_motion_speed(tp, t);
+
 		tp_unpin_finger(tp, t);
 
-		if (t->state == TOUCH_BEGIN)
+		if (t->state == TOUCH_BEGIN) {
+			have_new_touch = true;
 			restart_filter = true;
+		}
 	}
 
+	/* If we have one touch that exceeds the speed and we get a new
+	 * touch down while doing that, the second touch is a thumb */
+	if (have_new_touch &&
+	    tp->nfingers_down == 2 &&
+	    speed_exceeded_count > 5)
+		tp_detect_thumb_while_moving(tp);
+
 	if (restart_filter)
 		filter_restart(tp->device->pointer.filter, tp, time);
 
diff --git a/src/evdev-mt-touchpad.h b/src/evdev-mt-touchpad.h
index 3f0d821c..03e74a07 100644
--- a/src/evdev-mt-touchpad.h
+++ b/src/evdev-mt-touchpad.h
@@ -220,6 +220,11 @@ struct tp_touch {
 		uint64_t first_touch_time;
 		struct device_coords initial;
 	} thumb;
+
+	struct {
+		double last_speed; /* speed in mm/s at last sample */
+		unsigned int exceeded_count;
+	} speed;
 };
 
 struct tp_dispatch {
diff --git a/test/litest.h b/test/litest.h
index f33c05fa..0511136f 100644
--- a/test/litest.h
+++ b/test/litest.h
@@ -857,6 +857,15 @@ litest_enable_edge_scroll(struct litest_device *dev)
 	litest_assert_int_eq(status, expected);
 }
 
+static inline bool
+litest_has_clickfinger(struct litest_device *dev)
+{
+	struct libinput_device *device = dev->libinput_device;
+	uint32_t methods = libinput_device_config_click_get_methods(device);
+
+	return methods & LIBINPUT_CONFIG_CLICK_METHOD_CLICKFINGER;
+}
+
 static inline void
 litest_enable_clickfinger(struct litest_device *dev)
 {
diff --git a/test/test-touchpad.c b/test/test-touchpad.c
index a6bb4f53..6d2aee57 100644
--- a/test/test-touchpad.c
+++ b/test/test-touchpad.c
@@ -1428,8 +1428,8 @@ START_TEST(touchpad_no_palm_detect_2fg_scroll)
 	/* first finger is palm, second finger isn't so we trigger 2fg
 	 * scrolling */
 	litest_touch_down(dev, 0, 99, 50);
-	litest_touch_move_to(dev, 0, 99, 50, 99, 40, 10, 0);
-	litest_touch_move_to(dev, 0, 99, 40, 99, 50, 10, 0);
+	litest_touch_move_to(dev, 0, 99, 50, 99, 40, 35, 12);
+	litest_touch_move_to(dev, 0, 99, 40, 99, 50, 35, 12);
 	litest_assert_empty_queue(li);
 	litest_touch_down(dev, 1, 50, 50);
 	litest_assert_empty_queue(li);
@@ -5411,6 +5411,84 @@ START_TEST(touchpad_palm_detect_touch_size)
 }
 END_TEST
 
+START_TEST(touchpad_speed_ignore_finger)
+{
+	struct litest_device *dev = litest_current_device();
+	struct libinput *li = dev->libinput;
+
+	if (litest_has_clickfinger(dev))
+		litest_enable_clickfinger(dev);
+
+	litest_drain_events(li);
+
+	litest_touch_down(dev, 0, 20, 20);
+	litest_touch_move_to(dev, 0, 20, 20, 85, 80, 20, 0);
+	litest_touch_down(dev, 1, 20, 80);
+	litest_touch_move_two_touches(dev, 85, 80, 20, 80, -20, -20, 10, 0);
+	libinput_dispatch(li);
+
+	litest_touch_up(dev, 0);
+	litest_touch_up(dev, 1);
+
+	litest_assert_only_typed_events(li, LIBINPUT_EVENT_POINTER_MOTION);
+}
+END_TEST
+
+START_TEST(touchpad_speed_allow_nearby_finger)
+{
+	struct litest_device *dev = litest_current_device();
+	struct libinput *li = dev->libinput;
+
+	if (!litest_has_2fg_scroll(dev))
+		return;
+
+	if (litest_has_clickfinger(dev))
+		litest_enable_clickfinger(dev);
+
+	litest_enable_2fg_scroll(dev);
+
+	litest_drain_events(li);
+
+	litest_touch_down(dev, 0, 20, 20);
+	litest_touch_move_to(dev, 0, 20, 20, 80, 80, 20, 0);
+	litest_drain_events(li);
+	litest_touch_down(dev, 1, 79, 80);
+	litest_touch_move_two_touches(dev, 80, 80, 79, 80, -20, -20, 10, 0);
+	libinput_dispatch(li);
+
+	litest_touch_up(dev, 0);
+	litest_touch_up(dev, 1);
+
+	litest_assert_only_typed_events(li, LIBINPUT_EVENT_POINTER_AXIS);
+}
+END_TEST
+
+START_TEST(touchpad_speed_ignore_finger_edgescroll)
+{
+	struct litest_device *dev = litest_current_device();
+	struct libinput *li = dev->libinput;
+
+	litest_enable_edge_scroll(dev);
+	if (litest_has_clickfinger(dev))
+		litest_enable_clickfinger(dev);
+
+	litest_drain_events(li);
+
+	litest_touch_down(dev, 0, 20, 20);
+	litest_touch_move_to(dev, 0, 20, 20, 60, 80, 20, 0);
+	litest_drain_events(li);
+	litest_touch_down(dev, 1, 59, 80);
+	litest_touch_move_two_touches(dev, 60, 80, 59, 80, -20, -20, 10, 0);
+	libinput_dispatch(li);
+
+	litest_touch_up(dev, 0);
+	libinput_dispatch(li);
+	litest_touch_up(dev, 1);
+
+	litest_assert_only_typed_events(li, LIBINPUT_EVENT_POINTER_MOTION);
+}
+END_TEST
+
 void
 litest_setup_tests_touchpad(void)
 {
@@ -5576,4 +5654,8 @@ litest_setup_tests_touchpad(void)
 
 	litest_add("touchpad:touch-size", touchpad_touch_size, LITEST_APPLE_CLICKPAD, LITEST_ANY);
 	litest_add("touchpad:touch-size", touchpad_touch_size_2fg, LITEST_APPLE_CLICKPAD, LITEST_ANY);
+
+	litest_add("touchpad:speed", touchpad_speed_ignore_finger, LITEST_TOUCHPAD, LITEST_SINGLE_TOUCH|LITEST_SEMI_MT);
+	litest_add("touchpad:speed", touchpad_speed_allow_nearby_finger, LITEST_TOUCHPAD, LITEST_SINGLE_TOUCH|LITEST_SEMI_MT);
+	litest_add("touchpad:speed", touchpad_speed_ignore_finger_edgescroll, LITEST_TOUCHPAD, LITEST_SINGLE_TOUCH|LITEST_SEMI_MT);
 }
-- 
2.13.5



More information about the wayland-devel mailing list