diff --git a/doc/touchpad-edge-scrolling-state-machine.svg b/doc/touchpad-edge-scrolling-state-machine.svg new file mode 100644 index 00000000..7a82d88c --- /dev/null +++ b/doc/touchpad-edge-scrolling-state-machine.svg @@ -0,0 +1,262 @@ + + + + + + + NONE + on-entry: + edge = none + threshold = def + + + + EDGE_NEW + on-entry: + edge = get_edge() + set_timer() + + + + AREA + on-entry: + edge = none + set_pointer() + + + + release + + + + touch + + + + + + touch, + edge &= get_edge() + + + + + + + + + + + + + + + tp_edge_scroll_post_events() + + + + dirty? + + + + + + no + + + + + + yes + + + + + + current = buttons.state & 0x01 + old = buttons.old_state & 0x01 + button = 0 + is_top = 0 + + + + + + notify_axis(last_axis, 0.0) + last_axis = -1 + + + + edge == right + + + + + + yes + + + + axis = scroll_vertical + delta = dy + + + + + + edge == none + + + + + + no + + + + edge == bottom + + + + + + yes + + + + + + no + + + + axis = scroll_horizontal + delta = dx + + + + + + no + + + + get_delta() + + + + + + + + notify_axis(axis, delta) + last_axis = axis + emit(scroll_event_posted) + + + + delta < threshold + + + + + + yes + + + + + + no + + + + + + + + last_axis != -1 + + + + EDGE + on-entry: + threshold = 0.01 + + + + + + + + timeout || + scroll_event_posted + + + + + + + + + + yes + + + + + + yes + + + + + + no + + + + + + + + + + + + + + + + + + yes + + + + + + no + + + + get_edge() + + + + edge + + + + + + no + + + + + + yes + + + diff --git a/src/Makefile.am b/src/Makefile.am index 5cc52a66..027e08ca 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -15,6 +15,7 @@ libinput_la_SOURCES = \ evdev-mt-touchpad.h \ evdev-mt-touchpad-tap.c \ evdev-mt-touchpad-buttons.c \ + evdev-mt-touchpad-edge-scroll.c \ filter.c \ filter.h \ filter-private.h \ diff --git a/src/evdev-mt-touchpad-edge-scroll.c b/src/evdev-mt-touchpad-edge-scroll.c new file mode 100644 index 00000000..1dca0eab --- /dev/null +++ b/src/evdev-mt-touchpad-edge-scroll.c @@ -0,0 +1,374 @@ +/* + * Copyright © 2014 Red Hat, Inc. + * + * Permission to use, copy, modify, distribute, and sell this software and + * its documentation for any purpose is hereby granted without fee, provided + * that the above copyright notice appear in all copies and that both that + * copyright notice and this permission notice appear in supporting + * documentation, and that the name of the copyright holders not be used in + * advertising or publicity pertaining to distribution of the software + * without specific, written prior permission. The copyright holders make + * no representations about the suitability of this software for any + * purpose. It is provided "as is" without express or implied warranty. + * + * THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS + * SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY + * SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER + * RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF + * CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN + * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include +#include +#include +#include +#include +#include "linux/input.h" + +#include "evdev-mt-touchpad.h" + +#define DEFAULT_SCROLL_LOCK_TIMEOUT 300 /* ms */ +/* Use a reasonably large threshold until locked into scrolling mode, to + avoid accidentally locking in scrolling mode when trying to use the entire + touchpad to move the pointer. The user can wait for the timeout to trigger + to do a small scroll. */ +/* In mm for touchpads with valid resolution, see tp_init_accel() */ +#define DEFAULT_SCROLL_THRESHOLD 10.0 + +enum scroll_event { + SCROLL_EVENT_TOUCH, + SCROLL_EVENT_MOTION, + SCROLL_EVENT_RELEASE, + SCROLL_EVENT_TIMEOUT, + SCROLL_EVENT_POSTED, +}; + +static uint32_t +tp_touch_get_edge(struct tp_dispatch *tp, struct tp_touch *touch) +{ + uint32_t edge = EDGE_NONE; + + if (tp->scroll.method != LIBINPUT_CONFIG_SCROLL_EDGE) + return EDGE_NONE; + + if (touch->x > tp->scroll.right_edge) + edge |= EDGE_RIGHT; + + if (touch->y > tp->scroll.bottom_edge) + edge |= EDGE_BOTTOM; + + return edge; +} + +static void +tp_edge_scroll_set_state(struct tp_dispatch *tp, + struct tp_touch *t, + enum tp_edge_scroll_touch_state state) +{ + libinput_timer_cancel(&t->scroll.timer); + + t->scroll.state = state; + + switch (state) { + case EDGE_SCROLL_TOUCH_STATE_NONE: + t->scroll.edge = EDGE_NONE; + t->scroll.threshold = DEFAULT_SCROLL_THRESHOLD; + break; + case EDGE_SCROLL_TOUCH_STATE_EDGE_NEW: + t->scroll.edge = tp_touch_get_edge(tp, t); + libinput_timer_set(&t->scroll.timer, + t->millis + DEFAULT_SCROLL_LOCK_TIMEOUT); + break; + case EDGE_SCROLL_TOUCH_STATE_EDGE: + t->scroll.threshold = 0.01; /* Do not allow 0.0 events */ + break; + case EDGE_SCROLL_TOUCH_STATE_AREA: + t->scroll.edge = EDGE_NONE; + tp_set_pointer(tp, t); + break; + } +} + +static void +tp_edge_scroll_handle_none(struct tp_dispatch *tp, + struct tp_touch *t, + enum scroll_event event) +{ + struct libinput *libinput = tp->device->base.seat->libinput; + + switch (event) { + case SCROLL_EVENT_TOUCH: + if (tp_touch_get_edge(tp, t)) { + tp_edge_scroll_set_state(tp, t, + EDGE_SCROLL_TOUCH_STATE_EDGE_NEW); + } else { + tp_edge_scroll_set_state(tp, t, + EDGE_SCROLL_TOUCH_STATE_AREA); + } + break; + case SCROLL_EVENT_MOTION: + case SCROLL_EVENT_RELEASE: + case SCROLL_EVENT_TIMEOUT: + case SCROLL_EVENT_POSTED: + log_bug_libinput(libinput, + "unexpect scroll event in none state\n"); + break; + } +} + +static void +tp_edge_scroll_handle_edge_new(struct tp_dispatch *tp, + struct tp_touch *t, + enum scroll_event event) +{ + struct libinput *libinput = tp->device->base.seat->libinput; + + switch (event) { + case SCROLL_EVENT_TOUCH: + log_bug_libinput(libinput, + "unexpect scroll event in edge new state\n"); + break; + case SCROLL_EVENT_MOTION: + t->scroll.edge &= tp_touch_get_edge(tp, t); + if (!t->scroll.edge) + tp_edge_scroll_set_state(tp, t, + EDGE_SCROLL_TOUCH_STATE_AREA); + break; + case SCROLL_EVENT_RELEASE: + tp_edge_scroll_set_state(tp, t, EDGE_SCROLL_TOUCH_STATE_NONE); + break; + case SCROLL_EVENT_TIMEOUT: + case SCROLL_EVENT_POSTED: + tp_edge_scroll_set_state(tp, t, EDGE_SCROLL_TOUCH_STATE_EDGE); + break; + } +} + +static void +tp_edge_scroll_handle_edge(struct tp_dispatch *tp, + struct tp_touch *t, + enum scroll_event event) +{ + struct libinput *libinput = tp->device->base.seat->libinput; + + switch (event) { + case SCROLL_EVENT_TOUCH: + case SCROLL_EVENT_TIMEOUT: + log_bug_libinput(libinput, + "unexpect scroll event in edge state\n"); + break; + case SCROLL_EVENT_MOTION: + /* If started at the bottom right, decide in which dir to scroll */ + if (t->scroll.edge == (EDGE_RIGHT | EDGE_BOTTOM)) { + t->scroll.edge &= tp_touch_get_edge(tp, t); + if (!t->scroll.edge) + tp_edge_scroll_set_state(tp, t, + EDGE_SCROLL_TOUCH_STATE_AREA); + } + break; + case SCROLL_EVENT_RELEASE: + tp_edge_scroll_set_state(tp, t, EDGE_SCROLL_TOUCH_STATE_NONE); + break; + case SCROLL_EVENT_POSTED: + break; + } +} + +static void +tp_edge_scroll_handle_area(struct tp_dispatch *tp, + struct tp_touch *t, + enum scroll_event event) +{ + struct libinput *libinput = tp->device->base.seat->libinput; + + switch (event) { + case SCROLL_EVENT_TOUCH: + case SCROLL_EVENT_TIMEOUT: + case SCROLL_EVENT_POSTED: + log_bug_libinput(libinput, + "unexpect scroll event in area state\n"); + break; + case SCROLL_EVENT_MOTION: + break; + case SCROLL_EVENT_RELEASE: + tp_edge_scroll_set_state(tp, t, EDGE_SCROLL_TOUCH_STATE_NONE); + break; + } +} + +static void +tp_edge_scroll_handle_event(struct tp_dispatch *tp, + struct tp_touch *t, + enum scroll_event event) +{ + switch (t->scroll.state) { + case EDGE_SCROLL_TOUCH_STATE_NONE: + tp_edge_scroll_handle_none(tp, t, event); + break; + case EDGE_SCROLL_TOUCH_STATE_EDGE_NEW: + tp_edge_scroll_handle_edge_new(tp, t, event); + break; + case EDGE_SCROLL_TOUCH_STATE_EDGE: + tp_edge_scroll_handle_edge(tp, t, event); + break; + case EDGE_SCROLL_TOUCH_STATE_AREA: + tp_edge_scroll_handle_area(tp, t, event); + break; + } +} + +static void +tp_edge_scroll_handle_timeout(uint64_t now, void *data) +{ + struct tp_touch *t = data; + + tp_edge_scroll_handle_event(t->tp, t, SCROLL_EVENT_TIMEOUT); +} + +int +tp_edge_scroll_init(struct tp_dispatch *tp, struct evdev_device *device) +{ + struct tp_touch *t; + int width, height; + int edge_width, edge_height; + + width = device->abs.absinfo_x->maximum - device->abs.absinfo_x->minimum; + height = device->abs.absinfo_y->maximum - device->abs.absinfo_y->minimum; + + switch (tp->model) { + case MODEL_ALPS: + edge_width = width * .15; + edge_height = height * .15; + break; + case MODEL_APPLETOUCH: /* unibody are all clickpads, so N/A */ + edge_width = width * .085; + edge_height = height * .085; + break; + default: + /* For elantech and synaptics, note for lenovo #40 series, + * e.g. the T440s min/max are the absolute edges, not the + * recommended ones as usual with synaptics. But these are + * clickpads, so N/A. + */ + edge_width = width * .04; + edge_height = height * .054; + } + + tp->scroll.right_edge = device->abs.absinfo_x->maximum - edge_width; + tp->scroll.bottom_edge = device->abs.absinfo_y->maximum - edge_height; + + tp_for_each_touch(tp, t) { + t->scroll.direction = -1; + t->scroll.threshold = DEFAULT_SCROLL_THRESHOLD; + libinput_timer_init(&t->scroll.timer, + device->base.seat->libinput, + tp_edge_scroll_handle_timeout, t); + } + + return 0; +} + +void +tp_destroy_edge_scroll(struct tp_dispatch *tp) +{ + struct tp_touch *t; + + tp_for_each_touch(tp, t) + libinput_timer_cancel(&t->scroll.timer); +} + +void +tp_edge_scroll_handle_state(struct tp_dispatch *tp, uint64_t time) +{ + struct tp_touch *t; + + tp_for_each_touch(tp, t) { + if (!t->dirty) + continue; + + switch (t->state) { + case TOUCH_NONE: + break; + case TOUCH_BEGIN: + tp_edge_scroll_handle_event(tp, t, SCROLL_EVENT_TOUCH); + break; + case TOUCH_UPDATE: + tp_edge_scroll_handle_event(tp, t, SCROLL_EVENT_MOTION); + break; + case TOUCH_END: + tp_edge_scroll_handle_event(tp, t, SCROLL_EVENT_RELEASE); + break; + } + } +} + +int +tp_edge_scroll_post_events(struct tp_dispatch *tp, uint64_t time) +{ + struct libinput_device *device = &tp->device->base; + struct tp_touch *t; + enum libinput_pointer_axis axis; + double dx, dy, *delta; + + tp_for_each_touch(tp, t) { + if (!t->dirty) + continue; + + switch (t->scroll.edge) { + case EDGE_NONE: + if (t->scroll.direction != -1) { + /* Send stop scroll event */ + pointer_notify_axis(device, time, + t->scroll.direction, 0.0); + t->scroll.direction = -1; + } + continue; + case EDGE_RIGHT: + axis = LIBINPUT_POINTER_AXIS_SCROLL_VERTICAL; + delta = &dy; + break; + case EDGE_BOTTOM: + axis = LIBINPUT_POINTER_AXIS_SCROLL_HORIZONTAL; + delta = &dx; + break; + default: /* EDGE_RIGHT | EDGE_BOTTOM */ + continue; /* Don't know direction yet, skip */ + } + + tp_get_delta(t, &dx, &dy); + tp_filter_motion(tp, &dx, &dy, time); + + if (fabs(*delta) < t->scroll.threshold) + continue; + + pointer_notify_axis(device, time, axis, *delta); + t->scroll.direction = axis; + + tp_edge_scroll_handle_event(tp, t, SCROLL_EVENT_POSTED); + } + + return 0; /* Edge touches are suppressed by edge_scroll_touch_active */ +} + +void +tp_edge_scroll_stop_events(struct tp_dispatch *tp, uint64_t time) +{ + struct libinput_device *device = &tp->device->base; + struct tp_touch *t; + + tp_for_each_touch(tp, t) { + if (t->scroll.direction != -1) { + pointer_notify_axis(device, time, + t->scroll.direction, 0.0); + t->scroll.direction = -1; + } + } +} + +int +tp_edge_scroll_touch_active(struct tp_dispatch *tp, struct tp_touch *t) +{ + return t->scroll.state == EDGE_SCROLL_TOUCH_STATE_AREA; +} diff --git a/src/evdev-mt-touchpad.c b/src/evdev-mt-touchpad.c index 6d4b5835..8f76ddb8 100644 --- a/src/evdev-mt-touchpad.c +++ b/src/evdev-mt-touchpad.c @@ -56,7 +56,7 @@ tp_motion_history_offset(struct tp_touch *t, int offset) return &t->history.samples[offset_index]; } -static void +void tp_filter_motion(struct tp_dispatch *tp, double *dx, double *dy, uint64_t time) { @@ -339,7 +339,9 @@ tp_touch_active(struct tp_dispatch *tp, struct tp_touch *t) { return (t->state == TOUCH_BEGIN || t->state == TOUCH_UPDATE) && !t->palm.is_palm && - !t->pinned.is_pinned && tp_button_touch_active(tp, t); + !t->pinned.is_pinned && + tp_button_touch_active(tp, t) && + tp_edge_scroll_touch_active(tp, t); } void @@ -430,15 +432,12 @@ tp_post_twofinger_scroll(struct tp_dispatch *tp, uint64_t time) } static int -tp_post_scroll_events(struct tp_dispatch *tp, uint64_t time) +tp_twofinger_scroll_post_events(struct tp_dispatch *tp, uint64_t time) { struct tp_touch *t; int nfingers_down = 0; - if (tp->scroll.method != LIBINPUT_CONFIG_SCROLL_2FG) - return 0; - - /* No scrolling during tap-n-drag */ + /* No 2fg scrolling during tap-n-drag */ if (tp_tap_dragging(tp)) return 0; @@ -457,6 +456,60 @@ tp_post_scroll_events(struct tp_dispatch *tp, uint64_t time) return 1; } +static void +tp_scroll_handle_state(struct tp_dispatch *tp, uint64_t time) +{ + /* Note this must be always called, so that it knows the state of + * touches when the scroll-mode changes. + */ + tp_edge_scroll_handle_state(tp, time); +} + +static int +tp_post_scroll_events(struct tp_dispatch *tp, uint64_t time) +{ + struct libinput *libinput = tp->device->base.seat->libinput; + + switch (tp->scroll.method) { + case LIBINPUT_CONFIG_SCROLL_NO_SCROLL: + break; + case LIBINPUT_CONFIG_SCROLL_2FG: + return tp_twofinger_scroll_post_events(tp, time); + case LIBINPUT_CONFIG_SCROLL_EDGE: + return tp_edge_scroll_post_events(tp, time); + case LIBINPUT_CONFIG_SCROLL_ON_BUTTON_DOWN: + log_bug_libinput(libinput, "Unexpected scroll mode\n"); + break; + } + return 0; +} + +static void +tp_stop_scroll_events(struct tp_dispatch *tp, uint64_t time) +{ + struct libinput *libinput = tp->device->base.seat->libinput; + + switch (tp->scroll.method) { + case LIBINPUT_CONFIG_SCROLL_NO_SCROLL: + break; + case LIBINPUT_CONFIG_SCROLL_2FG: + evdev_stop_scroll(tp->device, time); + break; + case LIBINPUT_CONFIG_SCROLL_EDGE: + tp_edge_scroll_stop_events(tp, time); + break; + case LIBINPUT_CONFIG_SCROLL_ON_BUTTON_DOWN: + log_bug_libinput(libinput, "Unexpected scroll mode\n"); + break; + } +} + +static void +tp_destroy_scroll(struct tp_dispatch *tp) +{ + tp_destroy_edge_scroll(tp); +} + static void tp_process_state(struct tp_dispatch *tp, uint64_t time) { @@ -490,6 +543,7 @@ tp_process_state(struct tp_dispatch *tp, uint64_t time) } tp_button_handle_state(tp, time); + tp_scroll_handle_state(tp, time); /* * We have a physical button down event on a clickpad. To avoid @@ -525,6 +579,7 @@ tp_post_process_state(struct tp_dispatch *tp, uint64_t time) tp->queued = TOUCHPAD_EVENT_NONE; } + static void tp_post_events(struct tp_dispatch *tp, uint64_t time) { @@ -542,7 +597,7 @@ tp_post_events(struct tp_dispatch *tp, uint64_t time) filter_motion |= tp_post_button_events(tp, time); if (filter_motion || tp->sendevents.trackpoint_active) { - evdev_stop_scroll(tp->device, time); + tp_stop_scroll_events(tp, time); return; } @@ -621,6 +676,7 @@ tp_destroy(struct evdev_dispatch *dispatch) tp_destroy_tap(tp); tp_destroy_buttons(tp); tp_destroy_sendevents(tp); + tp_destroy_scroll(tp); free(tp->touches); free(tp); @@ -912,6 +968,9 @@ tp_scroll_config_scroll_method_get_methods(struct libinput_device *device) if (tp->ntouches >= 2) methods |= LIBINPUT_CONFIG_SCROLL_2FG; + if (!tp->buttons.is_clickpad) + methods |= LIBINPUT_CONFIG_SCROLL_EDGE; + return methods; } @@ -925,7 +984,7 @@ tp_scroll_config_scroll_method_set_method(struct libinput_device *device, if (method == tp->scroll.method) return LIBINPUT_CONFIG_STATUS_SUCCESS; - evdev_stop_scroll(evdev, libinput_now(device->seat->libinput)); + tp_stop_scroll_events(tp, libinput_now(device->seat->libinput)); tp->scroll.method = method; return LIBINPUT_CONFIG_STATUS_SUCCESS; @@ -940,15 +999,29 @@ tp_scroll_config_scroll_method_get_method(struct libinput_device *device) return tp->scroll.method; } +static enum libinput_config_scroll_method +tp_scroll_get_default_method(struct tp_dispatch *tp) +{ + if (tp->ntouches >= 2) + return LIBINPUT_CONFIG_SCROLL_2FG; + else + return LIBINPUT_CONFIG_SCROLL_EDGE; +} + static enum libinput_config_scroll_method tp_scroll_config_scroll_method_get_default_method(struct libinput_device *device) { - return LIBINPUT_CONFIG_SCROLL_2FG; + struct evdev_device *evdev = (struct evdev_device*)device; + struct tp_dispatch *tp = (struct tp_dispatch*)evdev->dispatch; + + return tp_scroll_get_default_method(tp); } static int tp_init_scroll(struct tp_dispatch *tp, struct evdev_device *device) { + if (tp_edge_scroll_init(tp, device) != 0) + return -1; evdev_init_natural_scroll(device); @@ -956,7 +1029,7 @@ tp_init_scroll(struct tp_dispatch *tp, struct evdev_device *device) tp->scroll.config_method.set_method = tp_scroll_config_scroll_method_set_method; tp->scroll.config_method.get_method = tp_scroll_config_scroll_method_get_method; tp->scroll.config_method.get_default_method = tp_scroll_config_scroll_method_get_default_method; - tp->scroll.method = tp_scroll_config_scroll_method_get_default_method(&tp->device->base); + tp->scroll.method = tp_scroll_get_default_method(tp); tp->device->base.config.scroll_method = &tp->scroll.config_method; /* In mm for touchpads with valid resolution, see tp_init_accel() */ diff --git a/src/evdev-mt-touchpad.h b/src/evdev-mt-touchpad.h index 7f3ce493..b2603b44 100644 --- a/src/evdev-mt-touchpad.h +++ b/src/evdev-mt-touchpad.h @@ -103,6 +103,20 @@ enum tp_tap_touch_state { TAP_TOUCH_STATE_DEAD, /**< exceeded motion/timeout */ }; +/* For edge scrolling, so we only care about right and bottom */ +enum tp_edge { + EDGE_NONE = 0, + EDGE_RIGHT = (1 << 0), + EDGE_BOTTOM = (1 << 1), +}; + +enum tp_edge_scroll_touch_state { + EDGE_SCROLL_TOUCH_STATE_NONE, + EDGE_SCROLL_TOUCH_STATE_EDGE_NEW, + EDGE_SCROLL_TOUCH_STATE_EDGE, + EDGE_SCROLL_TOUCH_STATE_AREA, +}; + struct tp_motion { int32_t x; int32_t y; @@ -150,6 +164,14 @@ struct tp_touch { enum tp_tap_touch_state state; } tap; + struct { + enum tp_edge_scroll_touch_state state; + uint32_t edge; + int direction; + double threshold; + struct libinput_timer timer; + } scroll; + struct { bool is_palm; int32_t x, y; /* first coordinates if is_palm == true */ @@ -214,6 +236,8 @@ struct tp_dispatch { struct { struct libinput_device_config_scroll_method config_method; enum libinput_config_scroll_method method; + int32_t right_edge; + int32_t bottom_edge; } scroll; enum touchpad_event queued; @@ -250,6 +274,10 @@ tp_get_delta(struct tp_touch *t, double *dx, double *dy); void tp_set_pointer(struct tp_dispatch *tp, struct tp_touch *t); +void +tp_filter_motion(struct tp_dispatch *tp, + double *dx, double *dy, uint64_t time); + int tp_tap_handle_state(struct tp_dispatch *tp, uint64_t time); @@ -304,4 +332,22 @@ tp_tap_resume(struct tp_dispatch *tp, uint64_t time); bool tp_tap_dragging(struct tp_dispatch *tp); +int +tp_edge_scroll_init(struct tp_dispatch *tp, struct evdev_device *device); + +void +tp_destroy_edge_scroll(struct tp_dispatch *tp); + +void +tp_edge_scroll_handle_state(struct tp_dispatch *tp, uint64_t time); + +int +tp_edge_scroll_post_events(struct tp_dispatch *tp, uint64_t time); + +void +tp_edge_scroll_stop_events(struct tp_dispatch *tp, uint64_t time); + +int +tp_edge_scroll_touch_active(struct tp_dispatch *tp, struct tp_touch *t); + #endif