Add XI2 smooth scrolling support for X11 backend

Fixes #9369
This commit is contained in:
copilot-swe-agent[bot] 2026-01-09 08:23:34 +00:00 committed by Kovid Goyal
parent dd9c61e5f6
commit c1a54a0cc0
No known key found for this signature in database
GPG key ID: 06BC317B515ACE7C
4 changed files with 219 additions and 29 deletions

2
.gitignore vendored
View file

@ -29,3 +29,5 @@ __pycache__/
.cache
bypy/b
bypy/virtual-machines.conf
_codeql_detected_source_root

60
glfw/x11_init.c vendored
View file

@ -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;
}
}
}
}
}
}
}

20
glfw/x11_platform.h vendored
View file

@ -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 {

166
glfw/x11_window.c vendored
View file

@ -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
{