Skip to content

Commit

Permalink
touchpad: 90-degree scroll helper
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
mdmayfield committed Aug 8, 2018
1 parent 916474b commit bb87a3d
Show file tree
Hide file tree
Showing 2 changed files with 150 additions and 1 deletion.
143 changes: 142 additions & 1 deletion src/evdev-mt-touchpad-gestures.c
Expand Up @@ -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)
{
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -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,
Expand Down
8 changes: 8 additions & 0 deletions src/evdev-mt-touchpad.h
Expand Up @@ -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;
Expand Down

0 comments on commit bb87a3d

Please sign in to comment.