Mouse Input Basics

Discover how to process mouse input, including position tracking and button presses
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

Get Started for Free
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Updated

Building on our basic SDL window setup, this lesson introduces interactivity by focusing on mouse input.

We'll explore how SDL represents mouse actions through its event system. You'll learn to detect when the user moves the mouse, clicks buttons, or even moves the cursor into or out of the application window.

Key topics include:

  • Processing SDL_MOUSEMOTION events to get cursor coordinates.
  • Understanding SDL's coordinate system.
  • Detecting mouse button presses and releases (SDL_MOUSEBUTTONDOWN, SDL_MOUSEBUTTONUP).
  • Identifying which mouse button was clicked.
  • Handling double clicks.
  • Responding to the cursor entering or leaving the window (SDL_WINDOWEVENT_ENTER, SDL_WINDOWEVENT_LEAVE).

Starting Point

We'll start with the basic SDL application structure from our previous lessons. This includes initializing SDL, creating a window using our Window class, and setting up the main event loop. The code below provides this foundation:

#include <SDL.h>
#include "Window.h"

int main(int argc, char** argv) {
  SDL_Init(SDL_INIT_VIDEO);
  Window GameWindow;

  SDL_Event E;
  while (true) {
    while (SDL_PollEvent(&E)) {
      if (E.type == SDL_QUIT) {
        SDL_Quit();
        return 0;
      }
    }

    GameWindow.Render();
    GameWindow.Update();
  }

  return 0;
}
#pragma once
#include <iostream>
#include <SDL.h>

class Window {
public:
  Window() {
    SDLWindow = SDL_CreateWindow(
      "Hello Window",
      SDL_WINDOWPOS_UNDEFINED,
      SDL_WINDOWPOS_UNDEFINED,
      700, 300, 0
    );
  }

  void Render() {
    SDL_FillRect(
      GetSurface(),
      nullptr,
      SDL_MapRGB(
        GetSurface()->format, 50, 50, 50
      )
    );
  }

  void Update() {
    SDL_UpdateWindowSurface(SDLWindow);
  }

  SDL_Surface* GetSurface() const {
    return SDL_GetWindowSurface(SDLWindow);
  }

  Window(const Window&) = delete;
  Window& operator=(const Window&) = delete;
  ~Window() {
    if (SDLWindow && SDL_WasInit(SDL_INIT_VIDEO)) {
      SDL_DestroyWindow(SDLWindow);
    }
  }

private:
  SDL_Window* SDLWindow{nullptr};
};

As a reminder, our SDL_PollEvent(&Event) statement will update our Event object with any mouse action that the user performs. We’ll then detect and react to those actions within the body of the event loop.

Mouse Motion Events

Every time the player moves their mouse within our window, an SDL_Event is pushed onto the event queue.

That event will have a type of SDL_MOUSEMOTION, and the mouse’s new position is available through the x and y members of the motion object:

// main.cpp
#include <SDL.h>
#include "Window.h"

int main(int argc, char** argv) {
  SDL_Init(SDL_INIT_VIDEO);
  Window GameWindow;

  SDL_Event E;
  while (true) {
    while (SDL_PollEvent(&E)) {
      if (E.type == SDL_MOUSEMOTION) {
        std::cout << "Mouse Motion Detected - "
          << "x: " << E.motion.x
          << ", y: " << E.motion.y << '\n';
      } else if (E.type == SDL_QUIT) {
        SDL_Quit();
        return 0;
      }
    }

    GameWindow.Render();
    GameWindow.Update();
  }

  return 0;
}
Mouse Motion Detected - x: 2, y: 77
Mouse Motion Detected - x: 6, y: 79
Mouse Motion Detected - x: 8, y: 80

SDL’s Coordinate System

When working with SDL, and pixel data in general, the coordinate system that is used tends to be different to what we might expect.

It’s common that the origin - that is, the position $(0, 0)$ - represents the top left of the window, surface, or image. Increasing x values corresponds to moving right, and increasing y values corresponds to moving down.

So, if our window was 700 units wide and 300 units tall, it’s coordinate system would look like this:

Diagram showing our screen space

For example, if we have a mouse motion event with an x value of 300, that means the pointer was moved to a position that is 300 units from the left edge of our window. If y is 200, that means the pointer is 200 pixels from the top edge of the window.

Diagram showing a mouse position in screen space

This is sometimes called a y-down coordinate system. The system we typically use in other contexts, and we learn about in geometry class, is y-up

Diagram comparing y-up and y-down coordsinate systems

We cover coordinate systems and moving objects between them in much more detail later in the course.

Preview: Multiple Windows

