Customising Mouse Cursors

Learn how to control cursor visibility, switch between default system cursors, and create custom cursors
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

Free, Unlimited Access
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Posted

When we’re working on a mouse-based game, creating an engaging user interface typically requires us to change the visual appearance of the cursor.

For example, we might customize the cursor to fit the theme of our game. We may also want to apply further modifications to the cursor based on what the user is pointing at, like changing the cursor to a sword if the pointer is hovering over an enemy. This helps players quickly understand what effect clicking will have.

This lesson will guide you through the various cursor customization options available in SDL2. We’ll cover:

  • Simple visibility toggles to show and hide the cursor
  • Switching between a range of default cursor options provided by the platform
  • Implementing custom cursors from image files
  • Completely replacing the cursor implementation to take full control on how mouse interaction is implemented

Showing and Hiding the Cursor

Similar to trapping the cursor, we also often want to hide it. That can be done with SDL_ShowCursor(). We pass SDL_ENABLE to show the cursor, or SDL_DISABLE to hide it:

// Show the Cursor
SDL_ShowCursor(SDL_ENABLE);

// Hide the Cursor
SDL_ShowCursor(SDL_DISABLE);

Querying Cursor Visibility

We can also use SDL_ShowCursor() to determine the current visibility of the cursor, without changing it. To do this, we pass SDL_QUERY as the argument, and examine the return value:

if (SDL_ShowCursor(SDL_QUERY) == SDL_ENABLE) {
  std::cout << "Cursor is visible";
} else {
  std::cout << "Cursor is hidden";
}
Cursor is visible

Error Handling

In addition to returning the new visibility of the cursor (SDL_ENABLE or SDL_DISABLE), the SDL_GetError() function can alternatively return a negative value. This happens when something goes wrong, and we can call SDL_GetError() to get an explanation of the error:

if (SDL_ShowCursor(SDL_DISABLE) < 0) {
  std::cout << "Error hiding cursor: "
    << SDL_GetError();
}

Premade System Cursors

We pass an SDL_SystemCursor enum value describing which cursor we’d like to create. For example, we can create the default arrow cursor by passing SDL_SYSTEM_CURSOR_ARROW:

SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_ARROW);

The value returned by SDL_CreateSystemCursor() is an SDL_Cursor pointer (an SDL_Cursor*):

SDL_Cursor* Cursor{
  SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_ARROW)
};

We should treat these SDL_Cursor objects as opaque - that is, they are not designed for us to read or modify their properties. However, we still need these SDL_Cursor pointers for use with other SDL functions that we’ll cover later.

SDL_SystemCursor Values

The SDL_SystemCursor enum includes a set of values that are commonly cursors. Its possible values are as follows:

  • SDL_SYSTEM_CURSOR_ARROW: The default cursor, usually designed to look like an arrow
  • SDL_SYSTEM_CURSOR_HAND: An alternative cursor designed to look like a hand
  • SDL_SYSTEM_CURSOR_CROSSHAIR: An alternative cursor designed to look like a crosshair
  • SDL_SYSTEM_CURSOR_IBEAM: The I-beam cursor, typically used for text editing
  • SDL_SYSTEM_CURSOR_WAIT: A cursor designed to indicate something is loading
  • SDL_SYSTEM_CURSOR_WAITARROW: An alternative design to the loading cursor
  • SDL_SYSTEM_CURSOR_NO: Used to indicate blocked or disabled actions

Most systems have cursors designed to indicate resizing actions, such as when you move your pointer next to the edge of a resizable window. We can load these system cursors using the following enum values:

  • SDL_SYSTEM_CURSOR_SIZENWSE: Double arrow pointing northwest and southeast,
  • SDL_SYSTEM_CURSOR_SIZENESW: Double arrow pointing northeast and southwest
  • SDL_SYSTEM_CURSOR_SIZEWE: Double arrow pointing west and east
  • SDL_SYSTEM_CURSOR_SIZENS: Double arrow pointing north and south
  • SDL_SYSTEM_CURSOR_SIZEALL: Four pointed arrow pointing north, south, east, and west

Error Handling

It is possible for the SDL_Cursor pointer returned by SDL_CreateSystemCursor() to be a nullptr. This represents an error, and we can call SDL_GetError() for an explanation:

SDL_Cursor* Cursor{
  SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_ARROW)
};

if (!Cursor) {
  std::cout << "Failed to create cursor: "
    << SDL_GetError();
}

Setting Cursors

Once we’ve created a cursor and have access to it through an SDL_Cursor pointer, we can instruct SDL to use that cursor for our mouse. We do this by passing the pointer to SDL_SetCursor():

SDL_Cursor* Cursor{
  SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_ARROW)
};

SDL_SetCursor(Cursor);

