summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMatt Mayfield <mdmayfield@users.noreply.github.com>2018-08-08 11:31:57 -0500
committerMatt Mayfield <mdmayfield@users.noreply.github.com>2018-08-08 11:36:39 -0500
commitbb87a3d9e933eab34b6cb8d840095eb1e99e3f93 (patch)
tree698fcc75468dfb77cbc7e7476391b3058dd995ce
parent916474b09cb7451f35095c48f7918722a9d940af (diff)
touchpad: 90-degree scroll helper
This makes two-finger scrolling in straight lines easier, while still allowing free/diagonal movement. It works in three stages: 1) Initial movement - For the first few millimeters, scroll movements within 30 degrees of horizontal or vertical are straightened to 90-degree angles. - Scroll movements close to 45 degree diagonals are unchanged. - If movement continues very close to straight horizontal or vertical, stage 2 begins and the axis lock engages. - If movement continues along a diagonal, stage 2 is skipped and free scrolling is immediately enabled. 2) Axis lock - If the user scrolls fairly closely to straight vertical, no horizontal movement will happen at all, and vice versa. - It is possible to switch between straight vertical and straight horizontal, and the axis lock will automatically change. - If deliberate diagonal movement is detected at any point, stage 3 begins and the axis lock disengages. 3) Free scrolling - Scrolling is unconstrained until the fingers are lifted.
-rw-r--r--src/evdev-mt-touchpad-gestures.c143
-rw-r--r--src/evdev-mt-touchpad.h8
2 files changed, 150 insertions, 1 deletions
diff --git a/src/evdev-mt-touchpad-gestures.c b/src/evdev-mt-touchpad-gestures.c
index 8f2dda7f..f62c942e 100644
--- a/src/evdev-mt-touchpad-gestures.c
+++ b/src/evdev-mt-touchpad-gestures.c
@@ -80,6 +80,18 @@ tp_get_touches_delta(struct tp_dispatch *tp, bool average)
return delta;
}
+static void
+tp_gesture_init_scroll(struct tp_dispatch *tp)
+{
+ struct phys_coords zero = {0.0, 0.0};
+ tp->scroll.active.h = false;
+ tp->scroll.active.v = false;
+ tp->scroll.duration.h = 0;
+ tp->scroll.duration.v = 0;
+ tp->scroll.vector = zero;
+ tp->scroll.time_prev = 0;
+}
+
static inline struct device_float_coords
tp_get_combined_touches_delta(struct tp_dispatch *tp)
{
@@ -108,7 +120,7 @@ tp_gesture_start(struct tp_dispatch *tp, uint64_t time)
__func__);
break;
case GESTURE_STATE_SCROLL:
- /* NOP */
+ tp_gesture_init_scroll(tp);
break;
case GESTURE_STATE_PINCH:
gesture_notify_pinch(&tp->device->base, time,
@@ -236,6 +248,134 @@ tp_gesture_set_scroll_buildup(struct tp_dispatch *tp)
tp->device->scroll.buildup = tp_normalize_delta(tp, average);
}
+static void
+tp_gesture_apply_scroll_constraints(struct tp_dispatch *tp,
+ struct device_float_coords *raw,
+ struct normalized_coords *delta,
+ uint64_t time)
+{
+ uint64_t tdelta = 0;
+ struct phys_coords delta_mm, vector;
+ double vector_decay, vector_length, slope;
+
+ const uint64_t ACTIVE_THRESHOLD = ms2us(100),
+ INACTIVE_THRESHOLD = ms2us(50),
+ EVENT_TIMEOUT = ms2us(100);
+
+ /* Both axes active == true means free scrolling is enabled */
+ if (tp->scroll.active.h && tp->scroll.active.v)
+ return;
+
+ /* Determine time delta since last movement event */
+ if (tp->scroll.time_prev != 0)
+ tdelta = time - tp->scroll.time_prev;
+ if (tdelta > EVENT_TIMEOUT)
+ tdelta = 0;
+ tp->scroll.time_prev = time;
+
+ /* Delta since last movement event in mm */
+ delta_mm = tp_phys_delta(tp, *raw);
+
+ /* Old vector data "fades" over time. This is a two-part linear
+ * approximation of an exponential function - for example, for
+ * EVENT_TIMEOUT of 100, vector_decay = (0.97)^tdelta. This linear
+ * approximation allows easier tweaking of EVENT_TIMEOUT and is faster.
+ */
+ if (tdelta > 0) {
+ double recent, later;
+ recent = ((EVENT_TIMEOUT / 2.0) - tdelta) /
+ (EVENT_TIMEOUT / 2.0);
+ later = (EVENT_TIMEOUT - tdelta) /
+ (EVENT_TIMEOUT * 2.0);
+ vector_decay = tdelta <= (0.33 * EVENT_TIMEOUT) ?
+ recent : later;
+ } else {
+ vector_decay = 0.0;
+ }
+
+ /* Calculate windowed vector from delta + weighted historic data */
+ vector.x = (tp->scroll.vector.x * vector_decay) + delta_mm.x;
+ vector.y = (tp->scroll.vector.y * vector_decay) + delta_mm.y;
+ vector_length = hypot(vector.x, vector.y);
+ tp->scroll.vector = vector;
+
+ /* We care somewhat about distance and speed, but more about
+ * consistency of direction over time. Keep track of the time spent
+ * primarily along each axis. If one axis is active, time spent NOT
+ * moving much in the other axis is subtracted, allowing a switch of
+ * axes in a single scroll + ability to "break out" and go diagonal.
+ *
+ * Slope to degree conversions (infinity = 90°, 0 = 0°):
+ */
+ const double DEGREE_75 = 3.73;
+ const double DEGREE_60 = 1.73;
+ const double DEGREE_30 = 0.57;
+ const double DEGREE_15 = 0.27;
+ slope = (vector.x != 0) ? fabs(vector.y / vector.x) : INFINITY;
+
+ /* Ensure vector is big enough (in mm per EVENT_TIMEOUT) to be confident
+ * of direction. Larger = harder to enable diagonal/free scrolling.
+ */
+ const double MIN_VECTOR = 0.25;
+
+ if (slope >= DEGREE_30 && vector_length > MIN_VECTOR) {
+ tp->scroll.duration.v += tdelta;
+ if (tp->scroll.duration.v > ACTIVE_THRESHOLD)
+ tp->scroll.duration.v = ACTIVE_THRESHOLD;
+ if (slope >= DEGREE_75) {
+ if (tp->scroll.duration.h > tdelta)
+ tp->scroll.duration.h -= tdelta;
+ else
+ tp->scroll.duration.h = 0;
+ }
+ }
+ if (slope < DEGREE_60 && vector_length > MIN_VECTOR) {
+ tp->scroll.duration.h += tdelta;
+ if (tp->scroll.duration.h > ACTIVE_THRESHOLD)
+ tp->scroll.duration.h = ACTIVE_THRESHOLD;
+ if (slope < DEGREE_15) {
+ if (tp->scroll.duration.v > tdelta)
+ tp->scroll.duration.v -= tdelta;
+ else
+ tp->scroll.duration.v = 0;
+ }
+ }
+
+ if (tp->scroll.duration.h == ACTIVE_THRESHOLD) {
+ tp->scroll.active.h = true;
+ if (tp->scroll.duration.v < INACTIVE_THRESHOLD)
+ tp->scroll.active.v = false;
+ }
+ if (tp->scroll.duration.v == ACTIVE_THRESHOLD) {
+ tp->scroll.active.v = true;
+ if (tp->scroll.duration.h < INACTIVE_THRESHOLD)
+ tp->scroll.active.h = false;
+ }
+
+ /* If vector is big enough in a diagonal direction, always unlock
+ * both axes regardless of thresholds
+ */
+ if (vector_length > 5.0 && slope < 1.73 && slope >= 0.57) {
+ tp->scroll.active.v = true;
+ tp->scroll.active.h = true;
+ }
+
+ /* If only one axis is active, constrain motion accordingly. If both
+ * are set, we've detected deliberate diagonal movement; enable free
+ * scrolling for the life of the gesture.
+ */
+ if (!tp->scroll.active.h && tp->scroll.active.v)
+ delta->x = 0.0;
+ if (tp->scroll.active.h && !tp->scroll.active.v)
+ delta->y = 0.0;
+
+ /* If we haven't determined an axis, use the slope in the meantime */
+ if (!tp->scroll.active.h && !tp->scroll.active.v) {
+ delta->x = (slope >= DEGREE_60) ? 0.0 : delta->x;
+ delta->y = (slope < DEGREE_30) ? 0.0 : delta->y;
+ }
+}
+
static enum tp_gesture_state
tp_gesture_handle_state_none(struct tp_dispatch *tp, uint64_t time)
{
@@ -408,6 +548,7 @@ tp_gesture_handle_state_scroll(struct tp_dispatch *tp, uint64_t time)
return GESTURE_STATE_SCROLL;
tp_gesture_start(tp, time);
+ tp_gesture_apply_scroll_constraints(tp, &raw, &delta, time);
evdev_post_scroll(tp->device,
time,
LIBINPUT_POINTER_AXIS_SOURCE_FINGER,
diff --git a/src/evdev-mt-touchpad.h b/src/evdev-mt-touchpad.h
index 7a28c457..9c3b95db 100644
--- a/src/evdev-mt-touchpad.h
+++ b/src/evdev-mt-touchpad.h
@@ -358,6 +358,14 @@ struct tp_dispatch {
enum libinput_config_scroll_method method;
int32_t right_edge; /* in device coordinates */
int32_t bottom_edge; /* in device coordinates */
+ struct {
+ bool h, v;
+ } active;
+ struct phys_coords vector;
+ uint64_t time_prev;
+ struct {
+ uint64_t h, v;
+ } duration;
} scroll;
enum touchpad_event queued;