diff --git a/touchpad-hold-tap/90-touchpad-hold-tap.lua b/touchpad-hold-tap/90-touchpad-hold-tap.lua index 5995fae..4d0032d 100644 --- a/touchpad-hold-tap/90-touchpad-hold-tap.lua +++ b/touchpad-hold-tap/90-touchpad-hold-tap.lua @@ -153,13 +153,18 @@ local function emit_action() return true end +local function reset_contacts(state) + state.mode = "idle" + state.contacts = {} + state.anchor_slot = nil + state.candidate_slot = nil + state.candidate_anchor_x = nil + state.candidate_anchor_y = nil +end + local function reset_if_clear(state) if contact_count(state, false) == 0 then - state.mode = "idle" - state.anchor_slot = nil - state.candidate_slot = nil - state.candidate_anchor_x = nil - state.candidate_anchor_y = nil + reset_contacts(state) end end @@ -178,6 +183,12 @@ local function new_contact(state, slot, tracking_id, timestamp) end local function handle_tracking_begin(device, state, tracking_id, timestamp) + if state.contacts[state.source_slot] then + libinput:log_debug( + "touchpad-hold-tap: resetting stale contact state") + reset_contacts(state) + end + if state.mode == "candidate" then restore_candidate(device, state, "extra contact") end @@ -361,6 +372,13 @@ local function handle_frame(device, frame, timestamp) else modified = true end + elseif event.usage == evdev.BTN_TOUCH and event.value == 0 then + table.insert(output, event) + if contact_count(state, false) > 0 then + libinput:log_debug( + "touchpad-hold-tap: resetting empty touch state") + reset_contacts(state) + end elseif tool_counts[event.usage] then if state.mode == "candidate" or override_tools then local desired = tool_counts[event.usage] == diff --git a/touchpad-hold-tap/tests/test-touchpad-hold-tap.lua b/touchpad-hold-tap/tests/test-touchpad-hold-tap.lua index 4c2c601..03b2b4c 100644 --- a/touchpad-hold-tap/tests/test-touchpad-hold-tap.lua +++ b/touchpad-hold-tap/tests/test-touchpad-hold-tap.lua @@ -307,6 +307,37 @@ function tests.rejected_tap_does_not_poison_anchor() up(device, 300000, 0, 0) end +function tests.empty_touch_state_clears_stale_contacts() + local device = new_device("touchpad") + + -- A suspend may interrupt the stream without individual tracking-ID + -- releases. The following no-touch report must still reset the plugin. + down(device, 0, 1, 70, 1000, 1000, 1) + feed(device, 100000, { + event(evdev.BTN_TOUCH, 0), + event(evdev.BTN_TOOL_FINGER, 0), + }) + + down(device, 200000, 0, 71, 1000, 1000, 1) + down(device, 400000, 1, 72, 1400, 1000, 2) + up(device, 450000, 1, 1) + assert(action_count() == 1) + up(device, 500000, 0, 0) +end + +function tests.reused_slot_clears_stale_contacts() + local device = new_device("touchpad") + + down(device, 0, 0, 73, 1000, 1000, 1) + -- A new tracking ID in an occupied slot also proves that the previous + -- contact disappeared without a complete event sequence. + down(device, 200000, 0, 74, 1000, 1000, 1) + down(device, 400000, 1, 75, 1400, 1000, 2) + up(device, 450000, 1, 1) + assert(action_count() == 1) + up(device, 500000, 0, 0) +end + function tests.tap_while_moving_does_not_poison_anchor() if allow_moving_anchor then return diff --git a/touchpad-hold-tap/touchpad-hold-tap.spec b/touchpad-hold-tap/touchpad-hold-tap.spec index a6ed1cd..0cfdf37 100644 --- a/touchpad-hold-tap/touchpad-hold-tap.spec +++ b/touchpad-hold-tap/touchpad-hold-tap.spec @@ -1,6 +1,6 @@ Name: touchpad-hold-tap Version: 0.1.0 -Release: 9%{?dist} +Release: 10%{?dist} Summary: Hold-tap middle-click gesture for touchpads License: MIT @@ -57,6 +57,9 @@ if [ "$1" -eq 0 ]; then fi %changelog +* Sat Sep 05 2026 fedora-tools contributors - 0.1.0-10 +- Reset stale touch contacts after an interrupted input stream + * Sat Sep 05 2026 fedora-tools contributors - 0.1.0-9 - Separate minimum anchor age from the optional stationary period - Allow a held anchor to recover after rejected taps