Skip to content

Commit 0a0d914

Browse files
committed
x11: Implement precision/pixel scrolling
Manual rebase of libsdl-org#5382 with some changes for SDL3 (thanks @wooosh).
1 parent 7b501ae commit 0a0d914

File tree

8 files changed

+196
-26
lines changed

8 files changed

+196
-26
lines changed

cmake/sdlchecks.cmake

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,17 @@ macro(CheckX11)
416416
endif()
417417
set(SDL_VIDEO_DRIVER_X11_XINPUT2 1)
418418

419+
# Check for scroll info
420+
check_c_source_compiles("
421+
#include <X11/Xlib.h>
422+
#include <X11/Xproto.h>
423+
#include <X11/extensions/XInput2.h>
424+
XIScrollClassInfo *s;
425+
int main(int argc, char **argv) {}" HAVE_XINPUT2_SCROLLINFO)
426+
if(HAVE_XINPUT2_SCROLLINFO)
427+
set(SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_SCROLLINFO 1)
428+
endif()
429+
419430
# Check for multitouch
420431
check_c_source_compiles_static("
421432
#include <X11/Xlib.h>

include/build_config/SDL_build_config.h.cmake

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,7 @@
430430
#cmakedefine SDL_VIDEO_DRIVER_X11_XFIXES 1
431431
#cmakedefine SDL_VIDEO_DRIVER_X11_XINPUT2 1
432432
#cmakedefine SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_MULTITOUCH 1
433+
#cmakedefine SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_SCROLLINFO 1
433434
#cmakedefine SDL_VIDEO_DRIVER_X11_XRANDR 1
434435
#cmakedefine SDL_VIDEO_DRIVER_X11_XSCRNSAVER 1
435436
#cmakedefine SDL_VIDEO_DRIVER_X11_XSHAPE 1

src/video/x11/SDL_x11events.c

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1032,6 +1032,16 @@ void X11_HandleButtonPress(SDL_VideoDevice *_this, SDL_WindowData *windowdata, S
10321032
}
10331033

10341034
if (X11_IsWheelEvent(display, button, &xticks, &yticks)) {
1035+
1036+
/* If the system version of XInput2 supports precision scrolling, then
1037+
* it will perform 2 way emulation between the old button system and the
1038+
* new valuator system. handling both results in duplicate events, so if
1039+
* the system supports xinput, we ignore all wheel button events.
1040+
*/
1041+
if (videodata->xinput_scrolling) {
1042+
return;
1043+
}
1044+
10351045
SDL_SendMouseWheel(timestamp, window, mouseID, (float)-xticks, (float)yticks, SDL_MOUSEWHEEL_NORMAL);
10361046
} else {
10371047
bool ignore_click = false;

src/video/x11/SDL_x11video.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,7 @@ void X11_VideoQuit(SDL_VideoDevice *_this)
468468
}
469469
#endif
470470

471+
X11_QuitXinput2(_this);
471472
X11_QuitModes(_this);
472473
X11_QuitKeyboard(_this);
473474
X11_QuitMouse(_this);

src/video/x11/SDL_x11video.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ struct SDL_VideoData
139139
SDL_XInput2DeviceInfo *mouse_device_info;
140140
int xinput_master_pointer_device;
141141
bool xinput_hierarchy_changed;
142+
bool xinput_scrolling;
142143

143144
int xrandr_event_base;
144145
struct

src/video/x11/SDL_x11window.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -475,7 +475,7 @@ static void SetupWindowInput(SDL_VideoDevice *_this, SDL_Window *window)
475475
}
476476
#endif
477477

478-
X11_Xinput2SelectTouch(_this, window);
478+
X11_Xinput2Select(_this, window);
479479

480480
{
481481
unsigned int x11_keyboard_events = KeyPressMask | KeyReleaseMask;

src/video/x11/SDL_x11xinput2.c

Lines changed: 169 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,20 @@ static bool xinput2_multitouch_supported;
4545
* this extension */
4646
static int xinput2_opcode;
4747

48+
#ifdef SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_SCROLLINFO
49+
typedef struct
50+
{
51+
int device_id;
52+
int axis_id[2]; // 0 = horizontal, 1 = vertical
53+
double prev_coord[2];
54+
bool prev_coord_valid[2];
55+
double line_unit[2]; // Specifies the amount of coordinate change to scroll one line
56+
} SDL_XInput2ScrollableDevice;
57+
58+
static SDL_XInput2ScrollableDevice *scrollable_devices;
59+
static int scrollable_device_count;
60+
#endif
61+
4862
static void parse_valuators(const double *input_values, const unsigned char *mask, int mask_len,
4963
double *output_values, int output_values_len)
5064
{
@@ -95,6 +109,56 @@ static SDL_Window *xinput2_get_sdlwindow(SDL_VideoData *videodata, Window window
95109
return windowdata ? windowdata->window : NULL;
96110
}
97111

112+
#ifdef SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_SCROLLINFO
113+
static void xinput2_reset_scrollable_devices(SDL_VideoData *videodata)
114+
{
115+
for (int i = 0; i < scrollable_device_count; ++i) {
116+
scrollable_devices[i].prev_coord_valid[0] = false;
117+
scrollable_devices[i].prev_coord_valid[1] = false;
118+
}
119+
}
120+
121+
static void xinput2_parse_scrollable_valuators(const XIDeviceEvent *xev)
122+
{
123+
SDL_Mouse *mouse = SDL_GetMouse();
124+
125+
for (int i = 0; i < scrollable_device_count; ++i) {
126+
SDL_XInput2ScrollableDevice *sd = &scrollable_devices[i];
127+
// Filter out events from the master device to avoid duplicates.
128+
if (xev->sourceid == sd->device_id) {
129+
int values_i = 0;
130+
for (int j = 0; j < xev->valuators.mask_len * 8; ++j) {
131+
if (!XIMaskIsSet(xev->valuators.mask, j)) {
132+
continue;
133+
}
134+
135+
for (int k = 0; k < 2; ++k) {
136+
if (sd->axis_id[k] == j) {
137+
const double current_val = xev->valuators.values[values_i];
138+
const double delta = (sd->prev_coord[k] - current_val) / sd->line_unit[k];
139+
/* Ignore very large jumps that can happen as a result of overflowing
140+
* the maximum range, as the driver will reset the position to zero
141+
* at "something that's close to 2^32".
142+
*
143+
* http://who-t.blogspot.com/2012/06/xi-21-protocol-design-issues.html
144+
*/
145+
if (sd->prev_coord_valid[k] && SDL_fabs(delta) < (double)SDL_MAX_UINT32 * 0.95) {
146+
const double x = k == 0 ? delta : 0;
147+
const double y = k == 1 ? delta : 0;
148+
SDL_SendMouseWheel(xev->time, mouse->focus, (SDL_MouseID)xev->sourceid, x, y, SDL_MOUSEWHEEL_NORMAL);
149+
}
150+
sd->prev_coord[k] = current_val;
151+
sd->prev_coord_valid[k] = true;
152+
}
153+
}
154+
155+
++values_i;
156+
}
157+
}
158+
}
159+
}
160+
#endif // SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_SCROLLINFO
161+
98162
#ifdef SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_MULTITOUCH
99163
static void xinput2_normalize_touch_coordinates(SDL_Window *window, double in_x, double in_y, float *out_x, float *out_y)
100164
{
@@ -126,7 +190,7 @@ bool X11_InitXinput2(SDL_VideoDevice *_this)
126190

127191
int version = 0;
128192
XIEventMask eventmask;
129-
unsigned char mask[4] = { 0, 0, 0, 0 };
193+
unsigned char mask[5] = { 0, 0, 0, 0, 0 };
130194
int event, err;
131195

132196
/* XInput2 is required for relative mouse mode, so you probably want to leave this enabled */
@@ -156,6 +220,11 @@ bool X11_InitXinput2(SDL_VideoDevice *_this)
156220

157221
xinput2_initialized = true;
158222

223+
#ifdef SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_SCROLLINFO
224+
/* Xinput 2.1 is required to provide precision scrolling info */
225+
data->xinput_scrolling = xinput2_version_atleast(version, 2, 1);
226+
#endif
227+
159228
#ifdef SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_MULTITOUCH // Multitouch needs XInput 2.2
160229
xinput2_multitouch_supported = xinput2_version_atleast(version, 2, 2);
161230
#endif
@@ -171,6 +240,12 @@ bool X11_InitXinput2(SDL_VideoDevice *_this)
171240
XISetMask(mask, XI_RawButtonPress);
172241
XISetMask(mask, XI_RawButtonRelease);
173242

243+
#ifdef SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_SCROLLINFO
244+
if (data->xinput_scrolling) {
245+
XISetMask(mask, XI_Motion);
246+
}
247+
#endif
248+
174249
#ifdef SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_MULTITOUCH
175250
// Enable raw touch events if supported
176251
if (X11_Xinput2IsMultitouchSupported()) {
@@ -199,6 +274,15 @@ bool X11_InitXinput2(SDL_VideoDevice *_this)
199274
#endif
200275
}
201276

277+
void X11_QuitXinput2(SDL_VideoDevice *_this)
278+
{
279+
#ifdef SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_SCROLLINFO
280+
SDL_free(scrollable_devices);
281+
scrollable_devices = NULL;
282+
scrollable_device_count = 0;
283+
#endif
284+
}
285+
202286
#ifdef SDL_VIDEO_DRIVER_X11_XINPUT2
203287
// xi2 device went away? take it out of the list.
204288
static void xinput2_remove_device_info(SDL_VideoData *videodata, const int device_id)
@@ -435,13 +519,19 @@ void X11_HandleXinput2Event(SDL_VideoDevice *_this, XGenericEventCookie *cookie)
435519
}
436520
} break;
437521

522+
#ifdef SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_SCROLLINFO
523+
case XI_Enter:
524+
xinput2_reset_scrollable_devices(videodata);
525+
break;
526+
#endif
527+
438528
/* Register to receive XI_Motion (which deactivates MotionNotify), so that we can distinguish
439529
real mouse motions from synthetic ones, for multitouch and pen support. */
440530
case XI_Motion:
441531
{
442532
const XIDeviceEvent *xev = (const XIDeviceEvent *)cookie->data;
443-
#ifdef SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_MULTITOUCH
444-
bool pointer_emulated = ((xev->flags & XIPointerEmulated) != 0);
533+
#if defined(SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_SCROLLINFO) || defined(SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_MULTITOUCH)
534+
bool pointer_emulated = (xev->flags & XIPointerEmulated) != 0;
445535
#else
446536
bool pointer_emulated = false;
447537
#endif
@@ -467,14 +557,23 @@ void X11_HandleXinput2Event(SDL_VideoDevice *_this, XGenericEventCookie *cookie)
467557
SDL_SendPenAxis(0, pen->pen, window, (SDL_PenAxis) i, axes[i]);
468558
}
469559
}
470-
} else if (!pointer_emulated && xev->deviceid == videodata->xinput_master_pointer_device) {
471-
// Use the master device for non-relative motion, as the slave devices can seemingly lag behind.
472-
SDL_Mouse *mouse = SDL_GetMouse();
473-
if (!mouse->relative_mode) {
474-
SDL_Window *window = xinput2_get_sdlwindow(videodata, xev->event);
475-
if (window) {
476-
X11_ProcessHitTest(_this, window->internal, (float)xev->event_x, (float)xev->event_y, false);
477-
SDL_SendMouseMotion(0, window, SDL_GLOBAL_MOUSE_ID, false, (float)xev->event_x, (float)xev->event_y);
560+
} else {
561+
#if SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_SCROLLINFO
562+
// Filter out events from the master device to avoid duplicates.
563+
if (videodata->xinput_scrolling && xev->deviceid == xev->sourceid) {
564+
xinput2_parse_scrollable_valuators(xev);
565+
}
566+
#endif
567+
568+
if (!pointer_emulated && xev->deviceid == videodata->xinput_master_pointer_device) {
569+
// Use the master device for non-relative motion, as the slave devices can seemingly lag behind.
570+
SDL_Mouse *mouse = SDL_GetMouse();
571+
if (!mouse->relative_mode) {
572+
SDL_Window *window = xinput2_get_sdlwindow(videodata, xev->event);
573+
if (window) {
574+
X11_ProcessHitTest(_this, window->internal, (float)xev->event_x, (float)xev->event_y, false);
575+
SDL_SendMouseMotion(0, window, SDL_GLOBAL_MOUSE_ID, false, (float)xev->event_x, (float)xev->event_y);
576+
}
478577
}
479578
}
480579
}
@@ -516,29 +615,36 @@ void X11_InitXinput2Multitouch(SDL_VideoDevice *_this)
516615
{
517616
}
518617

519-
void X11_Xinput2SelectTouch(SDL_VideoDevice *_this, SDL_Window *window)
618+
void X11_Xinput2Select(SDL_VideoDevice *_this, SDL_Window *window)
520619
{
521-
#ifdef SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_MULTITOUCH
522-
SDL_VideoData *data = NULL;
620+
#if defined(SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_SCROLLINFO) || defined(SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_MULTITOUCH)
621+
SDL_VideoData *data = _this->internal;
622+
SDL_WindowData *window_data = window->internal;
523623
XIEventMask eventmask;
524-
unsigned char mask[4] = { 0, 0, 0, 0 };
525-
SDL_WindowData *window_data = NULL;
624+
unsigned char mask[5] = { 0, 0, 0, 0, 0 };
526625

527-
if (!X11_Xinput2IsMultitouchSupported()) {
626+
if (!data->xinput_scrolling && !X11_Xinput2IsMultitouchSupported()) {
528627
return;
529628
}
530629

531-
data = _this->internal;
532-
window_data = window->internal;
533-
534630
eventmask.deviceid = XIAllMasterDevices;
535631
eventmask.mask_len = sizeof(mask);
536632
eventmask.mask = mask;
537633

538-
XISetMask(mask, XI_TouchBegin);
539-
XISetMask(mask, XI_TouchUpdate);
540-
XISetMask(mask, XI_TouchEnd);
541-
XISetMask(mask, XI_Motion);
634+
if (data->xinput_scrolling) {
635+
/* Track enter events that inform us that we need to update
636+
* the previous scroll coordinates since we cannot track
637+
* them outside our window.
638+
*/
639+
XISetMask(mask, XI_Enter);
640+
}
641+
642+
if (X11_Xinput2IsMultitouchSupported()) {
643+
XISetMask(mask, XI_TouchBegin);
644+
XISetMask(mask, XI_TouchUpdate);
645+
XISetMask(mask, XI_TouchEnd);
646+
XISetMask(mask, XI_Motion);
647+
}
542648

543649
X11_XISelectEvents(data->display, window_data->xwindow, &eventmask, 1);
544650
#endif
@@ -740,6 +846,13 @@ void X11_Xinput2UpdateDevices(SDL_VideoDevice *_this, bool initial_check)
740846
old_mice = SDL_GetMice(&old_mouse_count);
741847
old_touch_devices = SDL_GetTouchDevices(&old_touch_count);
742848

849+
#ifdef SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_SCROLLINFO
850+
// Scroll devices don't get add/remove events, so just rebuild the list.
851+
SDL_free(scrollable_devices);
852+
scrollable_devices = NULL;
853+
scrollable_device_count = 0;
854+
#endif
855+
743856
for (int i = 0; i < ndevices; i++) {
744857
XIDeviceInfo *dev = &info[i];
745858

@@ -770,6 +883,38 @@ void X11_Xinput2UpdateDevices(SDL_VideoDevice *_this, bool initial_check)
770883
break;
771884
}
772885

886+
#ifdef SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_SCROLLINFO
887+
bool found_scrollable_device = false;
888+
for (int j = 0; j < dev->num_classes; j++) {
889+
const XIAnyClassInfo *class = dev->classes[j];
890+
const XIScrollClassInfo *s = (XIScrollClassInfo *)class;
891+
892+
if (class->type != XIScrollClass) {
893+
continue;
894+
}
895+
896+
if (!found_scrollable_device) {
897+
scrollable_devices = SDL_realloc(scrollable_devices, (scrollable_device_count + 1) * sizeof(SDL_XInput2ScrollableDevice));
898+
if (!scrollable_devices) {
899+
// No memory, so just skip this
900+
break;
901+
}
902+
}
903+
904+
SDL_XInput2ScrollableDevice *sd = &scrollable_devices[scrollable_device_count];
905+
const int dir = s->scroll_type == XIScrollTypeVertical;
906+
sd->device_id = dev->deviceid;
907+
sd->axis_id[dir] = s->number;
908+
sd->line_unit[dir] = s->increment;
909+
910+
found_scrollable_device = true;
911+
}
912+
913+
if (found_scrollable_device) {
914+
++scrollable_device_count;
915+
}
916+
#endif
917+
773918
#ifdef SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_MULTITOUCH
774919
for (int j = 0; j < dev->num_classes; j++) {
775920
Uint64 touchID;

src/video/x11/SDL_x11xinput2.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,12 @@ typedef struct XGenericEventCookie XGenericEventCookie;
3131
#endif
3232

3333
extern bool X11_InitXinput2(SDL_VideoDevice *_this);
34+
extern void X11_QuitXinput2(SDL_VideoDevice *_this);
3435
extern void X11_InitXinput2Multitouch(SDL_VideoDevice *_this);
3536
extern void X11_HandleXinput2Event(SDL_VideoDevice *_this, XGenericEventCookie *cookie);
3637
extern bool X11_Xinput2IsInitialized(void);
3738
extern bool X11_Xinput2IsMultitouchSupported(void);
38-
extern void X11_Xinput2SelectTouch(SDL_VideoDevice *_this, SDL_Window *window);
39+
extern void X11_Xinput2Select(SDL_VideoDevice *_this, SDL_Window *window);
3940
extern void X11_Xinput2GrabTouch(SDL_VideoDevice *_this, SDL_Window *window);
4041
extern void X11_Xinput2UngrabTouch(SDL_VideoDevice *_this, SDL_Window *window);
4142
extern bool X11_Xinput2SelectMouseAndKeyboard(SDL_VideoDevice *_this, SDL_Window *window);

0 commit comments

Comments
 (0)