diff --git a/doc/Makefile.am b/doc/Makefile.am index 95a96e45..ddda230b 100644 --- a/doc/Makefile.am +++ b/doc/Makefile.am @@ -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 diff --git a/doc/palm-detection.dox b/doc/palm-detection.dox index d787455e..1a5e6572 100644 --- a/doc/palm-detection.dox +++ b/doc/palm-detection.dox @@ -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. + */ diff --git a/doc/svg/thumb-detection.svg b/doc/svg/thumb-detection.svg new file mode 100644 index 00000000..bc746981 --- /dev/null +++ b/doc/svg/thumb-detection.svg @@ -0,0 +1,116 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + diff --git a/src/evdev-mt-touchpad-buttons.c b/src/evdev-mt-touchpad-buttons.c index 687a613f..77abc55f 100644 --- a/src/evdev-mt-touchpad-buttons.c +++ b/src/evdev-mt-touchpad-buttons.c @@ -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) diff --git a/src/evdev-mt-touchpad-tap.c b/src/evdev-mt-touchpad-tap.c index 1354e899..b2977545 100644 --- a/src/evdev-mt-touchpad-tap.c +++ b/src/evdev-mt-touchpad-tap.c @@ -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); } diff --git a/src/evdev-mt-touchpad.c b/src/evdev-mt-touchpad.c index c7f55c38..c06b5177 100644 --- a/src/evdev-mt-touchpad.c +++ b/src/evdev-mt-touchpad.c @@ -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 void -tp_thumb_detect(struct tp_dispatch *tp, struct tp_touch *t) +static inline const char* +thumb_state_to_str(enum tp_thumb_state state) { - /* once a thumb, always a thumb */ - if (!tp->thumb.detect_thumbs || t->is_thumb) + 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, uint64_t time) +{ + 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; } diff --git a/src/evdev-mt-touchpad.h b/src/evdev-mt-touchpad.h index 61c41660..89801d6d 100644 --- a/src/evdev-mt-touchpad.h +++ b/src/evdev-mt-touchpad.h @@ -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; }; diff --git a/test/touchpad.c b/test/touchpad.c index 27485ae7..329eeb38 100644 --- a/test/touchpad.c +++ b/test/touchpad.c @@ -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);