Our program can have multiple windows open at the same time. The x and y variables of the motion object tell us where the cursor moved to within a window, but not which window the cursor is hovering over.

We cover multi-window applications later in the course but, outside of those lessons, we’ll assume that our program only has a single window.

Position Arithmetic

If we need to work with positions relative to some other location, such as the bottom-right of our window, that tends to be easy to work out with some arithmetic.

However, this arithmetic requires us to know the width and height of the window. We can add getters to our Window class to support that:

// Window.h
// ...

class Window {
public:
  Window() {
    SDLWindow = SDL_CreateWindow(
      "Scene",
      SDL_WINDOWPOS_UNDEFINED,
      SDL_WINDOWPOS_UNDEFINED,
      GetWidth(), GetHeight(),
      0
    );
  }

  int GetWidth() const { return 700; }
  int GetHeight() const { return 300; }
  
  // ...
};

Note that this code assumes our window is not resizable. We cover window sizing and resizing in a dedicated chapter later in the course.

With our getters in place, any code with access to our window can now easily retrieve it’s dimensions when needed:

if (E.type == SDL_MOUSEMOTION) {
  int DistanceFromLeftEdge{E.motion.x};
  int DistanceFromTopEdge{E.motion.y};
  int DistanceFromRightEdge{
    GameWindow.GetWidth() - E.motion.x};
  int DistanceFromBottomEdge{
    GameWindow.GetHeight() - E.motion.y};
}

SDL_MouseMotionEvent

When we’re dealing with an SDL_Event that has a type of SDL_MOUSEMOTION, the motion struct it contains has the SDL_MouseMotionEvent type.

When working with mouse motion events, this object is slightly easier to use than the top-level SDL_Event, as it does not require us to access the intermediate motion subobject to retrieve the information we care about.

This makes it useful for storing and transferring mouse motion events. The event loop body can get very large, so to mitigate this, we can move our mouse motion handler to a standalone function, and pass the Event.motion struct to it from our event loop:

// main.cpp
#include <SDL.h>
#include "Window.h"

void HandleMotion(SDL_MouseMotionEvent& E) {
  int DistanceFromLeft{E.x};
  int DistanceFromTop{E.y};
  // ...
}

int main(int argc, char** argv) {
  SDL_Init(SDL_INIT_VIDEO);
  Window GameWindow;

  SDL_Event E;
  while (true) {
    while (SDL_PollEvent(&E)) {
      if (E.type == SDL_MOUSEMOTION) {
        HandleMotion(E.motion);
      } else if (E.type == SDL_QUIT) {
        SDL_Quit();
        return 0;
      }
    }

    GameWindow.Render();
    GameWindow.Update();
  }

  return 0;
}

Mouse Enter / Mouse Leave Events

By default, SDL will only report mouse motion events when our window has mouse focus - that is, when the user’s cursor is hovering over our window.

When the user’s cursor enters or leaves our window, SDL provides events to notify us of this. These events have a type of SDL_WINDOWEVENT.

while(SDL_PollEvent(&E)) {
  if (E.type == SDL_WINDOWEVENT) {
    // ...
  }
}

However, a wide range of other events are categorized under this SDL_WINDOWEVENT type too, which we’ll cover in more detail later.

If our event is an SDL_WINDOWEVENT, we can access more specific information under the window struct of that SDL_Event. This subobject has another field called event, which gives us a more specific categorization.

If the window event represents the user’s cursor entering our window, the window.event member will be equal to SDL_WINDOWEVENT_ENTER. When the event represents the cursor leaving the window, it will be equal to SDL_WINDOWEVENT_LEAVE:

if (E.type == SDL_WINDOWEVENT) {
  if (E.window.event == SDL_WINDOWEVENT_ENTER) {
    std::cout << "Mouse Entered Window\n";
  } else if (E.window.event == SDL_WINDOWEVENT_LEAVE) {
    std::cout << "Mouse Left Window\n";
  }
}

SDL_WindowEvent

Similar to SDL_MouseMotionEvent, a dedicated SDL_WindowEvent type is available for window events, which is helpful if we want to send that event to some other function.

If the outer SDL_Event has a type of SDL_WINDOWEVENT, then the window variable of that event will contain the SDL_WindowEvent:

#include <SDL.h>
#include "Window.h"

void HandleWindowEvent(SDL_WindowEvent& E) {
  if (E.event == SDL_WINDOWEVENT_ENTER) {
    std::cout << "Mouse Entered Window\n";
  } else if (E.event == SDL_WINDOWEVENT_LEAVE) {
    std::cout << "Mouse Left Window\n";
  }
}

