Commit de5246da authored by Peter Hutterer's avatar Peter Hutterer

touchpad: use motion speed to ignore accidental 2fg touches

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=99703Signed-off-by: Peter Hutterer's avatarPeter Hutterer <peter.hutterer@who-t.net>
parent 10569680
......@@ -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)
......@@ -82,6 +83,41 @@ tp_filter_motion_unaccelerated(struct tp_dispatch *tp,
&raw, tp, time);
}
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)
{
......@@ -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;
}
......@@ -1277,12 +1315,55 @@ tp_detect_jumps(const struct tp_dispatch *tp, struct tp_touch *t)
return hypot(mm.x, mm.y) > JUMP_THRESHOLD_MM;
}
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);
......
......@@ -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 {
......
......@@ -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)
{
......
......@@ -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);
}
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