This tutorial explains how to lock the cursor and enable raw mouse input for X11, WinAPI, Cocoa, and Emscripten. The article archives this by looking into RGFW's source code and explaining how it locks the cursor and enables raw input on each OS.
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
- lock cursor
- center the cursor
- enable raw input
- handle raw input
- disable raw input
- 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;
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);
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
RECT clipRect;
GetClientRect(window, &clipRect);
ClientToScreen(window, (POINT*) &clipRect.left);
ClientToScreen(window, (POINT*) &clipRect.right);
ClipCursor(&clipRect);
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, 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);
On Windows, SetCursorPos
is used
SetCursorPos(window_x + (window_width / 2), window_y + (window_height / 2));
On MacOS, CGWarpMouseCursorPosition
is used
CGWarpMouseCursorPosition(window_x + (window_width / 2), window_y + (window_height / 2));
On Emscripten, RGFW does not move the mouse.
Step 3 (enable raw input)
With X11, XI is used to enable raw input
unsigned char mask[XIMaskLen(XI_RawMotion)] = { 0 };
XISetMask(mask, XI_RawMotion);
XIEventMask em;
em.deviceid = XIAllMasterDevices;
em.mask_len = sizeof(mask);
em.mask = mask;
XISelectEvents(display, XDefaultRootWindow(display), &em, 1);
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));
On MacOS you only need to run CGAssociateMouseAndMouseCursorPosition This also locks the cursor by disassociating the mouse cursor and the mouse movement
CGAssociateMouseAndMouseCursorPosition(0);
On Emscripten you only need to request the user to lock the pointer
emscripten_request_pointerlock("#canvas", 1);
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:
if ((win->_winArgs & RGFW_HOLD_MOUSE)) {
win->event.point.x = win->_lastMousePoint.x - E.xmotion.x;
win->event.point.y = win->_lastMousePoint.y - E.xmotion.y;
}
break;
case GenericEvent: {
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;
if (XIMaskIsSet(raw->valuators.mask, 0) != 0)
deltaX += raw->raw_values[0];
if (XIMaskIsSet(raw->valuators.mask, 1) != 0)
deltaY += raw->raw_values[1];
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;
}
On Windows, you only need to handle WM_INPUT
events and check for raw motion input
switch (msg.message) {
(...)
case WM_INPUT: {
if (!(win->_winArgs & RGFW_HOLD_MOUSE))
break;
unsigned size = sizeof(RAWINPUT);
static RAWINPUT raw[sizeof(RAWINPUT)];
GetRawInputData((HRAWINPUT)msg.lParam, RID_INPUT, raw, &size, sizeof(RAWINPUTHEADER));
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;
}
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) 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));
On Emscripten the mouse events can be checked as they normally are, except we're going to use and flip e->movementX/Y
EM_BOOL Emscripten_on_mousemove(int eventType, const EmscriptenMouseEvent* e, void* userData) {
if ((RGFW_root->_winArgs & RGFW_HOLD_MOUSE) == 0) return
RGFW_point p = RGFW_POINT(e->movementX, e->movementY);
}
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;
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);
For Windows, you pass a raw input device structure, RIDEV_REMOVE
to disable the raw input.
const RAWINPUTDEVICE id = { 0x01, 0x02, RIDEV_REMOVE, NULL };
RegisterRawInputDevices(&id, 1, sizeof(id));
On MacOS and Emscripten, unlocking the cursor also disables raw input.
Step 6 (unlock cursor)
On X11, XUngrabPoint
can be used to unlock the cursor.
XUngrabPointer(display, CurrentTime);
On Windows, pass a NULL rectangle pointer to ClipCursor to unclip the cursor.
ClipCursor(NULL);
On MacOS, associating the mouse cursor and the mouse movement will disable raw input and unlock the cursor
CGAssociateMouseAndMouseCursorPosition(1);
On Emscripten, exiting the pointer lock will unlock the cursor and disable raw input.
emscripten_exit_pointerlock();
Full code examples
X11
#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);
unsigned char mask[XIMaskLen(XI_RawMotion)] = { 0 };
XISetMask(mask, XI_RawMotion);
XIEventMask em;
em.deviceid = XIAllMasterDevices;
em.mask_len = sizeof(mask);
em.mask = mask;
XISelectEvents(display, XDefaultRootWindow(display), &em, 1);
Bool rawInput = True;
XPoint point;
XPoint _lastMousePoint;
XEvent event;
for (;;) {
XNextEvent(display, &event);
switch (event.type) {
case MotionNotify:
if (rawInput) {
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: {
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;
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);
}
Winapi
#include <windows.h>
#include <stdio.h>
#include <stdint.h>
#include <assert.h>
int main() {
WNDCLASS wc = {0};
wc.lpfnWndProc = DefWindowProc; 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);
RECT clipRect;
GetClientRect(hwnd, &clipRect);
ClientToScreen(hwnd, (POINT*) &clipRect.left);
ClientToScreen(hwnd, (POINT*) &clipRect.right);
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: {
if (holdMouse == FALSE)
break;
unsigned size = sizeof(RAWINPUT);
static RAWINPUT raw[sizeof(RAWINPUT)];
GetRawInputData((HRAWINPUT)msg.lParam, RID_INPUT, raw, &size, sizeof(RAWINPUTHEADER));
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;
}