touchpad: basic thumb detection within gestures
authorMatt Mayfield <mdmayfield@users.noreply.github.com>
Sat, 18 Aug 2018 06:10:34 +0000 (01:10 -0500)
committerPeter Hutterer <peter.hutterer@who-t.net>
Mon, 15 Jul 2019 03:08:47 +0000 (13:08 +1000)
When a touchpad has thumb detection enabled, avoid false-positive gestures
involving a resting thumb by using two thresholds: inner and outer.

While both touches remain inside their inner thresholds, remain in UNKNOWN
state to allow for accurate gesture detection even with no timeout.

If both touches move outside their inner thresholds, start a pinch or
swipe/scroll gesture according to direction, as usual.

If one touch moves outside its outer threshold while the other has not yet
exceeded its inner threshold, and thumb detection is enabled, then if one
touch is >20mm lower, mark it as a thumb and cancel the gesture.

Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
src/evdev-mt-touchpad-gestures.c
test/test-gestures.c

index 47e4ed8..d4133b5 100644 (file)
@@ -195,21 +195,14 @@ tp_gesture_get_active_touches(const struct tp_dispatch *tp,
 }
 
 static uint32_t
-tp_gesture_get_direction(struct tp_dispatch *tp, struct tp_touch *touch,
-                        unsigned int nfingers)
+tp_gesture_get_direction(struct tp_dispatch *tp, struct tp_touch *touch)
 {
        struct phys_coords mm;
        struct device_float_coords delta;
-       double move_threshold = 1.0; /* mm */
-
-       move_threshold *= (nfingers - 1);
 
        delta = device_delta(touch->point, touch->gesture.initial);
        mm = tp_phys_delta(tp, delta);
 
-       if (length_in_mm(mm) < move_threshold)
-               return UNDEFINED_DIRECTION;
-
        return phys_get_direction(mm);
 }
 
@@ -462,57 +455,88 @@ tp_gesture_init_pinch(struct tp_dispatch *tp)
        tp->gesture.prev_scale = 1.0;
 }
 
