From 1d9e307e2b92322a5102add55641f953d8e952fe Mon Sep 17 00:00:00 2001 From: Peter Hutterer Date: Fri, 6 Sep 2024 11:38:35 +1000 Subject: [PATCH] touchpad: implement support for three-finger drag Exposed via new configuration option this enables 3 and 4 finger dragging on touchpads. When enabled a 3/4 finger swipe gesture is actually a button down + motion + button up sequence. If tapping is disabled the drag starts immediately, if tapping is enabled the drag starts after the tap timeout/motion so we can distinguish between a tap and a drag. When fingers are released: - if two fingers remain -> keep dragging - if one finger remains -> release drag, switch to pointer motion When 3/4 fingers are set down immediately after releasing all fingers the drag continues, similar to the tap drag lock feature. This drag lock is not currently configurable. This matches the macos behavior for the same feature. Part-of: --- doc/user/configuration.rst | 13 + doc/user/drag-3fg.rst | 40 +++ doc/user/features.rst | 1 + doc/user/meson.build | 1 + src/evdev-mt-touchpad-gestures.c | 413 ++++++++++++++++++++++++- src/evdev-mt-touchpad.c | 2 + src/evdev-mt-touchpad.h | 15 + src/libinput-private.h | 10 + src/libinput.c | 42 +++ src/libinput.h | 101 ++++++ src/libinput.sym | 7 + test/litest.c | 6 + test/litest.h | 26 ++ test/test-gestures.c | 506 +++++++++++++++++++++++++++++++ tools/shared.c | 18 ++ tools/shared.h | 3 + 16 files changed, 1199 insertions(+), 5 deletions(-) create mode 100644 doc/user/drag-3fg.rst diff --git a/doc/user/configuration.rst b/doc/user/configuration.rst index 3b00767b..83ac706a 100644 --- a/doc/user/configuration.rst +++ b/doc/user/configuration.rst @@ -37,6 +37,19 @@ Tapping is usually available on touchpads and the touchpad part of external graphics tablets. Tapping is usually **not** available on touch screens, for those devices it is expected to be implemented by the toolkit. +------------------------------------------------------------------------------ +Three-finger drag +------------------------------------------------------------------------------ + +Three-finger drag allows emulates the mouse button down while three fingers +are down on a touchpad without the need to press a physical button or use +:ref:`tapndrag`. See :ref:`drag_3fg` for details on how this feature works. + +Three-finger drag is usually available on touchpads and the touchpad part of +external graphics tablets. Three-finger drag is usually **not** available on +touch screens, for those devices it is expected to be implemented by the +toolkit. + ------------------------------------------------------------------------------ Send Events Mode ------------------------------------------------------------------------------ diff --git a/doc/user/drag-3fg.rst b/doc/user/drag-3fg.rst new file mode 100644 index 00000000..d9ce0c4e --- /dev/null +++ b/doc/user/drag-3fg.rst @@ -0,0 +1,40 @@ +.. _drag_3fg: + +============================================================================== +Three-finger drag +============================================================================== + +Three-finger drag is a feature available on touchpads that emulates logical +button presses if three fingers are moving on the touchpad. + +Three-finger drag is independent from :ref:`tapping` though some specific +behaviors may change when both features are enabled. For example, with +tapping *disabled* a three-finger gesture will virtually always be a three-finger +drag. With tapping *enabled* a three finger gesture may be a three finger drag +and a short delay is required to disambiguate between the two. + + +The exact behavior of three-finger drag is implementation defined and may +subtly change. As a general rule, the following constraints can be expected: + +- three fingers down and movement trigger a button down and subsequent motion + events (i.e. a drag) +- releasing one finger while keeping two fingers down will keep the drag + and *not* switch to :ref:`twofinger_scrolling`. +- releasing two fingers while keeping one finger down will end the drag + (and thus release the button) and switch to normal pointer motion +- releasing all three fingers and putting three fingers back on the touchpad + immediately will keep the drag (i.e. behave as if the fingers were + never lifted) + + - if tapping is enabled: a three finger tap immediately after a three-finger + drag will *not* tap, the user needs to wait past the timeout to + three-finger tap + +- releasing all three fingers and putting one or two fingers back on + the touchpad will end the drag (and thus release the button) + and proceed with pointer motion or two-finger scrolling, if applicable + + - if tapping is enabled: a one or two finger tap immediately after a + three-finger drag will trigger a one or two finger tap. The user does + not have to wait past the drag release timeout diff --git a/doc/user/features.rst b/doc/user/features.rst index b9e45c0b..0553a056 100644 --- a/doc/user/features.rst +++ b/doc/user/features.rst @@ -22,6 +22,7 @@ to be useful. scrolling.rst t440-support.rst tapping.rst + drag-3fg.rst tablet-support.rst switches.rst touchpad-pressure.rst diff --git a/doc/user/meson.build b/doc/user/meson.build index 49ef2d50..bad2503a 100644 --- a/doc/user/meson.build +++ b/doc/user/meson.build @@ -145,6 +145,7 @@ src_rst = files( 'contributing.rst', 'device-configuration-via-udev.rst', 'device-quirks.rst', + 'drag-3fg.rst', 'faqs.rst', 'gestures.rst', 'incorrectly-enabled-hires.rst', diff --git a/src/evdev-mt-touchpad-gestures.c b/src/evdev-mt-touchpad-gestures.c index 6ec9f697..6b378205 100644 --- a/src/evdev-mt-touchpad-gestures.c +++ b/src/evdev-mt-touchpad-gestures.c @@ -55,6 +55,8 @@ enum gesture_event { GESTURE_EVENT_SCROLL_START, GESTURE_EVENT_SWIPE_START, GESTURE_EVENT_PINCH_START, + GESTURE_EVENT_3FG_DRAG_START, + GESTURE_EVENT_3FG_DRAG_RELEASE_TIMEOUT, }; /***************************************** @@ -81,6 +83,9 @@ gesture_state_to_str(enum tp_gesture_state state) CASE_RETURN_STRING(GESTURE_STATE_PINCH); CASE_RETURN_STRING(GESTURE_STATE_SWIPE_START); CASE_RETURN_STRING(GESTURE_STATE_SWIPE); + CASE_RETURN_STRING(GESTURE_STATE_3FG_DRAG_START); + CASE_RETURN_STRING(GESTURE_STATE_3FG_DRAG); + CASE_RETURN_STRING(GESTURE_STATE_3FG_DRAG_RELEASED); } return NULL; } @@ -101,6 +106,8 @@ gesture_event_to_str(enum gesture_event event) CASE_RETURN_STRING(GESTURE_EVENT_SCROLL_START); CASE_RETURN_STRING(GESTURE_EVENT_SWIPE_START); CASE_RETURN_STRING(GESTURE_EVENT_PINCH_START); + CASE_RETURN_STRING(GESTURE_EVENT_3FG_DRAG_START); + CASE_RETURN_STRING(GESTURE_EVENT_3FG_DRAG_RELEASE_TIMEOUT); } return NULL; } @@ -313,6 +320,16 @@ tp_gesture_init_pinch(struct tp_dispatch *tp) tp->gesture.prev_scale = 1.0; } +static inline void +tp_gesture_init_3fg_drag(struct tp_dispatch *tp, uint64_t time) +{ +} + +static inline void +tp_gesture_stop_3fg_drag(struct tp_dispatch *tp, uint64_t time) +{ +} + static void tp_gesture_set_scroll_buildup(struct tp_dispatch *tp) { @@ -540,8 +557,16 @@ tp_gesture_handle_event_on_state_none(struct tp_dispatch *tp, libinput_timer_cancel(&tp->gesture.hold_timer); break; case GESTURE_EVENT_FINGER_DETECTED: - tp_gesture_set_hold_timer(tp, time); - tp->gesture.state = GESTURE_STATE_UNKNOWN; + /* Note: this makes 3fg drag more responsive but disables + * 3fg pinch/hold. Those are niche enough to not worry about + * for now. + */ + if (!tp->tap.enabled && tp->drag_3fg.nfingers == tp->gesture.finger_count) { + tp->gesture.state = GESTURE_STATE_3FG_DRAG_START; + } else { + tp_gesture_set_hold_timer(tp, time); + tp->gesture.state = GESTURE_STATE_UNKNOWN; + } break; case GESTURE_EVENT_HOLD_TIMEOUT: case GESTURE_EVENT_TAP_TIMEOUT: @@ -556,6 +581,8 @@ tp_gesture_handle_event_on_state_none(struct tp_dispatch *tp, case GESTURE_EVENT_HOLD_AND_MOTION_START: case GESTURE_EVENT_SWIPE_START: case GESTURE_EVENT_PINCH_START: + case GESTURE_EVENT_3FG_DRAG_START: + case GESTURE_EVENT_3FG_DRAG_RELEASE_TIMEOUT: log_gesture_bug(tp, event); break; } @@ -600,8 +627,14 @@ tp_gesture_handle_event_on_state_unknown(struct tp_dispatch *tp, tp_gesture_init_pinch(tp); tp->gesture.state = GESTURE_STATE_PINCH_START; break; + case GESTURE_EVENT_3FG_DRAG_START: + libinput_timer_cancel(&tp->gesture.hold_timer); + tp_gesture_init_3fg_drag(tp, time); + tp->gesture.state = GESTURE_STATE_3FG_DRAG_START; + break; case GESTURE_EVENT_HOLD_AND_MOTION_START: case GESTURE_EVENT_FINGER_DETECTED: + case GESTURE_EVENT_3FG_DRAG_RELEASE_TIMEOUT: log_gesture_bug(tp, event); break; } @@ -649,9 +682,15 @@ tp_gesture_handle_event_on_state_hold(struct tp_dispatch *tp, tp_gesture_init_pinch(tp); tp->gesture.state = GESTURE_STATE_PINCH_START; break; + case GESTURE_EVENT_3FG_DRAG_START: + tp_gesture_cancel(tp, time); + tp_gesture_init_3fg_drag(tp, time); + tp->gesture.state = GESTURE_STATE_3FG_DRAG_START; + break; case GESTURE_EVENT_HOLD_TIMEOUT: case GESTURE_EVENT_TAP_TIMEOUT: case GESTURE_EVENT_FINGER_DETECTED: + case GESTURE_EVENT_3FG_DRAG_RELEASE_TIMEOUT: log_gesture_bug(tp, event); break; } @@ -689,6 +728,8 @@ tp_gesture_handle_event_on_state_hold_and_motion(struct tp_dispatch *tp, case GESTURE_EVENT_SCROLL_START: case GESTURE_EVENT_SWIPE_START: case GESTURE_EVENT_PINCH_START: + case GESTURE_EVENT_3FG_DRAG_START: + case GESTURE_EVENT_3FG_DRAG_RELEASE_TIMEOUT: log_gesture_bug(tp, event); break; } @@ -732,6 +773,8 @@ tp_gesture_handle_event_on_state_pointer_motion(struct tp_dispatch *tp, case GESTURE_EVENT_SCROLL_START: case GESTURE_EVENT_SWIPE_START: case GESTURE_EVENT_PINCH_START: + case GESTURE_EVENT_3FG_DRAG_START: + case GESTURE_EVENT_3FG_DRAG_RELEASE_TIMEOUT: log_gesture_bug(tp, event); break; } @@ -763,6 +806,8 @@ tp_gesture_handle_event_on_state_scroll_start(struct tp_dispatch *tp, case GESTURE_EVENT_POINTER_MOTION_START: case GESTURE_EVENT_SCROLL_START: case GESTURE_EVENT_SWIPE_START: + case GESTURE_EVENT_3FG_DRAG_START: + case GESTURE_EVENT_3FG_DRAG_RELEASE_TIMEOUT: log_gesture_bug(tp, event); break; } @@ -796,6 +841,8 @@ tp_gesture_handle_event_on_state_scroll(struct tp_dispatch *tp, case GESTURE_EVENT_POINTER_MOTION_START: case GESTURE_EVENT_SCROLL_START: case GESTURE_EVENT_SWIPE_START: + case GESTURE_EVENT_3FG_DRAG_START: + case GESTURE_EVENT_3FG_DRAG_RELEASE_TIMEOUT: log_gesture_bug(tp, event); break; } @@ -822,6 +869,8 @@ tp_gesture_handle_event_on_state_pinch_start(struct tp_dispatch *tp, case GESTURE_EVENT_SCROLL_START: case GESTURE_EVENT_SWIPE_START: case GESTURE_EVENT_PINCH_START: + case GESTURE_EVENT_3FG_DRAG_START: + case GESTURE_EVENT_3FG_DRAG_RELEASE_TIMEOUT: log_gesture_bug(tp, event); break; } @@ -858,6 +907,8 @@ tp_gesture_handle_event_on_state_pinch(struct tp_dispatch *tp, case GESTURE_EVENT_SCROLL_START: case GESTURE_EVENT_SWIPE_START: case GESTURE_EVENT_PINCH_START: + case GESTURE_EVENT_3FG_DRAG_START: + case GESTURE_EVENT_3FG_DRAG_RELEASE_TIMEOUT: log_gesture_bug(tp, event); break; } @@ -885,6 +936,8 @@ tp_gesture_handle_event_on_state_swipe_start(struct tp_dispatch *tp, case GESTURE_EVENT_SCROLL_START: case GESTURE_EVENT_SWIPE_START: case GESTURE_EVENT_PINCH_START: + case GESTURE_EVENT_3FG_DRAG_START: + case GESTURE_EVENT_3FG_DRAG_RELEASE_TIMEOUT: log_gesture_bug(tp, event); break; } @@ -921,11 +974,159 @@ tp_gesture_handle_event_on_state_swipe(struct tp_dispatch *tp, case GESTURE_EVENT_SCROLL_START: case GESTURE_EVENT_SWIPE_START: case GESTURE_EVENT_PINCH_START: + case GESTURE_EVENT_3FG_DRAG_START: + case GESTURE_EVENT_3FG_DRAG_RELEASE_TIMEOUT: log_gesture_bug(tp, event); break; } } +static void +tp_gesture_handle_event_on_state_3fg_drag_start(struct tp_dispatch *tp, + enum gesture_event event, + uint64_t time) +{ + switch(event) { + case GESTURE_EVENT_RESET: + case GESTURE_EVENT_END: + case GESTURE_EVENT_CANCEL: + libinput_timer_cancel(&tp->gesture.hold_timer); + tp->gesture.state = GESTURE_STATE_NONE; + break; + case GESTURE_EVENT_FINGER_SWITCH_TIMEOUT: + break; + case GESTURE_EVENT_HOLD_AND_MOTION_START: + case GESTURE_EVENT_FINGER_DETECTED: + case GESTURE_EVENT_TAP_TIMEOUT: + case GESTURE_EVENT_HOLD_TIMEOUT: + case GESTURE_EVENT_POINTER_MOTION_START: + case GESTURE_EVENT_SCROLL_START: + case GESTURE_EVENT_SWIPE_START: + case GESTURE_EVENT_PINCH_START: + case GESTURE_EVENT_3FG_DRAG_START: + case GESTURE_EVENT_3FG_DRAG_RELEASE_TIMEOUT: + log_gesture_bug(tp, event); + break; + } +} + +static void +tp_gesture_set_3fg_drag_timer(struct tp_dispatch *tp, uint64_t time) +{ + tp->gesture.drag_3fg_release_time = time; + libinput_timer_set(&tp->gesture.drag_3fg_timer, time + ms2us(700)); +} + +static void +tp_gesture_handle_event_on_state_3fg_drag(struct tp_dispatch *tp, + enum gesture_event event, + uint64_t time) +{ + switch(event) { + case GESTURE_EVENT_RESET: + log_gesture_bug(tp, event); + break; + case GESTURE_EVENT_CANCEL: + /* If the gesture is cancelled we release the button immediately */ + evdev_pointer_notify_button(tp->device, + tp->gesture.drag_3fg_release_time, + BTN_LEFT, + LIBINPUT_BUTTON_STATE_RELEASED); + tp->gesture.state = GESTURE_STATE_NONE; + break; + case GESTURE_EVENT_END: + /* If the gesture ends we start the timer so we + * can keep dragging */ + tp_gesture_set_3fg_drag_timer(tp, time); + tp->gesture.state = GESTURE_STATE_3FG_DRAG_RELEASED; + break; + case GESTURE_EVENT_FINGER_SWITCH_TIMEOUT: + if (tp->gesture.finger_count_pending < 2) { + evdev_pointer_notify_button(tp->device, + tp->gesture.drag_3fg_release_time, + BTN_LEFT, + LIBINPUT_BUTTON_STATE_RELEASED); + tp->gesture.state = GESTURE_STATE_NONE; + } + break; + case GESTURE_EVENT_TAP_TIMEOUT: + break; + case GESTURE_EVENT_HOLD_AND_MOTION_START: + case GESTURE_EVENT_FINGER_DETECTED: + case GESTURE_EVENT_HOLD_TIMEOUT: + case GESTURE_EVENT_POINTER_MOTION_START: + case GESTURE_EVENT_SCROLL_START: + case GESTURE_EVENT_SWIPE_START: + case GESTURE_EVENT_PINCH_START: + case GESTURE_EVENT_3FG_DRAG_START: + case GESTURE_EVENT_3FG_DRAG_RELEASE_TIMEOUT: + log_gesture_bug(tp, event); + break; + } +} + +static void +tp_gesture_handle_event_on_state_3fg_drag_released(struct tp_dispatch *tp, + enum gesture_event event, + uint64_t time) +{ + switch(event) { + case GESTURE_EVENT_RESET: + log_gesture_bug(tp, event); + break; + case GESTURE_EVENT_END: + case GESTURE_EVENT_CANCEL: + case GESTURE_EVENT_3FG_DRAG_RELEASE_TIMEOUT: + tp_gesture_stop_3fg_drag(tp, time); + libinput_timer_cancel(&tp->gesture.drag_3fg_timer); + libinput_timer_cancel(&tp->gesture.finger_count_switch_timer); + evdev_pointer_notify_button(tp->device, + tp->gesture.drag_3fg_release_time, + BTN_LEFT, + LIBINPUT_BUTTON_STATE_RELEASED); + tp->gesture.state = GESTURE_STATE_NONE; + break; + case GESTURE_EVENT_FINGER_SWITCH_TIMEOUT: + case GESTURE_EVENT_TAP_TIMEOUT: + if (tp->gesture.finger_count_pending == tp->drag_3fg.nfingers) { + libinput_timer_cancel(&tp->gesture.drag_3fg_timer); + tp->gesture.state = GESTURE_STATE_3FG_DRAG; + } + break; + case GESTURE_EVENT_FINGER_DETECTED: + break; + case GESTURE_EVENT_POINTER_MOTION_START: + tp_gesture_stop_3fg_drag(tp, time); + libinput_timer_cancel(&tp->gesture.drag_3fg_timer); + evdev_pointer_notify_button(tp->device, + tp->gesture.drag_3fg_release_time, + BTN_LEFT, + LIBINPUT_BUTTON_STATE_RELEASED); + tp->gesture.state = GESTURE_STATE_POINTER_MOTION; + break; + case GESTURE_EVENT_HOLD_AND_MOTION_START: + case GESTURE_EVENT_HOLD_TIMEOUT: + log_gesture_bug(tp, event); + break; + /* Anything that's detected as gesture in this state + * will be continue the current 3fg drag gesture */ + case GESTURE_EVENT_SCROLL_START: + libinput_timer_cancel(&tp->gesture.drag_3fg_timer); + evdev_pointer_notify_button(tp->device, + tp->gesture.drag_3fg_release_time, + BTN_LEFT, + LIBINPUT_BUTTON_STATE_RELEASED); + tp->gesture.state = GESTURE_STATE_SCROLL_START; + break; + case GESTURE_EVENT_SWIPE_START: + case GESTURE_EVENT_PINCH_START: + case GESTURE_EVENT_3FG_DRAG_START: + libinput_timer_cancel(&tp->gesture.drag_3fg_timer); + tp->gesture.state = GESTURE_STATE_3FG_DRAG; + break; + } +} + static void tp_gesture_handle_event(struct tp_dispatch *tp, enum gesture_event event, @@ -969,6 +1170,15 @@ tp_gesture_handle_event(struct tp_dispatch *tp, case GESTURE_STATE_SWIPE: tp_gesture_handle_event_on_state_swipe(tp, event, time); break; + case GESTURE_STATE_3FG_DRAG_START: + tp_gesture_handle_event_on_state_3fg_drag_start(tp, event, time); + break; + case GESTURE_STATE_3FG_DRAG: + tp_gesture_handle_event_on_state_3fg_drag(tp, event, time); + break; + case GESTURE_STATE_3FG_DRAG_RELEASED: + tp_gesture_handle_event_on_state_3fg_drag_released(tp, event, time); + break; } if (oldstate != tp->gesture.state) { @@ -1002,6 +1212,14 @@ tp_gesture_tap_timeout(struct tp_dispatch *tp, uint64_t time) tp_gesture_handle_event(tp, GESTURE_EVENT_TAP_TIMEOUT, time); } +static void +tp_gesture_3fg_drag_timeout(uint64_t now, void *data) +{ + struct tp_dispatch *tp = data; + + tp_gesture_handle_event(tp, GESTURE_EVENT_3FG_DRAG_RELEASE_TIMEOUT, now); +} + static void tp_gesture_detect_motion_gestures(struct tp_dispatch *tp, uint64_t time) { @@ -1044,10 +1262,13 @@ tp_gesture_detect_motion_gestures(struct tp_dispatch *tp, uint64_t time) } /* If we have more fingers than slots, we don't know where the - * fingers are. Default to swipe */ + * fingers are. Default to swipe/3fg drag */ if (tp->gesture.enabled && tp->gesture.finger_count > 2 && tp->gesture.finger_count > tp->num_slots) { - tp_gesture_handle_event(tp, GESTURE_EVENT_SWIPE_START, time); + if (tp->drag_3fg.nfingers == tp->gesture.finger_count) + tp_gesture_handle_event(tp, GESTURE_EVENT_3FG_DRAG_START, time); + else + tp_gesture_handle_event(tp, GESTURE_EVENT_SWIPE_START, time); return; } @@ -1073,6 +1294,8 @@ tp_gesture_detect_motion_gestures(struct tp_dispatch *tp, uint64_t time) time > (tp->gesture.initial_time + DEFAULT_GESTURE_SWIPE_TIMEOUT)) { if (tp->gesture.finger_count == 2) tp_gesture_handle_event(tp, GESTURE_EVENT_SCROLL_START, time); + else if (tp->drag_3fg.nfingers == tp->gesture.finger_count) + tp_gesture_handle_event(tp, GESTURE_EVENT_3FG_DRAG_START, time); else tp_gesture_handle_event(tp, GESTURE_EVENT_SWIPE_START, time); @@ -1154,6 +1377,11 @@ tp_gesture_detect_motion_gestures(struct tp_dispatch *tp, uint64_t time) return; } + if (tp->drag_3fg.nfingers == tp->gesture.finger_count) { + tp_gesture_handle_event(tp, GESTURE_EVENT_3FG_DRAG_START, time); + return; + } + if (tp->gesture.enabled) { tp_gesture_handle_event(tp, GESTURE_EVENT_SWIPE_START, time); return; @@ -1468,6 +1696,40 @@ tp_gesture_handle_state_pinch(struct tp_dispatch *tp, uint64_t time) tp->gesture.prev_scale = scale; } +static void +tp_gesture_handle_state_3fg_drag_start(struct tp_dispatch *tp, uint64_t time) +{ + evdev_pointer_notify_button(tp->device, + time, + BTN_LEFT, + LIBINPUT_BUTTON_STATE_PRESSED); + /* FIXME: immediately send a motion event? */ + tp->gesture.state = GESTURE_STATE_3FG_DRAG; +} + +static void +tp_gesture_handle_state_3fg_drag(struct tp_dispatch *tp, uint64_t time) +{ + if (!(tp->queued & TOUCHPAD_EVENT_MOTION)) + return; + + struct device_float_coords raw = tp_get_average_touches_delta(tp); + struct normalized_coords delta = tp_filter_motion(tp, &raw, time); + + if (!normalized_is_zero(delta) || !device_float_is_zero(raw)) { + if (tp->queued & TOUCHPAD_EVENT_MOTION) + tp_gesture_post_pointer_motion(tp, time); + } +} + +static void +tp_gesture_handle_state_3fg_drag_released(struct tp_dispatch *tp, + uint64_t time, + bool ignore_motion) +{ + tp_gesture_detect_motion_gestures(tp, time); +} + static void tp_gesture_handle_state(struct tp_dispatch *tp, uint64_t time, bool ignore_motion) @@ -1529,6 +1791,18 @@ tp_gesture_handle_state(struct tp_dispatch *tp, uint64_t time, tp_gesture_handle_state_pinch_start(tp, time); REMEMBER_TRANSITION(transition_state, tp->gesture.state); } + if (tp->gesture.state == GESTURE_STATE_3FG_DRAG) { + tp_gesture_handle_state_3fg_drag(tp, time); + REMEMBER_TRANSITION(transition_state, tp->gesture.state); + } + if (tp->gesture.state == GESTURE_STATE_3FG_DRAG_START) { + tp_gesture_handle_state_3fg_drag_start(tp, time); + REMEMBER_TRANSITION(transition_state, tp->gesture.state); + } + if (tp->gesture.state == GESTURE_STATE_3FG_DRAG_RELEASED) { + tp_gesture_handle_state_3fg_drag_released(tp, time, ignore_motion); + REMEMBER_TRANSITION(transition_state, tp->gesture.state); + } #undef REMEMBER_TRANSITION @@ -1628,6 +1902,7 @@ tp_gesture_end(struct tp_dispatch *tp, uint64_t time, enum gesture_cancelled can case GESTURE_STATE_SCROLL_START: case GESTURE_STATE_PINCH_START: case GESTURE_STATE_SWIPE_START: + case GESTURE_STATE_3FG_DRAG_START: tp_gesture_handle_event(tp, GESTURE_EVENT_RESET, time); break; case GESTURE_STATE_HOLD: @@ -1636,6 +1911,8 @@ tp_gesture_end(struct tp_dispatch *tp, uint64_t time, enum gesture_cancelled can case GESTURE_STATE_SCROLL: case GESTURE_STATE_PINCH: case GESTURE_STATE_SWIPE: + case GESTURE_STATE_3FG_DRAG: + case GESTURE_STATE_3FG_DRAG_RELEASED: switch (cancelled) { case CANCEL_GESTURE: tp_gesture_handle_event(tp, GESTURE_EVENT_CANCEL, time); @@ -1657,12 +1934,14 @@ tp_gesture_cancel(struct tp_dispatch *tp, uint64_t time) void tp_gesture_cancel_motion_gestures(struct tp_dispatch *tp, uint64_t time) { + switch (tp->gesture.state) { case GESTURE_STATE_NONE: case GESTURE_STATE_UNKNOWN: case GESTURE_STATE_SCROLL_START: case GESTURE_STATE_PINCH_START: case GESTURE_STATE_SWIPE_START: + case GESTURE_STATE_3FG_DRAG_START: break; case GESTURE_STATE_HOLD: break; @@ -1671,8 +1950,13 @@ tp_gesture_cancel_motion_gestures(struct tp_dispatch *tp, uint64_t time) case GESTURE_STATE_SCROLL: case GESTURE_STATE_PINCH: case GESTURE_STATE_SWIPE: + evdev_log_debug(tp->device, "Cancelling motion gestures\n"); tp_gesture_cancel(tp, time); break; + case GESTURE_STATE_3FG_DRAG: + break; + case GESTURE_STATE_3FG_DRAG_RELEASED: + break; } } @@ -1711,6 +1995,9 @@ tp_gesture_debounce_finger_changes(struct tp_dispatch *tp) case GESTURE_STATE_SCROLL: case GESTURE_STATE_PINCH: case GESTURE_STATE_SWIPE: + case GESTURE_STATE_3FG_DRAG_START: + case GESTURE_STATE_3FG_DRAG_RELEASED: + case GESTURE_STATE_3FG_DRAG: return true; } @@ -1751,7 +2038,7 @@ tp_gesture_update_finger_state(struct tp_dispatch *tp, uint64_t time) } else if (active_touches != tp->gesture.finger_count_pending) { tp->gesture.finger_count_pending = active_touches; libinput_timer_set(&tp->gesture.finger_count_switch_timer, - time + DEFAULT_GESTURE_SWITCH_TIMEOUT); + time + DEFAULT_GESTURE_SWITCH_TIMEOUT); } } else { tp->gesture.finger_count_pending = 0; @@ -1800,6 +2087,94 @@ tp_gesture_get_hold_default(struct libinput_device *device) LIBINPUT_CONFIG_HOLD_DISABLED; } +static int +tp_3fg_drag_count(struct libinput_device *device) +{ + struct evdev_dispatch *dispatch = evdev_device(device)->dispatch; + struct tp_dispatch *tp = tp_dispatch(dispatch); + + /* If we can't to gestures we can't do 3fg drag */ + if (!tp_gesture_are_gestures_enabled(tp)) + return 0; + + /* For now return the number of MT slots until we need to figure out + * if we can implement this on a 2-finger BTN_TOOL_TRIPLETAP device */ + return tp->num_slots; +} + +static enum libinput_config_status +tp_3fg_drag_set_enabled(struct libinput_device *device, + enum libinput_config_3fg_drag_state enabled) +{ + struct evdev_dispatch *dispatch = evdev_device(device)->dispatch; + struct tp_dispatch *tp = tp_dispatch(dispatch); + + if (tp_3fg_drag_count(device) < 3) + return LIBINPUT_CONFIG_STATUS_UNSUPPORTED; + + switch (enabled) { + case LIBINPUT_CONFIG_3FG_DRAG_DISABLED: + tp->drag_3fg.want_nfingers = 0; + break; + case LIBINPUT_CONFIG_3FG_DRAG_ENABLED_3FG: + tp->drag_3fg.want_nfingers = 3; + break; + case LIBINPUT_CONFIG_3FG_DRAG_ENABLED_4FG: + tp->drag_3fg.want_nfingers = 4; + break; + } + + tp_3fg_drag_apply_config(evdev_device(device)); + + return LIBINPUT_CONFIG_STATUS_SUCCESS; +} + +static enum libinput_config_3fg_drag_state +tp_3fg_drag_get_enabled(struct libinput_device *device) +{ + struct evdev_dispatch *dispatch = evdev_device(device)->dispatch; + struct tp_dispatch *tp = tp_dispatch(dispatch); + + switch (tp->drag_3fg.want_nfingers) { + case 3: + return LIBINPUT_CONFIG_3FG_DRAG_ENABLED_3FG; + case 4: + return LIBINPUT_CONFIG_3FG_DRAG_ENABLED_4FG; + } + return LIBINPUT_CONFIG_3FG_DRAG_DISABLED; +} + +static enum libinput_config_3fg_drag_state +tp_3fg_drag_default(struct tp_dispatch *tp) +{ + return LIBINPUT_CONFIG_3FG_DRAG_DISABLED; +} + +static enum libinput_config_3fg_drag_state +tp_3fg_drag_get_default_enabled(struct libinput_device *device) +{ + struct evdev_dispatch *dispatch = evdev_device(device)->dispatch; + struct tp_dispatch *tp = tp_dispatch(dispatch); + + return tp_3fg_drag_default(tp); +} + +void +tp_3fg_drag_apply_config(struct evdev_device *device) +{ + struct tp_dispatch *tp = (struct tp_dispatch *)device->dispatch; + + if (tp->drag_3fg.want_nfingers == tp->drag_3fg.nfingers) + return; + + if (tp->nfingers_down) + return; + + tp->drag_3fg.nfingers = tp->drag_3fg.want_nfingers; + + evdev_log_debug(device, "touchpad-3fg-drag: drag is now for %zd fingers\n", tp->drag_3fg.nfingers); +} + void tp_init_gesture(struct tp_dispatch *tp) { @@ -1810,6 +2185,25 @@ tp_init_gesture(struct tp_dispatch *tp) tp->gesture.config.get_hold_default = tp_gesture_get_hold_default; tp->device->base.config.gesture = &tp->gesture.config; + tp->drag_3fg.config.count = tp_3fg_drag_count; + tp->drag_3fg.config.set_enabled = tp_3fg_drag_set_enabled; + tp->drag_3fg.config.get_enabled = tp_3fg_drag_get_enabled; + tp->drag_3fg.config.get_default = tp_3fg_drag_get_default_enabled; + tp->device->base.config.drag_3fg = &tp->drag_3fg.config; + + switch (tp_3fg_drag_default(tp)) { + case LIBINPUT_CONFIG_3FG_DRAG_DISABLED: + tp->drag_3fg.nfingers = 0; + break; + case LIBINPUT_CONFIG_3FG_DRAG_ENABLED_3FG: + tp->drag_3fg.nfingers = 3; + break; + case LIBINPUT_CONFIG_3FG_DRAG_ENABLED_4FG: + tp->drag_3fg.nfingers = 4; + break; + } + tp->drag_3fg.want_nfingers = tp->drag_3fg.nfingers; + /* two-finger scrolling is always enabled, this flag just * decides whether we detect pinch. semi-mt devices are too * unreliable to do pinch gestures. */ @@ -1835,6 +2229,14 @@ tp_init_gesture(struct tp_dispatch *tp) tp_libinput_context(tp), timer_name, tp_gesture_hold_timeout, tp); + snprintf(timer_name, + sizeof(timer_name), + "%s drag_3fg", + evdev_device_get_sysname(tp->device)); + libinput_timer_init(&tp->gesture.drag_3fg_timer, + tp_libinput_context(tp), + timer_name, + tp_gesture_3fg_drag_timeout, tp); } void @@ -1842,4 +2244,5 @@ tp_remove_gesture(struct tp_dispatch *tp) { libinput_timer_cancel(&tp->gesture.finger_count_switch_timer); libinput_timer_cancel(&tp->gesture.hold_timer); + libinput_timer_cancel(&tp->gesture.drag_3fg_timer); } diff --git a/src/evdev-mt-touchpad.c b/src/evdev-mt-touchpad.c index d92b9eb0..909e8f0c 100644 --- a/src/evdev-mt-touchpad.c +++ b/src/evdev-mt-touchpad.c @@ -1910,6 +1910,7 @@ tp_handle_state(struct tp_dispatch *tp, tp_clickpad_middlebutton_apply_config(tp->device); tp_apply_rotation(tp->device); + tp_3fg_drag_apply_config(tp->device); } LIBINPUT_UNUSED @@ -2023,6 +2024,7 @@ tp_interface_destroy(struct evdev_dispatch *dispatch) libinput_timer_destroy(&tp->tap.timer); libinput_timer_destroy(&tp->gesture.finger_count_switch_timer); libinput_timer_destroy(&tp->gesture.hold_timer); + libinput_timer_destroy(&tp->gesture.drag_3fg_timer); free(tp->touches); free(tp); } diff --git a/src/evdev-mt-touchpad.h b/src/evdev-mt-touchpad.h index 48d1b113..34dd576d 100644 --- a/src/evdev-mt-touchpad.h +++ b/src/evdev-mt-touchpad.h @@ -166,6 +166,9 @@ enum tp_gesture_state { GESTURE_STATE_PINCH, GESTURE_STATE_SWIPE_START, GESTURE_STATE_SWIPE, + GESTURE_STATE_3FG_DRAG_START, + GESTURE_STATE_3FG_DRAG, + GESTURE_STATE_3FG_DRAG_RELEASED, }; enum tp_thumb_state { @@ -365,6 +368,9 @@ struct tp_dispatch { struct device_float_coords center; struct libinput_timer hold_timer; bool hold_enabled; + + struct libinput_timer drag_3fg_timer; + uint64_t drag_3fg_release_time; } gesture; struct { @@ -442,6 +448,12 @@ struct tp_dispatch { unsigned int nfingers_down; /* number of fingers down for tapping (excl. thumb/palm) */ } tap; + struct { + struct libinput_device_config_3fg_drag config; + size_t nfingers; + size_t want_nfingers; + } drag_3fg; + struct { struct libinput_device_config_dwtp config; bool dwtp_enabled; @@ -775,4 +787,7 @@ tp_init_thumb(struct tp_dispatch *tp); struct tp_touch* tp_thumb_get_touch(struct tp_dispatch *tp); +void +tp_3fg_drag_apply_config(struct evdev_device *device); + #endif diff --git a/src/libinput-private.h b/src/libinput-private.h index baca85aa..266a61e6 100644 --- a/src/libinput-private.h +++ b/src/libinput-private.h @@ -218,6 +218,15 @@ struct libinput_device_config_tap { enum libinput_config_drag_lock_state (*get_default_draglock_enabled)(struct libinput_device *device); }; +struct libinput_device_config_3fg_drag { + int (*count)(struct libinput_device *device); + enum libinput_config_status (*set_enabled)(struct libinput_device *device, + enum libinput_config_3fg_drag_state enable); + enum libinput_config_3fg_drag_state (*get_enabled)(struct libinput_device *device); + enum libinput_config_3fg_drag_state (*get_default)(struct libinput_device *device); + +}; + struct libinput_device_config_calibration { int (*has_matrix)(struct libinput_device *device); enum libinput_config_status (*set_matrix)(struct libinput_device *device, @@ -411,6 +420,7 @@ struct libinput_device_config { struct libinput_device_config_dwtp *dwtp; struct libinput_device_config_rotation *rotation; struct libinput_device_config_gesture *gesture; + struct libinput_device_config_3fg_drag *drag_3fg; }; struct libinput_device_group { diff --git a/src/libinput.c b/src/libinput.c index 1b5da805..6efa896f 100644 --- a/src/libinput.c +++ b/src/libinput.c @@ -4118,6 +4118,48 @@ libinput_device_config_tap_get_default_drag_lock_enabled(struct libinput_device return device->config.tap->get_default_draglock_enabled(device); } +LIBINPUT_EXPORT int +libinput_device_config_3fg_drag_get_finger_count(struct libinput_device *device) +{ + return device->config.drag_3fg ? device->config.drag_3fg->count(device) : 0; +} + +LIBINPUT_EXPORT enum libinput_config_status +libinput_device_config_3fg_drag_set_enabled(struct libinput_device *device, + enum libinput_config_3fg_drag_state enable) +{ + if (libinput_device_config_3fg_drag_get_finger_count(device) < 3) + return LIBINPUT_CONFIG_STATUS_UNSUPPORTED; + + switch (enable) { + case LIBINPUT_CONFIG_3FG_DRAG_DISABLED: + case LIBINPUT_CONFIG_3FG_DRAG_ENABLED_3FG: + case LIBINPUT_CONFIG_3FG_DRAG_ENABLED_4FG: + return device->config.drag_3fg->set_enabled(device, enable); + break; + } + + return LIBINPUT_CONFIG_STATUS_INVALID; +} + +LIBINPUT_EXPORT enum libinput_config_3fg_drag_state +libinput_device_config_3fg_drag_get_enabled(struct libinput_device *device) +{ + if (libinput_device_config_3fg_drag_get_finger_count(device) < 3) + return LIBINPUT_CONFIG_3FG_DRAG_DISABLED; + + return device->config.drag_3fg->get_enabled(device); +} + +LIBINPUT_EXPORT enum libinput_config_3fg_drag_state +libinput_device_config_3fg_drag_get_default_enabled(struct libinput_device *device) +{ + if (libinput_device_config_3fg_drag_get_finger_count(device) < 3) + return LIBINPUT_CONFIG_3FG_DRAG_DISABLED; + + return device->config.drag_3fg->get_default(device); +} + LIBINPUT_EXPORT int libinput_device_config_calibration_has_matrix(struct libinput_device *device) { diff --git a/src/libinput.h b/src/libinput.h index 945bf6f9..9a0ad73e 100644 --- a/src/libinput.h +++ b/src/libinput.h @@ -5024,6 +5024,107 @@ libinput_device_config_tap_get_drag_lock_enabled(struct libinput_device *device) enum libinput_config_drag_lock_state libinput_device_config_tap_get_default_drag_lock_enabled(struct libinput_device *device); +/** + * @ingroup config + * + * A config status to distinguish or set 3-finger dragging on a device. + * + * @since 1.27 + */ +enum libinput_config_3fg_drag_state { + /** + * Drag is to be disabled, or is + * currently disabled. + */ + LIBINPUT_CONFIG_3FG_DRAG_DISABLED, + /** + * Drag is to be enabled for 3 fingers, or is + * currently enabled + */ + LIBINPUT_CONFIG_3FG_DRAG_ENABLED_3FG, + /** + * Drag is to be enabled for 4 fingers, or is + * currently enabled + */ + LIBINPUT_CONFIG_3FG_DRAG_ENABLED_4FG, +}; + +/** + * @ingroup config + * + * Returns the maximum number of fingers available for 3-finger dragging. + * + * @param device The device to check + * + * @see libinput_device_config_3fg_drag_set_enabled + * @see libinput_device_config_3fg_drag_get_enabled + * @see libinput_device_config_3fg_drag_get_default_enabled + * + * @since 1.27 + */ +int +libinput_device_config_3fg_drag_get_finger_count(struct libinput_device *device); + +/** + * @ingroup config + * + * Enable or disable 3-finger drag on this device. When enabled, three fingers + * down will result in a button down event, subsequent finger motion triggers + * a drag. The button is released shortly after all fingers are logically up. + * See the libinput documentation for more details. + * + * @param device The device to configure + * @param enable @ref LIBINPUT_CONFIG_DRAG_ENABLED to enable, @ref + * LIBINPUT_CONFIG_DRAG_DISABLED to disable 3-finger drag + * + * @see libinput_device_config_3fg_drag_is_available + * @see libinput_device_config_3fg_drag_get_enabled + * @see libinput_device_config_3fg_drag_get_default_enabled + * + * @since 1.27 + */ +enum libinput_config_status +libinput_device_config_3fg_drag_set_enabled(struct libinput_device *device, + enum libinput_config_3fg_drag_state enable); + +/** + * @ingroup config + * + * Return whether 3-finger drag is enabled or disabled on this device. + * + * @param device The device to check + * @retval LIBINPUT_CONFIG_DRAG_ENABLED if 3-finger drag is enabled + * @retval LIBINPUT_CONFIG_DRAG_DISABLED if 3-finger drag is + * disabled + * + * @see libinput_device_config_3fg_drag_is_available + * @see libinput_device_config_3fg_drag_set_enabled + * @see libinput_device_config_3fg_drag_get_default_enabled + * + * @since 1.27 + */ +enum libinput_config_3fg_drag_state +libinput_device_config_3fg_drag_get_enabled(struct libinput_device *device); + +/** + * @ingroup config + * + * Return whether 3-finger drag is enabled or disabled by default on this device. + * + * @param device The device to check + * @retval LIBINPUT_CONFIG_DRAG_ENABLED if 3-finger drag is enabled + * @retval LIBINPUT_CONFIG_DRAG_DISABLED if 3-finger drag is + * disabled + * + * @see libinput_device_config_3fg_drag_is_available + * @see libinput_device_config_3fg_drag_set_enabled + * @see libinput_device_config_3fg_drag_get_enabled + * + * @since 1.27 + */ +enum libinput_config_3fg_drag_state +libinput_device_config_3fg_drag_get_default_enabled(struct libinput_device *device); + /** * @ingroup config * diff --git a/src/libinput.sym b/src/libinput.sym index 2f3a7eee..68c8651f 100644 --- a/src/libinput.sym +++ b/src/libinput.sym @@ -357,3 +357,10 @@ LIBINPUT_1.27 { libinput_device_config_area_get_rectangle; libinput_device_config_area_get_default_rectangle; } LIBINPUT_1.26; + +LIBINPUT_1.28 { + libinput_device_config_3fg_drag_get_finger_count; + libinput_device_config_3fg_drag_set_enabled; + libinput_device_config_3fg_drag_get_enabled; + libinput_device_config_3fg_drag_get_default_enabled; +} LIBINPUT_1.27; diff --git a/test/litest.c b/test/litest.c index a3c1868c..32e18d4f 100644 --- a/test/litest.c +++ b/test/litest.c @@ -4914,6 +4914,12 @@ litest_timeout_hysteresis(void) msleep(90); } +void +litest_timeout_3fg_drag(void) +{ + msleep(800); +} + void litest_push_event_frame(struct litest_device *dev) { diff --git a/test/litest.h b/test/litest.h index 380c9e44..5f4a5f09 100644 --- a/test/litest.h +++ b/test/litest.h @@ -1236,6 +1236,9 @@ litest_timeout_touch_arbitration(void); void litest_timeout_hysteresis(void); +void +litest_timeout_3fg_drag(void); + void litest_push_event_frame(struct litest_device *dev); @@ -1268,6 +1271,29 @@ litest_semi_mt_touch_up(struct litest_device *d, struct litest_semi_mt *semi_mt, unsigned int slot); +static inline +void litest_enable_3fg_drag(struct libinput_device *device, + unsigned int nfingers) +{ + enum libinput_config_3fg_drag_state enabled; + + switch (nfingers) { + case 3: + enabled = LIBINPUT_CONFIG_3FG_DRAG_ENABLED_3FG; + break; + case 4: + enabled = LIBINPUT_CONFIG_3FG_DRAG_ENABLED_4FG; + break; + default: + litest_abort_msg("Invalid finger count"); + break; + } + + enum libinput_config_status status = + libinput_device_config_3fg_drag_set_enabled(device, enabled); + litest_assert_enum_eq(status, LIBINPUT_CONFIG_STATUS_SUCCESS); +} + static inline void litest_enable_tap(struct libinput_device *device) { diff --git a/test/test-gestures.c b/test/test-gestures.c index e50a946a..ff775366 100644 --- a/test/test-gestures.c +++ b/test/test-gestures.c @@ -1782,11 +1782,491 @@ START_TEST(gestures_hold_and_motion_after_timeout) } END_TEST +START_TEST(gestures_3fg_drag) +{ + struct litest_device *dev = litest_current_device(); + struct libinput *li = dev->libinput; + + uint32_t finger_count; + bool tap_enabled; + litest_test_param_fetch(test_env->params, + "fingers", 'u', &finger_count, + "tap-enabled", 'b', &tap_enabled); + + if (litest_slot_count(dev) < 3) + return LITEST_NOT_APPLICABLE; + + if (libinput_device_config_3fg_drag_get_finger_count(dev->libinput_device) < (int)finger_count) + return LITEST_NOT_APPLICABLE; + + litest_enable_3fg_drag(dev->libinput_device, finger_count); + if (tap_enabled) + litest_enable_tap(dev->libinput_device); + else + litest_disable_tap(dev->libinput_device); + + litest_drain_events(li); + + double y = 30.0; + for (uint32_t i = 0; i < finger_count; i++) + litest_touch_down(dev, i, 10 + i, y); + + litest_dispatch(li); + + litest_drain_events_of_type(li, LIBINPUT_EVENT_GESTURE_HOLD_BEGIN, LIBINPUT_EVENT_GESTURE_HOLD_END); + + if (tap_enabled) { + litest_checkpoint("Expecting no immediate button press as tapping is enabled"); + litest_assert_empty_queue(li); + } else { + litest_checkpoint("Expecting immediate button press as tapping is disabled"); + litest_assert_button_event(li, BTN_LEFT, LIBINPUT_BUTTON_STATE_PRESSED); + } + + while (y < 60.0) { + y += 2; + for (uint32_t i = 0; i < finger_count; i++) + litest_touch_move(dev, i, 10 + i, y); + litest_dispatch(li); + } + + if (tap_enabled) { + litest_checkpoint("Expecting late button press as tapping is enabled"); + litest_assert_button_event(li, BTN_LEFT, LIBINPUT_BUTTON_STATE_PRESSED); + } + litest_assert_only_typed_events(li, LIBINPUT_EVENT_POINTER_MOTION); + + for (uint32_t i = 0; i < finger_count; i++) + litest_touch_up(dev, i); + + litest_dispatch(li); + litest_assert_empty_queue(li); + + litest_timeout_3fg_drag(); + litest_dispatch(li); + + litest_assert_button_event(li, BTN_LEFT, LIBINPUT_BUTTON_STATE_RELEASED); +} +END_TEST + +START_TEST(gestures_3fg_drag_lock_resume_3fg_motion) +{ + struct litest_device *dev = litest_current_device(); + struct libinput *li = dev->libinput; + + uint32_t finger_count; + bool tap_enabled; + bool wait_for_timeout; + litest_test_param_fetch(test_env->params, + "fingers", 'u', &finger_count, + "tap-enabled", 'b', &tap_enabled, + "wait", 'b', &wait_for_timeout); + + if (litest_slot_count(dev) < 3) + return LITEST_NOT_APPLICABLE; + + if (libinput_device_config_3fg_drag_get_finger_count(dev->libinput_device) < (int)finger_count) + return LITEST_NOT_APPLICABLE; + + litest_enable_3fg_drag(dev->libinput_device, finger_count); + if (tap_enabled) + litest_enable_tap(dev->libinput_device); + else + litest_disable_tap(dev->libinput_device); + + litest_drain_events(li); + + litest_checkpoint("Putting three fingers down + movement)"); + double y = 30.0; + for (uint32_t i = 0; i < finger_count; i++) + litest_touch_down(dev, i, 10 + i, y); + + litest_dispatch(li); + + while (y < 60.0) { + y += 2; + for (uint32_t i = 0; i < finger_count; i++) + litest_touch_move(dev, i, 10 + i, y); + litest_dispatch(li); + } + litest_assert_button_event(li, BTN_LEFT, LIBINPUT_BUTTON_STATE_PRESSED); + litest_assert_only_typed_events(li, LIBINPUT_EVENT_POINTER_MOTION); + + litest_checkpoint("Releasing all fingers"); + for (uint32_t i = 0; i < finger_count; i++) + litest_touch_up(dev, i); + + litest_dispatch(li); + litest_assert_empty_queue(li); + + litest_checkpoint("Putting three fingers down (no movement)"); + y = 30.0; + for (uint32_t i = 0; i < finger_count; i++) + litest_touch_down(dev, i, 10 + i, y); + litest_dispatch(li); + litest_assert_empty_queue(li); + + litest_dispatch(li); + + litest_checkpoint("Waiting past finger switch timeout"); + litest_timeout_finger_switch(); + litest_dispatch(li); + + if (wait_for_timeout) { + litest_checkpoint("Waiting past tap/3fg drag timeout"); + litest_timeout_3fg_drag(); + litest_dispatch(li); + litest_assert_empty_queue(li); + } + + litest_checkpoint("Moving three fingers"); + while (y < 60.0) { + y += 2; + for (uint32_t i = 0; i < finger_count; i++) + litest_touch_move(dev, i, 10 + i, y); + litest_dispatch(li); + } + litest_assert_only_typed_events(li, LIBINPUT_EVENT_POINTER_MOTION); + + litest_checkpoint("Releasing three fingers"); + for (uint32_t i = 0; i < finger_count; i++) + litest_touch_up(dev, i); + litest_dispatch(li); + litest_assert_empty_queue(li); + + litest_timeout_3fg_drag(); + litest_dispatch(li); + + litest_assert_button_event(li, BTN_LEFT, LIBINPUT_BUTTON_STATE_RELEASED); +} +END_TEST + +START_TEST(gestures_3fg_drag_lock_resume_3fg_release_no_motion) +{ + struct litest_device *dev = litest_current_device(); + struct libinput *li = dev->libinput; + + uint32_t finger_count; + bool tap_enabled; + bool wait_for_timeout; + litest_test_param_fetch(test_env->params, + "fingers", 'u', &finger_count, + "tap-enabled", 'b', &tap_enabled, + "wait", 'b', &wait_for_timeout); + + /* tap-enabled for 4fg finger count doesn't make a difference */ + bool expect_tap = finger_count <= 3 && tap_enabled && !wait_for_timeout; + + if (litest_slot_count(dev) < 3) + return LITEST_NOT_APPLICABLE; + + if (libinput_device_config_3fg_drag_get_finger_count(dev->libinput_device) < (int)finger_count) + return LITEST_NOT_APPLICABLE; + + litest_enable_3fg_drag(dev->libinput_device, finger_count); + + if (tap_enabled) + litest_enable_tap(dev->libinput_device); + else + litest_disable_tap(dev->libinput_device); + + litest_drain_events(li); + + litest_checkpoint("Putting 3 fingers down with motion for drag"); + double y = 30.0; + for (uint32_t i = 0; i < finger_count; i++) + litest_touch_down(dev, i, 10 + i, y); + + litest_dispatch(li); + while (y < 60.0) { + y += 2; + for (uint32_t i = 0; i < finger_count; i++) + litest_touch_move(dev, i, 10 + i, y); + litest_dispatch(li); + } + litest_assert_button_event(li, BTN_LEFT, LIBINPUT_BUTTON_STATE_PRESSED); + litest_assert_only_typed_events(li, LIBINPUT_EVENT_POINTER_MOTION); + + for (uint32_t i = 0; i < finger_count; i++) + litest_touch_up(dev, i); + + litest_dispatch(li); + litest_assert_empty_queue(li); + + litest_checkpoint("Putting 3 fingers down again (no motion)"); + y = 30.0; + for (uint32_t i = 0; i < finger_count; i++) + litest_touch_down(dev, i, 10 + i, y); + litest_dispatch(li); + litest_assert_empty_queue(li); + + litest_checkpoint("Waiting past finger switch timeout"); + litest_timeout_finger_switch(); + litest_dispatch(li); + + if (wait_for_timeout) { + litest_checkpoint("Waiting past tap/3fg drag timeout"); + litest_timeout_3fg_drag(); + litest_dispatch(li); + litest_assert_empty_queue(li); + } + + litest_checkpoint("Releasing three fingers"); + for (uint32_t i = 0; i < finger_count; i++) + litest_touch_up(dev, i); + litest_dispatch(li); + + if (expect_tap) { + /* If we're not waiting and tapping is enabled, this is + * the equivalent of a 3fg tap within the drag timeout */ + litest_checkpoint("Expecting 3fg drag release"); + litest_assert_button_event(li, BTN_LEFT, LIBINPUT_BUTTON_STATE_RELEASED); + litest_checkpoint("Expecting 3fg tap"); + litest_assert_button_event(li, BTN_MIDDLE, LIBINPUT_BUTTON_STATE_PRESSED); + litest_assert_button_event(li, BTN_MIDDLE, LIBINPUT_BUTTON_STATE_RELEASED); + } + + litest_assert_empty_queue(li); + litest_timeout_3fg_drag(); + litest_dispatch(li); + + if (!expect_tap) + litest_assert_button_event(li, BTN_LEFT, LIBINPUT_BUTTON_STATE_RELEASED); + + litest_assert_empty_queue(li); +} +END_TEST + +START_TEST(gestures_3fg_drag_lock_resume_1fg_motion) +{ + struct litest_device *dev = litest_current_device(); + struct libinput *li = dev->libinput; + + uint32_t finger_count; + bool tap_enabled; + litest_test_param_fetch(test_env->params, + "fingers", 'u', &finger_count, + "tap-enabled", 'b', &tap_enabled); + + if (litest_slot_count(dev) < 3) + return LITEST_NOT_APPLICABLE; + + if (libinput_device_config_3fg_drag_get_finger_count(dev->libinput_device) < (int)finger_count) + return LITEST_NOT_APPLICABLE; + + litest_enable_3fg_drag(dev->libinput_device, finger_count); + + if (tap_enabled) + litest_enable_tap(dev->libinput_device); + else + litest_disable_tap(dev->libinput_device); + + litest_drain_events(li); + + litest_checkpoint("Putting 3 fingers down + motion to trigger 3fg drag"); + double y = 30.0; + for (uint32_t i = 0; i < finger_count; i++) + litest_touch_down(dev, i, 10 + i, y); + + while (y < 60.0) { + y += 2; + for (uint32_t i = 0; i < finger_count; i++) + litest_touch_move(dev, i, 10 + i, y); + litest_dispatch(li); + } + litest_assert_button_event(li, BTN_LEFT, LIBINPUT_BUTTON_STATE_PRESSED); + litest_assert_only_typed_events(li, LIBINPUT_EVENT_POINTER_MOTION); + + litest_checkpoint("Releasing 3 fingers"); + for (uint32_t i = 0; i < finger_count; i++) + litest_touch_up(dev, i); + + litest_dispatch(li); + litest_assert_empty_queue(li); + + litest_checkpoint("Putting 1 finger down and moving it"); + /* fingers are up, now let's put one finger down and move it */ + y = 30.0; + litest_touch_down(dev, 0, 10, y); + litest_dispatch(li); + litest_assert_empty_queue(li); + + /* We need to wait until the gesture code accepts this is one finger only */ + litest_timeout_finger_switch(); + litest_dispatch(li); + + while (y < 60.0) { + y += 2; + litest_touch_move(dev, 0, 10, y); + litest_dispatch(li); + } + + litest_checkpoint("Expecting drag button release and motion"); + litest_assert_button_event(li, BTN_LEFT, LIBINPUT_BUTTON_STATE_RELEASED); + litest_assert_only_typed_events(li, LIBINPUT_EVENT_POINTER_MOTION); + + litest_touch_up(dev, 0); + litest_dispatch(li); + litest_assert_empty_queue(li); + + litest_timeout_3fg_drag(); + litest_dispatch(li); +} +END_TEST + +START_TEST(gestures_3fg_drag_lock_resume_2fg_scroll) +{ + struct litest_device *dev = litest_current_device(); + struct libinput *li = dev->libinput; + + uint32_t finger_count; + bool tap_enabled; + litest_test_param_fetch(test_env->params, + "fingers", 'u', &finger_count, + "tap-enabled", 'b', &tap_enabled); + + if (litest_slot_count(dev) < 3) + return LITEST_NOT_APPLICABLE; + + if (libinput_device_config_3fg_drag_get_finger_count(dev->libinput_device) < (int)finger_count) + return LITEST_NOT_APPLICABLE; + + litest_enable_3fg_drag(dev->libinput_device, finger_count); + + if (tap_enabled) + litest_enable_tap(dev->libinput_device); + else + litest_disable_tap(dev->libinput_device); + + litest_drain_events(li); + + litest_checkpoint("Putting 3 fingers down + motion to trigger 3fg drag"); + double y = 30.0; + for (uint32_t i = 0; i < finger_count; i++) + litest_touch_down(dev, i, 10 + i, y); + + while (y < 60.0) { + y += 2; + for (uint32_t i = 0; i < finger_count; i++) + litest_touch_move(dev, i, 10 + i, y); + litest_dispatch(li); + } + litest_assert_button_event(li, BTN_LEFT, LIBINPUT_BUTTON_STATE_PRESSED); + litest_assert_only_typed_events(li, LIBINPUT_EVENT_POINTER_MOTION); + + litest_checkpoint("Releasing 3 fingers"); + for (uint32_t i = 0; i < finger_count; i++) + litest_touch_up(dev, i); + + litest_dispatch(li); + litest_assert_empty_queue(li); + + litest_checkpoint("Putting 2 fingers down and moving them"); + y = 30.0; + litest_touch_down(dev, 0, 10, y); + litest_touch_down(dev, 1, 20, y); + litest_dispatch(li); + litest_assert_empty_queue(li); + + litest_timeout_finger_switch(); + litest_dispatch(li); + + while (y < 60.0) { + y += 2; + litest_touch_move(dev, 0, 10, y); + litest_touch_move(dev, 1, 20, y); + litest_dispatch(li); + } + + litest_checkpoint("Expecting drag button release and scroll"); + litest_assert_button_event(li, BTN_LEFT, LIBINPUT_BUTTON_STATE_RELEASED); + litest_assert_only_axis_events(li, LIBINPUT_EVENT_POINTER_SCROLL_FINGER); + + litest_touch_up(dev, 0); + litest_dispatch(li); + litest_assert_empty_queue(li); + + litest_timeout_3fg_drag(); + litest_dispatch(li); +} +END_TEST + +START_TEST(gestures_3fg_drag_lock_resume_1fg_tap) +{ + struct litest_device *dev = litest_current_device(); + struct libinput *li = dev->libinput; + int finger_count = _i; /* ranged test */ + + if (litest_slot_count(dev) < 3) + return LITEST_NOT_APPLICABLE; + + if (libinput_device_config_3fg_drag_get_finger_count(dev->libinput_device) < finger_count) + return LITEST_NOT_APPLICABLE; + + litest_enable_3fg_drag(dev->libinput_device, finger_count); + litest_enable_tap(dev->libinput_device); + + litest_drain_events(li); + + litest_checkpoint("Putting 3 fingers down for 3fg drag"); + double y = 30.0; + for (int i = 0; i < finger_count; i++) + litest_touch_down(dev, i, 10 + i, y); + + litest_dispatch(li); + + while (y < 60.0) { + y += 2; + for (int i = 0; i < finger_count; i++) + litest_touch_move(dev, i, 10 + i, y); + litest_dispatch(li); + } + litest_drain_events_of_type(li, + LIBINPUT_EVENT_GESTURE_HOLD_BEGIN, + LIBINPUT_EVENT_GESTURE_HOLD_END, + -1); + litest_assert_button_event(li, BTN_LEFT, LIBINPUT_BUTTON_STATE_PRESSED); + litest_assert_only_typed_events(li, LIBINPUT_EVENT_POINTER_MOTION); + + litest_checkpoint("Releasing 3 fingers"); + for (int i = 0; i < finger_count; i++) + litest_touch_up(dev, i); + + litest_dispatch(li); + litest_assert_empty_queue(li); + + litest_checkpoint("Tapping with 1 finger"); + /* fingers are up, now let's tap with one finger */ + y = 30.0; + litest_touch_down(dev, 0, 10, y); + litest_dispatch(li); + litest_assert_empty_queue(li); + litest_touch_up(dev, 0); + litest_dispatch(li); + + litest_timeout_tap(); + litest_dispatch(li); + + litest_checkpoint("Expecting drag release followed by 1fg tap"); + + /* 3fg drag lock must be cancelled */ + litest_assert_button_event(li, BTN_LEFT, LIBINPUT_BUTTON_STATE_RELEASED); + /* And a 1fg tap */ + litest_assert_button_event(li, BTN_LEFT, LIBINPUT_BUTTON_STATE_PRESSED); + litest_assert_button_event(li, BTN_LEFT, LIBINPUT_BUTTON_STATE_RELEASED); + litest_assert_empty_queue(li); + + litest_timeout_3fg_drag(); + litest_dispatch(li); +} +END_TEST + TEST_COLLECTION(gestures) { struct range cardinals = { N, N + NCARDINALS }; struct range range_hold = { 1, 5 }; struct range range_multifinger_tap = {1, 4}; + struct range range_3fg_drag = { 3, 5 }; litest_add(gestures_cap, LITEST_TOUCHPAD, LITEST_SINGLE_TOUCH); litest_add(gestures_nocap, LITEST_ANY, LITEST_TOUCHPAD); @@ -1830,6 +2310,32 @@ TEST_COLLECTION(gestures) litest_add(gestures_hold_and_motion_before_timeout, LITEST_TOUCHPAD, LITEST_SINGLE_TOUCH); litest_add(gestures_hold_and_motion_after_timeout, LITEST_TOUCHPAD, LITEST_SINGLE_TOUCH); + { + struct litest_parameters *params = litest_parameters_new("fingers", 'u', 2, 3, 4, + "tap-enabled", 'b'); + litest_add_parametrized(gestures_3fg_drag, LITEST_TOUCHPAD, LITEST_SINGLE_TOUCH, params); + litest_parameters_unref(params); + } + + { + struct litest_parameters *params = litest_parameters_new("fingers", 'u', 2, 3, 4, + "tap-enabled", 'b', + "wait", 'b'); + litest_add_parametrized(gestures_3fg_drag_lock_resume_3fg_motion, LITEST_TOUCHPAD, LITEST_SINGLE_TOUCH, params); + litest_add_parametrized(gestures_3fg_drag_lock_resume_3fg_release_no_motion, LITEST_TOUCHPAD, LITEST_SINGLE_TOUCH, params); + litest_parameters_unref(params); + } + + { + struct litest_parameters *params = litest_parameters_new("fingers", 'u', 2, 3, 4, + "tap-enabled", 'b'); + litest_add_parametrized(gestures_3fg_drag_lock_resume_1fg_motion, LITEST_TOUCHPAD, LITEST_SINGLE_TOUCH, params); + litest_add_parametrized(gestures_3fg_drag_lock_resume_2fg_scroll, LITEST_TOUCHPAD, LITEST_SINGLE_TOUCH, params); + litest_parameters_unref(params); + } + litest_add_ranged(gestures_3fg_drag_lock_resume_1fg_tap, LITEST_TOUCHPAD, LITEST_SINGLE_TOUCH, &range_3fg_drag); + + /* Timing-sensitive test, valgrind is too slow */ if (!RUNNING_ON_VALGRIND) litest_add(gestures_swipe_3fg_unaccel, LITEST_TOUCHPAD, LITEST_SINGLE_TOUCH); diff --git a/tools/shared.c b/tools/shared.c index 4d7fae6e..13306f9d 100644 --- a/tools/shared.c +++ b/tools/shared.c @@ -411,6 +411,21 @@ tools_parse_option(int option, options->area.y2 = y2; break; } + case OPT_3FG_DRAG: + if (!optarg) + return 1; + if (streq(optarg, "3fg")) + options->drag_3fg = LIBINPUT_CONFIG_3FG_DRAG_ENABLED_3FG; + else if (streq(optarg, "4fg")) + options->drag_3fg = LIBINPUT_CONFIG_3FG_DRAG_ENABLED_4FG; + else if (streq(optarg, "disabled")) + options->drag_3fg = LIBINPUT_CONFIG_3FG_DRAG_DISABLED; + else { + fprintf(stderr, "Invalid --enable-3fg-drag\n" + "Valid options: 3fg|4fg|disabled\n"); + return 1; + } + break; } return 0; } @@ -629,6 +644,9 @@ tools_device_apply_config(struct libinput_device *device, if (libinput_device_config_area_has_rectangle(device)) libinput_device_config_area_set_rectangle(device, &options->area); + + if (libinput_device_config_3fg_drag_get_finger_count(device) >= 3) + libinput_device_config_3fg_drag_set_enabled(device, options->drag_3fg); } void diff --git a/tools/shared.h b/tools/shared.h index cadb36e3..d7090bc7 100644 --- a/tools/shared.h +++ b/tools/shared.h @@ -69,6 +69,7 @@ enum configuration_options { OPT_PRESSURE_RANGE, OPT_CALIBRATION, OPT_AREA, + OPT_3FG_DRAG, }; #define CONFIGURATION_OPTIONS \ @@ -91,6 +92,7 @@ enum configuration_options { { "disable-dwtp", no_argument, 0, OPT_DWTP_DISABLE }, \ { "enable-scroll-button-lock", no_argument, 0, OPT_SCROLL_BUTTON_LOCK_ENABLE }, \ { "disable-scroll-button-lock",no_argument, 0, OPT_SCROLL_BUTTON_LOCK_DISABLE }, \ + { "enable-3fg-drag", required_argument, 0, OPT_3FG_DRAG }, \ { "set-click-method", required_argument, 0, OPT_CLICK_METHOD }, \ { "set-clickfinger-map", required_argument, 0, OPT_CLICKFINGER_MAP }, \ { "set-scroll-method", required_argument, 0, OPT_SCROLL_METHOD }, \ @@ -141,6 +143,7 @@ struct tools_options { double pressure_range[2]; float calibration[6]; struct libinput_config_area_rectangle area; + enum libinput_config_3fg_drag_state drag_3fg; }; void tools_init_options(struct tools_options *options);