[PATCH libinput v2 4/4] touchpad: Implement pinch gesture support

Hans de Goede hdegoede at redhat.com
Thu Mar 26 02:04:40 PDT 2015


Implement touchpad pinch (and rotate) gesture support.

Note that two two-finger scrolling tests are slightly tweaked to assure that
there is enough touch movement to allow the scroll-or-pinch detect code to do
its works.

Signed-off-by: Hans de Goede <hdegoede at redhat.com>
---
 src/evdev-mt-touchpad-gestures.c | 268 ++++++++++++++++++++++++++++++++++++++-
 src/evdev-mt-touchpad.h          |  17 +++
 src/libinput-private.h           |  33 +++++
 test/touchpad.c                  |   4 +-
 4 files changed, 315 insertions(+), 7 deletions(-)

diff --git a/src/evdev-mt-touchpad-gestures.c b/src/evdev-mt-touchpad-gestures.c
index 017354c..3d3dcaf 100644
--- a/src/evdev-mt-touchpad-gestures.c
+++ b/src/evdev-mt-touchpad-gestures.c
@@ -22,7 +22,6 @@
 
 #include "config.h"
 
-#include <assert.h>
 #include <math.h>
 #include <stdbool.h>
 #include <limits.h>
@@ -30,6 +29,7 @@
 #include "evdev-mt-touchpad.h"
 
 #define DEFAULT_GESTURE_SWITCH_TIMEOUT 100 /* ms */
+#define DEFAULT_GESTURE_2FG_SCROLL_TIMEOUT 1000 /* ms */
 
 static struct normalized_coords
 tp_get_touches_delta(struct tp_dispatch *tp, bool average)
@@ -75,6 +75,7 @@ tp_get_average_touches_delta(struct tp_dispatch *tp)
 static void
 tp_gesture_start(struct tp_dispatch *tp, uint64_t time)
 {
+	struct libinput *libinput = tp->device->base.seat->libinput;
 	const struct normalized_coords zero = { 0.0, 0.0 };
 
 	if (tp->gesture.started)
@@ -82,7 +83,21 @@ tp_gesture_start(struct tp_dispatch *tp, uint64_t time)
 
 	switch (tp->gesture.finger_count) {
 	case 2:
-		/* NOP */
+		switch (tp->gesture.twofinger_state) {
+		case GESTURE_2FG_STATE_NONE:
+		case GESTURE_2FG_STATE_UNKNOWN:
+			log_bug_libinput(libinput,
+				 "%s in unknown gesture mode\n", __func__);
+			break;
+		case GESTURE_2FG_STATE_SCROLL:
+			/* NOP */
+			break;
+		case GESTURE_2FG_STATE_PINCH:
+			gesture_notify_pinch(&tp->device->base, time,
+					    LIBINPUT_EVENT_GESTURE_PINCH_START,
+					    &zero, &zero, 0.0, 0.0);
+			break;
+		}
 		break;
 	case 3:
 	case 4:
@@ -114,8 +129,172 @@ tp_gesture_post_pointer_motion(struct tp_dispatch *tp, uint64_t time)
 	}
 }
 
