Implement proper scroll event type detection in X11

Uses libinput scroll mode property to detect high res devices.
This commit is contained in:
Kovid Goyal 2026-01-10 08:38:45 +05:30
parent 15206eda0b
commit 813c560e29
No known key found for this signature in database
GPG key ID: 06BC317B515ACE7C
3 changed files with 128 additions and 125 deletions

128
glfw/x11_init.c vendored
View file

@ -148,6 +148,75 @@ static void detectEWMH(void)
XFree(supportedAtoms);
}
static void
read_xi_scroll_devices(void) {
#define xi _glfw.x11.xi
xi.num_scroll_devices = 0;
// Require XI2.1 or later for smooth scrolling
if (!xi.available || xi.major < 2 || (xi.major == 2 && xi.minor < 1) || !xi.LIBINPUT_SCROLL_METHOD_ENABLED) return;
#undef xi
int deviceCount;
XIDeviceInfo* devices = XIQueryDevice(_glfw.x11.display, XIAllDevices, &deviceCount);
if (!devices) return;
for (int i = 0; i < deviceCount; i++) {
XIDeviceInfo* device = &devices[i];
if (device->use != XISlavePointer || !device->enabled) continue;
Atom actual_type;
int actual_format;
unsigned long nitems, bytes_after;
unsigned char *data = NULL;
bool is_highres = false;
XIGetProperty(_glfw.x11.display, device->deviceid, _glfw.x11.xi.LIBINPUT_SCROLL_METHOD_ENABLED, 0, 2, False, XA_INTEGER, &actual_type, &actual_format, &nitems, &bytes_after, &data);
if (data) {
if (nitems > 1) is_highres = data[0] || data[1];
XFree(data);
}
for (int j = 0; j < device->num_classes; j++) {
if (device->classes[j]->type != XIScrollClass) continue;
XIScrollClassInfo* scroll = (XIScrollClassInfo*)device->classes[j];
XIScrollDevice *d = NULL;
for (unsigned k = 0; k < _glfw.x11.xi.num_scroll_devices; k++) {
XIScrollDevice *t = _glfw.x11.xi.scroll_devices + k;
if (t->deviceid == device->deviceid && t->sourceid == scroll->sourceid) { d = t; break; }
}
if (!d) {
if (_glfw.x11.xi.num_scroll_devices >= arraysz(_glfw.x11.xi.scroll_devices)) continue;
d = &_glfw.x11.xi.scroll_devices[_glfw.x11.xi.num_scroll_devices++];
*d = (XIScrollDevice){
.is_highres=is_highres, .deviceid=device->deviceid, .sourceid=scroll->sourceid,
};
memcpy(d->name, device->name, MIN(sizeof(d->name)-1, strlen(device->name)));
}
if (d->num_valuators >= arraysz(d->valuators)) continue;
XIScrollValuator *v = d->valuators + d->num_valuators++;
*v = (XIScrollValuator){
.number=scroll->number, .is_vertical=scroll->scroll_type == XIScrollTypeVertical,
.increment=scroll->increment,
};
}
for (int j = 0; j < device->num_classes; j++) {
if (device->classes[j]->type != XIValuatorClass) continue;
XIValuatorClassInfo* vi = (XIValuatorClassInfo*)device->classes[j];
XIScrollDevice *d = NULL;
for (unsigned k = 0; k < _glfw.x11.xi.num_scroll_devices; k++) {
XIScrollDevice *t = _glfw.x11.xi.scroll_devices + k;
if (t->deviceid == device->deviceid && t->sourceid == vi->sourceid) { d = t; break; }
}
if (!d) continue;
XIScrollValuator *v = NULL;
for (unsigned k = 0; k < d->num_valuators; k++) {
XIScrollValuator *t = d->valuators + k;
if (t->number == vi->number) { v = t; break; }
}
if (!v) continue;
v->value = vi->value; v->mode = vi->mode; v->resolution = vi->resolution;
v->min = vi->min; v->max = vi->max;
}
}
XIFreeDeviceInfo(devices);
}
// Look for and initialize supported X11 extensions
//
static bool initExtensions(void)
@ -193,62 +262,6 @@ static bool initExtensions(void)
&_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;
}
}
}
}
}
}
}
@ -496,6 +509,9 @@ static bool initExtensions(void)
_glfw.x11.MOTIF_WM_HINTS =
XInternAtom(_glfw.x11.display, "_MOTIF_WM_HINTS", False);
_glfw.x11.xi.LIBINPUT_SCROLL_METHOD_ENABLED = XInternAtom(_glfw.x11.display, "libinput Scroll Method Enabled", False);
read_xi_scroll_devices();
// The compositing manager selection name contains the screen number
{
char name[32];

23
glfw/x11_platform.h vendored
View file

@ -234,6 +234,18 @@ typedef struct AtomArray {
size_t sz, capacity;
} AtomArray;
typedef struct XIScrollValuator {
double increment, value, min, max; int number, resolution, mode; bool is_vertical;
} XIScrollValuator;
typedef struct XIScrollDevice {
bool is_highres;
int deviceid, sourceid;
XIScrollValuator valuators[8];
unsigned num_valuators;
char name[32];
} XIScrollDevice;
// X11-specific global data
//
typedef struct _GLFWlibraryX11
@ -411,14 +423,9 @@ typedef struct _GLFWlibraryX11
PFN_XIQueryDevice QueryDevice;
PFN_XIFreeDeviceInfo FreeDeviceInfo;
PFN_XIGetProperty GetProperty;
// Smooth scrolling support
struct {
bool available;
int verticalAxis;
int horizontalAxis;
double verticalIncrement;
double horizontalIncrement;
} smoothScroll;
XIScrollDevice scroll_devices[16];
unsigned num_scroll_devices;
Atom LIBINPUT_SCROLL_METHOD_ENABLED;
} xi;
struct {

102
glfw/x11_window.c vendored
View file

@ -499,18 +499,12 @@ static void disableRawMouseMotion(_GLFWwindow* window UNUSED)
//
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;
if (!_glfw.x11.xi.num_scroll_devices) return;
// Select XI_Motion events on the window
XIEventMask em;
unsigned char mask[XIMaskLen(XI_Motion)] = { 0 };
em.deviceid = XIAllMasterDevices;
em.deviceid = XIAllDevices;
em.mask_len = sizeof(mask);
em.mask = mask;
XISetMask(mask, XI_Motion);
@ -1343,71 +1337,57 @@ static void processEvent(XEvent *event)
else if (event->xcookie.evtype == XI_Motion)
{
XIDeviceEvent* de = (XIDeviceEvent*)event->xcookie.data;
XIScrollDevice *d = NULL;
for (unsigned i = 0; i < _glfw.x11.xi.num_scroll_devices; i++) {
XIScrollDevice *t = &_glfw.x11.xi.scroll_devices[i];
if (t->deviceid == de->deviceid && t->sourceid == de->sourceid) {
d = t; break;
}
}
// 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;
if (d && de->valuators.mask_len && XFindContext(
_glfw.x11.display, de->event, _glfw.x11.context, (XPointer*)&window) == 0) {
double xOffset = 0, yOffset = 0;
// 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;
}
GLFWOffsetType type = GLFW_SCROLL_OFFEST_HIGHRES;
unsigned vidx = 0;
for (int i = 0; i < de->valuators.mask_len * 8; i++) {
if (!XIMaskIsSet(de->valuators.mask, i)) continue;
const double value = de->valuators.values[vidx++];
XIScrollValuator *v = NULL;
for (unsigned k = 0; k < d->num_valuators; k++) {
XIScrollValuator *t = d->valuators + k;
if (t->number == i) { v = t; break; }
}
if (!v) continue;
double delta = value - v->value;
v->value = value;
if (v->is_vertical) delta *= -1;
double *off = v->is_vertical ? &yOffset : &xOffset;
*off = delta;
if (!d->is_highres) {
if (v->increment == 120.) type = GLFW_SCROLL_OFFEST_V120;
else {
type = GLFW_SCROLL_OFFSET_LINES;
if (v->increment != 0) *off /= v->increment;
}
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)
{
if (xOffset != 0 || yOffset != 0) {
// 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,
.x_offset = xOffset * (type == GLFW_SCROLL_OFFEST_HIGHRES ? _glfw.x11.contentScaleX : 1),
.y_offset = yOffset * (type == GLFW_SCROLL_OFFEST_HIGHRES ? _glfw.x11.contentScaleY : 1),
.unscaled = {.x = xOffset, .y = yOffset},
.offset_type = GLFW_SCROLL_OFFEST_HIGHRES
.offset_type = type,
});
}
}
}
XFreeEventData(_glfw.x11.display, &event->xcookie);
}
}
@ -1544,22 +1524,22 @@ static void processEvent(XEvent *event)
// Only use these if smooth scrolling is not available
else if (event->xbutton.button == Button4)
{
if (!_glfw.x11.xi.smoothScroll.available)
if (!_glfw.x11.xi.num_scroll_devices)
_glfwInputScroll(window, &(GLFWScrollEvent){.keyboard_modifiers=mods, .y_offset=1, .unscaled.y=1});
}
else if (event->xbutton.button == Button5)
{
if (!_glfw.x11.xi.smoothScroll.available)
if (!_glfw.x11.xi.num_scroll_devices)
_glfwInputScroll(window, &(GLFWScrollEvent){.keyboard_modifiers=mods, .y_offset=-1, .unscaled.y=-1});
}
else if (event->xbutton.button == Button6)
{
if (!_glfw.x11.xi.smoothScroll.available)
if (!_glfw.x11.xi.num_scroll_devices)
_glfwInputScroll(window, &(GLFWScrollEvent){.keyboard_modifiers=mods, .x_offset=1, .unscaled.x=1});
}
else if (event->xbutton.button == Button7)
{
if (!_glfw.x11.xi.smoothScroll.available)
if (!_glfw.x11.xi.num_scroll_devices)
_glfwInputScroll(window, &(GLFWScrollEvent){.keyboard_modifiers=mods, .x_offset=-1, .unscaled.x=-1});
}