DEV Community

Cover image for RGFW Under the Hood: Raw Mouse Input and Mouse Locking
Colleague Riley
Colleague Riley

Posted on

RGFW Under the Hood: Raw Mouse Input and Mouse Locking

A tutorial that explains how to lock the cursor and enable raw mouse input for X11, WinAPI, Cocoa, and Emscripten.

Introduction

RGFW is a lightweight single-header windowing library, its source code can be found here.
This tutorial is based on its source code.

When you create an application that locks the cursor, such as a game with a first-person camera, it's important to be able to disable the cursor.
This means locking the cursor in the middle of the screen and getting raw input.

The only alternative to this method would be a hack that pulls the mouse back to the center of the window when it moves. However, this is a hack so it can be buggy
and does not work on all OSes. Therefore, it's important to properly lock the mouse by using raw input.

This tutorial explains how RGFW handles raw mouse input so you can understand how to implement it yourself.

Overview

A quick overview of the steps required

  1. lock cursor
  2. center the cursor
  3. enable raw input
  4. handle raw input
  5. disable raw input
  6. unlock cursor

When the user asks RGFW to hold the cursor, RGFW enables a bit flag that says the cursor is held.

win->_winArgs |= RGFW_HOLD_MOUSE;
Enter fullscreen mode Exit fullscreen mode

Step 1 (Lock Cursor)

On X11 the cursor can be locked by grabbing it via XGrabPointer

XGrabPointer(display, window, True, PointerMotionMask, GrabModeAsync, GrabModeAsync, None, None, CurrentTime);
Enter fullscreen mode Exit fullscreen mode

This gives the window full control of the pointer.

On Windows, ClipCursor locks the cursor to a specific rect on the screen.
This means we must find the window rectangle on the screen and then clip the mouse to that rectangle.

Also using: GetClientRect) and ClientToScreen

//First get the window size (the RGFW_window struct also includes this information, but using this ensures it's correct)
RECT clipRect;
GetClientRect(window, &clipRect);

// ClipCursor needs screen coordinates, not coordinates relative to the window
ClientToScreen(window, (POINT*) &clipRect.left);
ClientToScreen(window, (POINT*) &clipRect.right);

// Now we can lock the cursor
ClipCursor(&clipRect);
Enter fullscreen mode Exit fullscreen mode

On MacOS and Emscripten the function to enable raw input also locks the cursor. So I'll get to its function in step 4.

Step 2 (center the cursor)

After the cursor is locked, it should be centered in the middle of the screen.
This ensures the cursor is locked in the right place and won't mess with anything else.

RGFW uses an RGFW function called RGFW_window_moveMouse to move the mouse in the middle of the window.

On X11, XWarpPointer can be used to move the cursor to the center of the window

XWarpPointer(display, None, window, 0, 0, 0, 0, window_width / 2, window_height / 2);
Enter fullscreen mode Exit fullscreen mode

On Windows, SetCursorPos is used

SetCursorPos(window_x + (window_width / 2), window_y + (window_height / 2));
Enter fullscreen mode Exit fullscreen mode

On MacOS, CGWarpMouseCursorPosition is used

CGWarpMouseCursorPosition(window_x + (window_width / 2), window_y + (window_height / 2));
Enter fullscreen mode Exit fullscreen mode

On Emscripten, RGFW does not move the mouse.

Step 3 (enable raw input)

With X11, XI is used to enable raw input

// mask for XI and set mouse for raw mouse input ("RawMotion")
unsigned char mask[XIMaskLen(XI_RawMotion)] = { 0 };
XISetMask(mask, XI_RawMotion);

// set up X1 struct
XIEventMask em;
em.deviceid = XIAllMasterDevices;
em.mask_len = sizeof(mask);
em.mask = mask;

//Enable raw input using the structure
XISelectEvents(display, XDefaultRootWindow(display), &em, 1);
Enter fullscreen mode Exit fullscreen mode

On Windows, you need to set up the RAWINPUTDEVICE structure and enable it with RegisterRawInputDevices

const RAWINPUTDEVICE id = { 0x01, 0x02, 0, window };
RegisterRawInputDevices(&id, 1, sizeof(id));
Enter fullscreen mode Exit fullscreen mode

On MacOS you only need to run CGAssociateMouseAndMouseCursorPosition
This also locks the cursor by disassociating the mouse cursor and the mouse movement