int main(int argc, char** argv) {
  SDL_Init(SDL_INIT_VIDEO);
  Window GameWindow;

  SDL_Event E;
  while (true) {
    while (SDL_PollEvent(&E)) {
      if (E.type == SDL_WINDOWEVENT) {
        HandleWindowEvent(E.window);
      } else if (E.type == SDL_QUIT) {
        SDL_Quit();
        return 0;
      }
    }

    GameWindow.Render();
    GameWindow.Update();
  }

  return 0;
}

Reminder: Function Overloading

In our previous examples, we’re giving our event handlers different names - HandleMotion() and HandleWindowEvent() - based on the type of events they are for. However, this is not entirely necessary as the functions also have different parameter types - SDL_MouseMotionEvent and SDL_WindowEvent.

We can give our functions the same name if we prefer, and let the compiler select the correct one based on the argument provided in our event loop:

#include <SDL.h>
#include "Window.h"

void HandleEvent(SDL_MouseMotionEvent& E) {
  // ...
}

void HandleEvent(SDL_WindowEvent& E) {
  // ...
}

int main(int argc, char** argv) {
  SDL_Init(SDL_INIT_VIDEO);
  Window GameWindow;

  SDL_Event E;
  while (true) {
    while (SDL_PollEvent(&E)) {
      if (E.type == SDL_MOUSEMOTION) {
        HandleEvent(E.motion);
      } else if (E.type == SDL_WINDOWEVENT) {
        HandleEvent(E.window);
      } else if (E.type == SDL_QUIT) {
        SDL_Quit();
        return 0;
      }
    }

    GameWindow.Render();
    GameWindow.Update();
  }

  return 0;
}

Mouse Click Events

When any mouse button is pressed or released, we get an SDL_Event whose type is equal to SDL_MOUSEBUTTONDOWN or SDL_MOUSEBUTTONUP respectively:

while (SDL_PollEvent(&E)) {
  if (E.type == SDL_MOUSEBUTTONDOWN) {
    std::cout << "Button Pressed\n";
  } else if (E.type == SDL_MOUSEBUTTONUP) {
    std::cout << "Button Released\n";
  }
}

Both of these event types include an SDL_MouseButtonEvent in the button variable:

#include <SDL.h>
#include "Window.h"

void HandleButtonEvent(SDL_MouseButtonEvent& E) {
  // ...
}

int main(int argc, char** argv) {
  SDL_Init(SDL_INIT_VIDEO);
  Window GameWindow;

  SDL_Event E;
  while (true) {
    while (SDL_PollEvent(&E)) {
      if (
        E.type == SDL_MOUSEBUTTONDOWN ||
        E.type == SDL_MOUSEBUTTONUP
      ) {
        HandleButtonEvent(E.button);
      } else if (E.type == SDL_QUIT) {
        SDL_Quit();
        return 0;
      }
    }

    GameWindow.Render();
    GameWindow.Update();
  }

  return 0;
}

Button Pressed vs Button Released

This SDL_MouseButtonEvent also includes the type value, so we can still determine if it corresponds to a button being pressed or released. Alternatively, we can access the state variable, and compare it to SDL_PRESSED or SDL_RELEASED:

#include <SDL.h>
#include "Window.h"

void HandleButtonEvent(SDL_MouseButtonEvent& E) {
  if (E.type == SDL_MOUSEBUTTONDOWN) {
    // Button Pressed
  } else if (E.type == SDL_MOUSEBUTTONUP) {
    // Button Released
  }
  
  // Alternatively:
  if (E.state == SDL_PRESSED) {
    // Button Pressed
  } else if (E.state == SDL_RELEASED) {
    // Button Released
  }
}

int main(int argc, char** argv) {/*...*/}

Which Button was Pressed or Released?

To understand which button was pressed or released, the SDL_MouseButtonEvent includes a button member, storing a numeric identifier for the mouse button that triggered the event.

SDL provides some helper values to compare this identifier to the common mouse buttons:

  • SDL_BUTTON_LEFT - The left mouse button
  • SDL_BUTTON_RIGHT - The right mouse button
  • SDL_BUTTON_MIDDLE - The middle mouse button
  • SDL_BUTTON_X1 - The first extra mouse button, which is on the side of some mice
  • SDL_BUTTON_X2 - The second extra mouse button, which is on the side of some mice

Using these values in code might look something like this:

#include <SDL.h>
#include "Window.h"

void HandleButtonEvent(SDL_MouseButtonEvent& E) {
  if (E.button == SDL_BUTTON_LEFT) {
    // The left button was pressed or released
  } else if (E.button == SDL_BUTTON_RIGHT) {
    // The right button was pressed or released
  }

  if (
    E.button == SDL_BUTTON_LEFT &&
    E.state == SDL_PRESSED
  ) {
    // The left button was pressed
  }
}

