Jan 2022
If you want to listen to mouse (motion) events under X even when the
cursor is outside your client’s window or the cursor bumps against your
window’s border when grabbed, you need to use a different API from the
usual XEvents.
The core protocol only seems to report absolute cursor positions to your
client when the cursor intersects your client’s window and when the
cursor actually moved. When you are writing a game e.g. and want to
confine the cursor to your window you can do that with XGrabPointer.
But once the cursor hits your window’s border, X won’t generate and
motion events anymore (afterall, the cursor hasn’t moved, so there is
nothing to report).
What you want to do is use the XInput2 protocol extension to listen to
RawMotion events. These report all mouse movement as relative offsets
in floating point.
NOTE: Searching the web for this, I found that most results are about using xinput(1) to configure input devices. Finding anything on how to use XInput is weirdly difficult (just like with X in general).
Using XInput to listen for motion events entails checking whether
XInpuit is available (strictly speaking not necessary), creating an
event mask and passing that to XISelectEvents. And of course linking
against XInput (-lXi).
Here is an minimal working example:
#include <stdio.h>
#include <stdlib.h>
// These header locations are pretty standard, obviously you need to
// have the headers installed in your include-dir.
// This is using Xlib because everything is simpler in Xlib, but
// obviously xcb can use XInput too.
#include <X11/Xlib.h>
#include <X11/extensions/XInput.h>
#include <X11/extensions/XInput2.h>
int main () {
Display* dpy = XOpenDisplay(NULL);
if (!dpy) {
fprintf(stderr, "FATAL: Failed to connect to display.\n");
exit(EXIT_FAILURE);
}
// Checking whether XInput is available.
int opcode, event, error;
if (!XQueryExtension(dpy, "XInputExtension", &opcode, &event, &error)) {
// Nothing we can do at this point.
fprintf(stderr, "FATAL: XInput not available.\n");
exit(EXIT_FAILURE);
}
// Initialize empty bitmask to hold event-type flags.
unsigned char mask[(XI_LASTEVENT+7)/8] = {0};
// `XISetMask` is just a macro to do the bit-twiddling for us.
XISetMask(mask, XI_RawMotion);
// Initialize the XIEventMask struct...
XIEventMask event_mask = {
// Let's just listen on all input devices...
.deviceid = XIAllMasterDevices,
.mask_len = (XI_LASTEVENT+7)/8,
.mask = mask,
};
// Actually tell the X server that we want to receive the events in our mask
XISelectEvents(dpy, DefaultRootWindow(dpy), &event_mask, 1);
// Standard event-loop:
XEvent ev;
XGenericEventCookie cookie;
while (1) {
while (XPending(dpy)) {
XNextEvent(dpy, &ev);
switch (ev.type) {
// The XInput extension does not have its own event type in the
// core protocol's event system and instead overloads the
// `GenericEvent` type
case GenericEvent:
// Get the XInput event data from the generic event
cookie = ev.xcookie;
XGetEventData(dpy, &cookie);
XIDeviceEvent* xdevice = cookie.data;
switch (xidevice->evtype) {
// Switch over the XInput event types. Here we just handle
// the RawMotion event that we selected earlier. We are not
// receiving any other type right now.
case XI_RawMotion:
// Print out the relative x and y offsets.
printf("Received XIRawMotion event (event_x,event_y): (%f,%f)\n",
xidevice->event_x, xidevice->event_y);
break;
}
XFreeEventData(dpy, &cookie);
break;
}
}
}
}
Notice how we don’t even create a window but instead listen on all
events on DefaultRootWindow(dpy). Whether this is what you want
depends on your use-case, but I think it’s nice to know that this is
possible.
NOTE: These RawMotion events only give us relative movement offsets, no absolute coordinates.
NOTE: Depending on what events you want to listen to, you might want to
make sure that a sufficiently recent version of XInput is available. You
can do that using XIQueryVersion, passing it a major and minor version
number.
See also: