SDL_Rect
SDL_Rect
and SDL_Color
types.So far, we've learned how to create a window and fill it with a solid color. While useful, most applications need to draw more specific shapes and images.
One of the most fundamental shapes in 2D graphics is the rectangle. Rectangles are essential for user interfaces (buttons, text boxes), game elements (sprites, collision boxes), and defining areas on the screen (viewports, selection boxes).
SDL provides tools specifically for working with rectangles. We'll learn about the SDL_Rect
type for defining position and size, and the SDL_Color
type for managing colors independently of screen formats. We'll then combine these to draw rectangles and make them respond to basic mouse interaction.
We'll start with the basic SDL application structure we developed previously. This includes initializing SDL, creating a Window class to manage the SDL_Window
and its surface, and running the main event loop.
#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};
};
Let’s add a Rectangle
class to manage our rectangles. We want our rectangle to render itself to the screen, so we’ll add a Render()
function that accepts an SDL_Surface
pointer. This is the surface that our Rectangle
instance will render itself to:
// Rectangle.h
#pragma once
#include <SDL.h>
class Rectangle {
public:
void Render(SDL_Surface* Surface) const {
// ...
}
};
Over in our main
function, let’s construct a Rectangle
and Render()
it at the appropriate time.
As we covered in our double buffering lesson, the rendering process starts with the call to GameWindow.Render()
- which fills the window surface with a solid color, and it ends with the call to GameWindow.Update()
- which triggers the buffer swap.
All of our other objects, such as our Rectangle
, should be rendered between these two steps:
#include <SDL.h>
#include "Window.h"
#include "Rectangle.h"
int main(int argc, char** argv) {
SDL_Init(SDL_INIT_VIDEO);
Window GameWindow;
Rectangle Rect;
SDL_Event E;
while (true) {
while (SDL_PollEvent(&E)) {
if (E.type == SDL_QUIT) {
SDL_Quit();
return 0;
}
}
GameWindow.Render();
Rect.Render(GameWindow.GetSurface());
GameWindow.Update();
}
return 0;
}
You may remember from our Window
class that we can draw a rectangle using SDL_FillRect()
, which requires three arguments:
Render()
function.Window
class, we’re passing a nullptr
as this argument, indicating we want the color to fill the entire surface. In this lesson, we’ll learn how to provide an argument here, in the form of an SDL_Rect
SDL_MapRGB()
.// Rectangle.h
#pragma once
#include <SDL.h>
class Rectangle {
public:
void Render(SDL_Surface* Surface) const {
SDL_FillRect(
Surface,
// We'll provide this later
nullptr,
SDL_MapRGB(Surface->format, 255, 0, 0)
);
}
};
SDL_Rect
TypeTo represent a rectangle, SDL provides the SDL_Rect
type. It is a simple struct with four integer members that stores the position and dimensions of the rectangle:
x
and y
members represent the position of the top-left corner of the rectanglew
member represents the width of the rectangleh
member represents the height of the rectangleVisually, it looks like this:
Remember, SDL’s coordinate system uses the "y-down" convention, so increasing the y
value corresponds to moving the rectangle down.
Let’s add an SDL_Rect
to our Rectangle
class. We’ll also add it as a constructor parameter:
// Rectangle.h
#pragma once
#include <SDL.h>
class Rectangle {
public:
Rectangle(const SDL_Rect& Rect)
: Rect{Rect} {}
void Render(SDL_Surface* Surface) {/*...*/}
private:
SDL_Rect Rect;
};
We’ll provide the argument to this constructor in our main.cpp
:
#include <SDL.h>
#include "Window.h"
#include "Rectangle.h"
int main(int argc, char** argv) {
SDL_Init(SDL_INIT_VIDEO);
Window GameWindow;
Rectangle Rect{SDL_Rect{50, 50, 50, 50}};
SDL_Event E;
while (true) {/*...*/}
return 0;
}
To use this rectangle for rendering, we provide a pointer to it as our second argument to SDL_FillRect()
:
// Rectangle.h
#pragma once
#include <SDL.h>
class Rectangle {
public:
Rectangle(const SDL_Rect& Rect)
: Rect{Rect} {}
void Render(SDL_Surface* Surface) const {
SDL_FillRect(
Surface,
&Rect,
SDL_MapRGB(Surface->format, 255, 0, 0)
);
}
private:
SDL_Rect Rect;
};
With these changes, we should now see our red rectangle being rendered to the screen:
The SDL_Rect
type is used to represent a specific subset of rectangles, called axis-aligned rectangles. We can think of an axis-aligned rectangle as not having any rotation:
SDL doesn’t include a type for rotated rectangles. If we need that capability, it is something we’d have to build ourselves.
SDL_Color
TypeIn our Render()
function, we’re currently generating a red color for our rectangle. However, what if we want to store this color as a member variable, such that we change it from other functions in our Rectangle
class?
// Rectangle.h
#pragma once
#include <SDL.h>
class Rectangle {
public:
// ...
void SetColor(/* ??? */) {
// ???
}
// ...
};
Previously, we saw how we could store a specific color in a specific pixel format using a 32 bit unsigned integer. This is typically the value returned by a function such as SDL_MapRGB()
:
Uint32 Red{SDL_MapRGB(Surface->format, 255, 0, 0)};
However, storing our color as a Uint32
in the correct pixel format may not be easy.
In our simple program, we know we’re only ever rendering to one surface - the window surface - so, we could design the Rectangle
class on that basis. However, this violates the design principle of encapsulation.
Ideally, our Rectangle
should be a self-contained component that does not know or care about what is going on elsewhere in our code. In that context, our Rectangle
doesn’t know what surface it will be rendering to until Render()
is invoked. At that point, the SDL_Surface
is available as a parameter, and we can retrieve it’s pixel format from the format
member variable.
A simple option for storing a color in a way that lets us apply the pixel format later would be to use three integers - one for each of the red, green, and blue channels:
// Rectangle.h
#pragma once
#include <SDL.h>
class Rectangle {
public:
Rectangle(const SDL_Rect& Rect)
: Rect{Rect} {}
void Render(SDL_Surface* Surface) const {
SDL_FillRect(
Surface,
&Rect,
SDL_MapRGB(
Surface->format,
Red, Green, Blue
)
);
}
void SetColor(int R, int G, int B) {
Red = R;
Green = G;
Blue = B;
}
private:
SDL_Rect Rect;
int Red, Green, Blue;
};
This works, but is a little messy, and it makes writting the getter more difficult. We’d prefer to have a type that can represent a color as a single value. To help with this, SDL provides the SDL_Color
type, which is useful for storing a color without locking that color down to a specific pixel format.
SDL_Color
is a basic struct with 4 members - r
, g
, b
, and a
, representing the red, green, blue, and alpha channels respectively. Each variable is an 8-bit unsigned integer, meaning they can store values from 0 to 255:
SDL_Color Black { 0, 0, 0, 255};
SDL_Color Gray {100, 100, 100, 255};
SDL_Color White {255, 255, 255, 255};
SDL_Color Red {255, 0, 0, 255};
SDL_Color Green { 0, 255, 0, 255};
SDL_Color Blue { 0, 0, 255, 255};
SDL_Color Orange {255, 165, 0, 255};
Let’s update our Rectangle
to store its color as this type, alongside a getter and setter:
// Rectangle.h
#pragma once
#include <SDL.h>
class Rectangle {
public:
Rectangle(const SDL_Rect& Rect)
: Rect{Rect} {}
void Render(SDL_Surface* Surface) const {
SDL_FillRect(
Surface,
&Rect,
SDL_MapRGB(
Surface->format,
Color.r, Color.g, Color.b
)
);
}
void SetColor(const SDL_Color& NewColor) {
Color = NewColor;
}
SDL_Color GetColor() const {
return Color;
}
private:
SDL_Rect Rect;
// Default to red
SDL_Color Color{255, 0, 0, 255};
};
A function like Render()
is called on every frame of our program, so we should be cautious about performing too much work here. The more work our program needs to do on each frame, the longer it takes to generate that frame. This makes our program less responsive.
Instead, we may want to move the SDL_MapRGB()
step out of the Render()
function, and pass the required format to the constructor. This allows us to call SDL_MapRGB()
a single time, rather than on every frame:
// Rectangle.h
#pragma once
#include <SDL.h>
class Rectangle {
public:
Rectangle(
const SDL_Rect& Rect,
const SDL_PixelFormat* Format
)
: Rect{Rect},
Color{SDL_MapRGB(Format, 255, 0, 0)}
{}
void Render(SDL_Surface* Surface) const {
SDL_FillRect(
Surface,
&Rect,
SDL_MapRGB(
Surface->format,
Color.r, Color.g, Color.b
)
Color
);
}
private:
SDL_Rect Rect;
Uint32 Color{0};
};
This is reasonable, however, we should equally be cautious about making changes like this too early, before we have sufficient information to make such decisions. This is a practice known as premature optimization, and has its own set of problems:
Rectangle
assumes that it only ever renders to surfaces that have the same pixel format. Our simpler, less optimized implementation had no such restriction.Unless we’re certain something will be a problem, optimization is generally best left until later in the project. This has a few advantages:
As we get more experienced, we’ll develop better intuition about where early optimization is justified, and we’ll get better at minimising the negative impact of those optimizations. But otherwise, if we’re not sure, we should prioritise approaches that are quick to implement, simple to understand and easy to change in the future.
Let’s give our Rectangle
the ability to react to events. To set this up, we’ll add a HandleEvent()
function to our class:
// Rectangle.h
// ...
class Rectangle {
public:
// ...
void HandleEvent(SDL_Event& E) {
// We'll implement this next
}
// ...
};
We’ll also forward events to this function from our event loop:
// main.cpp
#include <SDL.h>
#include "Window.h"
#include "Rectangle.h"
int main(int argc, char** argv) {
SDL_Init(SDL_INIT_VIDEO);
Window GameWindow;
Rectangle Rect{SDL_Rect{50, 50, 50, 50}};
SDL_Event E;
while (true) {
while (SDL_PollEvent(&E)) {
Rect.HandleEvent(E);
if (E.type == SDL_QUIT) {
SDL_Quit();
return 0;
}
}
GameWindow.Render();
Rect.Render(GameWindow.GetSurface());
GameWindow.Update();
}
return 0;
}
To detect when the user hovers their mouse over the rectangle, we’ll apply the techniques we introduced earlier in the chapter. We’ll check for SDL_MOUSEMOTION
events and, when they occur, we’ll update an isPointerHovering
boolean to true
if the cursor was moved over our rectangle:
// Rectangle.h
// ...
class Rectangle {
public:
// ...
void HandleEvent(SDL_Event& E) {
if (E.type == SDL_MOUSEMOTION) {
isPointerHovering = isWithinRect(
E.motion.x, E.motion.y
);
}
}
// ...
private:
// ...
bool isPointerHovering{false};
bool isWithinRect(int x, int y) {
// We'll implement this next
return false;
}
};
SDL_Rect
Our isWithinRect()
function needs to return true
if the x
and y
coordinates are within the bounds of our Rect
variable. There are a few ways of determining this this. One approach is to check if the point is not outside the bounds in all 4 directions:
If the point is not in any of the areas we marked in red, we can deduce that it must be within the rectangle. In code, we can implement the check like this:
// Rectangle.h
// ...
class Rectangle {
// ...
private:
// ...
bool isWithinRect(int x, int y) {
// Too far left
if (x < Rect.x) return false;
// Too far right
if (x > Rect.x + Rect.w) return false;
// Too far up
if (y < Rect.y) return false;
// Too far down
if (y > Rect.y + Rect.h) return false;
// If we got this far, that means the
// point is within the rectangle
return true;
}
};
Now that our Rectangle
is keeping track of whether the user’s cursor is hovering over it, other functions in our class can react to it. For example, let’s have our rectangle change color when hovered. We’ll add a new member variable, getter, and setter:
// Rectangle.h
// ...
class Rectangle {
public:
// ...
void SetHoverColor(const SDL_Color& NewColor) {
HoverColor = NewColor;
}
SDL_Color GetHoverColor() const {
return HoverColor;
}
private:
// ...
SDL_Color HoverColor{0, 0, 255, 255};
};
Let’s update our Render()
function to select a color based on the isPointerHovering
state. This is an ideal place to use structured binding:
// Rectangle.h
// ...
class Rectangle {
public:
// ...
void Render(SDL_Surface* Surface) const {
auto [r, g, b, a]{
isPointerHovering ? HoverColor : Color
};
SDL_FillRect(
Surface, &Rect,
SDL_MapRGB(Surface->format, r, g, b)
);
}
// ...
};
Note that, to use structured binding, we need to use all the variables of the SDL_Color
type, even though we don’t need the a
(alpha) channel in this scenario.
Now, when we hover our rectangle, it should change color from red to blue:
Note that, in the previous example, we’re comparing the x
and y
coordinates from the mouse motion event to the x
and y
coordinates of SDL_Rect
controlling where our rectangle is rendered.
The x
and y
coordinates of the event represent the distance from the top left corner of the window, whilst the x
and y
coordinates of the SDL_Rect
represent the distance from the top-left corner of the surface we’re rendering on.
Our logic is assuming that these values are equivalent. In our simple program, they are. That is because the surface we’re rendering to is the window surface. In more complicated programs where this is not always the case, we would need more advanced logic that accounts for the fact that these positions may be in different coordinate spaces.
We cover coordinate spaces and mapping between them in more detail later in the course.
We’re also assuming our program only has a single window, so we’re not checking which window the event came from.
SDL_Point
TypeSDL includes a simple SDL_Point
type that stores an x
and y
integer:
SDL_Point SomePoint{100, 200};
SDL also includes a few functions that work with SDL_Point
objects. This includes SDL_PointInRect()
, which can replicate our isWithinRect()
method. To use SDL_PointInRect()
, we pass a pointer to an SDL_Point
and a pointer to an SDL_Rect
:
// Rectangle.h
#pragma once
#include <SDL.h>
class Rectangle {
public:
// ...
void HandleEvent(SDL_Event& E) {
if (E.type == SDL_MOUSEMOTION) {
// Before
isPointerHovering = isWithinRect(
E.motion.x, E.motion.y
);
// After
SDL_Point MousePosition{
E.motion.x, E.motion.y};
isPointerHovering = SDL_PointInRect(
&MousePosition, &Rect
);
}
}
// ...
};
However, SDL_Point
is rarely used, even within the SDL API itself. As we’ve seen, SDL events represent positions as individual x
and y
values, rather than as SDL_Point
s.
We’ll also stick to representing points as individual x
and y
coordinates in our own examples for now. Later in the course, we’ll create a more powerful Vec2
type that uses concepts that are more common in the industry.
By default, SDL stops sending us mouse motion events once the pointer leaves our window.
If the rectangle is at the edge of the window, or the mouse is moving quickly, this means we may not be notified that the cursor has left our rectangle through leaving the window entirely.
To address this, we can additionally keep track of SDL_WINDOWEVENT_LEAVE
events, using the techniques we covered in the previous lesson:
// Rectangle.h
// ...
class Rectangle {
public:
// ...
void HandleEvent(SDL_Event& E) {
if (E.type == SDL_MOUSEMOTION) {
isPointerHovering = isWithinRect(
E.motion.x, E.motion.y
);
} else if (
E.type == SDL_WINDOWEVENT &&
E.window.event == SDL_WINDOWEVENT_LEAVE
) {
isPointerHovering = false;
}
}
// ...
};
Let’s also allow our Rectangle
objects to react to mouse click events - that is, events with a type
of SDL_MOUSEBUTTONDOWN
:
// Rectangle.h
// ...
class Rectangle {
public:
// ...
void HandleEvent(SDL_Event& E) {
if (E.type == SDL_MOUSEMOTION) {
isPointerHovering = isWithinRect(
E.motion.x, E.motion.y
);
} else if (
E.type == SDL_WINDOWEVENT &&
E.window.event == SDL_WINDOWEVENT_LEAVE
) {
isPointerHovering = false;
} else if (E.type == SDL_MOUSEBUTTONDOWN) {
if (E.button.button == SDL_BUTTON_LEFT) {
std::cout << "A left-click happened "
"somewhere in the window\n";
}
}
}
// ...
};
The click events include x
and y
members, informing us of where the cursor was when the user clicked on our window. We can use this to determine whether they specifically clicked on the Rectangle
instance:
// Rectangle.h
// ...
class Rectangle {
public:
// ...
void HandleEvent(SDL_Event& E) {
if (E.type == SDL_MOUSEMOTION) {
isPointerHovering = isWithinRect(
E.motion.x, E.motion.y
);
} else if (
E.type == SDL_WINDOWEVENT &&
E.window.event == SDL_WINDOWEVENT_LEAVE
) {
isPointerHovering = false;
} else if (E.type == SDL_MOUSEBUTTONDOWN) {
if (isWithinRect(E.button.x, E.button.y)
&& E.button.button == SDL_BUTTON_LEFT
) {
std::cout << "A left-click happened "
"on me!\n";
}
}
}
// ...
};
Alternatively, we can use the isHovering
boolean we added in the previous section:
// Rectangle.h
// ...
class Rectangle {
public:
// ...
void HandleEvent(SDL_Event& E) {
if (E.type == SDL_MOUSEMOTION) {
isPointerHovering = isWithinRect(
E.motion.x, E.motion.y
);
} else if (
E.type == SDL_WINDOWEVENT &&
E.window.event == SDL_WINDOWEVENT_LEAVE
) {
isPointerHovering = false;
} else if (E.type == SDL_MOUSEBUTTONDOWN) {
if (isPointerHovering &&
E.button.button == SDL_BUTTON_LEFT
) {
std::cout << "A left-click happened "
"on me!\n";
}
}
}
// ...
};
Our program now includes a dedicated Rectangle.h
file defining our Rectangle
class. The main.cpp file creates an instance of this class and integrates its rendering and event handling into the main loop. This setup provides a good starting point for adding more complex features through the rest of this chapter.
#include <SDL.h>
#include "Window.h"
#include "Rectangle.h"
int main(int argc, char** argv) {
SDL_Init(SDL_INIT_VIDEO);
Window GameWindow;
Rectangle Rect{SDL_Rect{50, 50, 50, 50}};
SDL_Event E;
while (true) {
while (SDL_PollEvent(&E)) {
Rect.HandleEvent(E);
if (E.type == SDL_QUIT) {
SDL_Quit();
return 0;
}
}
GameWindow.Render();
Rect.Render(GameWindow.GetSurface());
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};
};
#pragma once
#include <SDL.h>
class Rectangle {
public:
Rectangle(const SDL_Rect& Rect)
: Rect{Rect} {}
void Render(SDL_Surface* Surface) const {
auto [r, g, b, a]{
isPointerHovering ? HoverColor : Color
};
SDL_FillRect(
Surface, &Rect,
SDL_MapRGB(Surface->format, r, g, b)
);
}
void HandleEvent(SDL_Event& E) {
if (E.type == SDL_MOUSEMOTION) {
isPointerHovering = isWithinRect(
E.motion.x, E.motion.y
);
} else if (
E.type == SDL_WINDOWEVENT &&
E.window.event == SDL_WINDOWEVENT_LEAVE
) {
isPointerHovering = false;
} else if (E.type == SDL_MOUSEBUTTONDOWN) {
if (isPointerHovering &&
E.button.button == SDL_BUTTON_LEFT
) {
std::cout << "A left-click happened "
"on me!\n";
}
}
}
void SetColor(const SDL_Color& NewColor) {
Color = NewColor;
}
SDL_Color GetColor() const {
return Color;
}
void SetHoverColor(const SDL_Color& NewColor) {
HoverColor = NewColor;
}
SDL_Color GetHoverColor() const {
return HoverColor;
}
private:
SDL_Rect Rect;
SDL_Color Color{255, 0, 0, 255};
SDL_Color HoverColor{0, 0, 255, 255};
bool isPointerHovering{false};
bool isWithinRect(int x, int y) {
if (x < Rect.x) return false;
if (x > Rect.x + Rect.w) return false;
if (y < Rect.y) return false;
if (y > Rect.y + Rect.h) return false;
return true;
}
};
In this lesson, we learned how to define, render, and interact with rectangles in SDL2. We created a Rectangle
class encapsulating an SDL_Rect
for geometry and an SDL_Color
for appearance, allowing us to draw shapes and respond to mouse input.
Key Takeaways:
SDL_Rect
defines a rectangle using x
, y
(top-left corner), w
(width), and h
(height).SDL_Color
stores RGBA color values (0-255) independently of pixel formats.SDL_FillRect()
draws a filled rectangle onto an SDL_Surface
.SDL_MapRGB()
converts RGBA values into a surface-specific color format.SDL_Rect
by comparing its coordinates against the rectangle's boundaries.SDL_MOUSEMOTION
) and click (SDL_MOUSEBUTTONDOWN
) events provide cursor coordinates.SDL_WINDOWEVENT_LEAVE
) help track when the cursor leaves the window.SDL_Rect
Learn to create, render, and interact with basic rectangles using the SDL_Rect
and SDL_Color
types.
Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games