Freeing Cursors

When we create a cursor using a function like SDL_CreateSystemCursor(), SDL allocates the memory required for that cursor and manages it internally.

However, when we no longer need that cursor, we should communicate that to SDL so it can free the memory and prevent leaks. We do this by passing the SDL_Cursor* to SDL_FreeCursor():

// Create the cursor
SDL_Cursor* Cursor{
  SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_ARROW)
};

// Use the cursor...

// Free it
SDL_FreeCursor(Cursor);

Note this only applies to cursors we create using functions like SDL_CreateSystemCursor() or SDL_CreateColorCursor(). Every invocation of a cursor creation function should be matched with an invocation to SDL_FreeCursor().

Functions that retrieve a pointer to an existing SDL_Cursor, such as SDL_GetCursor() and SDL_GetDefaultCursor(), do not allocate new memory, so we do not need to free the pointer they return.

Getting the Current Cursor

We can get the cursor we are currently using SDL_GetCursor(). This will return the SDL_Cursor pointer applied from our most recent call to SDL_SetCursor(), or the default cursor if we never changed it:

SDL_Cursor* Cursor{
  SDL_GetCursor()
};

We can use this to check if a specific cursor is currently active:

SDL_Cursor* Crosshair{
  SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_CROSSHAIR)
};

if (SDL_GetCursor() != Crosshair) {
  std::cout << "Crosshair cursor is not active";
}

SDL_SetCursor(Cursor);

if (SDL_GetCursor() == Crosshair) {
  std::cout << "\nBut now it is";
}
Crosshair cursor is not active
But now it is

Getting the Default Cursor

We can get a pointer to the system’s default cursor using the SDL_GetDefaultCursor() function:

SDL_Cursor* DefaultCursor{
  SDL_GetDefaultCursor()
};

This is primarily used when we need to revert a change we made using SDL_SetCursor():

SDL_Cursor* Crosshair{
  SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_CROSSHAIR)
};

// Use a non-default cursor
SDL_SetCursor(Crosshair);

// ...

// Revert to the default
SDL_SetCursor(SDL_GetDefaultCursor());

If SDL_GetDefaultCursor() fails, it will return a nullptr, and we can call SDL_GetError() to understand why:

SDL_Cursor* DefaultCursor{
  SDL_GetDefaultCursor()
};

if (!DefaultCursor) {
  std::cout << "Unable to get default cursor: "
    << SDL_GetError();
}

Creating a Custom Cursor

The first step of creating a custom cursor is to get the pixel data for that cursor into an SDL_Surface. Remember, we can use the SDL_image library to load an image file into an SDL_Surface:

#include <SDL.h>
#include <SDL_image.h>
#include <iostream>

int main(int, char**) {
  // 1. Initialize SDL
  SDL_Init(SDL_INIT_VIDEO);
  IMG_Init(IMG_INIT_PNG);

  // 2. Load image into an SDL_Surface
  SDL_Surface* Surface{IMG_Load("cursor.png")};
  if (!Surface) {
    std::cout << "IMG_Load Error: "
      << SDL_GetError();
  }

  // 3. Use surface
  // ...

  // 4. Cleanup resources
  SDL_FreeSurface(Surface);
  IMG_Quit();
  SDL_Quit();

  return 0;
}

Once we have our SDL_Surface, we can create a cursor from it using the SDL_CreateColorCursor() function. We pass the SDL_Surface pointer as the first argument, in addition to two integers representing horizontal (x) and vertical (y) coordinates:

#include <SDL.h>
#include <SDL_image.h>
#include <iostream>

int main(int, char**) {
  // 1. Initialize SDL
  SDL_Init(SDL_INIT_VIDEO);
  IMG_Init(IMG_INIT_PNG);

  // 2. Load image into an SDL_Surface
  SDL_Surface* Surface{IMG_Load("cursor.png")};
  if (!Surface) {
    std::cout << "IMG_Load Error: "
      << SDL_GetError();
  }

  // 3. Create and use cursor
  SDL_Cursor* Cursor{SDL_CreateColorCursor(
    Surface, 10, 20)};
  SDL_SetCursor(Cursor);

  // The SDL_CreateColorCursor() function copies
  // the image data so we can safely free the
  // surface after it completes
  SDL_FreeSurface(Surface);

  // 4. Game Loop
  // ...

  // 5. Cleanup resources
  IMG_Quit();
  SDL_Quit();

  return 0;
}

The integer x and y arguments tell SDL where the "hot spot" is within the cursor image. The hot spot is the exact part of the cursor image that corresponds to what the user is pointing at.

For example, if our cursor was an image of an arrow, the hot spot would be the exact tip of that arrow. For a cursor designed to look like a hand, the hot spot would be the tip of the finger.