CGAssociateMouseAndMouseCursorPosition(0);
Enter fullscreen mode Exit fullscreen mode

On Emscripten you only need to request the user to lock the pointer

emscripten_request_pointerlock("#canvas", 1);
Enter fullscreen mode Exit fullscreen mode

Step 4 (handle raw input events)

These all happen during event loops.

For X11, you must handle the normal MotionNotify, manually converting the input to raw input.
To check for raw mouse input events, you need to use GenericEvent.

switch (E.type) {
    (...)
    case MotionNotify:
        /* check if mouse hold is enabled */
        if ((win->_winArgs & RGFW_HOLD_MOUSE)) {
            /* convert E.xmotion to raw input by subtracting the previous point */
            win->event.point.x = win->_lastMousePoint.x - E.xmotion.x;
            win->event.point.y = win->_lastMousePoint.y - E.xmotion.y;
        }

        break;

    case GenericEvent: {
        /* MotionNotify is used for mouse events if the mouse isn't held */                
        if (!(win->_winArgs & RGFW_HOLD_MOUSE)) {
            XFreeEventData(display, &E.xcookie);
            break;
        }

        XGetEventData(display, &E.xcookie);
        if (E.xcookie.evtype == XI_RawMotion) {
            XIRawEvent *raw = (XIRawEvent *)E.xcookie.data;
            if (raw->valuators.mask_len == 0) {
                XFreeEventData(display, &E.xcookie);
                break;
            }

            double deltaX = 0.0f; 
            double deltaY = 0.0f;

            /* check if relative motion data exists where we think it does */
            if (XIMaskIsSet(raw->valuators.mask, 0) != 0)
                deltaX += raw->raw_values[0];
            if (XIMaskIsSet(raw->valuators.mask, 1) != 0)
                deltaY += raw->raw_values[1];

            //The mouse must be moved back to the center when it moves
            XWarpPointer(display, None, window, 0, 0, 0, 0, window_width / 2, window_height / 2);
            win->event.point = RGFW_POINT((i32)deltaX, (i32)deltaY);
        }

        XFreeEventData(display, &E.xcookie);
        break;
    }
Enter fullscreen mode Exit fullscreen mode

On Windows, you only need to handle WM_INPUT events and check for raw motion input

switch (msg.message) {
    (...)
    case WM_INPUT: {
        /* check if the mouse is being held */
        if (!(win->_winArgs & RGFW_HOLD_MOUSE))
            break;

        /* get raw data as an array */
        unsigned size = sizeof(RAWINPUT);
        static RAWINPUT raw[sizeof(RAWINPUT)];
        GetRawInputData((HRAWINPUT)msg.lParam, RID_INPUT, raw, &size, sizeof(RAWINPUTHEADER));

        //Make sure raw data is valid 
        if (raw->header.dwType != RIM_TYPEMOUSE || (raw->data.mouse.lLastX == 0 && raw->data.mouse.lLastY == 0) )
            break;

        win->event.point.x = raw->data.mouse.lLastX;
        win->event.point.y = raw->data.mouse.lLastY;
        break;
    }
Enter fullscreen mode Exit fullscreen mode

On macOS, you can check mouse input as normal while using deltaX and deltaY to fetch the mouse point

switch (objc_msgSend_uint(e, sel_registerName("type"))) {
    case NSEventTypeLeftMouseDragged:
    case NSEventTypeOtherMouseDragged:
    case NSEventTypeRightMouseDragged:
    case NSEventTypeMouseMoved:
        if ((win->_winArgs & RGFW_HOLD_MOUSE) == 0) // if the mouse is not held
                    break;

                NSPoint p;
        p.x = ((CGFloat(*)(id, SEL))abi_objc_msgSend_fpret)(e, sel_registerName("deltaX"));
        p.y = ((CGFloat(*)(id, SEL))abi_objc_msgSend_fpret)(e, sel_registerName("deltaY"));


        win->event.point = RGFW_POINT((i32)p.x, (i32) p.y));
Enter fullscreen mode Exit fullscreen mode

On Emscripten the mouse events can be checked as they normally are, except we're going to use e->movementX/Y

EM_BOOL Emscripten_on_mousemove(int eventType, const EmscriptenMouseEvent* e, void* userData) {
    if ((RGFW_root->_winArgs & RGFW_HOLD_MOUSE) == 0) // if the mouse is not held
            return


        RGFW_point p = RGFW_POINT(e->movementX, e->movementY);
}
Enter fullscreen mode Exit fullscreen mode

Step 5 (disable raw input)

Finally, RGFW allows disabling the raw input and unlocking the cursor to revert to normal mouse input.

First, RGFW disables the bit flag.

win->_winArgs ^= RGFW_HOLD_MOUSE;
Enter fullscreen mode Exit fullscreen mode

In X11, first, you must create a structure with a blank mask.
This will disable raw input.

unsigned char mask[] = { 0 };
XIEventMask em;
em.deviceid = XIAllMasterDevices;

em.mask_len = sizeof(mask);
em.mask = mask;
XISelectEvents(display, XDefaultRootWindow(display), &em, 1);
Enter fullscreen mode Exit fullscreen mode

For Windows, you pass a raw input device structure with RIDEV_REMOVE to disable the raw input.

const RAWINPUTDEVICE id = { 0x01, 0x02, RIDEV_REMOVE, NULL };
RegisterRawInputDevices(&id, 1, sizeof(id));
Enter fullscreen mode Exit fullscreen mode

On MacOS and Emscripten, unlocking the cursor also disables raw input.

Step 6 (unlock cursor)

On X11, XUngrabPoint is used for unlocking the cursor.

XUngrabPointer(display, CurrentTime);
Enter fullscreen mode Exit fullscreen mode

On Windows, pass a NULL rectangle pointer to ClipCursor inorder to the cursor.

ClipCursor(NULL);
Enter fullscreen mode Exit fullscreen mode

On MacOS, associating the mouse cursor and the mouse movement will disable raw input and unlock the cursor

CGAssociateMouseAndMouseCursorPosition(1);
Enter fullscreen mode Exit fullscreen mode

On Emscripten, exiting the pointer lock will unlock the cursor and disable raw input.

emscripten_exit_pointerlock();
Enter fullscreen mode Exit fullscreen mode

Full code examples

X11

// This can be compiled with 
// gcc x11.c -lX11 -lXi

#include <X11/Xlib.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <X11/extensions/XInput2.h>

int main(void) {
    unsigned int window_width = 200;
    unsigned int window_height = 200;

    Display* display = XOpenDisplay(NULL);  
    Window window = XCreateSimpleWindow(display, RootWindow(display, DefaultScreen(display)), 400, 400, window_width, window_height, 1, BlackPixel(display, DefaultScreen(display)), WhitePixel(display, DefaultScreen(display)));

    XSelectInput(display, window, ExposureMask | KeyPressMask);
    XMapWindow(display, window);

    XGrabPointer(display, window, True, PointerMotionMask, GrabModeAsync, GrabModeAsync, None, None, CurrentTime);

    XWarpPointer(display, None, window, 0, 0, 0, 0, window_width / 2, window_height / 2);

    // mask for XI and set mouse for raw mouse input ("RawMotion")
    unsigned char mask[XIMaskLen(XI_RawMotion)] = { 0 };
    XISetMask(mask, XI_RawMotion);

    // set up X1 struct
    XIEventMask em;
    em.deviceid = XIAllMasterDevices;
    em.mask_len = sizeof(mask);
    em.mask = mask;

    // enable raw input using the structure
    XISelectEvents(display, XDefaultRootWindow(display), &em, 1);

    Bool rawInput = True;
    XPoint point;
    XPoint _lastMousePoint;

    XEvent event;

    for (;;) {
        XNextEvent(display, &event);
        switch (event.type) {
            case MotionNotify:
                /* check if mouse hold is enabled */
                if (rawInput) {
                    /* convert E.xmotion to rawinput by substracting the previous point */
                    point.x = _lastMousePoint.x - event.xmotion.x;
                    point.y = _lastMousePoint.y - event.xmotion.y;
                    printf("rawinput %i %i\n", point.x, point.y);
                }

                break;

            case GenericEvent: {
                /* MotionNotify is used for mouse events if the mouse isn't held */                
                if (rawInput == False) {
                    XFreeEventData(display, &event.xcookie);
                    break;
                }

                XGetEventData(display, &event.xcookie);
                if (event.xcookie.evtype == XI_RawMotion) {
                    XIRawEvent *raw = (XIRawEvent *)event.xcookie.data;
                    if (raw->valuators.mask_len == 0) {
                        XFreeEventData(display, &event.xcookie);
                        break;
                    }

                    double deltaX = 0.0f; 
                    double deltaY = 0.0f;

                    /* check if relative motion data exists where we think it does */
                    if (XIMaskIsSet(raw->valuators.mask, 0) != 0)
                        deltaX += raw->raw_values[0];
                    if (XIMaskIsSet(raw->valuators.mask, 1) != 0)
                        deltaY += raw->raw_values[1];

                    point = (XPoint){deltaX, deltaY};
                    XWarpPointer(display, None, window, 0, 0, 0, 0, window_width / 2, window_height / 2);

                    printf("rawinput %i %i\n", point.x, point.y);
                }   

                XFreeEventData(display, &event.xcookie);
                break;
            }
            case KeyPress:
                if (rawInput == False)
                    break;

                unsigned char mask[] = { 0 };
                XIEventMask em;
                em.deviceid = XIAllMasterDevices;

                em.mask_len = sizeof(mask);
                em.mask = mask;
                XISelectEvents(display, XDefaultRootWindow(display), &em, 1);
                XUngrabPointer(display, CurrentTime);

                printf("Raw input disabled\n");
                break;
            default: break;
        }
    }

    XCloseDisplay(display);
 }
Enter fullscreen mode Exit fullscreen mode

Winapi

// compile with gcc winapi.c

#include <windows.h>

#include <stdio.h>
#include <stdint.h>
#include <assert.h>

int main() {
    WNDCLASS wc = {0};
    wc.lpfnWndProc   = DefWindowProc; // Default window procedure
    wc.hInstance     = GetModuleHandle(NULL);
    wc.lpszClassName = "SampleWindowClass";

    RegisterClass(&wc);

    int window_width = 300;
    int window_height = 300;
    int window_x = 400;
    int window_y = 400;

    HWND hwnd = CreateWindowA(wc.lpszClassName, "Sample Window", 0,
            window_x, window_y, window_width, window_height,
            NULL, NULL, wc.hInstance, NULL);

    ShowWindow(hwnd, SW_SHOW);
    UpdateWindow(hwnd);

    // first get the window size (the RGFW_window struct also includes this informaton, but using this ensures it's correct)
    RECT clipRect;
    GetClientRect(hwnd, &clipRect);

    // ClipCursor needs screen coords, not coords relative to the window
    ClientToScreen(hwnd, (POINT*) &clipRect.left);
    ClientToScreen(hwnd, (POINT*) &clipRect.right);

    // now we can lock the cursor
    ClipCursor(&clipRect);

    SetCursorPos(window_x + (window_width / 2), window_y + (window_height / 2));    
    const RAWINPUTDEVICE id = { 0x01, 0x02, 0, hwnd };
    RegisterRawInputDevices(&id, 1, sizeof(id));

    MSG msg;

    BOOL holdMouse = TRUE;

    BOOL running = TRUE;

    POINT point;

    while (running) {
        if (PeekMessageA(&msg, hwnd, 0u, 0u, PM_REMOVE)) {
            switch (msg.message) {
                case WM_CLOSE:
                case WM_QUIT:
                    running = FALSE;
                    break;
                case WM_INPUT: {
                    /* check if the mouse is being held */
                    if (holdMouse == FALSE)
                        break;

                    /* get raw data as an array */
                    unsigned size = sizeof(RAWINPUT);
                    static RAWINPUT raw[sizeof(RAWINPUT)];
                    GetRawInputData((HRAWINPUT)msg.lParam, RID_INPUT, raw, &size, sizeof(RAWINPUTHEADER));

                    // make sure raw data is valid 
                    if (raw->header.dwType != RIM_TYPEMOUSE || (raw->data.mouse.lLastX == 0 && raw->data.mouse.lLastY == 0) )
                        break;


                    point.x = raw->data.mouse.lLastX;
                    point.y = raw->data.mouse.lLastY;
                    printf("raw input: %i %i\n", point.x, point.y);
                    break;
                }
                case WM_KEYDOWN:
                    if (holdMouse == FALSE)
                        break;

                    const RAWINPUTDEVICE id = { 0x01, 0x02, RIDEV_REMOVE, NULL };
                    RegisterRawInputDevices(&id, 1, sizeof(id));
                    ClipCursor(NULL);

                    printf("rawinput disabled\n");
                    holdMouse = FALSE;
                    break;

                default: break;
            }
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }

        running = IsWindow(hwnd);
    }

    DestroyWindow(hwnd);
    return 0;
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)