+static struct phys_coords
+tp_gesture_mm_moved(struct tp_dispatch *tp, struct tp_touch *t)
+{
+       struct device_coords delta;
+
+       delta.x = abs(t->point.x - t->gesture.initial.x);
+       delta.y = abs(t->point.y - t->gesture.initial.y);
+
+       return evdev_device_unit_delta_to_mm(tp->device, &delta);
+}
+
 static enum tp_gesture_state
 tp_gesture_handle_state_unknown(struct tp_dispatch *tp, uint64_t time)
 {
        struct tp_touch *first = tp->gesture.touches[0],
                        *second = tp->gesture.touches[1];
        uint32_t dir1, dir2;
-       struct phys_coords mm;
-       int vert_distance, horiz_distance;
+       struct device_coords delta;
+       struct phys_coords first_moved, second_moved, distance_mm;
+       double first_mm, second_mm; /* movement since gesture start in mm */
+       double inner = 1.5; /* inner threshold in mm - count this touch */
+       double outer = 4.0; /* outer threshold in mm - ignore other touch */
 
-       vert_distance = abs(first->point.y - second->point.y);
-       horiz_distance = abs(first->point.x - second->point.x);
+       /* Need more margin for error when there are more fingers */
+       outer += 2.0 * (tp->gesture.finger_count - 2);
+       inner += 0.5 * (tp->gesture.finger_count - 2);
 
-       if (time > (tp->gesture.initial_time + DEFAULT_GESTURE_SWIPE_TIMEOUT)) {
-               /* for two-finger gestures, if the fingers stay unmoving for a
-                * while, assume (slow) scroll */
-               if (tp->gesture.finger_count == 2) {
-                       tp_gesture_set_scroll_buildup(tp);
-                       return GESTURE_STATE_SCROLL;
-               /* more fingers than slots, don't bother with pinch, always
-                * assume swipe */
-               } else if (tp->gesture.finger_count > tp->num_slots) {
-                       return GESTURE_STATE_SWIPE;
-               }
+       first_moved = tp_gesture_mm_moved(tp, first);
+       first_mm = hypot(first_moved.x, first_moved.y);
 
-               /* for 3+ finger gestures, check if one finger is > 20mm
-                  below the others */
-               mm = evdev_convert_xy_to_mm(tp->device,
-                                           horiz_distance,
-                                           vert_distance);
-               if (mm.y > 20 && tp->gesture.enabled) {
-                       tp_gesture_init_pinch(tp);
-                       return GESTURE_STATE_PINCH;
-               } else {
-                       return GESTURE_STATE_SWIPE;
-               }
-       }
+       second_moved = tp_gesture_mm_moved(tp, second);
+       second_mm = hypot(second_moved.x, second_moved.y);
+
+       delta.x = abs(first->point.x - second->point.x);
+       delta.y = abs(first->point.y - second->point.y);
+       distance_mm = evdev_device_unit_delta_to_mm(tp->device, &delta);
 
-       if (time > (tp->gesture.initial_time + DEFAULT_GESTURE_SWIPE_TIMEOUT)) {
-               mm = evdev_convert_xy_to_mm(tp->device, horiz_distance, vert_distance);
-               if (tp->gesture.finger_count == 2 && mm.x > 40 && mm.y > 40)
-                       return GESTURE_STATE_PINCH;
+       /* If both touches moved less than a mm, we cannot decide yet */
+       if (first_mm < 1 && second_mm < 1)
+               return GESTURE_STATE_UNKNOWN;
+
+       /* If one touch exceeds the outer threshold while the other has not
+        * yet passed the inner threshold, this is not a valid gesture.
+        * If thumb detection is enabled, and one of the touches is >20mm
+        * below the other, cancel the gesture and mark the thumb.
+        *
+        * Give the thumb a larger effective outer threshold for more reliable
+        * detection of pinch vs. resting thumb.
+        */
+       if (tp->thumb.detect_thumbs && distance_mm.y > 20.0 &&
+           time > (tp->gesture.initial_time + DEFAULT_GESTURE_SWIPE_TIMEOUT)) {
+               if ((first->point.y >= second->point.y) &&
+                  ((first_mm >= outer * 2.0) ||
+                  (second_mm >= outer))) {
+                       tp_gesture_cancel(tp, time);
+                       first->thumb.state = THUMB_STATE_YES;
+                       return GESTURE_STATE_NONE;
+               }
+               if ((second->point.y >= first->point.y) &&
+                   ((second_mm >= outer * 2.0) ||
+                   (first_mm >= outer))) {
+                       tp_gesture_cancel(tp, time);
+                       second->thumb.state = THUMB_STATE_YES;
+                       return GESTURE_STATE_NONE;
+               }
        }
 
-       /* Else wait for both fingers to have moved */
-       dir1 = tp_gesture_get_direction(tp, first, tp->gesture.finger_count);
-       dir2 = tp_gesture_get_direction(tp, second, tp->gesture.finger_count);
-       if (dir1 == UNDEFINED_DIRECTION || dir2 == UNDEFINED_DIRECTION)
+       /* If either touch is still inside the inner threshold, we can't
+        * tell what kind of gesture this is.
+        */
+       if ((first_mm < inner) || (second_mm < inner))
                return GESTURE_STATE_UNKNOWN;
 
-       /* If both touches are moving in the same direction assume
-        * scroll or swipe */
+       /* Both touches have exceeded the inner threshold; get their directions
+        * gesture. G directions so we know if it's a pinch or swipe/scroll.
+        */
+       dir1 = tp_gesture_get_direction(tp, first);
+       dir2 = tp_gesture_get_direction(tp, second);
+
+       /* If we can't accurately detect pinches, or if the touches are moving
+        * the same way, this is a scroll or swipe.
+        */
        if (tp->gesture.finger_count > tp->num_slots ||
            tp_gesture_same_directions(dir1, dir2)) {
                if (tp->gesture.finger_count == 2) {
@@ -521,12 +545,11 @@ tp_gesture_handle_state_unknown(struct tp_dispatch *tp, uint64_t time)
                } else if (tp->gesture.enabled) {
                        return GESTURE_STATE_SWIPE;
                }
-       } else {
-               tp_gesture_init_pinch(tp);
-               return GESTURE_STATE_PINCH;
        }
 
-       return GESTURE_STATE_UNKNOWN;
+       /* If the touches are moving away from each other, this is a pinch */
+       tp_gesture_init_pinch(tp);
+       return GESTURE_STATE_PINCH;
 }
 
 static enum tp_gesture_state
@@ -772,6 +795,10 @@ tp_gesture_handle_state(struct tp_dispatch *tp, uint64_t time)
                } else if (!tp->gesture.started) {
                        tp->gesture.finger_count = active_touches;
                        tp->gesture.finger_count_pending = 0;
+                       /* If in UNKNOWN state, go back to NONE to
+                        * re-evaluate leftmost and rightmost touches
+                        */
+                       tp->gesture.state = GESTURE_STATE_NONE;
                /* Else debounce finger changes */
                } else if (active_touches != tp->gesture.finger_count_pending) {
                        tp->gesture.finger_count_pending = active_touches;
index 4a9596d..76f80f3 100644 (file)
@@ -489,58 +489,6 @@ START_TEST(gestures_swipe_4fg_btntool)
 }
 END_TEST
 
