406 lines
13 KiB
Lua
406 lines
13 KiB
Lua
-- SPDX-License-Identifier: MIT
|
|
|
|
local function usage(event_type, code)
|
|
return event_type * 65536 + code
|
|
end
|
|
|
|
evdev = {
|
|
BTN_LEFT = usage(1, 272),
|
|
BTN_RIGHT = usage(1, 273),
|
|
BTN_MIDDLE = usage(1, 274),
|
|
BTN_TOUCH = usage(1, 330),
|
|
BTN_TOOL_FINGER = usage(1, 325),
|
|
BTN_TOOL_QUINTTAP = usage(1, 328),
|
|
BTN_TOOL_DOUBLETAP = usage(1, 333),
|
|
BTN_TOOL_TRIPLETAP = usage(1, 334),
|
|
BTN_TOOL_QUADTAP = usage(1, 335),
|
|
REL_X = usage(2, 0),
|
|
REL_Y = usage(2, 1),
|
|
ABS_MT_SLOT = usage(3, 47),
|
|
ABS_MT_TOUCH_MAJOR = usage(3, 48),
|
|
ABS_MT_POSITION_X = usage(3, 53),
|
|
ABS_MT_POSITION_Y = usage(3, 54),
|
|
ABS_MT_TOOL_Y = usage(3, 61),
|
|
ABS_MT_TRACKING_ID = usage(3, 57),
|
|
KEY_F13 = usage(1, 183),
|
|
}
|
|
|
|
local output_event = arg[2] or "BTN_MIDDLE"
|
|
local allow_moving_anchor = arg[3] == "allow-moving-anchor"
|
|
local expected_output = evdev[output_event]
|
|
if arg[2] or allow_moving_anchor then
|
|
TOUCHPAD_HOLD_TAP_SETTINGS = {
|
|
output_event = output_event,
|
|
minimum_pause_ms = allow_moving_anchor and 0 or nil,
|
|
}
|
|
end
|
|
|
|
local plugin_callbacks = {}
|
|
libinput = {
|
|
register = function(_, versions)
|
|
assert(versions[1] == 1)
|
|
return 1
|
|
end,
|
|
connect = function(_, name, callback)
|
|
plugin_callbacks[name] = callback
|
|
end,
|
|
log_debug = function() end,
|
|
log_error = function() end,
|
|
}
|
|
|
|
dofile(arg[1])
|
|
|
|
local function new_device(device_type)
|
|
local callbacks = {}
|
|
local device = {
|
|
callbacks = callbacks,
|
|
prepended = {},
|
|
appended = {},
|
|
enabled = {},
|
|
}
|
|
local usages = {
|
|
[evdev.ABS_MT_SLOT] = true,
|
|
[evdev.ABS_MT_TRACKING_ID] = true,
|
|
[evdev.ABS_MT_POSITION_X] = true,
|
|
[evdev.ABS_MT_POSITION_Y] = true,
|
|
[evdev.BTN_TOUCH] = true,
|
|
[evdev.BTN_TOOL_FINGER] = true,
|
|
[evdev.BTN_TOOL_DOUBLETAP] = true,
|
|
[evdev.BTN_TOOL_TRIPLETAP] = true,
|
|
[evdev.BTN_TOOL_QUADTAP] = true,
|
|
[evdev.BTN_TOOL_QUINTTAP] = true,
|
|
}
|
|
if device_type == "mouse" then
|
|
usages[evdev.REL_X] = true
|
|
usages[evdev.REL_Y] = true
|
|
end
|
|
function device:udev_properties()
|
|
if device_type == "touchpad" then
|
|
return { ID_INPUT_TOUCHPAD = true }
|
|
elseif device_type == "mouse" then
|
|
return { ID_INPUT_MOUSE = true }
|
|
elseif device_type == "keyboard" then
|
|
return { ID_INPUT_KEYBOARD = true }
|
|
end
|
|
return {}
|
|
end
|
|
function device:usages()
|
|
return usages
|
|
end
|
|
function device:absinfos()
|
|
return {
|
|
[evdev.ABS_MT_POSITION_X] = {
|
|
minimum = 0, maximum = 2833, resolution = 24,
|
|
},
|
|
[evdev.ABS_MT_POSITION_Y] = {
|
|
minimum = 0, maximum = 1723, resolution = 24,
|
|
},
|
|
}
|
|
end
|
|
function device:enable_evdev_usage(code)
|
|
self.enabled[code] = true
|
|
usages[code] = true
|
|
end
|
|
function device:connect(name, callback)
|
|
callbacks[name] = callback
|
|
end
|
|
function device:prepend_frame(frame)
|
|
table.insert(self.prepended, frame)
|
|
end
|
|
function device:append_frame(frame)
|
|
table.insert(self.appended, frame)
|
|
end
|
|
|
|
plugin_callbacks["new-evdev-device"](device)
|
|
return device
|
|
end
|
|
|
|
local function event(code, value)
|
|
return { usage = code, value = value }
|
|
end
|
|
|
|
local function contains(frame, code, value)
|
|
if not frame then
|
|
return false
|
|
end
|
|
for _, item in ipairs(frame) do
|
|
if item.usage == code and (value == nil or item.value == value) then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
local function feed(device, timestamp, frame)
|
|
local output = device.callbacks["evdev-frame"](device, frame, timestamp)
|
|
if output == nil then
|
|
return frame
|
|
end
|
|
return output
|
|
end
|
|
|
|
local function down(device, timestamp, slot, tracking_id, x, y, count)
|
|
local frame = {
|
|
event(evdev.ABS_MT_SLOT, slot),
|
|
event(evdev.ABS_MT_TRACKING_ID, tracking_id),
|
|
event(evdev.ABS_MT_POSITION_X, x),
|
|
event(evdev.ABS_MT_POSITION_Y, y),
|
|
}
|
|
if count == 1 then
|
|
table.insert(frame, event(evdev.BTN_TOUCH, 1))
|
|
table.insert(frame, event(evdev.BTN_TOOL_FINGER, 1))
|
|
elseif count == 2 then
|
|
table.insert(frame, event(evdev.BTN_TOOL_FINGER, 0))
|
|
table.insert(frame, event(evdev.BTN_TOOL_DOUBLETAP, 1))
|
|
elseif count == 3 then
|
|
table.insert(frame, event(evdev.BTN_TOOL_DOUBLETAP, 0))
|
|
table.insert(frame, event(evdev.BTN_TOOL_TRIPLETAP, 1))
|
|
end
|
|
return feed(device, timestamp, frame)
|
|
end
|
|
|
|
local function up(device, timestamp, slot, count_after)
|
|
local frame = {
|
|
event(evdev.ABS_MT_SLOT, slot),
|
|
event(evdev.ABS_MT_TRACKING_ID, -1),
|
|
}
|
|
if count_after == 1 then
|
|
table.insert(frame, event(evdev.BTN_TOOL_FINGER, 1))
|
|
table.insert(frame, event(evdev.BTN_TOOL_DOUBLETAP, 0))
|
|
elseif count_after == 0 then
|
|
table.insert(frame, event(evdev.BTN_TOUCH, 0))
|
|
table.insert(frame, event(evdev.BTN_TOOL_FINGER, 0))
|
|
end
|
|
return feed(device, timestamp, frame)
|
|
end
|
|
|
|
local output_device = new_device(output_event:sub(1, 4) == "KEY_" and
|
|
"keyboard" or "mouse")
|
|
|
|
local function action_count()
|
|
local count = 0
|
|
for _, frame in ipairs(output_device.appended) do
|
|
if contains(frame, expected_output, 1) then
|
|
count = count + 1
|
|
end
|
|
end
|
|
return count
|
|
end
|
|
|
|
local tests = {}
|
|
|
|
function tests.hold_tap_emits_middle_and_hides_tapper()
|
|
local device = new_device("touchpad")
|
|
down(device, 0, 0, 10, 1000, 1000, 1)
|
|
local second_down = down(device, 200000, 1, 11, 1400, 1000, 2)
|
|
assert(not contains(second_down, evdev.ABS_MT_TRACKING_ID, 11))
|
|
assert(not contains(second_down, evdev.BTN_TOOL_DOUBLETAP, 1))
|
|
local second_up = up(device, 260000, 1, 1)
|
|
assert(not contains(second_up, evdev.ABS_MT_TRACKING_ID, -1))
|
|
assert(action_count() == 1)
|
|
assert(#device.appended == 0)
|
|
up(device, 440000, 0, 0)
|
|
end
|
|
|
|
function tests.hold_doubletap_emits_two_middle_clicks()
|
|
local device = new_device("touchpad")
|
|
down(device, 0, 0, 20, 1000, 1000, 1)
|
|
down(device, 200000, 1, 21, 1400, 1000, 2)
|
|
up(device, 250000, 1, 1)
|
|
down(device, 360000, 1, 22, 1400, 1000, 2)
|
|
up(device, 420000, 1, 1)
|
|
assert(action_count() == 2)
|
|
up(device, 600000, 0, 0)
|
|
end
|
|
|
|
function tests.ordinary_two_finger_tap_passes_through()
|
|
local device = new_device("touchpad")
|
|
down(device, 0, 0, 30, 1000, 1000, 1)
|
|
local second = down(device, 10000, 1, 31, 1400, 1000, 2)
|
|
assert(contains(second, evdev.ABS_MT_TRACKING_ID, 31))
|
|
assert(contains(second, evdev.BTN_TOOL_DOUBLETAP, 1))
|
|
up(device, 70000, 1, 1)
|
|
up(device, 80000, 0, 0)
|
|
assert(action_count() == 0)
|
|
end
|
|
|
|
function tests.long_stationary_tap_is_consumed_without_click()
|
|
local device = new_device("touchpad")
|
|
down(device, 0, 0, 40, 1000, 1000, 1)
|
|
down(device, 200000, 1, 41, 1400, 1000, 2)
|
|
local second_up = up(device, 360001, 1, 1)
|
|
assert(not contains(second_up, evdev.ABS_MT_TRACKING_ID, -1))
|
|
assert(action_count() == 0)
|
|
up(device, 500000, 0, 0)
|
|
end
|
|
|
|
function tests.implausibly_short_contact_is_consumed_without_click()
|
|
local device = new_device("touchpad")
|
|
down(device, 0, 0, 45, 1000, 1000, 1)
|
|
down(device, 200000, 1, 46, 1400, 1000, 2)
|
|
up(device, 201000, 1, 1)
|
|
assert(action_count() == 0)
|
|
up(device, 400000, 0, 0)
|
|
end
|
|
|
|
function tests.tapper_movement_restores_normal_input()
|
|
local device = new_device("touchpad")
|
|
down(device, 0, 0, 50, 1000, 1000, 1)
|
|
down(device, 200000, 1, 51, 1400, 1000, 2)
|
|
local moved = feed(device, 230000, {
|
|
event(evdev.ABS_MT_POSITION_X, 1430),
|
|
})
|
|
assert(#device.prepended == 1)
|
|
assert(contains(device.prepended[1], evdev.ABS_MT_TRACKING_ID, 51))
|
|
assert(contains(device.prepended[1], evdev.ABS_MT_POSITION_X, 1430))
|
|
assert(not contains(moved, evdev.ABS_MT_POSITION_X, 1430))
|
|
up(device, 260000, 1, 1)
|
|
up(device, 400000, 0, 0)
|
|
assert(action_count() == 0)
|
|
end
|
|
|
|
function tests.anchor_movement_prevents_recognition()
|
|
local device = new_device("touchpad")
|
|
down(device, 0, 0, 60, 1000, 1000, 1)
|
|
feed(device, 150000, { event(evdev.ABS_MT_POSITION_X, 1040) })
|
|
local second = down(device, 200000, 1, 61, 1400, 1000, 2)
|
|
assert(contains(second, evdev.ABS_MT_TRACKING_ID, 61) ~=
|
|
allow_moving_anchor)
|
|
up(device, 250000, 1, 1)
|
|
up(device, 400000, 0, 0)
|
|
assert(action_count() == (allow_moving_anchor and 1 or 0))
|
|
end
|
|
|
|
function tests.anchor_movement_during_tap_restores_normal_input()
|
|
local device = new_device("touchpad")
|
|
down(device, 0, 0, 62, 1000, 1000, 1)
|
|
down(device, 200000, 1, 63, 1400, 1000, 2)
|
|
local moved = feed(device, 230000, {
|
|
event(evdev.ABS_MT_SLOT, 0),
|
|
event(evdev.ABS_MT_POSITION_X, 1040),
|
|
})
|
|
assert(#device.prepended == (allow_moving_anchor and 0 or 1))
|
|
if not allow_moving_anchor then
|
|
assert(contains(device.prepended[1], evdev.ABS_MT_TRACKING_ID, 63))
|
|
end
|
|
assert(contains(moved, evdev.ABS_MT_POSITION_X, 1040))
|
|
up(device, 260000, 1, 1)
|
|
up(device, 400000, 0, 0)
|
|
assert(action_count() == (allow_moving_anchor and 1 or 0))
|
|
end
|
|
|
|
function tests.rejected_tap_does_not_poison_anchor()
|
|
local device = new_device("touchpad")
|
|
down(device, 0, 0, 64, 1000, 1000, 1)
|
|
|
|
-- This near-simultaneous contact is passed through as an ordinary
|
|
-- two-finger interaction.
|
|
local early = down(device, 10000, 1, 65, 1400, 1000, 2)
|
|
assert(contains(early, evdev.ABS_MT_TRACKING_ID, 65))
|
|
up(device, 60000, 1, 1)
|
|
|
|
-- The same anchor can recognize a later tap without being lifted first.
|
|
local later = down(device, 200000, 1, 66, 1400, 1000, 2)
|
|
assert(not contains(later, evdev.ABS_MT_TRACKING_ID, 66))
|
|
up(device, 250000, 1, 1)
|
|
assert(action_count() == 1)
|
|
up(device, 300000, 0, 0)
|
|
end
|
|
|
|
function tests.tap_while_moving_does_not_poison_anchor()
|
|
if allow_moving_anchor then
|
|
return
|
|
end
|
|
|
|
local device = new_device("touchpad")
|
|
down(device, 0, 0, 67, 1000, 1000, 1)
|
|
feed(device, 150000, {
|
|
event(evdev.ABS_MT_SLOT, 0),
|
|
event(evdev.ABS_MT_POSITION_X, 1040),
|
|
})
|
|
local moving_tap = down(device, 160000, 1, 68, 1400, 1000, 2)
|
|
assert(contains(moving_tap, evdev.ABS_MT_TRACKING_ID, 68))
|
|
up(device, 210000, 1, 1)
|
|
|
|
local later = down(device, 320000, 1, 69, 1400, 1000, 2)
|
|
assert(not contains(later, evdev.ABS_MT_TRACKING_ID, 69))
|
|
up(device, 370000, 1, 1)
|
|
assert(action_count() == 1)
|
|
up(device, 400000, 0, 0)
|
|
end
|
|
|
|
function tests.anchor_can_move_pause_and_tap_repeatedly()
|
|
local device = new_device("touchpad")
|
|
down(device, 0, 0, 65, 1000, 1000, 1)
|
|
|
|
feed(device, 500000, {
|
|
event(evdev.ABS_MT_SLOT, 0),
|
|
event(evdev.ABS_MT_POSITION_X, 1300),
|
|
event(evdev.ABS_MT_POSITION_Y, 1100),
|
|
})
|
|
down(device, 650000, 1, 66, 1500, 1100, 2)
|
|
up(device, 700000, 1, 1)
|
|
assert(action_count() == 1)
|
|
|
|
feed(device, 800000, {
|
|
event(evdev.ABS_MT_SLOT, 0),
|
|
event(evdev.ABS_MT_POSITION_X, 1800),
|
|
event(evdev.ABS_MT_POSITION_Y, 1200),
|
|
})
|
|
down(device, 950000, 1, 67, 2000, 1200, 2)
|
|
up(device, 1000000, 1, 1)
|
|
assert(action_count() == 2)
|
|
|
|
up(device, 1100000, 0, 0)
|
|
end
|
|
|
|
function tests.third_contact_restores_candidate()
|
|
local device = new_device("touchpad")
|
|
down(device, 0, 0, 70, 1000, 1000, 1)
|
|
down(device, 200000, 1, 71, 1400, 1000, 2)
|
|
local third = down(device, 230000, 2, 72, 1800, 1000, 3)
|
|
assert(#device.prepended == 1)
|
|
assert(contains(device.prepended[1], evdev.ABS_MT_TRACKING_ID, 71))
|
|
assert(contains(third, evdev.ABS_MT_TRACKING_ID, 72))
|
|
up(device, 260000, 2, 2)
|
|
up(device, 280000, 1, 1)
|
|
up(device, 400000, 0, 0)
|
|
assert(action_count() == 0)
|
|
end
|
|
|
|
function tests.two_finger_hold_tap_remains_passthrough()
|
|
local device = new_device("touchpad")
|
|
down(device, 0, 0, 80, 1000, 1000, 1)
|
|
down(device, 10000, 1, 81, 1400, 1000, 2)
|
|
local third = down(device, 300000, 2, 82, 1800, 1000, 3)
|
|
assert(contains(third, evdev.ABS_MT_TRACKING_ID, 82))
|
|
up(device, 360000, 2, 2)
|
|
up(device, 500000, 1, 1)
|
|
up(device, 510000, 0, 0)
|
|
assert(action_count() == 0)
|
|
assert(#device.prepended == 0)
|
|
end
|
|
|
|
function tests.non_touchpad_is_ignored()
|
|
local device = new_device("other")
|
|
assert(device.callbacks["evdev-frame"] == nil)
|
|
assert(not device.enabled[evdev.BTN_MIDDLE])
|
|
end
|
|
|
|
function tests.pointer_device_is_used_for_button_output()
|
|
assert(output_device.enabled[expected_output])
|
|
assert(output_device.callbacks["evdev-frame"] == nil)
|
|
end
|
|
|
|
local names = {}
|
|
for name in pairs(tests) do
|
|
table.insert(names, name)
|
|
end
|
|
table.sort(names)
|
|
for _, name in ipairs(names) do
|
|
output_device.appended = {}
|
|
tests[name]()
|
|
print("ok - " .. name)
|
|
end
|
|
print(string.format("%d tests passed", #names))
|