Image showing two cursors with their hot spots highlighted

The x value is the distance of this pixel from the left edge of the image, and the y value is the distance from the top edge.

Error Handling

If SDL_CreateCursor() is unable to create a cursor, it will return a nullptr. We can react accordingly to this, and call SDL_GetError() if we want an explanation of what went wrong:

SDL_Cursor* Cursor{SDL_CreateColorCursor(
  nullptr, // Passing nullptr as surface 
  10, 20
)};

if (!Cursor) {
  std::cout << "Error creating cursor: "
    << SDL_GetError();
}
Error creating cursor: Parameter 'surface' is invalid

Creating a Cursor Class

To manage custom cursors and take care of all the memory management required, we’d commonly want to create a dedicated class for this. For example:

// Cursor.h
#pragma once
#include <SDL.h>
#include <SDL_image.h>
#include <iostream>

class Cursor {
public:
  Cursor(std::string Path, int HotX, int HotY)
    : CursorSurface{IMG_Load(Path.c_str())},
      CursorPtr{nullptr},
      HotspotX{HotX},
      HotspotY{HotY}
  {
    if (!CursorSurface) {
      std::cout << "IMG_Load Error: "
        << SDL_GetError();
      return;
    }

    CursorPtr = SDL_CreateColorCursor(
      CursorSurface, HotspotX, HotspotY);

    if (!CursorPtr) {
      std::cout <<
        "SDL_CreateColorCursor Error: "
        << SDL_GetError();
      SDL_FreeSurface(CursorSurface);
      CursorSurface = nullptr;
    }
  }

  ~Cursor() {
    if (CursorPtr) {
      SDL_FreeCursor(CursorPtr);
    }
    if (CursorSurface) {
      SDL_FreeSurface(CursorSurface);
    }
  }

  void Activate() const {
    if (CursorPtr) {
      SDL_SetCursor(CursorPtr);
    }
  }

  void Deactivate() {
    SDL_SetCursor(SDL_GetDefaultCursor());
  }

private:
  SDL_Cursor* CursorPtr;
  SDL_Surface* CursorSurface;
  int HotspotX, HotspotY;
};

The following program uses this class to create a custom cursor. The cursor is applied when the player presses 1, and reverted to the default when they press 2:

// main.cpp
#include <SDL.h>
#include <SDL_image.h>
#include <iostream>
#include "Cursor.h"

int main(int, char**) {
  SDL_Init(SDL_INIT_VIDEO);
  IMG_Init(IMG_INIT_PNG);

  // Create a window
  SDL_Window* Window{SDL_CreateWindow(
    "Cursor Example",
    SDL_WINDOWPOS_UNDEFINED,
    SDL_WINDOWPOS_UNDEFINED,
    800, 600, 0
  )};

  // Create a Cursor instance
  Cursor CustomCursor{"cursor.png", 16, 16};

  // Event loop
  SDL_Event E;
  while (true) {
    while (SDL_PollEvent(&E)) {
      if (E.type == SDL_QUIT) {
        SDL_DestroyWindow(Window);
        IMG_Quit();
        SDL_Quit();
        return 0;
      }
      if (E.type == SDL_KEYDOWN) {
        if (E.key.keysym.sym == SDLK_1) {
          CustomCursor.Activate();
        } else if (E.key.keysym.sym == SDLK_2) {
          CustomCursor.Deactivate();
        }
      }
    }
  }
}

SDL_CreateCursor()

SDL also includes the SDL_CreateCursor() function, but it is less capable and more difficult to use than SDL_CreateColorCursor().

The SDL_CreateCursor() function can only be used to create monochrome cursors, and the API for providing the pixel data is less friendly than the SDL_Surface used by SDL_CreateColorCursor().

As such, we tend to not use it, but the official documentation is available here for those interested.

Bespoke Cursor Rendering

We’re not forced to use SDL’s mechanisms for cursor customization if we don’t want to. We can take on full responsibility for cursor rendering within our code. This allows us to do more advanced things with the cursor, such as animation or physics-based motion

To set this up, we’d hide the system-managed cursor using SDL_ShowCursor(SDL_DISABLE), and then render our object on the screen using, for example, the surface blitting techniques we covered earlier.

We’d update the blitting position based on SDL_MouseMotion events coming through our event loop, or SDL_GetMouseState() calls within a tick function.

Below, we’ve updated our Cursor class to use this technique. We also included a Tick() function as an example, but it’s not currently doing anything:

// Cursor.h
#pragma once
#include <SDL.h>
#include <SDL_image.h>
#include <iostream>

