From 2248c3eab61f295002ae8a6e1e40df1c4cd42d01 Mon Sep 17 00:00:00 2001 From: Peter Hutterer Date: Wed, 11 Mar 2026 11:00:34 +1000 Subject: [PATCH] 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); }