evdev: add new high performance button invoked scrolling mode

This patch contains a new button invoked scrolling mode optimized for
use with 5 button mice. The preferred invokation button is the thumb
button, BTN_SIDE in evdev parlance. The initial direction of motion of
the mouse following press of the invoking button sets the scroll axis
and the sign of the subsequent scroll reports. Once set the axis and sign
of the scroll reports can only be changed by releasing and re-pressing
the invocation button. This scheme allows for arbitrarily long scroll
distances without lifting and replacing the mouse.

Only one axis is active for scrolling at a time. The preferred scroll
motion is circular, but any type of mouse motion is acceptable. The
distance scrolled is proportional to the manhattan distance moved by the
mouse. The dynamic range is expanded non-linearly based on movement
velocity such that the +-2^8 input range of mouse motion reports is
expanded to a +-2^16 range of scroll reports. The scheme takes maximum
advantage of the precision of the mouse, both arbitrarily slow and quite
fast scrolling are accommodated. The performance of the mode is such that
the scroll thumb is only needed for reference, it need never be "touched".

The mode is optimized for y axis use. The y axis is preferred in that axis
determination happens more easily for the y axis than for the x axis. This
is accomplished by creating a wider movement cone for y axis selection
than for x axis selection. Also, x axis selection can only begin following
a delay, now 250ms, from button press, whereas y axis selection is
immediate with as little dead/wasted movement as possible. The reason
for this asymmetry is personal. I find that when I am reflexively using
the mode I may move the mouse more to the left than I am intending,
particularly while I am simultaneously pressing the button and starting
motion. This behavior can result in x axis selection when y axis is
intended. Since I almost never scroll the x axis, I solved the problem
by heavily preferring the y axis. The more deliberate criteria for
selecting the x axis do not bother me at all. Of course I could have
defeated x axis selection entirely, but after significant use of the mode
I find the current setup to be almost foolproof for the y axis while
still allowing x selection without using up another button.

The mode is optimized for the internet browsing use case, following
hyperlinks and scrolling the resulting pages. Care is taken to assure
that the user is not confused by overloading the scroll modal button
with any other action. Once the new mode is assigned to a button,
that button cannot be used for any other purpose. Therefore, those that
are used to using mouse buttons 4 and 5 for their historical design use,
browsing forward and backward, may not initially take to the new mode.
However, patience might be rewarded sooner than one would think. At least
for some, the new mode easily becomes intuitive and automatic. And since
scrolling is performed far more frequently on modern systems than browsing
forward and backward, the increased efficiency of entering the scrolling
mode more than pays for the loss of button browsing behavior.

I've not before submitted code to the libinput project. Therefore I have
limited the changes to a single source file, evdev.c. Also, I have
conditioned the submitted changes so that the new mode is only operational
on devices that have the udev environment variable LIBINPUT_SIGNED_SCROLL
defined with a value of "1". An example udev rule to add such an
environment variable to a Microsoft 5-Button Mouse with IntelliEye follows:

ACTION=="add|change", KERNEL=="event[0-9]*", ENV{ID_VENDOR_ID}=="045e", \
ENV{ID_MODEL_ID}=="0039", ENV{LIBINPUT_SIGNED_SCROLL}="1"

I don't really know how to go about getting this change incorporated into
the libinput project. I've attempted to make the change in such a way as
to not interfere with any existing functionality. Further, I've made the
new functionality available only via the above not easily discoverable
incantation. In this way I'm hoping to get the change incorporated into
the main project so that others can somewhat easily try out the new mode.
Once people are comfortable that the new mode doesn't break anything, the
next step would be to make it more publicly usable via changes further
libinput changes plus changes to xorg-xf86-input-libinput and/or the
similar? wayland input driver.

Signed-off-by: Paul Ausbeck <paula@alumni.cse.ucsc.edu>
This commit is contained in:
Paul Ausbeck 2021-11-03 16:19:45 -07:00
parent be3c09bc15
commit cd3a229fbb

View file

