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.
NOTE: RGFW is a lightweight single-header windowing library, its source code can be found here. This tutorial is based on its source code.
A quick overview of the steps required
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;
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
//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);
On MacOS and Emscripten the function to enable raw input also locks the cursor. So I'll get to its function in step 4.
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);
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.
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);
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);
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(deltaX, 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: { /* 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; }
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));
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); }
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 with 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.
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();
// 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); }
// 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; }
Thanks, I appreciate seeing how it's working on different platforms.