int main(int argc, char** argv) {/*...*/}

SDL_Event is a Union

SDL_Event is an example of a union. With a union, all members are stored in the same memory location.

This is used when we want a variable to store one of several possible types. In the SDL_Event case, those types are things like SDL_MouseMotionEvent, SDL_WindowEvent, and SDL_MouseButtonEvent.

We cover unions, and modern, safer alternatives to them in our advanced course:

For now, a key point to note about unions is that they’re not type safe. That is, we don’t know what data type is stored in that memory address. If get it wrong by, for example, assuming an SDL_MouseButtonEvent is stored there when it’s actually an SDL_WindowEvent, then the compiler can’t detect that error - we just have a bug.

Before we do almost anything with an SDL_Event, we should ascertain exactly what type it is pointing at. We do this by checking the type field, as shown in our earlier examples. The following code violates this principle:

void HandleEvent(SDL_Event& E) {
  bool isLeftClick{
    E.button.button == SDL_BUTTON_LEFT &&
    E.type == SDL_MOUSEBUTTONDOWN
  };
}

This is accessing the E.button.button variable before we confirmed that the memory address really is storing an SDL_MouseButtonEvent. If it’s not, our program will have a bug.

To fix this, we should change the order of our operands to check the type first, and only access the button member once we know it’s an SDL_MouseButtonEvent:

void HandleEvent(SDL_Event& E) {
  bool isLeftClick{
    E.type == SDL_MOUSEBUTTONDOWN &&
    E.button.button == SDL_BUTTON_LEFT
  };
}

As a reminder, C++, like most programming languages, performs short circuit evaluation. In the second example, if E.type == SDL_MOUSEBUTTONDOWN is false, then E.button.button == SDL_BUTTON_LEFT is never evaluated.

With the && operator, if the left operand is false, then the entire expression will be false. It doesn’t matter what the right operand is, so it’s effectively ignored.

Double Clicks

If our application needs to detect double clicks, we can check the clicks member variable of the SDL_MouseButtonEvent. This represents the number of times the user has clicked that same button in quick succession:

void HandleButtonEvent(const SDL_MouseButtonEvent& E) {
  if (E.clicks >= 2) {
    // Double Click Detected
  }
}

Note that the clicks variable will also be set on the SDL_MOUSEBUTTONUP event, so the code in this if block will be executed twice:

  1. On the SDL_MOUSEBUTTONDOWN event of the second click
  2. On the SDL_MOUSEBUTTONUP event of the second click

It will also detect double-clicks of any button. We’ll often want to filter on the specific user action we care about:

#include <SDL.h>
#include "Window.h"

void HandleButtonEvent(SDL_MouseButtonEvent& E) {
  if (E.button == SDL_BUTTON_LEFT &&
      E.state == SDL_PRESSED &&
      E.clicks >= 2
  ) {
    // The left button was double-clicked
  }
}

int main(int argc, char** argv) {/*...*/}

Using the clicks variable tends to be preferred over trying to create double-click detection from scratch, as it’s more complex than it first appears.

Additionally, most operating systems allow users to change their double-click sensitivity and, by using SDL’s solution, our application can respect those settings.

Preview: Advanced Mouse Handling

This lesson covers the minimal basics of mouse handling, so we can start to build practical projects as quickly as possible. There are many more powerful options that we have access to, including:

  • Accepting input from the mouse scroll wheel
  • Querying the mouse state on demand, without using events
  • Hiding, showing, and customizing the cursor
  • Mouse grabbing, for when we want to prevent the cursor from leaving the window
  • Relative mouse mode, for when we want to create "mouse look" style systems such as those used in first-person shooters

We have a full chapter dedicated to mouse handling later in the course.

Complete Code

Here's the complete code incorporating all the mouse event handling techniques discussed in this lesson. It demonstrates how to detect motion, window enter/leave events, button clicks, and double clicks using simple std::cout statements for illustration. While the specific output functions like HandleMotionEvent() won't be carried forward into subsequent lessons, the fundamental principles – checking event types (Event.type), accessing specific event data (Event.motion, Event.window, Event.button), and processing input within the event loop – are crucial concepts that we will build upon throughout the rest of the course.

#include <SDL.h>
#include "Window.h"

void HandleMotionEvent(
  SDL_MouseMotionEvent& E,
  Window& GameWindow
) {
  std::cout << "Mouse Motion Detected - "
    << "x: " << E.x
    << ", y: " << E.y;

  std::cout << "\n  Distance from Right: "
    << GameWindow.GetWidth() - E.x;

  std::cout << "\n  Distance from Bottom: "
    << GameWindow.GetHeight() - E.y << '\n';
}

