Merge branch 'wip/auto-drag-lock' into 'main'

touchpad: support automatic drag-lock when releasing at the edge

See merge request libinput/libinput!1447
This commit is contained in:
Peter Hutterer 2026-03-19 10:50:27 +10:00
commit 210a2be499
5 changed files with 241 additions and 16 deletions

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 181 KiB

After

Width:  |  Height:  |  Size: 489 KiB

View file

@ -59,13 +59,16 @@ tap-and-drag enabled by default.
middle-click drag, tap with three fingers followed by a
single-finger drag.
Also optional is a feature called "drag lock". With drag lock disabled, lifting
the finger will stop any drag process. When enabled, the drag
process continues even after lifting a finger but can be ended
with an additional tap. If timeout-based drag-locks are enabled
the drag process will also automatically end once the finger has
been lifted for an implementation-specific timeout. Drag lock can be
enabled and disabled with **libinput_device_config_tap_set_drag_lock_enabled()**.
Also optional is a feature called "drag lock". With drag lock **disabled**,
lifting the finger will stop any drag process. When **enabled**, the drag
process continues even after lifting a finger, allowing the user to
reset the finger position and keep moving without releasing the drag.
libinput supports two variations of this drag lock: "sticky" and "timeout".
In sticky mode, the drag lock must be ended with an explicit additional tap.
In timeout mode, the drag lock ends automatically if no finger was put back on
the touchpad within a timeout. Drag lock can be enabled and disabled with
**libinput_device_config_tap_set_drag_lock_enabled()**.
Note that drag lock only applies if tap-and-drag is enabled.
.. figure:: tap-n-drag.svg
@ -81,11 +84,13 @@ position can be reset by lifting and quickly setting it down again on the
touchpad (d). This will be interpreted as continuing move and is especially
useful on small touchpads or with slow pointer acceleration.
If drag lock is enabled, the release of the mouse buttons after the finger
release (e) is triggered by a timeout. To release the button immediately,
simply tap again (f).
release (e) is triggered by a timeout (if in timeout mode) or by tapping again (f).
If drag lock is enabled in sticky mode there is no timeout after
releasing a finger and an extra tap is required to release the button.
libinput also supports an "auto drag-lock" feature: if drag lock is **disabled**
but the dragging finger is released at the very edge of the touchpad,
a drag lock automatically activates for a short timeout. This allows a user to
quickly reset the finger to elsewhere on the touchpad and continue the dragging
motion. If the finger is released elsewhere, no drag lock activates.
If two fingers are supported by the hardware, a second finger can be used to
drag while the first is held in-place.

View file

