Relative Mouse Mode
Learn how to restrict cursor movement to a window whilst capturing mouse motion continuously.
So far, our example programs have been using the mouse to control a pointer within our window, letting users point and click on UI elements. However, many programs, especially first-person games, use the mouse differently. For example:
- Rather than controlling a pointer, the mouse controls the direction that the player's character is looking.
- The pointer can never leave the window. For example, the player can move their mouse as far as they want in any direction - our program will continue to react appropriately by, for example, continuously rotating their character.
Setting Relative Mouse Mode
SDL supports this interaction mode directly. It is called relative mouse mode, and we can toggle it using the SDL_SetWindowRelativeMouseMode() function. In SDL3, this function is window-specific, requiring the SDL_Window* as its first argument. We pass true as the second argument to enable it.
The following program will immediately enable relative mouse mode. To make quitting easier without a mouse cursor, we'll also let our program be closed using the escape key:
Files
We can turn relative mode off by passing false to SDL_SetWindowRelativeMouseMode():
SDL_SetWindowRelativeMouseMode(GameWindow.GetRaw(), false);Relative Mode and Input Focus
Relative mouse mode is closely related to input focus. A window with relative mouse mode enabled can only influence the cursor when that window has input focus. If it loses input focus (by the user alt-tabbing, for example) the cursor will return to normal until that window regains focus.
To ensure relative mode is applied correctly, especially in multi-window applications, it's good practice to first give the window input focus using SDL_RaiseWindow():
void SetRelativeMode(
SDL_Window* Window, bool Enable
) {
if (Enable) {
SDL_RaiseWindow(Window);
SDL_SetWindowRelativeMouseMode(Window, true);
} else {
SDL_SetWindowRelativeMouseMode(Window, false);
}
}We cover input focus and SDL_RaiseWindow() in more detail in our lesson on .
Errors when Setting Relative Mode
Enabling relative mode isn't always supported. In SDL3, SDL_SetWindowRelativeMouseMode() returns a bool - true if it succeeds, and false if it fails.
We can check this return value to determine if an error occurred, and use SDL_GetError() for an explanation of the error:
if (SDL_SetWindowRelativeMouseMode(GameWindow.GetRaw(), true)) {
std::cout << "Relative mode enabled\n";
} else {
std::cout << "Error enabling relative mode: "
<< SDL_GetError() << '\n';
}Getting Relative Mouse Mode
We can check if relative mode is currently enabled for a specific window using SDL_GetWindowRelativeMouseMode():
if (SDL_GetWindowRelativeMouseMode(GameWindow.GetRaw())) {
std::cout << "Relative mode is enabled\n";
} else {
std::cout << "Relative mode not enabled\n";
}Hybrid Interaction Models
Many applications require switching dynamically between relative and non-relative modes based on the user's context. For instance, a first-person game might use relative mode during gameplay for seamless camera control but switch to non-relative mode for navigating menus.
To implement this, you can toggle relative mode dynamically based on user input. Below is an example where pressing the Tab key toggles between the two modes:
src/main.cpp
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include "Window.h"
void HandleKeyboardEvent(
const SDL_KeyboardEvent& E, Window& GameWindow
) {
if (E.key == SDLK_TAB) {
bool CurrentMode{SDL_GetWindowRelativeMouseMode(
GameWindow.GetRaw()
)};
SDL_SetWindowRelativeMouseMode(
GameWindow.GetRaw(), !CurrentMode
);
}
}
int main(int, char**) {
SDL_Init(SDL_INIT_VIDEO);
Window GameWindow;
SDL_Event Event;
bool IsRunning = true;
while (IsRunning) {
while (SDL_PollEvent(&Event)) {
if (Event.type == SDL_EVENT_KEY_DOWN) {
HandleKeyboardEvent(Event.key, GameWindow);
} else if (Event.type == SDL_EVENT_QUIT) {
IsRunning = false;
}
}
GameWindow.Render();
GameWindow.Update();
}
SDL_Quit();
return 0;
}Mouse Motion Events in Relative Mode
When relative mode is enabled, we typically don't care about where the cursor is in our window. Often, the cursor is just going to be locked in the middle of the window, and will be hidden anyway.
Instead, we care about the relative movement of the mouse - that is, the direction and distance the mouse has moved compared to some previous moment in time.
Within our event loop, these values are available within the xrel and yrel members of the SDL_MouseMotionEvent. In SDL3, these values are floats, providing higher precision. They report where the mouse has moved in relation to the previous SDL_MouseMotionEvent:
src/main.cpp
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <iostream>
#include "Window.h"
void HandleMotionEvent(const SDL_MouseMotionEvent& E) {
std::cout << "Relative mouse motion: "
<< E.xrel << ", " << E.yrel << '\n';
}
int main(int, char**) {
SDL_Init(SDL_INIT_VIDEO);
Window GameWindow;
SDL_SetWindowRelativeMouseMode(GameWindow.GetRaw(), true);
SDL_Event Event;
bool IsRunning = true;
while (IsRunning) {
while (SDL_PollEvent(&Event)) {
if (Event.type == SDL_EVENT_MOUSE_MOTION) {
HandleMotionEvent(Event.motion);
} else if (Event.type == SDL_EVENT_QUIT) {
IsRunning = false;
} else if (Event.type == SDL_EVENT_KEY_DOWN) {
if (Event.key.key == SDLK_ESCAPE) {
IsRunning = false;
}
}
}
GameWindow.Render();
GameWindow.Update();
}
SDL_Quit();
return 0;
}Relative mouse motion: -1, 0
Relative mouse motion: 0, -1
Relative mouse motion: 3, 0Using the xrel and yrel values doesn't require relative mode to be enabled. We can also use these values if we care about where the cursor moved relative to the previous mouse motion event.
Remember, we can also use SDL_GetWindowRelativeMouseMode() to check whether relative mode is enabled or not. This can help us when the behavior of our event handler needs to adapt accordingly:
src/main.cpp
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <iostream>
#include "Window.h"
void HandleMotionEvent(
const SDL_MouseMotionEvent& E, Window& GameWindow
) {
if (SDL_GetWindowRelativeMouseMode(GameWindow.GetRaw())) {
std::cout << "Relative mouse motion: "
<< E.xrel << ", " << E.yrel << '\n';
} else {
std::cout << "Cursor window position: "
<< E.x << ", " << E.y << '\n';
}
}
void HandleKeyboardEvent(
const SDL_KeyboardEvent& E, Window& GameWindow
) {
if (E.key == SDLK_TAB) {
bool CurrentMode{SDL_GetWindowRelativeMouseMode(
GameWindow.GetRaw()
)};
SDL_SetWindowRelativeMouseMode(
GameWindow.GetRaw(), !CurrentMode
);
}
}
int main(int, char**) {
SDL_Init(SDL_INIT_VIDEO);
Window GameWindow;
SDL_Event Event;
bool IsRunning = true;
while (IsRunning) {
while (SDL_PollEvent(&Event)) {
if (Event.type == SDL_EVENT_MOUSE_MOTION) {
HandleMotionEvent(Event.motion, GameWindow);
} else if (Event.type == SDL_EVENT_QUIT) {
IsRunning = false;
} else if (Event.type == SDL_EVENT_KEY_DOWN) {
HandleKeyboardEvent(Event.key, GameWindow);
}
}
GameWindow.Render();
GameWindow.Update();
}
SDL_Quit();
return 0;
}Cursor window position: 428, 81
Cursor window position: 428, 82
Cursor window position: 428, 83
[After pressing TAB]:
Relative mouse motion: -1, 0
Relative mouse motion: -1, -1
Relative mouse motion: 0, -2Relative Speed Scaling
In most scenarios, we'll want to apply some coefficient to the relative mouse movement reported by SDL. For example, if the mouse is being used to control a camera in our game, we can multiply the relative movement by some coefficients to make the camera move slower or faster.
This is often a user-configurable setting (e.g., "Mouse Sensitivity").
We can apply this scaling factor directly to the relative motion data we receive.
SDL_HINT_MOUSE_RELATIVE_SPEED_SCALE
In addition to scaling the relative speed on a case-by-case basis, we can prompt SDL to scale the speed before it is reported to us.
We can do this by setting the SDL_HINT_MOUSE_RELATIVE_SPEED_SCALE. This will be used to scale the movement reported by SDL within mouse motion events and SDL_GetRelativeMouseState() when relative mode is enabled. By default, this has a value of 1.0, but we can scale it up or down as desired:
// Double the relative movement values
SDL_SetHint(
SDL_HINT_MOUSE_RELATIVE_SPEED_SCALE, "2.0");
// Halve the relative movement values
SDL_SetHint(
SDL_HINT_MOUSE_RELATIVE_SPEED_SCALE, "0.5");SDL_HINT_MOUSE_RELATIVE_SYSTEM_SCALE
When our operating system receives mouse input, the platform typically reinterprets those inputs to tweak the speed and acceleration with which our cursor moves.
These remappings are designed on the assumption our mouse is controlling a cursor. In relative mode, we're usually not controlling a cursor, so this intervention generally degrades the user experience.
As such, by default, SDL ignores these system settings when relative mode is enabled. We can change this by setting the SDL_HINT_MOUSE_RELATIVE_SYSTEM_SCALE to "1", instead of its default value of "0":
// Respect system mouse acceleration
SDL_SetHint(
SDL_HINT_MOUSE_RELATIVE_SYSTEM_SCALE, "1");
// Ignore system mouse acceleration (default)
SDL_SetHint(
SDL_HINT_MOUSE_RELATIVE_SYSTEM_SCALE, "0");Relative Mouse State
Previously, we introduced how we could query the mouse state at any time using SDL_GetMouseState(), as covered in our lesson on .
When using relative mode, SDL_GetRelativeMouseState() is likely to be more useful. It reports how far the mouse has moved, relative to the previous invocation of SDL_GetRelativeMouseState().
In SDL3, it operates similarly to SDL_GetMouseState() but provides the relative movement of the mouse as float values. The function updates the provided pointers with the relative x and y values:
float x, y;
SDL_GetRelativeMouseState(&x, &y);We can pass a nullptr as either argument if we only care about relative movement in one direction:
// We only care about horizontal motion
float x;
SDL_GetRelativeMouseState(&x, nullptr);// We only care about vertical motion
float y;
SDL_GetRelativeMouseState(nullptr, &y);Let's create an object for tracking our mouse, and we'll connect it to our application loop via a Tick() function:
Files
Mouse movement this frame: 0, 1
Mouse movement this frame: -1, 2
Mouse movement this frame: 0, 2Because SDL_GetRelativeMouseState() is sensitive to previous invocations, it is typically the case that we only want a single object monitoring the relative mouse state across our entire application.
If we call the function more than once in a single tick, all invocations except the first will report no further movement. In the following example, TrackerA "consumes" all of our movement data, leaving nothing visible to TrackerB:
src/main.cpp
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <iostream>
#include "Window.h"
#include "MouseTracker.h"
int main(int, char**) {
SDL_Init(SDL_INIT_VIDEO);
Window GameWindow;
MouseTracker TrackerA;
MouseTracker TrackerB;
SDL_Event Event;
bool IsRunning = true;
while (IsRunning) {
while (SDL_PollEvent(&Event)) {
if (Event.type == SDL_EVENT_QUIT) {
IsRunning = false;
}
}
GameWindow.Render();
TrackerA.Tick();
TrackerB.Tick();
std::cout << '\n';
GameWindow.Update();
}
SDL_Quit();
return 0;
}Mouse movement this frame: 0, 1
Mouse movement this frame: 0, 0
Mouse movement this frame: -1, 2
Mouse movement this frame: 0, 0
Mouse movement this frame: 0, 2
Mouse movement this frame: 0, 0SDL_GetRelativeMouseState() doesn't require relative mode to be enabled. Even if the player is controlling a pointer, there are some use cases where we're interested in the relative motion of that pointer, and SDL_GetRelativeMouseState() can help with that.
Mouse Button State
In our previous lesson on mouse state, we covered how to determine if our mouse buttons are currently pressed using the return value of SDL_GetMouseState().
The SDL_GetRelativeMouseState() function returns this same value, so we can use it in the same way:
src/MouseTracker.h
#pragma once
#include <SDL3/SDL.h>
#include <iostream>
struct MouseTracker {
void Tick() {
float x, y;
SDL_MouseButtonFlags Buttons{
SDL_GetRelativeMouseState(&x, &y)};
if (Buttons & SDL_BUTTON_MASK(SDL_BUTTON_LEFT)) {
std::cout << "Left button is pressed\n";
}
if (Buttons & SDL_BUTTON_MASK(SDL_BUTTON_MIDDLE)) {
std::cout << "Middle button is pressed\n";
}
if (Buttons & SDL_BUTTON_MASK(SDL_BUTTON_RIGHT)) {
std::cout << "Right button is pressed\n";
}
}
};If we don't care about the mouse position and only care about the mouse button state, SDL_GetRelativeMouseState() and SDL_GetMouseState() are equivalent:
SDL_MouseButtonFlags ButtonsA{
SDL_GetRelativeMouseState(nullptr, nullptr)};
// Equivalent:
SDL_MouseButtonFlags ButtonsB{
SDL_GetMouseState(nullptr, nullptr)};Programattically Moving the Mouse
We can position the user's pointer anywhere on their screen using the SDL_WarpMouseGlobal() function. This the x and y coordinates we want to move the mouse to within their screen space.
The following expression sets the mouse 50 pixels to the right and 100 pixels below the top left corner of their primary display:
SDL_WarpMouseGlobal(50.0, 100.0);We can call this function at any time to forcibly move the user's pointer, but it's most useful within a hybrid interaction model to set where the cursor should be whenever the user leaves relative mouse mode.
In the following program, we record where the cursor was when relative mode was enabled, and then move the cursor back to that same position when it ends:
src/main.cpp
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <iostream>
#include "Window.h"
struct MouseState {
float x{0};
float y{0};
};
void HandleKeyboardEvent(
const SDL_KeyboardEvent& E, Window& GameWindow,
MouseState& Mouse
) {
if (E.key == SDLK_TAB) {
bool CurrentMode{SDL_GetWindowRelativeMouseMode(
GameWindow.GetRaw()
)};
if (!CurrentMode) {
// About to enable relative mode - save position
SDL_GetGlobalMouseState(&Mouse.x, &Mouse.y);
SDL_SetWindowRelativeMouseMode(
GameWindow.GetRaw(), true
);
} else {
// About to disable relative mode - restore position
SDL_SetWindowRelativeMouseMode(
GameWindow.GetRaw(), false
);
SDL_WarpMouseGlobal(Mouse.x, Mouse.y);
}
}
}
int main(int, char**) {
SDL_Init(SDL_INIT_VIDEO);
Window GameWindow;
MouseState Mouse;
SDL_Event Event;
bool IsRunning = true;
while (IsRunning) {
while (SDL_PollEvent(&Event)) {
if (Event.type == SDL_EVENT_QUIT) {
IsRunning = false;
} else if (Event.type == SDL_EVENT_KEY_DOWN) {
HandleKeyboardEvent(Event.key, GameWindow, Mouse);
}
}
GameWindow.Render();
GameWindow.Update();
}
SDL_Quit();
return 0;
}Errors with Mouse Movement
SDL_WarpMouseGlobal() returns a boolean representing whether it was successful. We can use this to handle errors in the usual way:
if (!SDL_WarpMouseGlobal(50.0, 100.0)) {
std::cout << "Error warping mouse: " << SDL_GetError();
}The most common reason for mouse warping to fail is that it's not supported in the current platform, but most desktop platforms do allow it.
Warping Mouse Within a Window
A window-based variation for mouse warping is available in the form of the SDL_WarpMouseInWindow() function. This positions the mouse relative to an SDL_Window, rather than relative to the global screen space.
It has a broadly similar API - the only difference is that the first argument will be the SDL_Window* we want to move the pointer within:
if (!SDL_WarpMouseInWindow(
GameWindow.GetRaw(), 50.0, 100.0
)) {
std::cout << "Error warping mouse: " << SDL_GetError();
}Summary
Relative mouse mode in SDL3 allows developers to capture continuous mouse motion within a window, particularly useful for first-person games and applications that require precise directional tracking.
This mode differs from traditional pointer-based interactions by focusing on relative movement instead of absolute cursor position. Key takeaways:
SDL_SetWindowRelativeMouseMode(): Enables or disables relative mouse mode for a specific window.SDL_GetWindowRelativeMouseMode(): Checks if relative mouse mode is currently active for a window.SDL_GetRelativeMouseState(): Retrieves relative mouse movement (asfloats) and button states.SDL_RaiseWindow(): Ensures the correct window has input focus before enabling relative mode.SDL_SetHint(): Set hints to control mouse speed scaling.
Customising Mouse Cursors
Learn how to control cursor visibility, switch between default system cursors, and create custom cursors