From 20178bce0d3244a7e774d030b79cee7fa3d5d8f1 Mon Sep 17 00:00:00 2001 From: Peter Hutterer Date: Mon, 16 Mar 2026 09:39:57 +1000 Subject: [PATCH 1/3] doc/user: document sticky vs timeout based drag lock a bit better --- doc/user/tapping.rst | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/doc/user/tapping.rst b/doc/user/tapping.rst index 14d273b4..0cdcd3a3 100644 --- a/doc/user/tapping.rst +++ b/doc/user/tapping.rst @@ -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,7 @@ 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). - -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. +release (e) is triggered by a timeout (if in timeout mode) or by tapping again (f). If two fingers are supported by the hardware, a second finger can be used to drag while the first is held in-place. From ea3c247e661339b8c57a55a719402daf44e1eb57 Mon Sep 17 00:00:00 2001 From: Peter Hutterer Date: Mon, 16 Mar 2026 15:05:05 +1000 Subject: [PATCH 2/3] test: change where our finger ends up before releasing No effect on the test right now but on some small test touchpads this is close enough to the edge to mess with future tests. --- test/test-touchpad-tap.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test-touchpad-tap.c b/test/test-touchpad-tap.c index 3ef82b54..b96a80c3 100644 --- a/test/test-touchpad-tap.c +++ b/test/test-touchpad-tap.c @@ -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); From 2248c3eab61f295002ae8a6e1e40df1c4cd42d01 Mon Sep 17 00:00:00 2001 From: Peter Hutterer Date: Wed, 11 Mar 2026 11:00:34 +1000 Subject: [PATCH 3/3] 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. --- doc/touchpad-tap-state-machine.svg | 3 +- doc/user/tapping.rst | 6 + src/evdev-mt-touchpad-tap.c | 30 ++++- src/evdev-mt-touchpad.h | 5 + test/test-touchpad-tap.c | 190 +++++++++++++++++++++++++++++ 5 files changed, 230 insertions(+), 4 deletions(-) diff --git a/doc/touchpad-tap-state-machine.svg b/doc/touchpad-tap-state-machine.svg index f3086d28..3a29658f 100644 --- a/doc/touchpad-tap-state-machine.svg +++ b/doc/touchpad-tap-state-machine.svg @@ -1,3 +1,4 @@ + -
(this section exists for [n] = 1, [n] = 2, and [n] = 3)
(this section exists for [n] = 1, [n] = 2, and [n] = 3)
IDLETOUCHfirstfinger downfinger upbutton 1presstimeoutmove > thresholdsecondfinger downTOUCH_2secondfinger upbutton 2pressmove > thresholdtimeoutbutton 1releasebutton 2release[n]FGTAP_TAPPEDtimeoutfirstfinger down[n]FGTAP_DRAGGINGfirstfinger upbutton [n]releaseIDLEthirdfinger downTOUCH_3button 3pressbutton 3releasemove > thresholdIDLEtimeoutfirstfinger upIDLEfourthfinger down[n]FGTAP_DRAGGING_OR_DOUBLETAPtimeoutfirstfinger upbutton [n]releasesecondfinger downmove > thresholdHOLDfirstfinger upsecondfinger downTOUCH_2_HOLDsecondfinger upthirdfinger downTOUCH_3_HOLDfourthfinger downDEADany finger upfourthfinger upany finger upany finger upIDLEif fingercount == 0[n]FGTAP_DRAGGING_2secondfinger downthirdfinger downphysbuttonpressphysbuttonpressbutton [n]release[n]FGTAP_DRAGGING_WAITtimeoutfirstfinger downTOUCH_TOUCHTOUCH_IDLETOUCH_DEADTOUCH_DEADTOUCH_IDLETOUCH_TOUCHTOUCH_IDLETOUCH_IDLETOUCH_TOUCHthat fingerTOUCH_IDLETOUCH_DEADthat fingerTOUCH_IDLETOUCH_TOUCHTOUCH_IDLETOUCH_TOUCHTOUCH_DEADTOUCH_IDLETOUCH_TOUCHTOUCH_TOUCHTOUCH_IDLETOUCH_TOUCHthat fingerTOUCH_IDLETOUCH_DEADTOUCH_DEADTOUCH_DEADTOUCH_DEADTOUCH_DEADTOUCH_DEADTOUCH_DEADTOUCH_DEADTOUCH_DEAD[n]FGTAP_DRAGGING_OR_TAPfirstfinger uptimeoutmove > thresholdTOUCH_IDLEdrag lockenabled?
no
no
yes
yes
thumbTOUCH_DEADTOUCH_2_RELEASEsecondfinger uptimeoutmove > thresholdfirstfinger downTOUCH_IDLEfirstfinger upsecondfinger downTOUCH_DEADTOUCH_DEADdragdisabled?
yes
yes
palmeither fingerpalmremaining fingerpalmany fingerpalmthat fingerTOUCH_DEADthat fingerTOUCH_DEADpalmany fingerpalmthat fingerTOUCH_DEADTOUCH_DEADpalmTOUCH_DEADany fingerpalmthat fingerTOUCH_DEADeither fingerpalmthat fingerTOUCH_DEADpalmTOUCH_DEADany fingerpalmthat fingerTOUCH_DEADTOUCH_DEADmove > thresholdmove > thresholdbutton 1press
[n] = 1
[n] = 1
[n] = 1
[n] = 1
no
no
button [n]releaseIDLEdragdisabled?
[n] = 2
[n] = 2
no
no
yes
yes
dragdisabled?
yes
yes
no
no
[n] = 3
[n] = 3
TOUCH_3_RELEASEtimeoutTOUCH_DEADTOUCH_DEADeither fingerpalmthat fingerTOUCH_DEADmove > thresholdthirdfinger downTOUCH_TOUCHTOUCH_3_RELEASE_2remainingfinger upthat fingerTOUCH_IDLEeither finger upthat fingerTOUCH_IDLEsecondfinger downremaining fingerpalmTOUCH_DEADtimeoutfirstfinger upbutton 3pressbutton 3releasebutton 3pressbutton 3releaseeither finger upthat fingerTOUCH_IDLEsecondfinger downbutton [n]releasebutton 3pressbutton 3releaseTOUCH_DEADbutton 3pressbutton 3releasebutton 3pressbutton 3release
Viewer does not support full SVG 1.1
+
(this section exists for [n] = 1, [n] = 2, and [n] = 3)
(this section exists for [n] = 1, [n] = 2, and [n] = 3)
IDLETOUCHfirstfinger downfinger upbutton 1presstimeoutmove > thresholdsecondfinger downTOUCH_2secondfinger upbutton 2pressmove > thresholdtimeoutbutton 1releasebutton 2release[n]FGTAP_TAPPEDtimeoutfirstfinger down[n]FGTAP_DRAGGINGfirstfinger upbutton [n]releaseIDLEthirdfinger downTOUCH_3button 3pressbutton 3releasemove > thresholdIDLEtimeoutfirstfinger upIDLEfourthfinger down[n]FGTAP_DRAGGING_OR_DOUBLETAPtimeoutfirstfinger upbutton [n]releasesecondfinger downmove > thresholdHOLDfirstfinger upsecondfinger downTOUCH_2_HOLDsecondfinger upthirdfinger downTOUCH_3_HOLDfourthfinger downDEADany finger upfourthfinger upany finger upany finger upIDLEif fingercount == 0[n]FGTAP_DRAGGING_2secondfinger downthirdfinger downphysbuttonpressphysbuttonpressbutton [n]release[n]FGTAP_DRAGGING_WAITtimeoutfirstfinger downTOUCH_TOUCHTOUCH_IDLETOUCH_DEADTOUCH_DEADTOUCH_IDLETOUCH_TOUCHTOUCH_IDLETOUCH_IDLETOUCH_TOUCHthat fingerTOUCH_IDLETOUCH_DEADthat fingerTOUCH_IDLETOUCH_TOUCHTOUCH_IDLETOUCH_TOUCHTOUCH_DEADTOUCH_IDLETOUCH_TOUCHTOUCH_TOUCHTOUCH_IDLETOUCH_TOUCHthat fingerTOUCH_IDLETOUCH_DEADTOUCH_DEADTOUCH_DEADTOUCH_DEADTOUCH_DEADTOUCH_DEADTOUCH_DEADTOUCH_DEADTOUCH_DEAD[n]FGTAP_DRAGGING_OR_TAPfirstfinger uptimeoutmove > thresholdTOUCH_IDLEdrag lockenabled?
no
no
yes
yes
thumbTOUCH_DEADTOUCH_2_RELEASEsecondfinger uptimeoutmove > thresholdfirstfinger downTOUCH_IDLEfirstfinger upsecondfinger downTOUCH_DEADTOUCH_DEADdragdisabled?
yes
yes
palmeither fingerpalmremaining fingerpalmany fingerpalmthat fingerTOUCH_DEADthat fingerTOUCH_DEADpalmany fingerpalmthat fingerTOUCH_DEADTOUCH_DEADpalmTOUCH_DEADany fingerpalmthat fingerTOUCH_DEADeither fingerpalmthat fingerTOUCH_DEADpalmTOUCH_DEADany fingerpalmthat fingerTOUCH_DEADTOUCH_DEADmove > thresholdmove > thresholdbutton 1press
[n] = 1
[n] = 1
[n] = 1
[n] = 1
no
no
button [n]releaseIDLEdragdisabled?
[n] = 2
[n] = 2
no
no
yes
yes
dragdisabled?
yes
yes
no
no
[n] = 3
[n] = 3
TOUCH_3_RELEASEtimeoutTOUCH_DEADTOUCH_DEADeither fingerpalmthat fingerTOUCH_DEADmove > thresholdthirdfinger downTOUCH_TOUCHTOUCH_3_RELEASE_2remainingfinger upthat fingerTOUCH_IDLEeither finger upthat fingerTOUCH_IDLEsecondfinger downremaining fingerpalmTOUCH_DEADtimeoutfirstfinger upbutton 3pressbutton 3releasebutton 3pressbutton 3releaseeither finger upthat fingerTOUCH_IDLEsecondfinger downbutton [n]releasebutton 3pressbutton 3releaseTOUCH_DEADbutton 3pressbutton 3releasebutton 3pressbutton 3release
no
no
yes
yes
fingerat edge?
Text is not SVG - cannot display
diff --git a/doc/user/tapping.rst b/doc/user/tapping.rst index 0cdcd3a3..7f7fb0d5 100644 --- a/doc/user/tapping.rst +++ b/doc/user/tapping.rst @@ -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. diff --git a/src/evdev-mt-touchpad-tap.c b/src/evdev-mt-touchpad-tap.c index 596ba944..565f2914 100644 --- a/src/evdev-mt-touchpad-tap.c +++ b/src/evdev-mt-touchpad-tap.c @@ -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", diff --git a/src/evdev-mt-touchpad.h b/src/evdev-mt-touchpad.h index 7e85baf7..544cf467 100644 --- a/src/evdev-mt-touchpad.h +++ b/src/evdev-mt-touchpad.h @@ -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 { diff --git a/test/test-touchpad-tap.c b/test/test-touchpad-tap.c index b96a80c3..7d444120 100644 --- a/test/test-touchpad-tap.c +++ b/test/test-touchpad-tap.c @@ -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); }