@ -178,6 +178,13 @@ tp_tap_clear_timer(struct tp_dispatch *tp)
libinput_timer_cancel(&tp->tap.timer);
}
static bool
tp_touch_near_any_edge(struct tp_dispatch *tp, struct tp_touch *t)
{
return (t->point.x < tp->tap.edges.left || t->point.x > tp->tap.edges.right ||
t->point.y < tp->tap.edges.top || t->point.y > tp->tap.edges.bottom);
}
static void
tp_tap_move_to_dead(struct tp_dispatch *tp, struct tp_touch *t)
{
@ -812,6 +819,7 @@ tp_tap_dragging_handle_event(struct tp_dispatch *tp,
usec_t time,
int nfingers_tapped)
{
bool at_edge = false;
switch (event) {
case TAP_EVENT_TOUCH: {
@ -825,7 +833,8 @@ tp_tap_dragging_handle_event(struct tp_dispatch *tp,
break;
}
case TAP_EVENT_RELEASE:
if (tp->tap.drag_lock != LIBINPUT_CONFIG_DRAG_LOCK_DISABLED) {
if (tp->tap.drag_lock != LIBINPUT_CONFIG_DRAG_LOCK_DISABLED ||
(at_edge = tp_touch_near_any_edge(tp, t))) {
enum tp_tap_state dest[3] = {
TAP_STATE_1FGTAP_DRAGGING_WAIT,
TAP_STATE_2FGTAP_DRAGGING_WAIT,
@ -833,8 +842,9 @@ tp_tap_dragging_handle_event(struct tp_dispatch *tp,
};
assert(nfingers_tapped >= 1 && nfingers_tapped <= 3);
tp->tap.state = dest[nfingers_tapped - 1];
if (tp->tap.drag_lock ==
LIBINPUT_CONFIG_DRAG_LOCK_ENABLED_TIMEOUT)
if (at_edge ||
tp->tap.drag_lock ==
LIBINPUT_CONFIG_DRAG_LOCK_ENABLED_TIMEOUT)
tp_tap_set_draglock_timer(tp, time);
} else {
tp_tap_notify(tp,
@ -1579,6 +1589,20 @@ tp_init_tap(struct tp_dispatch *tp)
tp->tap.drag_enabled = tp_drag_default(tp->device);
tp->tap.drag_lock = tp_drag_lock_default(tp->device);
struct evdev_device *device = tp->device;
const struct input_absinfo *absx = device->abs.absinfo_x;
const struct input_absinfo *absy = device->abs.absinfo_y;
assert(absx && absy);
struct phys_coords mm = { 5.0, 5.0 };
struct device_coords edge_margin = evdev_device_mm_to_units(device, &mm);
tp->tap.edges.left = edge_margin.x;
tp->tap.edges.right = (absx->maximum - edge_margin.x + absx->minimum);
tp->tap.edges.top = edge_margin.y;
tp->tap.edges.bottom = (absy->maximum - edge_margin.y + absy->minimum);
snprintf(timer_name,
sizeof(timer_name),
"%s tap",

View file

@ -450,6 +450,11 @@ struct tp_dispatch {
unsigned int nfingers_down; /* number of fingers down for tapping (excl.
thumb/palm) */
/* Edges for auto drag-lock, in device coordinates */
struct {
int left, right, top, bottom;
} edges;
} tap;
struct {

View file

@ -1123,7 +1123,7 @@ START_TEST(touchpad_tap_n_drag)
break;
}
litest_touch_down(dev, 0, 50, 50);
litest_touch_move_to(dev, 0, 50, 50, 80, 80, 20);
litest_touch_move_to(dev, 0, 50, 50, 70, 70, 20);
litest_dispatch(li);
@ -1549,6 +1549,182 @@ START_TEST(touchpad_tap_n_drag_draglock_sticky)
}
END_TEST
enum edge { TOP, BOTTOM, LEFT, RIGHT };
START_TEST(touchpad_tap_n_drag_auto_draglock_edge)
{
/* Test: tap-and-drag with drag-lock disabled. When a finger is
* released at a touchpad edge, auto drag-lock activates.
*/
struct litest_device *dev = litest_current_device();
struct libinput *li = dev->libinput;
enum edge edge = litest_test_param_get_i32(test_env->params, "edge");
int nfingers = litest_test_param_get_i32(test_env->params, "fingers");
if (nfingers > litest_slot_count(dev))
return LITEST_NOT_APPLICABLE;
litest_enable_tap(dev->libinput_device);
litest_disable_drag_lock(dev->libinput_device);
litest_disable_hold_gestures(dev->libinput_device);
litest_drain_events(li);
unsigned int button = 0;
switch (nfingers) {
case 1:
button = BTN_LEFT;
break;
case 2:
button = BTN_RIGHT;
break;
case 3:
button = BTN_MIDDLE;
break;
}
/* tap and drag */
switch (nfingers) {
case 3:
litest_touch_down(dev, 2, 60, 30);
_fallthrough_;
case 2:
litest_touch_down(dev, 1, 50, 30);
_fallthrough_;
case 1:
litest_touch_down(dev, 0, 40, 30);
break;
}
switch (nfingers) {
case 3:
litest_touch_up(dev, 2);
_fallthrough_;
case 2:
litest_touch_up(dev, 1);
_fallthrough_;
case 1:
litest_touch_up(dev, 0);
break;
}
/* 1fg back down starts the drag */
litest_touch_down(dev, 0, 50, 50);
litest_timeout_tap(li);
litest_assert_button_event(li, button, LIBINPUT_BUTTON_STATE_PRESSED);
litest_assert_empty_queue(li);
int x = 50, y = 50;
switch (edge) {
case TOP:
y = 0;
break;
case BOTTOM:
y = 99;
break;
case LEFT:
x = 0;
break;
case RIGHT:
x = 99;
break;
}
litest_touch_move_to(dev, 0, 50, 50, x, y, 20);
litest_drain_events(li);
litest_touch_up(dev, 0);
/* auto drag-lock should be active - button still pressed */
litest_assert_empty_queue(li);
/* put finger down again within timeout */
litest_touch_down(dev, 0, 50, 50);
litest_touch_move_to(dev, 0, 50, 50, 60, 60, 10);
litest_assert_only_typed_events(li, LIBINPUT_EVENT_POINTER_MOTION);
litest_drain_events(li);
/* release normally (not at edge) */
litest_touch_up(dev, 0);
litest_timeout_tapndrag(li);
litest_assert_button_event(li, button, LIBINPUT_BUTTON_STATE_RELEASED);
litest_assert_empty_queue(li);
}
END_TEST
START_TEST(touchpad_tap_n_drag_auto_draglock_timeout)
{
/* Test: auto drag-lock times out if finger not replaced */
struct litest_device *dev = litest_current_device();
struct libinput *li = dev->libinput;
litest_enable_tap(dev->libinput_device);
litest_disable_drag_lock(dev->libinput_device);
litest_disable_hold_gestures(dev->libinput_device);
litest_drain_events(li);
/* Tap */
litest_touch_down(dev, 0, 50, 50);
litest_touch_up(dev, 0);
/* Start drag */
litest_touch_down(dev, 0, 50, 50);
litest_timeout_tap(li);
litest_assert_button_event(li, BTN_LEFT, LIBINPUT_BUTTON_STATE_PRESSED);
litest_assert_empty_queue(li);
/* Move to left edge and release */
litest_touch_move_to(dev, 0, 50, 50, 1, 50, 20);
litest_drain_events(li);
litest_touch_up(dev, 0);
/* Auto drag-lock active - button still pressed */
litest_assert_empty_queue(li);
litest_timeout_tapndrag(li);
libinput_dispatch(li);
litest_assert_button_event(li, BTN_LEFT, LIBINPUT_BUTTON_STATE_RELEASED);
litest_assert_empty_queue(li);
}
END_TEST
START_TEST(touchpad_tap_n_drag_auto_draglock_disabled_when_draglock_enabled_via_config)
{
struct litest_device *dev = litest_current_device();
struct libinput *li = dev->libinput;
litest_enable_tap(dev->libinput_device);
/* sticky draglock because it's not timing sensitive */
litest_enable_drag_lock_sticky(dev->libinput_device);
litest_disable_hold_gestures(dev->libinput_device);
litest_drain_events(li);
/* tap and drag */
litest_touch_down(dev, 0, 50, 50);
litest_touch_up(dev, 0);
litest_touch_down(dev, 0, 50, 50);
litest_timeout_tap(li);
litest_assert_button_event(li, BTN_LEFT, LIBINPUT_BUTTON_STATE_PRESSED);
litest_assert_empty_queue(li);
/* release at edge */
litest_touch_move_to(dev, 0, 50, 50, 99, 50, 20);
litest_drain_events(li);
litest_touch_up(dev, 0);
litest_assert_empty_queue(li);
/* no auto release after timeout */
litest_timeout_tapndrag(li);
litest_assert_empty_queue(li);
/* tap to end */
litest_touch_down(dev, 0, 50, 50);
litest_touch_up(dev, 0);
litest_assert_button_event(li, BTN_LEFT, LIBINPUT_BUTTON_STATE_RELEASED);
litest_assert_empty_queue(li);
}
END_TEST
START_TEST(touchpad_tap_n_drag_2fg)
{
/* Test: tap with 1-3 fingers (multiple times), then a 1fg move
@ -5502,7 +5678,21 @@ TEST_COLLECTION(touchpad_tap_drag)
litest_add_parametrized(touchpad_tap_n_drag_draglock, LITEST_TOUCHPAD, LITEST_ANY, params);
litest_add_parametrized(touchpad_tap_n_drag_draglock_timeout, LITEST_TOUCHPAD, LITEST_ANY, params);
litest_add_parametrized(touchpad_tap_n_drag_draglock_sticky, LITEST_TOUCHPAD, LITEST_ANY, params);
}
litest_with_parameters(params,
"fingers", 'i', 3, 1, 2, 3,
"edge", 'I', 4,
litest_named_i32(LEFT),
litest_named_i32(RIGHT),
litest_named_i32(TOP),
litest_named_i32(BOTTOM)) {
litest_add_parametrized(touchpad_tap_n_drag_auto_draglock_edge, LITEST_TOUCHPAD, LITEST_ANY, params);
}
litest_add(touchpad_tap_n_drag_auto_draglock_timeout, LITEST_TOUCHPAD, LITEST_ANY);
litest_add(touchpad_tap_n_drag_auto_draglock_disabled_when_draglock_enabled_via_config, LITEST_TOUCHPAD, LITEST_ANY);
litest_with_parameters(params, "fingers", 'i', 3, 1, 2, 3) {
litest_add_parametrized(touchpad_drag_disabled, LITEST_TOUCHPAD, LITEST_ANY, params);
litest_add_parametrized(touchpad_drag_disabled_immediate, LITEST_TOUCHPAD, LITEST_ANY, params);
}