Commit 689632cd authored by Peter Hutterer's avatar Peter Hutterer

touchpad: only try thumb detection in the lowest 15/8mm

That's the most likely area it will be resting in, if it's sitting anywhere
above that it's likely part of an interaction.

A thumb in the lowest 15mm needs to trigger the pressure threshold before it's
labelled a thumb. A thumb in the lowest 8mm is considered a thumb if it
remains there for 300ms. Regardless of the pressure, since we can't reliably
get pressure here. If a thumb moves out of the area, or starts outside of that
area it is never a thumb.

If edge scrolling is enabled, the 8mm threshold is ineffective since we'll
have normal interaction in that zone for horizontal scrolling.

The thumb tests now require all touchpads to be switched to clickfinger, if we
test for thumb detection on the bottom of the pad we won't get expected
motion events due to the software button area.
Signed-off-by: Peter Hutterer's avatarPeter Hutterer <peter.hutterer@who-t.net>
Reviewed-by: default avatarHans de Goede <hdegoede@redhat.com>
parent 7e9ef68d
......@@ -39,6 +39,7 @@ diagram_files = \
$(srcdir)/svg/pinch-gestures.svg \
$(srcdir)/svg/swipe-gestures.svg \
$(srcdir)/svg/tap-n-drag.svg \
$(srcdir)/svg/thumb-detection.svg \
$(srcdir)/svg/top-software-buttons.svg \
$(srcdir)/svg/touchscreen-gestures.svg \
$(srcdir)/svg/twofinger-scrolling.svg
......
......@@ -80,4 +80,32 @@ Notable behaviors of libinput's disable-while-typing feature:
- Physical buttons work even while the touchpad is disabled. This includes
software-emulated buttons.
@section thumb-detection Thumb detection
Many users rest their thumb on the touchpad while using the index finger to
move the finger around. For clicks, often the thumb is used rather than the
finger. The thumb should otherwise be ignored as a touch, i.e. it should not
count towards @ref clickfinger and it should not cause a single-finger
movement to trigger @ref twofinger_scrolling.
libinput uses two triggers for thumb detection: pressure and
location. A touch exceeding a pressure threshold is considered a thumb if it
is within the thumb detection zone.
@note "Pressure" on touchpads is synonymous with "contact area", a large
touch surface area has a higher pressure and thus hints at a thumb or palm
touching the surface.
Pressure readings are unreliable at the far bottom of the touchpad as a
thumb hanging mostly off the touchpad will have a small surface area.
libinput has a definitive thumb zone where any touch is considered a resting
thumb.
@image html thumb-detection.svg
The picture above shows the two detection areas. In the larger (light red)
area, a touch is labelled as thumb when it exceeds a device-specific
pressure threshold. In the lower (dark red) area, a touch is labelled as
thumb if it remains in that area for a time without moving outside.
*/
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="89.829216mm"
height="59.06765mm"
viewBox="0 0 318.2925 209.29482"
id="svg4184"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="thumb-detection.svg">
<defs
id="defs4186" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="1.979899"
inkscape:cx="270.39655"
inkscape:cy="139.75035"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1920"
inkscape:window-height="1136"
inkscape:window-x="0"
inkscape:window-y="27"
inkscape:window-maximized="1"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0" />
<metadata
id="metadata4189">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-257.99662,-299.41313)">
<rect
width="313.09872"
height="167.89594"
x="260.59351"
y="302.01001"
id="rect2858-0"
style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:#b3b3b3;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:5.19376326;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker:none;enable-background:accumulate" />
<rect
style="opacity:0.92000002;fill:#7b0000;fill-opacity:0.2983426;stroke:#000000;stroke-width:0.97031647;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="rect4788"
width="307.88782"
height="45.628574"
x="262.8418"
y="421.0347" />
<rect
y="445.40848"
x="262.68912"
height="21.407471"
width="308.19318"
id="rect4149"
style="opacity:0.92000002;fill:#7b0000;fill-opacity:0.2983426;stroke:#000000;stroke-width:0.66495597;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<g
id="g4151">
<path
sodipodi:nodetypes="sszzzcss"
d="m 353.70196,495.15765 c -24.01774,-7.29937 -29.0012,-10.10221 -30.51977,-10.54973 -10.67294,-3.14527 -18.27051,-5.54063 -23.77758,-13.4704 -5.50707,-7.92977 -5.34967,-20.78347 8.87612,-26.31604 14.2258,-5.53257 39.34351,8.79597 60.13061,16.16341 20.7871,7.36744 33.04563,11.44545 39.33422,13.87551 -8.10022,18.05041 -7.22129,21.15857 -10.11054,33.34117 -0.0481,0.20261 -17.87459,-5.12433 -43.93306,-13.04392 z"
id="path2824-1-1-3"
style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:#ffccaa;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1.00100005;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker:none;enable-background:accumulate"
inkscape:connector-curvature="0" />
<path
sodipodi:nodetypes="ccccc"
d="m 324.44991,483.39364 c -10.67294,-1.94747 -17.88441,-5.64478 -21.62691,-8.75386 -8.11652,-9.03765 -6.31775,-15.03428 -3.3272,-13.99784 8.90495,-0.9097 30.20384,9.01528 33.86042,10.17935 -5.80268,11.37909 -1.08919,13.70271 -8.90631,12.57235 z"
id="path2824-7-1-4-3"
style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:0.92000002;fill:#ffe6d5;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.2;marker:none;enable-background:accumulate"
inkscape:connector-curvature="0" />
</g>
<g
transform="matrix(0.79657897,0.11742288,-0.14814182,0.631399,276.6631,-158.96703)"
id="g3663-9-5">
<path
d="m 388.57143,893.79076 -57.14285,-130 c 0,0 -30.0247,-58.84827 4.28571,-70.00001 27.07438,-8.79984 37.32196,9.59496 40,14.64286 27.54455,51.91936 84.64285,173.21429 84.64285,173.21429 l -0.71428,0 -71.07143,12.14286 z"
id="path2820-6-6"
style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
inkscape:connector-curvature="0" />
<path
d="m 360.32021,827.78041 c -15.74169,-35.7991 -29.44655,-66.92657 -30.45523,-69.17214 -7.08929,-15.78239 -10.8761,-32.88254 -9.6176,-43.43026 1.39575,-11.69796 7.19746,-18.50389 18.22574,-21.38044 5.18218,-1.35169 8.54724,-1.76827 12.41155,-1.53649 4.43642,0.26609 6.95929,0.93715 11.03011,2.93391 3.93491,1.9301 8.0085,5.56248 10.68932,9.53159 3.68818,5.46055 26.56068,50.9623 49.57778,98.62829 16.60192,34.38082 37.06388,77.41994 36.89013,77.59369 -0.13286,0.13286 -69.01932,11.92114 -69.66286,11.92114 -0.27909,0 -12.00972,-26.24842 -29.08894,-65.08929 z"
id="path2824-1-1"
style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:#ffccaa;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:0.002;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker:none;enable-background:accumulate"
inkscape:connector-curvature="0" />
<path
d="m 334.75785,756.75053 c -7.08929,-15.78239 -10.28437,-26.89033 -9.02587,-37.43805 1.39575,-11.69796 5.8085,-16.73613 16.83678,-19.61268 12.44766,-3.59459 20.03902,-1.91353 27.39013,8.75815 11.42622,25.66382 13.40166,29.05484 15.06365,35.48866 -0.13286,0.13286 -42.89663,15.49027 -44.57776,16.18518 -1.72922,0.71479 -4.94789,-2.09377 -5.68693,-3.38126 z"
id="path2824-7-1-4"
style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:0.92000002;fill:#ffe6d5;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.2;marker:none;enable-background:accumulate"
inkscape:connector-curvature="0" />
</g>
</g>
</svg>
......@@ -810,7 +810,8 @@ tp_check_clickfinger_distance(struct tp_dispatch *tp,
if (!t1 || !t2)
return 0;
if (t1->is_thumb || t2->is_thumb)
if (t1->thumb.state == THUMB_STATE_YES ||
t2->thumb.state == THUMB_STATE_YES)
return 0;
x = abs(t1->point.x - t2->point.x);
......@@ -872,7 +873,7 @@ tp_clickfinger_set_button(struct tp_dispatch *tp)
if (t->state != TOUCH_BEGIN && t->state != TOUCH_UPDATE)
continue;
if (t->is_thumb)
if (t->thumb.state == THUMB_STATE_YES)
continue;
if (!first)
......
......@@ -740,7 +740,7 @@ tp_tap_handle_state(struct tp_dispatch *tp, uint64_t time)
/* The simple version: if a touch is a thumb on
* begin we ignore it. All other thumb touches
* follow the normal tap state for now */
if (t->is_thumb) {
if (t->thumb.state == THUMB_STATE_YES) {
t->tap.is_thumb = true;
continue;
}
......@@ -772,7 +772,7 @@ tp_tap_handle_state(struct tp_dispatch *tp, uint64_t time)
tp_tap_handle_event(tp, t, TAP_EVENT_MOTION, time);
} else if (tp->tap.state != TAP_STATE_IDLE &&
t->is_thumb &&
t->thumb.state == THUMB_STATE_YES &&
!t->tap.is_thumb) {
tp_tap_handle_event(tp, t, TAP_EVENT_THUMB, time);
}
......
......@@ -208,7 +208,8 @@ tp_begin_touch(struct tp_dispatch *tp, struct tp_touch *t, uint64_t time)
t->millis = time;
tp->nfingers_down++;
t->palm.time = time;
t->is_thumb = false;
t->thumb.state = THUMB_STATE_MAYBE;
t->thumb.first_touch_time = time;
t->tap.is_thumb = false;
assert(tp->nfingers_down >= 1);
}
......@@ -462,7 +463,7 @@ tp_touch_active(struct tp_dispatch *tp, struct tp_touch *t)
return (t->state == TOUCH_BEGIN || t->state == TOUCH_UPDATE) &&
t->palm.state == PALM_NONE &&
!t->pinned.is_pinned &&
!t->is_thumb &&
t->thumb.state != THUMB_STATE_YES &&
tp_button_touch_active(tp, t) &&
tp_edge_scroll_touch_active(tp, t);
}
......@@ -604,20 +605,47 @@ out:
t->palm.state == PALM_TYPING ? "typing" : "trackpoint");
}
static inline const char*
thumb_state_to_str(enum tp_thumb_state state)
{
switch(state){
CASE_RETURN_STRING(THUMB_STATE_NO);
CASE_RETURN_STRING(THUMB_STATE_YES);
CASE_RETURN_STRING(THUMB_STATE_MAYBE);
}
return NULL;
}
static void
tp_thumb_detect(struct tp_dispatch *tp, struct tp_touch *t)
tp_thumb_detect(struct tp_dispatch *tp, struct tp_touch *t, uint64_t time)
{
/* once a thumb, always a thumb */
if (!tp->thumb.detect_thumbs || t->is_thumb)
enum tp_thumb_state state = t->thumb.state;
/* once a thumb, always a thumb, once ruled out always ruled out */
if (!tp->thumb.detect_thumbs ||
t->thumb.state != THUMB_STATE_MAYBE)
return;
if (t->point.y < tp->thumb.upper_thumb_line) {
/* if a potential thumb is above the line, it won't ever
* label as thumb */
t->thumb.state = THUMB_STATE_NO;
goto out;
}
/* Note: a thumb at the edge of the touchpad won't trigger the
* threshold, the surface area is usually too small.
* threshold, the surface area is usually too small. So we have a
* two-stage detection: pressure and time within the area.
* A finger that remains at the very bottom of the touchpad becomes
* a thumb.
*/
if (t->pressure < tp->thumb.threshold)
return;
t->is_thumb = true;
if (t->pressure > tp->thumb.threshold)
t->thumb.state = THUMB_STATE_YES;
else if (t->point.y > tp->thumb.lower_thumb_line &&
tp->scroll.method != LIBINPUT_CONFIG_SCROLL_EDGE &&
t->thumb.first_touch_time + 300 < time)
t->thumb.state = THUMB_STATE_YES;
/* now what? we marked it as thumb, so:
*
......@@ -629,6 +657,12 @@ tp_thumb_detect(struct tp_dispatch *tp, struct tp_touch *t)
* - tapping: honour thumb on begin, ignore it otherwise for now,
* this gets a tad complicated otherwise
*/
out:
if (t->thumb.state != state)
log_debug(tp_libinput_context(tp),
"thumb state: %s → %s\n",
thumb_state_to_str(state),
thumb_state_to_str(t->thumb.state));
}
static void
......@@ -785,7 +819,7 @@ tp_process_state(struct tp_dispatch *tp, uint64_t time)
if (!t->dirty)
continue;
tp_thumb_detect(tp, t);
tp_thumb_detect(tp, t, time);
tp_palm_detect(tp, t, time);
tp_motion_hysteresis(tp, t);
......@@ -1535,6 +1569,7 @@ tp_init_thumb(struct tp_dispatch *tp)
const struct input_absinfo *abs;
double w = 0.0, h = 0.0;
int xres, yres;
int ymax;
double threshold;
if (!tp->buttons.is_clickpad)
......@@ -1567,6 +1602,13 @@ tp_init_thumb(struct tp_dispatch *tp)
tp->thumb.threshold = max(100, threshold);
tp->thumb.detect_thumbs = true;
/* detect thumbs by pressure in the bottom 15mm, detect thumbs by
* lingering in the bottom 8mm */
ymax = tp->device->abs.absinfo_y->maximum;
yres = tp->device->abs.absinfo_y->resolution;
tp->thumb.upper_thumb_line = ymax - yres * 15;
tp->thumb.lower_thumb_line = ymax - yres * 8;
return 0;
}
......
......@@ -136,12 +136,17 @@ enum tp_gesture_2fg_state {
GESTURE_2FG_STATE_PINCH,
};
enum tp_thumb_state {
THUMB_STATE_NO,
THUMB_STATE_YES,
THUMB_STATE_MAYBE,
};
struct tp_touch {
struct tp_dispatch *tp;
enum touch_state state;
bool has_ended; /* TRACKING_ID == -1 */
bool dirty;
bool is_thumb;
struct device_coords point;
uint64_t millis;
int distance; /* distance == 0 means touch */
......@@ -195,6 +200,11 @@ struct tp_touch {
struct {
struct device_coords initial;
} gesture;
struct {
enum tp_thumb_state state;
uint64_t first_touch_time;
} thumb;
};
struct tp_dispatch {
......@@ -324,6 +334,8 @@ struct tp_dispatch {
struct {
bool detect_thumbs;
int threshold;
int upper_thumb_line;
int lower_thumb_line;
} thumb;
};
......
......@@ -4028,8 +4028,8 @@ START_TEST(touchpad_thumb_begin_no_motion)
litest_drain_events(li);
litest_touch_down_extended(dev, 0, 50, 50, axes);
litest_touch_move_to(dev, 0, 50, 50, 80, 50, 10, 0);
litest_touch_down_extended(dev, 0, 50, 99, axes);
litest_touch_move_to(dev, 0, 50, 99, 80, 99, 10, 0);
litest_touch_up(dev, 0);
litest_assert_empty_queue(li);
......@@ -4046,18 +4046,19 @@ START_TEST(touchpad_thumb_update_no_motion)
};
litest_disable_tap(dev->libinput_device);
enable_clickfinger(dev);
if (!has_thumb_detect(dev))
return;
litest_drain_events(li);
litest_touch_down(dev, 0, 50, 50);
litest_touch_move_to(dev, 0, 50, 50, 60, 50, 10, 0);
litest_touch_down(dev, 0, 50, 99);
litest_touch_move_to(dev, 0, 50, 99, 60, 99, 10, 0);
litest_assert_only_typed_events(li, LIBINPUT_EVENT_POINTER_MOTION);
litest_touch_move_extended(dev, 0, 65, 50, axes);
litest_touch_move_to(dev, 0, 65, 50, 80, 50, 10, 0);
litest_touch_move_extended(dev, 0, 65, 99, axes);
litest_touch_move_to(dev, 0, 65, 99, 80, 99, 10, 0);
litest_touch_up(dev, 0);
litest_assert_empty_queue(li);
......@@ -4085,9 +4086,9 @@ START_TEST(touchpad_thumb_clickfinger)
litest_drain_events(li);
litest_touch_down(dev, 0, 50, 50);
litest_touch_down(dev, 1, 60, 50);
litest_touch_move_extended(dev, 0, 55, 50, axes);
litest_touch_down(dev, 0, 50, 99);
litest_touch_down(dev, 1, 60, 99);
litest_touch_move_extended(dev, 0, 55, 99, axes);
litest_button_click(dev, BTN_LEFT, true);
libinput_dispatch(li);
......@@ -4105,9 +4106,9 @@ START_TEST(touchpad_thumb_clickfinger)
litest_drain_events(li);
litest_touch_down(dev, 0, 50, 50);
litest_touch_down(dev, 1, 60, 50);
litest_touch_move_extended(dev, 1, 65, 50, axes);
litest_touch_down(dev, 0, 50, 99);
litest_touch_down(dev, 1, 60, 99);
litest_touch_move_extended(dev, 1, 65, 99, axes);
litest_button_click(dev, BTN_LEFT, true);
libinput_dispatch(li);
......@@ -4142,8 +4143,8 @@ START_TEST(touchpad_thumb_btnarea)
litest_drain_events(li);
litest_touch_down(dev, 0, 90, 95);
litest_touch_move_extended(dev, 0, 95, 95, axes);
litest_touch_down(dev, 0, 90, 99);
litest_touch_move_extended(dev, 0, 95, 99, axes);
litest_button_click(dev, BTN_LEFT, true);
/* button areas work as usual with a thumb */
......@@ -4203,17 +4204,18 @@ START_TEST(touchpad_thumb_tap_begin)
return;
litest_enable_tap(dev->libinput_device);
enable_clickfinger(dev);
litest_drain_events(li);
/* touch down is a thumb */
litest_touch_down_extended(dev, 0, 50, 50, axes);
litest_touch_down_extended(dev, 0, 50, 99, axes);
litest_touch_up(dev, 0);
litest_timeout_tap();
litest_assert_empty_queue(li);
/* make sure normal tap still works */
litest_touch_down(dev, 0, 50, 50);
litest_touch_down(dev, 0, 50, 99);
litest_touch_up(dev, 0);
litest_timeout_tap();
litest_assert_only_typed_events(li, LIBINPUT_EVENT_POINTER_BUTTON);
......@@ -4233,17 +4235,18 @@ START_TEST(touchpad_thumb_tap_touch)
return;
litest_enable_tap(dev->libinput_device);
enable_clickfinger(dev);
litest_drain_events(li);
/* event after touch down is thumb */
litest_touch_down(dev, 0, 50, 50);
litest_touch_move_extended(dev, 0, 51, 50, axes);
litest_touch_move_extended(dev, 0, 51, 99, axes);
litest_touch_up(dev, 0);
litest_timeout_tap();
litest_assert_empty_queue(li);
/* make sure normal tap still works */
litest_touch_down(dev, 0, 50, 50);
litest_touch_down(dev, 0, 50, 99);
litest_touch_up(dev, 0);
litest_timeout_tap();
litest_assert_only_typed_events(li, LIBINPUT_EVENT_POINTER_BUTTON);
......@@ -4263,18 +4266,19 @@ START_TEST(touchpad_thumb_tap_hold)
return;
litest_enable_tap(dev->libinput_device);
enable_clickfinger(dev);
litest_drain_events(li);
/* event in state HOLD is thumb */
litest_touch_down(dev, 0, 50, 50);
litest_touch_down(dev, 0, 50, 99);
litest_timeout_tap();
libinput_dispatch(li);
litest_touch_move_extended(dev, 0, 51, 50, axes);
litest_touch_move_extended(dev, 0, 51, 99, axes);
litest_touch_up(dev, 0);
litest_assert_empty_queue(li);
/* make sure normal tap still works */
litest_touch_down(dev, 0, 50, 50);
litest_touch_down(dev, 0, 50, 99);
litest_touch_up(dev, 0);
litest_timeout_tap();
litest_assert_only_typed_events(li, LIBINPUT_EVENT_POINTER_BUTTON);
......@@ -4294,13 +4298,14 @@ START_TEST(touchpad_thumb_tap_hold_2ndfg)
return;
litest_enable_tap(dev->libinput_device);
enable_clickfinger(dev);
litest_drain_events(li);
/* event in state HOLD is thumb */
litest_touch_down(dev, 0, 50, 50);
litest_touch_down(dev, 0, 50, 99);
litest_timeout_tap();
libinput_dispatch(li);
litest_touch_move_extended(dev, 0, 51, 50, axes);
litest_touch_move_extended(dev, 0, 51, 99, axes);
litest_assert_empty_queue(li);
......@@ -4319,7 +4324,7 @@ START_TEST(touchpad_thumb_tap_hold_2ndfg)
litest_assert_empty_queue(li);
/* make sure normal tap still works */
litest_touch_down(dev, 0, 50, 50);
litest_touch_down(dev, 0, 50, 99);
litest_touch_up(dev, 0);
litest_timeout_tap();
litest_assert_only_typed_events(li, LIBINPUT_EVENT_POINTER_BUTTON);
......@@ -4344,10 +4349,10 @@ START_TEST(touchpad_thumb_tap_hold_2ndfg_tap)
litest_drain_events(li);
/* event in state HOLD is thumb */
litest_touch_down(dev, 0, 50, 50);
litest_touch_down(dev, 0, 50, 99);
litest_timeout_tap();
libinput_dispatch(li);
litest_touch_move_extended(dev, 0, 51, 50, axes);
litest_touch_move_extended(dev, 0, 51, 99, axes);
litest_assert_empty_queue(li);
......@@ -4377,7 +4382,7 @@ START_TEST(touchpad_thumb_tap_hold_2ndfg_tap)
libinput_event_destroy(libinput_event_pointer_get_base_event(ptrev));
/* make sure normal tap still works */
litest_touch_down(dev, 0, 50, 50);
litest_touch_down(dev, 0, 50, 99);
litest_touch_up(dev, 0);
litest_timeout_tap();
litest_assert_only_typed_events(li, LIBINPUT_EVENT_POINTER_BUTTON);
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment