TIP 684: Add Support for Touchpad Scrolling to Tk

Login
Author:        Marc Culler
State:         Final
Type:          Project
Vote:		Done
Created:       28-11-2023
Tcl-Version:   8.7
Tk-Branch:     touchpad_events
Vote-Summary:	7 / 0 / 0
Votes-For:	MC,KW,FV,SL,JN,DF,AK
Votes-Against:	none
Votes-Present:	none

Abstract

This TIP proposes to add Tk support for scrolling via the two-finger gesture with a touchpad. It targets Tk 8.7 and the upcoming Tk 9.0.

Background

Tk has supported mouse wheels since 1998. The design was that a system-generated event reporting a click of the mouse wheel would be translated to a modified TkKeyEvent. The type of the TkKeyEvent would be a Tk-specific value (MouseWheelEvent), and the unsigned integer keycode field of the TkKeyEvent would be repurposed to hold a signed delta value. In the original implementation the value used in the keycode field varied between platforms.

When it became common for laptops to have touchpads which can interpret the two-finger scroll gesture, Tk inherited support for that gesture by default. On all three of the major Tk platforms - Windows, macOS and linux - the system would report touchpad scroll events by using the same framework as was used for mouse wheel events. Windows reports touchpad scroll events with MOUSEWHEEL and HMOUSEWHEEL messages, but these messages have delta values which are not multiples of 120. MacOS uses NSScrollWheel events to report touchpad scrolls, but sets the hasPreciseScrollingDeltas property to YES. Linux sends Button-4 and Button-5 events for touchpad scrolls. This accidental Tk support for touchpads did not work great - it was not able to take advantage of the high precision - but it was usable.

In 2020, TIP #563 was passed. The stated purpose of the TIP was to add a feature which made spinning the mouse wheel while the mouse pointer was inside a horizontal scrollbar cause horizontal scrolling to occur. The implementation of the TIP also devoted considerable effort to uniformizing the MouseWheel bindings across platforms.

However, the effect of TIP #563 on Tk's accidental touchpad interface was overlooked. A two-finger touchpad scroll almost always has non-zero values for both Δx and Δy. The implementation of TIP #563 had the effect that the result of performing a two-finger scroll gesture while the mouse pointer is in a vertical scrollbar would be to scroll vertically by Δx + Δy. In particular, when a user was attempting to slowly scroll downward the actual scrolling could move in both directions. This problem was reported in ticket [de3bbbcb68]

Rationale

This TIP proposes to both fix the issue reported in the ticket mentioned above and to improve Tk by providing deliberate support for scrolling with the two-finger gesture on a touchpad.

Limitations

The implementation is currently limited to Windows and macOS Aqua. An X11 implementation using libinput should be possible, but would require understanding how to incorporate libinput into Tk. Testing indicates that the undesirable artifacts are less severe in linux.

Specification

The implementation of this TIP adds a new Tk-specific event type named TouchpadScroll. As with MouseWheelEvent, a TouchpadScroll event repurposes the TkKeyEvent struct. The event type is set to TouchpadScroll and the 32 bit keycode field is used to store both Δx and Δy as signed 16 bit values with Δx in the high order 16 bits and Δy in the low order 16 bits.

To facilitate unpacking the delta values the implementation provides a utility function named ::tk::PreciseScrollDeltas. It can be used in a binding script as follows:

lassign [tk::PreciseScrollDeltas %D] deltaX deltaY

To take full advantage of touchpad scrolling a widget should be able to scroll at pixel resolution. While this is the case for text and canvas widgets, there are many other widgets, such as listbox and treeview, which can only scroll by "units", i.e. lines or average-width characters. Since touchpad events are generated about 60 times per second during a scroll gesture, it does not work to scroll by one unit each time that a TouchpadScroll event is received. In order to deal with this issue, the serial field of the TkKeyEvent is used to hold a counter which is incremented each time that a TouchpadScroll event is generated. This allows a binding script to, for example, only respond to every 5th TouchpadScroll event by testing if the counter is divisible by 5. The value of the serial field is accessible in a binding script via the %# substitution.

The TIP implementation includes TouchpadScroll event bindings for all of the standard tk and ttk widgets for which such events are appropriate. MouseWheelEvent bindings are also included, so these widgets can be scrolled with either a mouse wheel or a touchpad.

Backwards Compatibility

If the TIP implementation were merged, MouseWheelEvent bindings in third-party widgets would continue to work as before, provided that the user is using a mouse wheel. However, the two-finger scroll gesture would no longer cause MouseWheelEvents to be generated. Hence these third-party widgets would need to add TouchpadScroll bindings in order to be scrollable with the two-finger gesture.

Implementation

The implementation of this TIP is in the Tk touchpad_events branch.

Acknowledgements

This TIP project benefited from contributions from several people besides the author. Christopher Chavez provided a great deal of helpful background information about how scrolling works. Csaba Nemethi provided both feedback and code for the project. Nicolas Bats, who first reported the scrolling problem, contributed extensive testing. Thanks to all of them.

Copyright

This document has been placed in the public domain.