-START_TEST(gestures_pinch_vertical_position)
-{
-       struct litest_device *dev = litest_current_device();
-       struct libinput *li = dev->libinput;
-       struct libinput_event *event;
-       int nfingers = _i; /* ranged test */
-
-       if (libevdev_get_num_slots(dev->evdev) < nfingers ||
-           !libinput_device_has_capability(dev->libinput_device,
-                                           LIBINPUT_DEVICE_CAP_GESTURE))
-               return;
-
-       litest_disable_tap(dev->libinput_device);
-       litest_drain_events(li);
-
-       litest_touch_down(dev, 0, 40, 30);
-       litest_touch_down(dev, 1, 50, 70);
-       litest_touch_down(dev, 2, 60, 70);
-       if (nfingers > 3)
-               litest_touch_down(dev, 3, 70, 70);
-       libinput_dispatch(li);
-       litest_timeout_gesture_scroll();
-       libinput_dispatch(li);
-
-       /* This is actually a small swipe gesture, all three fingers moving
-        * down but we're checking for the code that triggers based on
-        * finger position. */
-       litest_touch_move(dev, 0, 40, 30.5);
-       litest_touch_move(dev, 1, 50, 70.5);
-       litest_touch_move(dev, 2, 60, 70.5);
-       if (nfingers > 3)
-               litest_touch_move(dev, 3, 70, 70.5);
-       libinput_dispatch(li);
-
-       event = libinput_get_event(li);
-       litest_is_gesture_event(event,
-                               LIBINPUT_EVENT_GESTURE_PINCH_BEGIN,
-                               nfingers);
-       libinput_event_destroy(event);
-
-       litest_touch_move_to(dev, 0, 40, 30.5, 40, 36, 5);
-       litest_touch_move_to(dev, 1, 50, 70.5, 50, 76, 5);
-       litest_touch_move_to(dev, 2, 60, 70.5, 60, 76, 5);
-       if (nfingers > 3)
-               litest_touch_move_to(dev, 3, 70, 70.5, 60, 76, 5);
-       libinput_dispatch(li);
-
-       litest_assert_only_typed_events(li,
-                                       LIBINPUT_EVENT_GESTURE_PINCH_UPDATE);
-}
-END_TEST
-
 START_TEST(gestures_pinch)
 {
        struct litest_device *dev = litest_current_device();
@@ -784,7 +732,7 @@ START_TEST(gestures_pinch_4fg)
        litest_touch_down(dev, 3, 52 - dir_x, 52 - dir_y);
        libinput_dispatch(li);
 
-       for (i = 0; i < 8; i++) {
+       for (i = 0; i < 7; i++) {
                litest_push_event_frame(dev);
                if (dir_x > 0.0)
                        dir_x -= 2;
@@ -1045,7 +993,6 @@ END_TEST
 TEST_COLLECTION(gestures)
 {
        struct range cardinals = { N, N + NCARDINALS };
-       struct range fingers = { 3, 5 };
 
        litest_add("gestures:cap", gestures_cap, LITEST_TOUCHPAD, LITEST_SINGLE_TOUCH);
        litest_add("gestures:cap", gestures_nocap, LITEST_ANY, LITEST_TOUCHPAD);
@@ -1058,7 +1005,6 @@ TEST_COLLECTION(gestures)
        litest_add_ranged("gestures:pinch", gestures_pinch_3fg, LITEST_TOUCHPAD, LITEST_SINGLE_TOUCH, &cardinals);
        litest_add_ranged("gestures:pinch", gestures_pinch_4fg, LITEST_TOUCHPAD, LITEST_SINGLE_TOUCH, &cardinals);
        litest_add_ranged("gestures:pinch", gestures_spread, LITEST_TOUCHPAD, LITEST_SINGLE_TOUCH, &cardinals);
-       litest_add_ranged("gestures:pinch", gestures_pinch_vertical_position, LITEST_TOUCHPAD, LITEST_SINGLE_TOUCH, &fingers);
 
        litest_add("gestures:swipe", gestures_3fg_buttonarea_scroll, LITEST_CLICKPAD, LITEST_SINGLE_TOUCH);
        litest_add("gestures:swipe", gestures_3fg_buttonarea_scroll_btntool, LITEST_CLICKPAD, LITEST_SINGLE_TOUCH);