From c1a54a0cc0006fb76e542f4bc1bfa7d1bb726336 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 9 Jan 2026 08:23:34 +0000 Subject: [PATCH] Add XI2 smooth scrolling support for X11 backend Fixes #9369 --- .gitignore | 2 + glfw/x11_init.c | 60 +++++++++++++++- glfw/x11_platform.h | 20 ++++++ glfw/x11_window.c | 166 ++++++++++++++++++++++++++++++++++++-------- 4 files changed, 219 insertions(+), 29 deletions(-) diff --git a/.gitignore b/.gitignore index b3609e1a0..23e1865f0 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,5 @@ __pycache__/ .cache bypy/b bypy/virtual-machines.conf +_codeql_detected_source_root + diff --git a/glfw/x11_init.c b/glfw/x11_init.c index 8c9688696..95d1d4c57 100644 --- a/glfw/x11_init.c +++ b/glfw/x11_init.c @@ -175,6 +175,8 @@ static bool initExtensions(void) { glfw_dlsym(_glfw.x11.xi.QueryVersion, _glfw.x11.xi.handle, "XIQueryVersion"); glfw_dlsym(_glfw.x11.xi.SelectEvents, _glfw.x11.xi.handle, "XISelectEvents"); + glfw_dlsym(_glfw.x11.xi.QueryDevice, _glfw.x11.xi.handle, "XIQueryDevice"); + glfw_dlsym(_glfw.x11.xi.FreeDeviceInfo, _glfw.x11.xi.handle, "XIFreeDeviceInfo"); if (XQueryExtension(_glfw.x11.display, "XInputExtension", @@ -183,13 +185,69 @@ static bool initExtensions(void) &_glfw.x11.xi.errorBase)) { _glfw.x11.xi.major = 2; - _glfw.x11.xi.minor = 0; + _glfw.x11.xi.minor = 1; if (XIQueryVersion(_glfw.x11.display, &_glfw.x11.xi.major, &_glfw.x11.xi.minor) == Success) { _glfw.x11.xi.available = true; + + // Detect smooth scrolling support once globally + _glfw.x11.xi.smoothScroll.available = false; + _glfw.x11.xi.smoothScroll.verticalAxis = -1; + _glfw.x11.xi.smoothScroll.horizontalAxis = -1; + _glfw.x11.xi.smoothScroll.verticalIncrement = 0.0; + _glfw.x11.xi.smoothScroll.horizontalIncrement = 0.0; + + // Require XI2.1 or later for smooth scrolling + if (_glfw.x11.xi.major >= 2 && (_glfw.x11.xi.major > 2 || _glfw.x11.xi.minor >= 1)) + { + if (XIQueryDevice && XIFreeDeviceInfo) + { + int deviceCount; + XIDeviceInfo* devices = XIQueryDevice(_glfw.x11.display, XIAllMasterDevices, &deviceCount); + if (devices) + { + for (int i = 0; i < deviceCount; i++) + { + XIDeviceInfo* device = &devices[i]; + + // Only process master pointer devices + if (device->use != XIMasterPointer) + continue; + + for (int j = 0; j < device->num_classes; j++) + { + if (device->classes[j]->type != XIScrollClass) + continue; + + XIScrollClassInfo* scroll = (XIScrollClassInfo*)device->classes[j]; + + if (scroll->scroll_type == XIScrollTypeVertical) + { + _glfw.x11.xi.smoothScroll.verticalAxis = scroll->number; + _glfw.x11.xi.smoothScroll.verticalIncrement = scroll->increment; + } + else if (scroll->scroll_type == XIScrollTypeHorizontal) + { + _glfw.x11.xi.smoothScroll.horizontalAxis = scroll->number; + _glfw.x11.xi.smoothScroll.horizontalIncrement = scroll->increment; + } + } + } + + XIFreeDeviceInfo(devices); + + // Enable smooth scrolling if we found at least one scroll axis + if (_glfw.x11.xi.smoothScroll.verticalAxis >= 0 || + _glfw.x11.xi.smoothScroll.horizontalAxis >= 0) + { + _glfw.x11.xi.smoothScroll.available = true; + } + } + } + } } } } diff --git a/glfw/x11_platform.h b/glfw/x11_platform.h index 0069f734f..beec5eb0b 100644 --- a/glfw/x11_platform.h +++ b/glfw/x11_platform.h @@ -118,8 +118,12 @@ typedef Bool (* PFN_XF86VidModeGetGammaRampSize)(Display*,int,int*); typedef Status (* PFN_XIQueryVersion)(Display*,int*,int*); typedef int (* PFN_XISelectEvents)(Display*,Window,XIEventMask*,int); +typedef XIDeviceInfo* (* PFN_XIQueryDevice)(Display*,int,int*); +typedef void (* PFN_XIFreeDeviceInfo)(XIDeviceInfo*); #define XIQueryVersion _glfw.x11.xi.QueryVersion #define XISelectEvents _glfw.x11.xi.SelectEvents +#define XIQueryDevice _glfw.x11.xi.QueryDevice +#define XIFreeDeviceInfo _glfw.x11.xi.FreeDeviceInfo typedef Bool (* PFN_XRenderQueryExtension)(Display*,int*,int*); typedef Status (* PFN_XRenderQueryVersion)(Display*dpy,int*,int*); @@ -205,6 +209,12 @@ typedef struct _GLFWwindowX11 // The last position the cursor was warped to by GLFW int warpCursorPosX, warpCursorPosY; + // XI2 smooth scrolling - track valuator values per window + struct { + double verticalValue; + double horizontalValue; + } smoothScroll; + struct { bool is_active; GLFWLayerShellConfig config; @@ -395,6 +405,16 @@ typedef struct _GLFWlibraryX11 int minor; PFN_XIQueryVersion QueryVersion; PFN_XISelectEvents SelectEvents; + PFN_XIQueryDevice QueryDevice; + PFN_XIFreeDeviceInfo FreeDeviceInfo; + // Smooth scrolling support + struct { + bool available; + int verticalAxis; + int horizontalAxis; + double verticalIncrement; + double horizontalIncrement; + } smoothScroll; } xi; struct { diff --git a/glfw/x11_window.c b/glfw/x11_window.c index b45f45a1a..8b42e169d 100644 --- a/glfw/x11_window.c +++ b/glfw/x11_window.c @@ -495,6 +495,29 @@ static void disableRawMouseMotion(_GLFWwindow* window UNUSED) XISelectEvents(_glfw.x11.display, _glfw.x11.root, &em, 1); } +// Enable XI2 smooth scrolling events on a window +// +static void enableSmoothScrolling(_GLFWwindow* window) +{ + if (!_glfw.x11.xi.smoothScroll.available) + return; + + // Initialize scroll valuator tracking for this window + window->x11.smoothScroll.verticalValue = 0.0; + window->x11.smoothScroll.horizontalValue = 0.0; + + // Select XI_Motion events on the window + XIEventMask em; + unsigned char mask[XIMaskLen(XI_Motion)] = { 0 }; + + em.deviceid = XIAllMasterDevices; + em.mask_len = sizeof(mask); + em.mask = mask; + XISetMask(mask, XI_Motion); + + XISelectEvents(_glfw.x11.display, window->x11.handle, &em, 1); +} + // Apply disabled cursor mode to a focused window // static void disableCursor(_GLFWwindow* window) @@ -850,6 +873,9 @@ static bool createNativeWindow(_GLFWwindow* window, if (_glfw.hints.window.blur_radius > 0) _glfwPlatformSetWindowBlur(window, _glfw.hints.window.blur_radius); + // Enable XI2 smooth scrolling if available + enableSmoothScrolling(window); + return true; } @@ -1282,37 +1308,108 @@ static void processEvent(XEvent *event) if (event->type == GenericEvent) { - if (_glfw.x11.xi.available) + if (_glfw.x11.xi.available && + event->xcookie.extension == _glfw.x11.xi.majorOpcode) { - _GLFWwindow* window = _glfw.x11.disabledCursorWindow; - - if (window && - window->rawMouseMotion && - event->xcookie.extension == _glfw.x11.xi.majorOpcode && - XGetEventData(_glfw.x11.display, &event->xcookie) && - event->xcookie.evtype == XI_RawMotion) + if (XGetEventData(_glfw.x11.display, &event->xcookie)) { - XIRawEvent* re = event->xcookie.data; - if (re->valuators.mask_len) + // Handle XI_RawMotion for disabled cursor + if (event->xcookie.evtype == XI_RawMotion) { - const double* values = re->raw_values; - double xpos = window->virtualCursorPosX; - double ypos = window->virtualCursorPosY; - - if (XIMaskIsSet(re->valuators.mask, 0)) + _GLFWwindow* window = _glfw.x11.disabledCursorWindow; + if (window && window->rawMouseMotion) { - xpos += *values; - values++; + XIRawEvent* re = event->xcookie.data; + if (re->valuators.mask_len) + { + const double* values = re->raw_values; + double xpos = window->virtualCursorPosX; + double ypos = window->virtualCursorPosY; + + if (XIMaskIsSet(re->valuators.mask, 0)) + { + xpos += *values; + values++; + } + + if (XIMaskIsSet(re->valuators.mask, 1)) + ypos += *values; + + _glfwInputCursorPos(window, xpos, ypos); + } } - - if (XIMaskIsSet(re->valuators.mask, 1)) - ypos += *values; - - _glfwInputCursorPos(window, xpos, ypos); } - } + // Handle XI_Motion for smooth scrolling + else if (event->xcookie.evtype == XI_Motion) + { + XIDeviceEvent* de = (XIDeviceEvent*)event->xcookie.data; + + // Find the window for this event + _GLFWwindow* window = NULL; + if (XFindContext(_glfw.x11.display, de->event, _glfw.x11.context, + (XPointer*)&window) == 0 && + _glfw.x11.xi.smoothScroll.available) + { + double xOffset = 0.0; + double yOffset = 0.0; + bool hasScroll = false; - XFreeEventData(_glfw.x11.display, &event->xcookie); + // Process valuators to detect scroll events + if (de->valuators.mask_len) + { + const double* values = de->valuators.values; + + for (int i = 0; i < de->valuators.mask_len * 8; i++) + { + if (!XIMaskIsSet(de->valuators.mask, i)) + continue; + + if (i == _glfw.x11.xi.smoothScroll.verticalAxis) + { + double delta = *values - window->x11.smoothScroll.verticalValue; + window->x11.smoothScroll.verticalValue = *values; + + if (_glfw.x11.xi.smoothScroll.verticalIncrement != 0.0) + { + yOffset = -delta / _glfw.x11.xi.smoothScroll.verticalIncrement; + hasScroll = true; + } + } + else if (i == _glfw.x11.xi.smoothScroll.horizontalAxis) + { + double delta = *values - window->x11.smoothScroll.horizontalValue; + window->x11.smoothScroll.horizontalValue = *values; + + if (_glfw.x11.xi.smoothScroll.horizontalIncrement != 0.0) + { + xOffset = delta / _glfw.x11.xi.smoothScroll.horizontalIncrement; + hasScroll = true; + } + } + + values++; + } + } + + if (hasScroll) + { + // Get keyboard modifiers + int mods = translateState(de->mods.effective); + + // Scale offsets by content scale + _glfwInputScroll(window, &(GLFWScrollEvent){ + .keyboard_modifiers = mods, + .x_offset = xOffset * _glfw.x11.contentScaleX, + .y_offset = yOffset * _glfw.x11.contentScaleY, + .unscaled = {.x = xOffset, .y = yOffset}, + .offset_type = GLFW_SCROLL_OFFEST_HIGHRES + }); + } + } + } + + XFreeEventData(_glfw.x11.display, &event->xcookie); + } } return; @@ -1444,14 +1541,27 @@ static void processEvent(XEvent *event) _glfwInputMouseClick(window, GLFW_MOUSE_BUTTON_RIGHT, GLFW_PRESS, mods); // Modern X provides scroll events as mouse button presses + // Only use these if smooth scrolling is not available else if (event->xbutton.button == Button4) - _glfwInputScroll(window, &(GLFWScrollEvent){.keyboard_modifiers=mods, .y_offset=1, .unscaled.y=1}); + { + if (!_glfw.x11.xi.smoothScroll.available) + _glfwInputScroll(window, &(GLFWScrollEvent){.keyboard_modifiers=mods, .y_offset=1, .unscaled.y=1}); + } else if (event->xbutton.button == Button5) - _glfwInputScroll(window, &(GLFWScrollEvent){.keyboard_modifiers=mods, .y_offset=-1, .unscaled.y=-1}); + { + if (!_glfw.x11.xi.smoothScroll.available) + _glfwInputScroll(window, &(GLFWScrollEvent){.keyboard_modifiers=mods, .y_offset=-1, .unscaled.y=-1}); + } else if (event->xbutton.button == Button6) - _glfwInputScroll(window, &(GLFWScrollEvent){.keyboard_modifiers=mods, .x_offset=1, .unscaled.x=1}); + { + if (!_glfw.x11.xi.smoothScroll.available) + _glfwInputScroll(window, &(GLFWScrollEvent){.keyboard_modifiers=mods, .x_offset=1, .unscaled.x=1}); + } else if (event->xbutton.button == Button7) - _glfwInputScroll(window, &(GLFWScrollEvent){.keyboard_modifiers=mods, .x_offset=-1, .unscaled.x=-1}); + { + if (!_glfw.x11.xi.smoothScroll.available) + _glfwInputScroll(window, &(GLFWScrollEvent){.keyboard_modifiers=mods, .x_offset=-1, .unscaled.x=-1}); + } else {