void HandleWindowEvent(SDL_WindowEvent& E) {
  if (E.event == SDL_WINDOWEVENT_ENTER) {
    std::cout << "Mouse Entered Window\n";
  } else if (E.event == SDL_WINDOWEVENT_LEAVE) {
    std::cout << "Mouse Left Window\n";
  }
}

void HandleButtonEvent(SDL_MouseButtonEvent& E) {
  if (E.button == SDL_BUTTON_RIGHT) {
    std::cout << "Right Click or Release\n";
  }

  if (E.button == SDL_BUTTON_LEFT &&
      E.state == SDL_PRESSED &&
      E.clicks >= 2
  ) {
    std::cout << "Left Double Click\n";
  }
}

int main(int argc, char** argv) {
  SDL_Init(SDL_INIT_VIDEO);
  Window GameWindow;

  SDL_Event E;
  while (true) {
    while (SDL_PollEvent(&E)) {
      if (E.type == SDL_MOUSEMOTION) {
        HandleMotionEvent(E.motion, GameWindow);
      } else if (E.type == SDL_WINDOWEVENT) {
        HandleWindowEvent(E.window);
      } else if (
        E.type == SDL_MOUSEBUTTONDOWN ||
        E.type == SDL_MOUSEBUTTONUP
      ) {
        HandleButtonEvent(E.button);
      } else if (E.type == SDL_QUIT) {
        SDL_Quit();
        return 0;
      }
    }

    GameWindow.Render();
    GameWindow.Update();
  }

  return 0;
}
#pragma once
#include <iostream>
#include <SDL.h>

class Window {
public:
  Window() {
    SDLWindow = SDL_CreateWindow(
      "Scene",
      SDL_WINDOWPOS_UNDEFINED,
      SDL_WINDOWPOS_UNDEFINED,
      GetWidth(), GetHeight(), 0
    );
  }

  int GetWidth() const { return 700; }
  int GetHeight() const { return 300; }

  ~Window() {
    if (SDLWindow) {
      SDL_DestroyWindow(SDLWindow);
    }
  }

  Window(const Window&) = delete;
  Window& operator=(const Window&) = delete;

  void Render() {
    SDL_FillRect(
      GetSurface(), nullptr,
      SDL_MapRGB(GetSurface()->format,
        220, 220, 220));
  }

  void Update() {
    SDL_UpdateWindowSurface(SDLWindow);
  }

  SDL_Surface* GetSurface() const {
    return SDL_GetWindowSurface(SDLWindow);
  }

private:
  SDL_Window* SDLWindow;
};
Mouse Motion Detected - x: 200, y: 124
  Distance from Right: 500
  Distance from Bottom: 176
Left Double Click
Right Click or Release
Right Click or Release

Summary

We've explored how to make SDL applications responsive to mouse input. By examining the SDL_Event structure within the main loop, we can detect and react to mouse movements, button clicks (including double clicks), and window boundary crossings by the cursor. This forms the basis for implementing mouse controls.

Key Takeaways:

  • The core of input handling is the while(SDL_PollEvent(&E)) loop.
  • Filter events based on E.type (e.g., SDL_MOUSEMOTION, SDL_MOUSEBUTTONDOWN).
  • Access event-specific data using union members (e.g., E.motion, E.button).
  • Mouse position is given by E.motion.x and E.motion.y (y-down).
  • Button identity uses constants like SDL_BUTTON_LEFT, SDL_BUTTON_RIGHT.
  • Button state is checked via E.button.state (SDL_PRESSED, SDL_RELEASED).
  • Double clicks are identified using E.button.clicks.
  • Window enter/leave events are a subtype of SDL_WINDOWEVENT.
  • Dedicated structs like SDL_MouseMotionEvent exist for specific event types.
Free and Unlimited Access

Professional C++

Unlock the true power of C++ by mastering complex features, optimizing performance, and learning expert workflows used in professional development

Screenshot from Warhammer: Total War
Screenshot from Tomb Raider
Screenshot from Jedi: Fallen Order
Ryan McCombe
Ryan McCombe
Updated
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

Get Started for Free
Implementing User Interaction
Free and Unlimited Access

Professional C++

Unlock the true power of C++ by mastering complex features, optimizing performance, and learning expert workflows used in professional development

Screenshot from Warhammer: Total War
Screenshot from Tomb Raider
Screenshot from Jedi: Fallen Order
Contact|Privacy Policy|Terms of Use
Copyright © 2025 - All Rights Reserved