From 865b2e748fa543418baccacbb4448ea6dafbd380 Mon Sep 17 00:00:00 2001 From: Peter Hutterer Date: Fri, 1 Nov 2024 14:30:51 +1000 Subject: tablet: ignore movements started outside the configured area If a tablet has an area configured and the pen goes into proximity outside this area, ignore all events from this sequence. This truly deactivates that area so it can even be used for e.g. placing a pen there. For simplicity, a sequence that starts outside the configured area will be completely ignored, i.e. moving into the tablet area will not trigger any fake proximity events as we cross into the allowed area. This requires quite a bit of effort and it's unclear if it's really needed by users - we can reconsider when we get complaints. We do however accept a proximity event within within 3% of the configured area. This gives us 6mm on a 200mm tablet where we can move in from the area and still have events work, i.e. some error margin for where a user needs both an area and work closes to the edge of that area. Part-of: --- src/evdev-tablet.c | 88 ++++++++++++++++----- src/evdev-tablet.h | 1 + test/test-tablet.c | 221 ++++++++++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 280 insertions(+), 30 deletions(-) diff --git a/src/evdev-tablet.c b/src/evdev-tablet.c index 27177b53..7ff97f2d 100644 --- a/src/evdev-tablet.c +++ b/src/evdev-tablet.c @@ -476,6 +476,27 @@ normalize_wheel(struct tablet_dispatch *tablet, return value * device->scroll.wheel_click_angle.x; } +static bool +is_inside_area(struct tablet_dispatch *tablet, + const struct device_coords *point, + double normalized_margin) +{ + if (tablet->area.rect.x1 == 0.0 && tablet->area.rect.x2 == 1.0 && + tablet->area.rect.y1 == 0.0 && tablet->area.rect.y2 == 1.0) + return true; + + assert(normalized_margin > 0.0); + assert(normalized_margin <= 1.0); + + int xmargin = (tablet->area.x.maximum - tablet->area.x.minimum) * normalized_margin; + int ymargin = (tablet->area.y.maximum - tablet->area.y.minimum) * normalized_margin; + + return (point->x >= tablet->area.x.minimum - xmargin && + point->x <= tablet->area.x.maximum + xmargin && + point->y >= tablet->area.y.minimum - ymargin && + point->y <= tablet->area.y.maximum + ymargin); +} + static void apply_tablet_area(struct tablet_dispatch *tablet, struct evdev_device *device, @@ -1818,17 +1839,20 @@ tablet_send_proximity_out(struct tablet_dispatch *tablet, if (!tablet_has_status(tablet, TABLET_TOOL_LEAVING_PROXIMITY)) return false; - tablet_notify_proximity(&device->base, - time, - tool, - LIBINPUT_TABLET_TOOL_PROXIMITY_STATE_OUT, - tablet->changed_axes, - axes, - &tablet->area.x, - &tablet->area.y); + if (!tablet_has_status(tablet, TABLET_TOOL_OUTSIDE_AREA)) { + tablet_notify_proximity(&device->base, + time, + tool, + LIBINPUT_TABLET_TOOL_PROXIMITY_STATE_OUT, + tablet->changed_axes, + axes, + &tablet->area.x, + &tablet->area.y); + } tablet_set_status(tablet, TABLET_TOOL_OUT_OF_PROXIMITY); tablet_unset_status(tablet, TABLET_TOOL_LEAVING_PROXIMITY); + tablet_unset_status(tablet, TABLET_TOOL_OUTSIDE_AREA); tablet_reset_changed_axes(tablet); axes->delta.x = 0; @@ -2172,19 +2196,45 @@ reprocess: if (tablet_has_status(tablet, TABLET_TOOL_IN_CONTACT)) tablet_set_status(tablet, TABLET_TOOL_LEAVING_CONTACT); apply_pressure_range_configuration(tablet, tool); - } else if (tablet_has_status(tablet, TABLET_TOOL_ENTERING_PROXIMITY)) { - tablet_mark_all_axes_changed(tablet, tool); - update_pressure_offset(tablet, device, tool); - detect_pressure_offset(tablet, device, tool); - detect_tool_contact(tablet, device, tool); - sanitize_tablet_axes(tablet, tool); - } else if (tablet_has_status(tablet, TABLET_AXES_UPDATED)) { - update_pressure_offset(tablet, device, tool); - detect_tool_contact(tablet, device, tool); - sanitize_tablet_axes(tablet, tool); + } else if (!tablet_has_status(tablet, TABLET_TOOL_OUTSIDE_AREA)) { + if (tablet_has_status(tablet, TABLET_TOOL_ENTERING_PROXIMITY)) { + /* If we get into proximity outside the tablet area, we ignore + * that whole sequence of events even if we later move into + * the allowed area. This may be bad UX but it's complicated to + * implement so let's wait for someone to actually complain + * about it. + * + * We allow a margin of 3% (6mm on a 200mm tablet) to be "within" + * the area - there we clip to the area but do not ignore the + * sequence. + */ + const struct device_coords point = { + device->abs.absinfo_x->value, + device->abs.absinfo_y->value, + }; + + const double margin = 0.03; + if (is_inside_area(tablet, &point, margin)) { + tablet_mark_all_axes_changed(tablet, tool); + update_pressure_offset(tablet, device, tool); + detect_pressure_offset(tablet, device, tool); + detect_tool_contact(tablet, device, tool); + sanitize_tablet_axes(tablet, tool); + } else { + tablet_set_status(tablet, TABLET_TOOL_OUTSIDE_AREA); + tablet_unset_status(tablet, TABLET_TOOL_ENTERING_PROXIMITY); + } + } else if (tablet_has_status(tablet, TABLET_AXES_UPDATED)) { + update_pressure_offset(tablet, device, tool); + detect_tool_contact(tablet, device, tool); + sanitize_tablet_axes(tablet, tool); + } + } - tablet_send_events(tablet, tool, device, time); + if (!tablet_has_status(tablet, TABLET_TOOL_OUTSIDE_AREA)) { + tablet_send_events(tablet, tool, device, time); + } if (process_tool_twice) goto reprocess; diff --git a/src/evdev-tablet.h b/src/evdev-tablet.h index 3d34c446..de0349ef 100644 --- a/src/evdev-tablet.h +++ b/src/evdev-tablet.h @@ -51,6 +51,7 @@ enum tablet_status { TABLET_TOOL_ENTERING_CONTACT = bit(9), TABLET_TOOL_LEAVING_CONTACT = bit(10), TABLET_TOOL_OUT_OF_RANGE = bit(11), + TABLET_TOOL_OUTSIDE_AREA = bit(12), }; struct button_state { diff --git a/test/test-tablet.c b/test/test-tablet.c index e7daa219..6ee40e53 100644 --- a/test/test-tablet.c +++ b/test/test-tablet.c @@ -4021,7 +4021,8 @@ START_TEST(tablet_area_set_rectangle) }; double x, y; double *scaled, *unscaled; - bool use_vertical = !!_i; /* ranged test */ + bool use_vertical = abs(_i) % 2 == 0; /* ranged test */ + int direction = _i < 0 ? -1 : 1; /* ranged test */ if (libevdev_has_property(dev->evdev, INPUT_PROP_DIRECT)) return LITEST_NOT_APPLICABLE; @@ -4046,14 +4047,15 @@ START_TEST(tablet_area_set_rectangle) litest_drain_events(li); - /* move vertically through the center */ - litest_tablet_proximity_in(dev, 5, 5, axes); + /* move from the center out */ + litest_tablet_proximity_in(dev, 50, 50, axes); libinput_dispatch(li); get_tool_xy(li, &x, &y); - litest_assert_double_eq_epsilon(*scaled, 0.0, 2); - litest_assert_double_eq_epsilon(*unscaled, 5.0, 2); + litest_assert_double_eq_epsilon(*scaled, 50.0, 2); + litest_assert_double_eq_epsilon(*unscaled, 50.0, 2); - for (int i = 10; i <= 100; i += 5) { + int i; + for (i = 50; i > 0 && i <= 100; i += 5 * direction) { /* Negate any smoothing */ litest_tablet_motion(dev, i, i, axes); litest_tablet_motion(dev, i - 1, i, axes); @@ -4072,9 +4074,10 @@ START_TEST(tablet_area_set_rectangle) litest_assert_double_eq_epsilon(*unscaled, i, 2); } + double final_stop = max(0.0, min(100.0, i)); /* Push through any smoothing */ - litest_tablet_motion(dev, 100, 100, axes); - litest_tablet_motion(dev, 100, 100, axes); + litest_tablet_motion(dev, final_stop, final_stop, axes); + litest_tablet_motion(dev, final_stop, final_stop, axes); libinput_dispatch(li); litest_drain_events(li); @@ -4082,9 +4085,202 @@ START_TEST(tablet_area_set_rectangle) litest_timeout_tablet_proxout(); libinput_dispatch(li); get_tool_xy(li, &x, &y); - litest_assert_double_eq_epsilon(x, 100, 1); - litest_assert_double_eq_epsilon(y, 100, 1); + litest_assert_double_eq_epsilon(x, final_stop, 1); + litest_assert_double_eq_epsilon(y, final_stop, 1); + +} +END_TEST + +START_TEST(tablet_area_set_rectangle_move_outside) +{ + struct litest_device *dev = litest_current_device(); + struct libinput *li = dev->libinput; + struct libinput_device *d = dev->libinput_device; + struct axis_replacement axes[] = { + { ABS_DISTANCE, 10 }, + { ABS_PRESSURE, 0 }, + { -1, -1 } + }; + double x, y; + + if (libevdev_has_property(dev->evdev, INPUT_PROP_DIRECT)) + return LITEST_NOT_APPLICABLE; + + struct libinput_config_area_rectangle rect = { + 0.25, 0.25, 0.75, 0.75, + }; + + enum libinput_config_status status = libinput_device_config_area_set_rectangle(d, &rect); + litest_assert_enum_eq(status, LIBINPUT_CONFIG_STATUS_SUCCESS); + + litest_drain_events(li); + + /* move in/out of prox outside the area */ + litest_tablet_proximity_in(dev, 5, 5, axes); + litest_tablet_proximity_out(dev); + libinput_dispatch(li); + litest_timeout_tablet_proxout(); + libinput_dispatch(li); + litest_assert_empty_queue(li); + + x = 5; + y = 5; + /* Move around the area - since we stay outside the area expect no events */ + litest_tablet_proximity_in(dev, x, y, axes); + libinput_dispatch(li); + for (; x < 90; x += 5) { + litest_tablet_motion(dev, x, y, axes); + libinput_dispatch(li); + litest_assert_empty_queue(li); + } + litest_axis_set_value(axes, ABS_PRESSURE, 30); + litest_tablet_tip_down(dev, x, y, axes); + for (; y < 90; y += 5) { + litest_tablet_motion(dev, x, y, axes); + libinput_dispatch(li); + litest_assert_empty_queue(li); + } + litest_axis_set_value(axes, ABS_PRESSURE, 0); + litest_tablet_tip_up(dev, x, y, axes); + for (; x > 5; x -= 5) { + litest_tablet_motion(dev, x, y, axes); + libinput_dispatch(li); + litest_assert_empty_queue(li); + } + litest_button_click(dev, BTN_STYLUS, LIBINPUT_BUTTON_STATE_PRESSED); + litest_button_click(dev, BTN_STYLUS, LIBINPUT_BUTTON_STATE_RELEASED); + litest_axis_set_value(axes, ABS_PRESSURE, 30); + litest_tablet_tip_down(dev, x, y, axes); + for (; y > 5; y -= 5) { + litest_tablet_motion(dev, x, y, axes); + libinput_dispatch(li); + litest_assert_empty_queue(li); + } + litest_axis_set_value(axes, ABS_PRESSURE, 0); + litest_tablet_tip_up(dev, x, y, axes); + + litest_tablet_proximity_out(dev); + litest_timeout_tablet_proxout(); + libinput_dispatch(li); + litest_assert_empty_queue(li); +} +END_TEST + +START_TEST(tablet_area_set_rectangle_move_outside_to_inside) +{ + struct litest_device *dev = litest_current_device(); + struct libinput *li = dev->libinput; + struct libinput_device *d = dev->libinput_device; + struct axis_replacement axes[] = { + { ABS_DISTANCE, 10 }, + { ABS_PRESSURE, 0 }, + { -1, -1 } + }; + double x, y; + + if (libevdev_has_property(dev->evdev, INPUT_PROP_DIRECT)) + return LITEST_NOT_APPLICABLE; + + struct libinput_config_area_rectangle rect = { + 0.25, 0.25, 0.75, 0.75, + }; + + enum libinput_config_status status = libinput_device_config_area_set_rectangle(d, &rect); + litest_assert_enum_eq(status, LIBINPUT_CONFIG_STATUS_SUCCESS); + + litest_drain_events(li); + + x = 5; + y = 50; + /* Move into the center of the area - since we started outside the area + * expect no events */ + litest_tablet_proximity_in(dev, x, y, axes); + libinput_dispatch(li); + for (; x < 50; x += 5) { + litest_tablet_motion(dev, x, y, axes); + libinput_dispatch(li); + litest_assert_empty_queue(li); + } + litest_button_click(dev, BTN_STYLUS, LIBINPUT_BUTTON_STATE_PRESSED); + litest_button_click(dev, BTN_STYLUS, LIBINPUT_BUTTON_STATE_RELEASED); + litest_axis_set_value(axes, ABS_PRESSURE, 30); + litest_tablet_tip_down(dev, x, y, axes); + litest_axis_set_value(axes, ABS_PRESSURE, 0); + litest_tablet_tip_up(dev, x, y, axes); + litest_tablet_proximity_out(dev); + litest_timeout_tablet_proxout(); + libinput_dispatch(li); + litest_assert_empty_queue(li); + + y = 5; + x = 50; + litest_tablet_proximity_in(dev, x, y, axes); + for (; y < 50; y += 5) { + litest_tablet_motion(dev, x, y, axes); + libinput_dispatch(li); + litest_assert_empty_queue(li); + } + litest_button_click(dev, BTN_STYLUS, LIBINPUT_BUTTON_STATE_PRESSED); + litest_button_click(dev, BTN_STYLUS, LIBINPUT_BUTTON_STATE_RELEASED); + litest_axis_set_value(axes, ABS_PRESSURE, 30); + litest_tablet_tip_down(dev, x, y, axes); + litest_axis_set_value(axes, ABS_PRESSURE, 0); + litest_tablet_tip_up(dev, x, y, axes); + litest_tablet_proximity_out(dev); + + litest_timeout_tablet_proxout(); + libinput_dispatch(li); + litest_assert_empty_queue(li); +} +END_TEST + +START_TEST(tablet_area_set_rectangle_move_in_margin) +{ + struct litest_device *dev = litest_current_device(); + struct libinput *li = dev->libinput; + struct libinput_device *d = dev->libinput_device; + struct libinput_event *ev; + struct libinput_event_tablet_tool *tev; + struct axis_replacement axes[] = { + { ABS_DISTANCE, 10 }, + { ABS_PRESSURE, 0 }, + { -1, -1 } + }; + double x, y; + + if (libevdev_has_property(dev->evdev, INPUT_PROP_DIRECT)) + return LITEST_NOT_APPLICABLE; + + struct libinput_config_area_rectangle rect = { + 0.25, 0.25, 0.75, 0.75, + }; + + enum libinput_config_status status = libinput_device_config_area_set_rectangle(d, &rect); + litest_assert_enum_eq(status, LIBINPUT_CONFIG_STATUS_SUCCESS); + + litest_drain_events(li); + + /* move in/out of prox outside the area but within the margin */ + litest_tablet_proximity_in(dev, 24, 24, axes); + litest_tablet_proximity_out(dev); + libinput_dispatch(li); + litest_timeout_tablet_proxout(); + libinput_dispatch(li); + ev = libinput_get_event(li); + tev = litest_is_proximity_event(ev, LIBINPUT_TABLET_TOOL_PROXIMITY_STATE_IN); + x = libinput_event_tablet_tool_get_x(tev); + y = libinput_event_tablet_tool_get_y(tev); + litest_assert_double_eq(x, 0.0); + litest_assert_double_eq(y, 0.0); + libinput_event_destroy(ev); + ev = libinput_get_event(li); + tev = litest_is_proximity_event(ev, LIBINPUT_TABLET_TOOL_PROXIMITY_STATE_OUT); + x = libinput_event_tablet_tool_get_x(tev); + y = libinput_event_tablet_tool_get_y(tev); + litest_assert_double_eq(x, 0.0); + litest_assert_double_eq(y, 0.0); + libinput_event_destroy(ev); } END_TEST @@ -6768,7 +6964,7 @@ TEST_COLLECTION(tablet) struct range with_timeout = { 0, 2 }; struct range xyaxes = { ABS_X, ABS_Y + 1 }; struct range tilt_cases = {TILT_MINIMUM, TILT_MAXIMUM + 1}; - struct range vert_horiz = { 0, 2 }; + struct range vert_horiz = { -2, 2 }; litest_add(tool_ref, LITEST_TABLET | LITEST_TOOL_SERIAL, LITEST_ANY); litest_add(tool_user_data, LITEST_TABLET | LITEST_TOOL_SERIAL, LITEST_ANY); @@ -6853,6 +7049,9 @@ TEST_COLLECTION(tablet) litest_add(tablet_area_has_rectangle, LITEST_TABLET, LITEST_ANY); litest_add(tablet_area_set_rectangle_invalid, LITEST_TABLET, LITEST_ANY); litest_add_ranged(tablet_area_set_rectangle, LITEST_TABLET, LITEST_ANY, &vert_horiz); + litest_add(tablet_area_set_rectangle_move_outside, LITEST_TABLET, LITEST_ANY); + litest_add(tablet_area_set_rectangle_move_outside_to_inside, LITEST_TABLET, LITEST_ANY); + litest_add(tablet_area_set_rectangle_move_in_margin, LITEST_TABLET, LITEST_ANY); litest_add(tablet_pressure_min_max, LITEST_TABLET, LITEST_ANY); /* Tests for pressure offset with distance */ -- cgit v1.2.3