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

If drag-lock is disabled but we're in a tap-and-drag state and the
finger is released near the edge (within 5mm), enable automatic drag
lock for 400ms.  This allows a user to quickly reset the finger and
continue with the drag.

The 400ms is a randomly guessed timeout - if you're using tap-and-drag
without draglock, finger dexterity should be high enough that resetting
the single finger can be done quickly but it's also short enough to not
make the occasional delayed button be painful in day-to-day use.
This commit is contained in:
Peter Hutterer 2026-03-11 11:00:34 +10:00
parent ea3c247e66
commit 2248c3eab6
5 changed files with 230 additions and 4 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

@ -86,6 +86,12 @@ 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 (if in timeout mode) or by tapping again (f).
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

@ -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);
}