+static unsigned int
+tp_gesture_get_active_touches(struct tp_dispatch *tp,
+			      struct tp_touch **touches,
+			      unsigned int count)
+{
+	unsigned int i, n = 0;
+	struct tp_touch *t;
+
+	memset(touches, 0, count * sizeof(struct tp_touch *));
+
+	for (i = 0; i < tp->real_touches; i++) {
+		t = &tp->touches[i];
+		if (tp_touch_active(tp, t)) {
+			touches[n++] = t;
+			if (n == count)
+				return count;
+		}
+	}
+
+	/*
+	 * This can happen when the user does .e.g:
+	 * 1) Put down 1st finger in center (so active)
+	 * 2) Put down 2nd finger in a button area (so inactive)
+	 * 3) Put down 3th finger somewhere, gets reported as a fake finger,
+	 *    so gets same coordinates as 1st -> active
+	 *
+	 * We could avoid this by looking at all touches, be we really only
+	 * want to look at real touches.
+	 */
+	return n;
+}
+
+static int
+tp_gesture_get_direction(struct tp_dispatch *tp, int touch)
+{
+	struct normalized_coords normalized;
+	double move_threshold;
+
+	/*
+	 * Semi-mt touchpads have somewhat inaccurate coordinates when
+	 * 2 fingers are down, so use a slightly larger threshold.
+	 */
+	if (tp->semi_mt)
+		move_threshold = TP_MM_TO_DPI_NORMALIZED(4);
+	else
+		move_threshold = TP_MM_TO_DPI_NORMALIZED(3);
+
+	normalized = tp_normalize_delta(tp,
+			device_delta(tp->gesture.touches[touch]->point,
+				tp->gesture.touches[touch]->gesture.initial));
+	if (normalized_length(normalized) < move_threshold)
+		return UNDEFINED_DIRECTION;
+
+	return normalized_get_direction(normalized);
+}
+
+static void
+tp_gesture_get_pinch_info(struct tp_dispatch *tp,
+			  double *distance,
+			  double *angle,
+			  struct device_float_coords *center)
+{
+	struct normalized_coords normalized;
+
+	normalized = tp_normalize_delta(tp,
+			device_delta(tp->gesture.touches[0]->point,
+				     tp->gesture.touches[1]->point));
+	*distance = normalized_length(normalized);
+
+	if (!tp->semi_mt)
+		*angle = atan2(normalized.y, normalized.x) * 180.0 / M_PI;
+	else
+		*angle = 0.0;
+
+	*center = device_average(tp->gesture.touches[0]->point,
+				 tp->gesture.touches[1]->point);
+}
+
+static void
+tp_gesture_set_scroll_buildup(struct tp_dispatch *tp)
+{
+	struct device_float_coords d0, d1;
+
+	d0 = device_delta(tp->gesture.touches[0]->point,
+			  tp->gesture.touches[0]->gesture.initial);
+	d1 = device_delta(tp->gesture.touches[1]->point,
+			  tp->gesture.touches[1]->gesture.initial);
+
+	tp->device->scroll.buildup =
+		tp_normalize_delta(tp, device_float_average(d0, d1));
+}
+
+static void
+tp_gesture_twofinger_state_none(struct tp_dispatch *tp, uint64_t time)
+{
+	if (tp_gesture_get_active_touches(tp, tp->gesture.touches, 2) != 2)
+		return;
+
+	tp->gesture.initial_time = time;
+	tp->gesture.touches[0]->gesture.initial =
+		tp->gesture.touches[0]->point;
+	tp->gesture.touches[1]->gesture.initial =
+		tp->gesture.touches[1]->point;
+
+	tp->gesture.twofinger_state = GESTURE_2FG_STATE_UNKNOWN;
+}
+
+static void
+tp_gesture_twofinger_state_unknown(struct tp_dispatch *tp, uint64_t time)
+{
+	struct normalized_coords normalized;
+	int dir0, dir1;
+
+	normalized = tp_normalize_delta(tp,
+			device_delta(tp->gesture.touches[0]->point,
+				     tp->gesture.touches[1]->point));
+
+	/* If fingers are further than 3 cm apart assume pinch */
+	if (normalized_length(normalized) > TP_MM_TO_DPI_NORMALIZED(30)) {
+		tp->gesture.twofinger_state = GESTURE_2FG_STATE_PINCH;
+		tp_gesture_get_pinch_info(tp,
+					  &tp->gesture.distance,
+					  &tp->gesture.angle,
+					  &tp->gesture.center);
+		return;
+	}
+
+	/* Elif fingers have been close together for a while, scroll */
+	if (time > (tp->gesture.initial_time + DEFAULT_GESTURE_2FG_SCROLL_TIMEOUT)) {
+		tp->gesture.twofinger_state = GESTURE_2FG_STATE_SCROLL;
+		tp_gesture_set_scroll_buildup(tp);
+		return;
+	}
+
+	/* Else wait for both fingers to have moved */
+	dir0 = tp_gesture_get_direction(tp, 0);
+	dir1 = tp_gesture_get_direction(tp, 1);
+	if (dir0 == UNDEFINED_DIRECTION || dir1 == UNDEFINED_DIRECTION)
+		return;
+
+	/*
+	 * If both touches are moving in the same direction assume scroll.
+	 *
+	 * In some cases (semi-mt touchpads) We may seen one finger move
+	 * e.g. N/NE and the other W/NW so we not only check for overlapping
+	 * directions, but also for neighboring bits being set.
+	 * The ((dira & 0x80) && (dirb & 0x01)) checks are to check for bit 0
+	 * and 7 being set as they also represent neighboring directions.
+	 */
+	if (((dir0 | (dir0 >> 1)) & dir1) ||
+	    ((dir1 | (dir1 >> 1)) & dir0) ||
+	    ((dir0 & 0x80) && (dir1 & 0x01)) ||
+	    ((dir1 & 0x80) && (dir0 & 0x01))) {
+		tp->gesture.twofinger_state = GESTURE_2FG_STATE_SCROLL;
+		tp_gesture_set_scroll_buildup(tp);
+	} else {
+		tp->gesture.twofinger_state = GESTURE_2FG_STATE_PINCH;
+		tp_gesture_get_pinch_info(tp,
+					  &tp->gesture.distance,
+					  &tp->gesture.angle,
+					  &tp->gesture.center);
+	}
+}
+
 static void
