tablet: ignore movements started outside the configured area
authorPeter Hutterer <peter.hutterer@who-t.net>
Fri, 1 Nov 2024 04:30:51 +0000 (14:30 +1000)
committerPeter Hutterer <peter.hutterer@who-t.net>
Tue, 5 Nov 2024 02:10:48 +0000 (12:10 +1000)
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: <https://gitlab.freedesktop.org/libinput/libinput/-/merge_requests/1013>

src/evdev-tablet.c
src/evdev-tablet.h
test/test-tablet.c

index 27177b5356994181535e5cb7b73cf6b7f8c05efb..7ff97f2da2bf2ff9a6602bdb258c76f98e45ca10 100644 (file)
@@ -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;
index 3d34c446e865bdf85a092f95e55c2baf80f30139..de0349eff629e0c34f53fe16dd79e4632d4e231b 100644 (file)
@@ -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 {
index e7daa219d9f59d5e77a0615c8a2caa24c3a057b8..6ee40e53da21ebdc057a5d7c81fd8fab2f40a605 100644 (file)
@@ -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 */