class Cursor {
public:
  Cursor(std::string Path, int HotX, int HotY)
    : CursorSurface{IMG_Load(Path.c_str())},
      HotspotX{HotX},
      HotspotY{HotY} {
    if (!CursorSurface) {
      std::cout << "IMG_Load Error: "
        << SDL_GetError();
    }
  }

  ~Cursor() {
    if (CursorSurface) {
      SDL_FreeSurface(CursorSurface);
    }
  }
  
  void Tick() {
    // Do tick things - eg animation
    // ...
  }

  void HandleMouseMotion(
    const SDL_MouseMotionEvent& E
  ) {
    // Update the cursor position such that the next
    // render blits the image in the correct place
    CursorX = E.x;
    CursorY = E.y;
  }

  void Render(SDL_Surface* targetSurface) const {
    if (!CursorSurface || !targetSurface) {
      return;
    }
    
    // Position the blitting such that the image's
    // hotspot matches the last reported cursor position
    SDL_Rect Destination{
      CursorX - HotspotX,
      CursorY - HotspotY,
      0, 0
    };

    SDL_BlitSurface(
      CursorSurface, nullptr,
      targetSurface, &Destination
    );
  }

private:
  SDL_Surface* CursorSurface;
  int HotspotX, HotspotY;
  int CursorX, CursorY;
};

Our event loop now needs to forward mouse motion events to our custom Cursor object, and our application loop needs to Tick() and Render() the cursor at the appropriate time.

Here’s an example:

// main.cpp
#include <SDL.h>
#include <SDL_image.h>
#include <iostream>
#include "Cursor.h"

int main(int, char**) {
  SDL_Init(SDL_INIT_VIDEO);
  IMG_Init(IMG_INIT_PNG);

  // Create a window
  SDL_Window* Window{SDL_CreateWindow(
    "Cursor Example",
    SDL_WINDOWPOS_UNDEFINED,
    SDL_WINDOWPOS_UNDEFINED,
    800, 600, 0
  )};

  SDL_Surface* WindowSurface{
    SDL_GetWindowSurface(Window)};

  // Create a Cursor instance
  Cursor CustomCursor{"cursor.png", 16, 16};

  // Application loop
  SDL_Event E;
  while (true) {
    // 1. HANDLE EVENTS
    while (SDL_PollEvent(&E)) {
      if (E.type == SDL_QUIT) {
        SDL_DestroyWindow(Window);
        IMG_Quit();
        SDL_Quit();
        return 0;
      }

      if (E.type == SDL_MOUSEMOTION) {
        CustomCursor.HandleMouseMotion(E.motion);
      }
    }
    
    // 2. TICK
    CustomCursor.Tick();
    
    // 3. RENDER
    // Start a new frame by clearing the surface
    SDL_FillRect(
      WindowSurface,
      nullptr,
      SDL_MapRGB(WindowSurface->format, 0, 0, 0
    ));
    
    // Render the game objects and UI elements
    // ...

    // Render the cursor last to ensure it is on
    // top of the other content
    CustomCursor.Render(WindowSurface);

    // Display the new frame
    SDL_UpdateWindowSurface(Window);
  }
}

Note that, whilst rendering the cursor through our application loop gives us more flexibility, there is a downside compared to using the native cursor rendering approach provided by functions like SDL_CreateColorCursor() and SDL_SetCursor().

When we render the cursor as part of our regular game loop, it means cursor updates are constrained by how fast our loop can output each frame.

Reduced frame rates are particularly noticable to players when it affects their cursor motion, so we should be careful here. We should only use this approach if it we require the more advanced capabilities it unlocks, or if we’re certain our application will run quickly enough to ensure the user’s mouse actions remain highly responsive.

Summary

In this lesson, we covered all the main cursor management techniques available within SDL games. Here are the key points:

  • Controlling cursor visibility using SDL_ShowCursor()
  • Using system-provided cursors through SDL_CreateSystemCursor()
  • Managing cursor resources with proper cleanup
  • Creating custom cursors from images with SDL_CreateColorCursor()
  • Understanding cursor hotspots and their importance
  • Creating a reusable Cursor class for better organization
  • Implementing custom cursor rendering for advanced effects

Was this lesson useful?

Next Lesson

Working with Data

Learn techniques for managing game data, including save systems, configuration, and networked multiplayer.
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Posted
sdl2-promo.jpg
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

Free, Unlimited Access
Mouse Input
sdl2-promo.jpg
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

Free, unlimited access

This course includes:

  • 79 Lessons
  • 100+ Code Samples
  • 91% Positive Reviews
  • Regularly Updated
  • Help and FAQ
Next Lesson

Working with Data

Learn techniques for managing game data, including save systems, configuration, and networked multiplayer.
Abstract art representing computer programming
Contact|Privacy Policy|Terms of Use
Copyright © 2025 - All Rights Reserved