-tp_gesture_post_twofinger_scroll(struct tp_dispatch *tp, uint64_t time)
+tp_gesture_twofinger_state_scroll(struct tp_dispatch *tp, uint64_t time)
 {
 	struct normalized_coords delta;
 
@@ -133,6 +312,65 @@ tp_gesture_post_twofinger_scroll(struct tp_dispatch *tp, uint64_t time)
 }
 
 static void
+tp_gesture_twofinger_state_pinch(struct tp_dispatch *tp, uint64_t time)
+{
+	double angle, d, distance;
+	struct device_float_coords center;
+	struct normalized_coords delta, unaccel;
+
+	tp_gesture_get_pinch_info(tp, &distance, &angle, &center);
+
+	d = distance - tp->gesture.distance;
+	tp->gesture.distance = distance;
+	distance = d;
+
+	d = angle - tp->gesture.angle;
+	if (d > 180.0)
+		d -= 360.0;
+	else if (d < -180.0)
+		d += 360.0;
+	tp->gesture.angle = angle;
+	angle = d;
+
+	unaccel = tp_normalize_delta(tp,
+			device_float_delta(center, tp->gesture.center));
+	tp->gesture.center = center;
+	delta = tp_filter_motion(tp, &unaccel, time);
+
+	if (normalized_is_zero(delta) && normalized_is_zero(unaccel) &&
+	    distance == 0.0 && angle == 0.0)
+		return;
+
+	tp_gesture_start(tp, time);
+	gesture_notify_pinch(&tp->device->base, time,
+			     LIBINPUT_EVENT_GESTURE_PINCH_UPDATE,
+			     &delta, &unaccel, distance, angle);
+}
+
+static void
+tp_gesture_post_twofinger(struct tp_dispatch *tp, uint64_t time)
+{
+	switch (tp->gesture.twofinger_state) {
+	case GESTURE_2FG_STATE_NONE:
+		tp_gesture_twofinger_state_none(tp, time);
+		if (tp->gesture.twofinger_state != GESTURE_2FG_STATE_UNKNOWN)
+			break;
+		/* fall through */
+	case GESTURE_2FG_STATE_UNKNOWN:
+		tp_gesture_twofinger_state_unknown(tp, time);
+		if (tp->gesture.twofinger_state != GESTURE_2FG_STATE_SCROLL)
+			break;
+		/* fall through */
+	case GESTURE_2FG_STATE_SCROLL:
+		tp_gesture_twofinger_state_scroll(tp, time);
+		break;
+	case GESTURE_2FG_STATE_PINCH:
+		tp_gesture_twofinger_state_pinch(tp, time);
+		break;
+	}
+}
+
+static void
 tp_gesture_post_swipe(struct tp_dispatch *tp, uint64_t time)
 {
 	struct normalized_coords delta, unaccel;
@@ -171,7 +409,7 @@ tp_gesture_post_events(struct tp_dispatch *tp, uint64_t time)
 		tp_gesture_post_pointer_motion(tp, time);
 		break;
 	case 2:
-		tp_gesture_post_twofinger_scroll(tp, time);
+		tp_gesture_post_twofinger(tp, time);
 		break;
 	case 3:
 	case 4:
@@ -191,14 +429,32 @@ tp_gesture_stop_twofinger_scroll(struct tp_dispatch *tp, uint64_t time)
 void
 tp_gesture_stop(struct tp_dispatch *tp, uint64_t time)
 {
+	struct libinput *libinput = tp->device->base.seat->libinput;
+	enum tp_gesture_2fg_state twofinger_state = tp->gesture.twofinger_state;
 	const struct normalized_coords zero = { 0.0, 0.0 };
 
+	tp->gesture.twofinger_state = GESTURE_2FG_STATE_NONE;
+
 	if (!tp->gesture.started)
 		return;
 
 	switch (tp->gesture.finger_count) {
 	case 2:
-		tp_gesture_stop_twofinger_scroll(tp, time);
+		switch (twofinger_state) {
+		case GESTURE_2FG_STATE_NONE:
+		case GESTURE_2FG_STATE_UNKNOWN:
+			log_bug_libinput(libinput,
+				 "%s in unknown gesture mode\n", __func__);
+			break;
+		case GESTURE_2FG_STATE_SCROLL:
+			tp_gesture_stop_twofinger_scroll(tp, time);
+			break;
+		case GESTURE_2FG_STATE_PINCH:
+			gesture_notify_pinch(&tp->device->base, time,
+					    LIBINPUT_EVENT_GESTURE_PINCH_END,
+					    &zero, &zero, 0.0, 0.0);
+			break;
+		}
 		break;
 	case 3:
 	case 4:
@@ -258,6 +514,8 @@ tp_gesture_handle_state(struct tp_dispatch *tp, uint64_t time)
 int
 tp_init_gesture(struct tp_dispatch *tp)
 {
+	tp->gesture.twofinger_state = GESTURE_2FG_STATE_NONE;
+
 	libinput_timer_init(&tp->gesture.finger_count_switch_timer,
 			    tp->device->base.seat->libinput,
 			    tp_gesture_finger_count_switch_timeout, tp);
diff --git a/src/evdev-mt-touchpad.h b/src/evdev-mt-touchpad.h
index 19a262e..83f0492 100644
--- a/src/evdev-mt-touchpad.h
+++ b/src/evdev-mt-touchpad.h
@@ -120,6 +120,13 @@ enum tp_edge_scroll_touch_state {
 	EDGE_SCROLL_TOUCH_STATE_AREA,
 };
 
+enum tp_gesture_2fg_state {
+	GESTURE_2FG_STATE_NONE,
+	GESTURE_2FG_STATE_UNKNOWN,
+	GESTURE_2FG_STATE_SCROLL,
+	GESTURE_2FG_STATE_PINCH,
+};
+
 struct tp_touch {
 	struct tp_dispatch *tp;
 	enum touch_state state;
@@ -171,6 +178,10 @@ struct tp_touch {
 		struct device_coords first; /* first coordinates if is_palm == true */
 		uint32_t time; /* first timestamp if is_palm == true */
 	} palm;
+
+	struct {
+		struct device_coords initial;
+	} gesture;
 };
 
 struct tp_dispatch {
@@ -205,6 +216,12 @@ struct tp_dispatch {
 		unsigned int finger_count;
 		unsigned int finger_count_pending;
 		struct libinput_timer finger_count_switch_timer;
+		enum tp_gesture_2fg_state twofinger_state;
+		struct tp_touch *touches[2];
+		uint64_t initial_time;
+		double distance;
+		double angle;
+		struct device_float_coords center;
 	} gesture;
 
 	struct {
diff --git a/src/libinput-private.h b/src/libinput-private.h
index 2c9f42b..b433126 100644
--- a/src/libinput-private.h
+++ b/src/libinput-private.h
@@ -391,6 +391,39 @@ device_delta(struct device_coords a, struct device_coords b)
 	return delta;
 }
 
+static inline struct device_float_coords
+device_average(struct device_coords a, struct device_coords b)
+{
+	struct device_float_coords average;
+
+	average.x = (a.x + b.x) / 2.0;
+	average.y = (a.y - b.y) / 2.0;
+
+	return average;
+}
+
+static inline struct device_float_coords
+device_float_delta(struct device_float_coords a, struct device_float_coords b)
+{
+	struct device_float_coords delta;
+
+	delta.x = a.x - b.x;
+	delta.y = a.y - b.y;
+
+	return delta;
+}
+
+static inline struct device_float_coords
+device_float_average(struct device_float_coords a, struct device_float_coords b)
+{
+	struct device_float_coords average;
+
+	average.x = (a.x + b.x) / 2.0;
+	average.y = (a.y - b.y) / 2.0;
+
+	return average;
+}
+
 static inline double
 normalized_length(struct normalized_coords norm)
 {
diff --git a/test/touchpad.c b/test/touchpad.c
index 6fa2301..62b5381 100644
--- a/test/touchpad.c
+++ b/test/touchpad.c
@@ -1872,7 +1872,7 @@ START_TEST(touchpad_2fg_scroll_slow_distance)
 		y_move = 7.0 * y->resolution /
 					(y->maximum - y->minimum) * 100;
 	} else {
-		y_move = 10.0;
+		y_move = 20.0;
 	}
 
 	litest_drain_events(li);
@@ -1921,7 +1921,7 @@ START_TEST(touchpad_2fg_scroll_source)
 
 	litest_drain_events(li);
 
-	test_2fg_scroll(dev, 0, 20, 0);
+	test_2fg_scroll(dev, 0, 30, 0);
 	litest_wait_for_event_of_type(li, LIBINPUT_EVENT_POINTER_AXIS, -1);
 
 	while ((event = libinput_get_event(li))) {
-- 
2.3.3



More information about the wayland-devel mailing list