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: