$(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
- 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.
+
*/
--- /dev/null
+<?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>
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);
if (t->state != TOUCH_BEGIN && t->state != TOUCH_UPDATE)
continue;
- if (t->is_thumb)
+ if (t->thumb.state == THUMB_STATE_YES)
continue;
if (!first)
/* 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;
}
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);
}
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);
}
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);
}
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:
*
* - 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
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);
const struct input_absinfo *abs;
double w = 0.0, h = 0.0;
int xres, yres;
+ int ymax;
double threshold;
if (!tp->buttons.is_clickpad)
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;
}
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 */
struct {
struct device_coords initial;
} gesture;
+
+ struct {
+ enum tp_thumb_state state;
+ uint64_t first_touch_time;
+ } thumb;
};
struct tp_dispatch {
struct {
bool detect_thumbs;
int threshold;
+ int upper_thumb_line;
+ int lower_thumb_line;
} thumb;
};
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);
};
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);
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);
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);
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 */
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);
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);
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);
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);
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);
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);
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);