diff options
author | Matt Mayfield <mdmayfield@users.noreply.github.com> | 2018-08-08 11:31:57 -0500 |
---|---|---|
committer | Matt Mayfield <mdmayfield@users.noreply.github.com> | 2018-08-08 11:36:39 -0500 |
commit | bb87a3d9e933eab34b6cb8d840095eb1e99e3f93 (patch) | |
tree | 698fcc75468dfb77cbc7e7476391b3058dd995ce | |
parent | 916474b09cb7451f35095c48f7918722a9d940af (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.c | 143 | ||||
-rw-r--r-- | src/evdev-mt-touchpad.h | 8 |
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; |