@ -53,18 +53,18 @@
#define DEFAULT_BUTTON_SCROLL_TIMEOUT ms2us(200)
enum evdev_device_udev_tags {
EVDEV_UDEV_TAG_INPUT = bit(0),
EVDEV_UDEV_TAG_KEYBOARD = bit(1),
EVDEV_UDEV_TAG_MOUSE = bit(2),
EVDEV_UDEV_TAG_TOUCHPAD = bit(3),
EVDEV_UDEV_TAG_TOUCHSCREEN = bit(4),
EVDEV_UDEV_TAG_TABLET = bit(5),
EVDEV_UDEV_TAG_JOYSTICK = bit(6),
EVDEV_UDEV_TAG_ACCELEROMETER = bit(7),
EVDEV_UDEV_TAG_TABLET_PAD = bit(8),
EVDEV_UDEV_TAG_POINTINGSTICK = bit(9),
EVDEV_UDEV_TAG_TRACKBALL = bit(10),
EVDEV_UDEV_TAG_SWITCH = bit(11),
EVDEV_UDEV_TAG_INPUT = bit(0),
EVDEV_UDEV_TAG_KEYBOARD = bit(1),
EVDEV_UDEV_TAG_MOUSE = bit(2),
EVDEV_UDEV_TAG_TOUCHPAD = bit(3),
EVDEV_UDEV_TAG_TOUCHSCREEN = bit(4),
EVDEV_UDEV_TAG_TABLET = bit(5),
EVDEV_UDEV_TAG_JOYSTICK = bit(6),
EVDEV_UDEV_TAG_ACCELEROMETER = bit(7),
EVDEV_UDEV_TAG_TABLET_PAD = bit(8),
EVDEV_UDEV_TAG_POINTINGSTICK = bit(9),
EVDEV_UDEV_TAG_TRACKBALL = bit(10),
EVDEV_UDEV_TAG_SWITCH = bit(11),
};
struct evdev_udev_tag_match {
@ -88,6 +88,17 @@ static const struct evdev_udev_tag_match evdev_udev_tag_matches[] = {
{"ID_INPUT_SWITCH", EVDEV_UDEV_TAG_SWITCH},
};
static inline bool
signed_scroll(struct evdev_device *device)
{
/* Abut the device signed scroll request bit next to the test bit.
* This is done to avoid making changes to other files in the initial
* signed scroll implementation and to not interfere with any other
* model flags.
*/
return device->model_flags & (EVDEV_MODEL_TEST_DEVICE >> 1);
}
static inline bool
parse_udev_flag(struct evdev_device *device,
struct udev_device *udev_device,
@ -230,9 +241,13 @@ evdev_button_scroll_button(struct evdev_device *device,
}
if (is_press) {
if (device->scroll.button < BTN_MOUSE + 5) {
/* For mouse buttons 1-5 (0x110 to 0x114) we apply a timeout before scrolling
* since the button could also be used for regular clicking. */
if (!signed_scroll(device) && device->scroll.button < BTN_MOUSE + 5) {
/* For mouse buttons 1-5 (0x110 to 0x114) apply a timeout before
* scrolling since the button could also be used for clicking.
* If the device has signed scrolling enabled don't do this.
* Intermixing clicking with modal scrolling is basically a bad idea,
* assume any signed scrolling device won't want this.
*/
enum timer_flags flags = TIMER_FLAG_NONE;
device->scroll.button_scroll_state = BUTTONSCROLL_BUTTON_DOWN;
@ -254,11 +269,12 @@ evdev_button_scroll_button(struct evdev_device *device,
time + DEFAULT_BUTTON_SCROLL_TIMEOUT,
flags);
} else {
/* For extra mouse buttons numbered 6 or more (0x115+) we assume it is
/* For extra mouse buttons numbered 6+ (0x115+) we assume it is
* dedicated exclusively to scrolling, so we don't apply the timeout
* in order to provide immediate scrolling responsiveness. */
device->scroll.button_scroll_state = BUTTONSCROLL_READY;
}
device->scroll.button_down_time = time;
evdev_log_debug(device, "btnscroll: down\n");
} else {
@ -272,16 +288,21 @@ evdev_button_scroll_button(struct evdev_device *device,
case BUTTONSCROLL_READY:
evdev_log_debug(device, "btnscroll: cancel\n");
/* If the button is released quickly enough or
* without scroll events, emit the
* button press/release events. */
evdev_pointer_post_button(device,
/* If the button is released quickly enough or without scroll events,
* emit the button press/release events.
* Don't do this for signed scrolling enabled devices as presumably
* anyone wanting this feature won't want to overload scrolling
* modal indication with an orthogonal operation like browsing.
*/
if (!signed_scroll(device) && device->scroll.button < BTN_MOUSE + 5) {
evdev_pointer_post_button(device,
device->scroll.button_down_time,
device->scroll.button,
LIBINPUT_BUTTON_STATE_PRESSED);
evdev_pointer_post_button(device, time,
evdev_pointer_post_button(device, time,
device->scroll.button,
LIBINPUT_BUTTON_STATE_RELEASED);
}
break;
case BUTTONSCROLL_SCROLLING:
evdev_log_debug(device, "btnscroll: up\n");
@ -306,6 +327,19 @@ evdev_pointer_notify_button(struct evdev_device *device,
return;
}
if (signed_scroll(device) &&
device->scroll.method == LIBINPUT_CONFIG_SCROLL_ON_BUTTON_DOWN &&
button == BTN_EXTRA && device->scroll.button == BTN_SIDE) {
/* If signed scrolling is enabled on the device,
* swallow BTN_EXTRA if scrolling on BTN_SIDE.
* Temporary solution for Logitech mouse equipped with two position
* capacitive touch sensor at mouse wheel location.
* This mouse can produce unwanted BTN_EXTRA events intermixed with
* desired BTN_SIDE events.
*/
return;
}
evdev_pointer_post_button(device, time, button, state);
}
@ -1050,10 +1084,10 @@ evdev_process_event(struct evdev_device *device, struct input_event *e)
struct evdev_dispatch *dispatch = device->dispatch;
uint64_t time = input_event_time(e);
#if 0
#if 0
evdev_print_event(device, e);
#endif
libinput_timer_flush(evdev_libinput_context(device), time);
dispatch->interface->process(dispatch, device, e, time);
@ -1548,6 +1582,12 @@ evdev_read_model_flags(struct evdev_device *device)
model_flags |= EVDEV_MODEL_TEST_DEVICE;
}
if (parse_udev_flag(device, device->udev_device,
"LIBINPUT_SIGNED_SCROLL")) {
evdev_log_debug(device, "requests signed scrolling\n");
model_flags |= EVDEV_MODEL_TEST_DEVICE >> 1;
}
return model_flags;
}
@ -2706,6 +2746,28 @@ evdev_start_scrolling(struct evdev_device *device,
device->scroll.direction |= bit(axis);
}
/* Nonlinear functions for signed scrolling dynamic range expansion.
* The basic idea is to expand the normal mouse range +-2^7 to +-2^14
* This allows both slow/controlled scrolling and very fast scrolling,
* fast enough to preclude the need for scrollbar use.
*
* The current scheme has only been tested on legacy, dpi < 1000, mice.
* It may need to be revisited for mice that report fast enough so that
* the number of mickeys in each report never reaches the expected
* +-2^7 range.
*/
static double sign(double x) { return x > 0 ? 1.0 : (x < 0 ? -1.0 : 0.0); }
static double ballistics_gain(double x, double dpi) {
/* Adjust nonlinear knee for legacy mice (dpi < 1000), otherwise
* acceleration becomes too touchy.
*/
double knee = dpi >= DEFAULT_MOUSE_DPI ? 1.0 : DEFAULT_MOUSE_DPI/dpi;
knee = knee*knee*knee*128.0;
x = fabs(x);
return x + floor(x*x*x/(knee));
}
void
evdev_post_scroll(struct evdev_device *device,
uint64_t time,
@ -2715,42 +2777,94 @@ evdev_post_scroll(struct evdev_device *device,
const struct normalized_coords *trigger;
struct normalized_coords event;
if (!evdev_is_scrolling(device,
LIBINPUT_POINTER_AXIS_SCROLL_VERTICAL))
device->scroll.buildup.y += delta->y;
if (!evdev_is_scrolling(device,
LIBINPUT_POINTER_AXIS_SCROLL_HORIZONTAL))
device->scroll.buildup.x += delta->x;
/* For signed scrolling, only accumulate axis selection motion
* until one axis is active. One axis active at a time.
*/
if (signed_scroll(device)) {
if (!evdev_is_scrolling(device,
LIBINPUT_POINTER_AXIS_SCROLL_VERTICAL) &&
!evdev_is_scrolling(device,
LIBINPUT_POINTER_AXIS_SCROLL_HORIZONTAL)) {
device->scroll.buildup.y += delta->y;
/* Defer x axis selection to make y axis selection more robust.
* In the signed scrolling use model, the x axis is used
* infrequently enough so that incurring a timing delay isn't
* objectionable and it further reduces spurious x axis
* activation when y is intended.
*/
if (time > device->scroll.button_down_time + ms2us(250))
device->scroll.buildup.x += delta->x;
}
} else {
if (!evdev_is_scrolling(device,
LIBINPUT_POINTER_AXIS_SCROLL_VERTICAL))
device->scroll.buildup.y += delta->y;
if (!evdev_is_scrolling(device,
LIBINPUT_POINTER_AXIS_SCROLL_HORIZONTAL))
device->scroll.buildup.x += delta->x;
}
trigger = &device->scroll.buildup;
/* If we're not scrolling yet, use a distance trigger: moving
past a certain distance starts scrolling */
if (!evdev_is_scrolling(device,
LIBINPUT_POINTER_AXIS_SCROLL_HORIZONTAL) &&
LIBINPUT_POINTER_AXIS_SCROLL_HORIZONTAL) &&
!evdev_is_scrolling(device,
LIBINPUT_POINTER_AXIS_SCROLL_VERTICAL)) {
if (fabs(trigger->y) >= device->scroll.threshold)
evdev_start_scrolling(device,
LIBINPUT_POINTER_AXIS_SCROLL_VERTICAL);
if (fabs(trigger->x) >= device->scroll.threshold)
evdev_start_scrolling(device,
LIBINPUT_POINTER_AXIS_SCROLL_HORIZONTAL);
LIBINPUT_POINTER_AXIS_SCROLL_VERTICAL)) {
if (signed_scroll(device)) {
/* Only one axis at a time with signed scrolling.
* Prefer y axis, predominant signed scroll use model, over x axis.
* Use a 1/100" mode trigger threshold.
*/
double t = device->dpi/100.0;
if (fabs(trigger->y) - 0.25*fabs(trigger->x) > t)
evdev_start_scrolling(device,
LIBINPUT_POINTER_AXIS_SCROLL_VERTICAL);
else if (0.25*fabs(trigger->x) - fabs(trigger->y) > t)
evdev_start_scrolling(device,
LIBINPUT_POINTER_AXIS_SCROLL_HORIZONTAL);
} else {
if (fabs(trigger->y) >= device->scroll.threshold)
evdev_start_scrolling(device,
LIBINPUT_POINTER_AXIS_SCROLL_VERTICAL);
if (fabs(trigger->x) >= device->scroll.threshold)
evdev_start_scrolling(device,
LIBINPUT_POINTER_AXIS_SCROLL_HORIZONTAL);
}
/* We're already scrolling in one direction. Require some
trigger speed to start scrolling in the other direction */
} else if (!evdev_is_scrolling(device,
LIBINPUT_POINTER_AXIS_SCROLL_VERTICAL)) {
if (fabs(delta->y) >= device->scroll.direction_lock_threshold)
evdev_start_scrolling(device,
LIBINPUT_POINTER_AXIS_SCROLL_VERTICAL);
} else if (!evdev_is_scrolling(device,
LIBINPUT_POINTER_AXIS_SCROLL_HORIZONTAL)) {
if (fabs(delta->x) >= device->scroll.direction_lock_threshold)
evdev_start_scrolling(device,
LIBINPUT_POINTER_AXIS_SCROLL_HORIZONTAL);
* trigger speed to start scrolling in the other direction.
* For the signed scrolling device, disallow dual axis scrolling
* on a single button.
*/
} else if (!signed_scroll(device)) {
if (!evdev_is_scrolling(device,
LIBINPUT_POINTER_AXIS_SCROLL_VERTICAL)) {
if (fabs(delta->y) >= device->scroll.direction_lock_threshold)
evdev_start_scrolling(device,
LIBINPUT_POINTER_AXIS_SCROLL_VERTICAL);
} else if (!evdev_is_scrolling(device,
LIBINPUT_POINTER_AXIS_SCROLL_HORIZONTAL)) {
if (fabs(delta->x) >= device->scroll.direction_lock_threshold)
evdev_start_scrolling(device,
LIBINPUT_POINTER_AXIS_SCROLL_HORIZONTAL);
}
}
event = *delta;
if (signed_scroll(device)) {
/* signed/freeform/scribble scrolling accumulator.
* Only one active axis per model invocation.
* Aaccumulate sign adjusted distance for both axes.
* Approximate euclidian distance with manhattan distance.
* Apply velocity ballistics to distance accumulated.
* Apply sign established at invocation.
*/
event.x = ballistics_gain(delta->x, device->dpi)*sign(trigger->x) +
ballistics_gain(delta->y, device->dpi)*sign(trigger->x);
event.y = ballistics_gain(delta->x, device->dpi)*sign(trigger->y) +
ballistics_gain(delta->y, device->dpi)*sign(trigger->y);
}
/* We use the trigger to enable, but the delta from this event for
* the actual scroll movement. Otherwise we get a jump once