Creating Buttons in SDL2

Expanding on our UI architecture to add a simple button to our application, complete with hover and click events.
This lesson is part of the course:

Game Dev with SDL3

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

Free, Unlimited Access
aSDL9a.jpg
Ryan McCombe
Ryan McCombe
Posted

In this lesson, we build upon our work on creating a UI architecture. Our goal is to introduce a Button class that has two properties:

  • When the user’s cursor is hovering over the button, it has a different color
  • When the user clicks the button, it quits out of the application.

Here’s our starting point. We have a Window header file, which is close to what we built on our lesson on creating an SDL2 window.

Our Window class looks like this:

#pragma once
#include <SDL.h>

class Window {
public:
  Window() {
    SDL_Init(SDL_INIT_VIDEO);

    SDLWindow = SDL_CreateWindow(
      "Hello Window",
      SDL_WINDOWPOS_UNDEFINED,
      SDL_WINDOWPOS_UNDEFINED,
      600, 200, 0
    );

    SDLWindowSurface = SDL_GetWindowSurface(SDLWindow);
    Update();
  }

  void Update() {
    SDL_FillRect(
      SDLWindowSurface,
      nullptr,
      SDL_MapRGB(SDLWindowSurface->format, 40, 40, 40)
    );
  }

  void RenderFrame() {
    SDL_UpdateWindowSurface(SDLWindow);
  }

  SDL_Surface* GetSurface() {
    return SDLWindowSurface;
  }

private:
  SDL_Window* SDLWindow;
  SDL_Surface* SDLWindowSurface;
};

We’ve also included code from our previous lesson on creating a UI architecture.

Based on that lesson, we have classes for Layer and EventReceiver, and our main.cpp pulls everything together:

#pragma once
#include <vector>
#include <SDL.h>
#include "EventReceiver.h"

class Layer {
public:
  bool HandleEvent(const SDL_Event* Event) {
    for (const auto Handler : Subscribers) {
      if (Handler->HandleEvent(Event)) {
        return true;
      }
    }
    return false;
  }

  void SubscribeToEvents(EventReceiver* Receiver) {
    Subscribers.push_back(Receiver);
  }

private:
  std::vector<EventReceiver*> Subscribers;
};
#pragma once
#include <SDL.h>

class EventReceiver {
public:
  virtual bool HandleEvent(const SDL_Event* Event) {
    return false;
  }
};
#include <SDL.h>
#include "Window.h"
#include "Layer.h"
#include "EventReceiver.h"

int main() {
  Window GameWindow;
  Layer UI;
  EventReceiver ExampleButton;
  UI.SubscribeToEvents(&ExampleButton);

  SDL_Event Event;
  while(true) {
    while(SDL_PollEvent(&Event)) {
      if (Event.type == SDL_QUIT) {
        SDL_Quit();
        return 0;
      }
      if (UI.HandleEvent(&Event)) {
        continue;
      }
    }
    GameWindow.RenderFrame();
  }
}

Compiling and running our code should generate a blank window:

Screenshot of a blank SDL2 window

Creating the Button Class

To be compatible with our other systems, our Button class should inherit from EventReceiver.

To make our button eventually do something, we’ll also need to override the virtual HandleEvent function:

#pragma once
#include <SDL.h>
#include "EventReceiver.h"

class Button : public EventReceiver {
public:
  bool HandleEvent(const SDL_Event* Event) override {
    // TODO
    return false;
  }
};

Over in main, lets update ExampleButton to use this class:

#include <SDL.h>
#include "Window.h"
#include "Layer.h"
#include "EventReceiver.h"
#include "Button.h"

int main() {
  Window GameWindow;
  Layer UI;
  EventReceiver ExampleButton;
  Button ExampleButton;
  UI.SubscribeToEvents(&ExampleButton);

  // ... remaining code unchanged
}

Our application should still compile and run at this point.

In our lesson on Windows, we saw we could render a color to part of the screen, using SDL_FillRect:

SDL_FillRect(
  PointerToWindowSurface,
  PointerToRectangle,
  SDL_MapRGB(WindowSurfaceFormat, Red, Green, Blue)
);

Lets use this to make our button visible.

We want our button colour to be different, depending on whether or not the user is hovering their cursor over it.

Let's add an Update function to our Button class for this. We can call this function any time our button color needs to change:

private:
void Update() {
  auto [r, g, b, a] { isHovered ? HoverColor : BGColor };
  SDL_FillRect(
    SDLWindowSurface,
    &Rect,
    SDL_MapRGB(SDLWindowSurface->format, r, g, b)
  );
}

We need to provide the isHovered, BackgroundColor, HoverColor, SDLWindowSurface and Rect arguments. Let's add private members to our class for them:

private:
  // TODO: Update me based on mouse movement
  bool isHovered { false };

  // TODO: Initialise me on construction
  SDL_Surface* SDLWindowSurface;

  SDL_Color BGColor { 255, 50, 50, 255 };
  SDL_Color HoverColor { 50, 50, 255, 255 };
  SDL_Rect Rect { 50, 50, 50, 50 };

Here, we've made use of SDL_Rect and SDL_Color. These are simple types that SDL provide for storing rectangles and colors.

SDL_Rect

The arguments we’re passing to the SDL_Rect constructor will cause our Rect to have a horizontal position of 50, a vertical position of 50, a width of 50, and a height of 50.

The official documentation for SDL_Rect is available here: https://wiki.libsdl.org/SDL_Rect

SDL_Color

The arguments we’re passing to the SDL_Color constructor represent the quantity of our color's red, green, blue and alpha (opaqueness) channels.

Each channel has a range of 0-255. Therefore, the argument list 255, 50, 50 and 255 represents an opaque, red colour.

The official documentation for SDL_Color is available here: https://wiki.libsdl.org/SDL_Color

SDL_Surface*

Getting access to the SDL Window surface from our button will require a little more thought.

Creating an Application Class

Out button needs a pointer to the SDL window surface. Arbitrary objects requiring access to important systems are pretty common, but we also need to be careful here.

Liberally allowing any object to talk to any other object is a recipe for disaster as our projects scale out.

It’s a good idea to standardize how our systems access each other and affect changes. A common approach is to create a single object, called something like Application.

The application grants access to subsystems and state changes, in a controlled way.

Let's create our Application class in a new header file, called Application.h:

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

class Application {
public:
  Application(Window* Window) : mWindow { Window }
  {}

  SDL_Surface* GetWindowSurface() {
    return mWindow->GetSurface();
  }

  void Quit() {
    SDL_Event QuitEvent { SDL_QUIT };
    SDL_PushEvent(&QuitEvent);
  }

private:
  Window* mWindow;
};

Let's update our Button class to accept a pointer to this application in its constructor. From there, we will set our SDLWindowSurface private variable. We’ll also add a private variable to hold a pointer to the application, which we’ll need later.

Finally, we’ll also call Update in the constructor, so our button triggers its first render:

#pragma once
#include <SDL.h>
#include "EventReceiver.h"
#include "Application.h"

class Button : public EventReceiver {
public:
  Button(Application* App) :
    SDLWindowSurface{App->GetWindowSurface()},
    App{App}
  {
    Update();
  }
  // ... remaining code unchanged
private:
  Application* App;
  // ... remaining code unchanged
};

Back in main.cpp, we need to connect everything up by:

  • Including our Application header
  • Creating a new Application object, passing in a pointer to our Window
  • Passing a pointer to our Application into the constructor for our Button
#include <SDL.h>
#include "Window.h"
#include "Layer.h"
#include "Button.h"
#include "Application.h"

int main() {
  Window GameWindow;
  Application App { &GameWindow };
  Layer UI;
  Button ExampleButton { &App };
  UI.SubscribeToEvents(&ExampleButton);

  // ... remaining code unchanged
}

We should now see our button being rendered:

Screenshot of an SDL2 window showing a button

Handling Button Hover and Click

We’re almost done! The last task is to update the HandleEvent function on our Button class.

The lesson on keyboard and mouse events laid the foundation here:

There are two events we care about. First, we need to listen for the click event, so our button can react to the user clicking on it. Secondly, we care about the mouse motion event so our button can react to the user hovering over it.

The click event is somewhat straightforward. The user clicked our button if three things are true:

  • The event was a SDL_MOUSEBUTTONDOWN event
  • The button was SDL_BUTTON_LEFT
  • The user was hovering our button when this event occured

We can check those three things, and trigger our function call to quit the application if they're all true. We also return true from our HandleEvent function, to indicate our button handled the event:

bool HandleEvent(const SDL_Event* Event) override {
  if (
    Event->type == SDL_MOUSEBUTTONDOWN &&
    Event->button.button == SDL_BUTTON_LEFT &&
    isHovered
  ) {
    App->Quit();
    return true;
  } else if (
    Event->type == SDL_MOUSEMOTION
  ) [[likely]] {
    // Handle hovering
  }
  // If we get this far, this button didn't handle the event
  return false;
}

Mouse motion is a little more difficult. We need to find if the user’s cursor ended up in the boundary of our button.

Let's create a separate function for that, and implement our logic there. We'll create a function that takes the cursor's x and y position, and returns true if that position is within the bounds of our button's rectangle:

private:
bool IsWithinBounds(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 high
  if (y < Rect.y) return false;

  // Too low
  if (y > Rect.y + Rect.h) return false;

  // Inside rectangle
  return true;
}

Back in HandleEvent, we can now use this function to determine if the user’s cursor is currently hovering over our button after a mouse motion event.

As long as the user is currently hovering our button, their mouse motion events can be handled by our button, so we should return true.

Additionally, we need to consider whether the user’s cursor has entered or left our button as a result of this event.

That is, we need to check if the boolean returned from IsWithinBounds is different from our isHovered member variable.

If they are different, that means we need to update our isHovered variable to reflect the change, and update our button color.

Our implementation could look something like this:

// ... remaining HandleEvent code unchanged
} else if (
  Event->type == SDL_MOUSEMOTION
) [[likely]] {
  // Has the user enterred or left our button this frame?
  if (isHovered != IsWithinBounds(
    Event->motion.x, Event->motion.y
  )) {
    // If so, update our internal variable...
    isHovered = !isHovered;
    // ...and update our color
    Update();
  }
  // Return true if the user is currently hovering our button
  return isHovered;
}
// ... remaining HandleEvent code unchanged

With that, our button should be working! Hovering over it should change its color from red to blue, and clicking on it should quit our application.

Complete Code

Our complete code is available here:

#include <SDL.h>
#include "Window.h"
#include "Layer.h"
#include "Button.h"
#include "Application.h"

int main() {
  Window GameWindow;
  Application App { &GameWindow };
  Layer UI;
  Button ExampleButton { &App };
  UI.SubscribeToEvents(&ExampleButton);

  SDL_Event Event;
  while(true) {
    while(SDL_PollEvent(&Event)) {
      if (Event.type == SDL_QUIT) {
        SDL_Quit();
        return 0;
      }
      if (UI.HandleEvent(&Event)) {
        continue;
      }
    }
    GameWindow.RenderFrame();
  }
}
#pragma once
#include <SDL.h>
#include "Window.h"

class Application {
public:
  Application(Window* Window) : mWindow { Window }
  {}

  SDL_Surface* GetWindowSurface() {
    return mWindow->GetSurface();
  }

  void Quit() {
    SDL_Event QuitEvent { SDL_QUIT };
    SDL_PushEvent(&QuitEvent);
  }

private:
  Window* mWindow;
};
#pragma once
#include <SDL.h>
#include "EventReceiver.h"
#include "Application.h"
#include <iostream>

class Button : public EventReceiver {
public:
  Button(Application* App) :
    SDLWindowSurface{App->GetWindowSurface()},
    App{App}
  {
    Update();
  }

  bool HandleEvent(const SDL_Event* Event) override {
    if (
      Event->type == SDL_MOUSEBUTTONDOWN &&
      Event->button.button == SDL_BUTTON_LEFT &&
      isHovered
      ) {
        App->Quit();
      } else if (
        Event->type == SDL_MOUSEMOTION) [[likely]]
      {
        if (isHovered != IsWithinBounds(
          Event->motion.x, Event->motion.y
        )) {
          isHovered = !isHovered;
          Update();
        }
        return isHovered;
      }
    return false;
  }


private:
  bool IsWithinBounds(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 high
    if (y < Rect.y) return false;

    // Too low
    if (y > Rect.y + Rect.h) return false;

    // Inside rectangle
    return true;
  }
  
  void Update() {
    auto [r, g, b, a] { isHovered ? HoverColor : BGColor };
    SDL_FillRect(
      SDLWindowSurface,
      &Rect,
      SDL_MapRGB(SDLWindowSurface->format, r, g, b)
    );
  }

  bool isHovered { false };
  SDL_Color BGColor { 255, 50, 50, 255 };
  SDL_Color HoverColor { 50, 50, 255, 255 };
  SDL_Rect Rect { 50, 50, 50, 50 };
  Application* App { nullptr };
  SDL_Surface* SDLWindowSurface { nullptr };
};
#pragma once
#include <SDL.h>

class EventReceiver {
public:
  virtual bool HandleEvent(const SDL_Event* Event) {
    return false;
  }
};
#pragma once
#include <vector>
#include <SDL.h>
#include "EventReceiver.h"

class Layer {
public:
  bool HandleEvent(const SDL_Event* Event) {
    for (const auto Handler : Subscribers) {
      if (Handler->HandleEvent(Event)) {
        return true;
      }
    }
    return false;
  }

  void SubscribeToEvents(EventReceiver* Receiver) {
    Subscribers.push_back(Receiver);
  }

private:
  std::vector<EventReceiver*> Subscribers;
};
#pragma once
#include <SDL.h>

class Window {
public:
  Window() {
    SDL_Init(SDL_INIT_VIDEO);

    SDLWindow = SDL_CreateWindow(
      "Hello Window",
      SDL_WINDOWPOS_UNDEFINED,
      SDL_WINDOWPOS_UNDEFINED,
      600, 150, 0
    );

    SDLWindowSurface = SDL_GetWindowSurface(SDLWindow);
    Update();
  }

  void Update() {
    SDL_FillRect(
      SDLWindowSurface,
      nullptr,
      SDL_MapRGB(SDLWindowSurface->format, 40, 40, 40)
    );
  }

  void RenderFrame() {
    SDL_UpdateWindowSurface(SDLWindow);
  }

  SDL_Surface* GetSurface() {
    return SDLWindowSurface;
  }

private:
  SDL_Window* SDLWindow { nullptr };
  SDL_Surface* SDLWindowSurface { nullptr };
};

Up next, we’ll add the ability to render images within our application!

Was this lesson useful?

Ryan McCombe
Ryan McCombe
Posted
DreamShaper_v7_cyberpunk_woman_playing_video_games_modest_clot_0.jpg
This lesson is part of the course:

Game Dev with SDL3

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

Free, Unlimited Access
Implementing User Interaction
  • 25.Making Minesweeper with C++ and SDL2
  • 26.Project Setup
  • 27.GPUs and Rasterization
  • 28.SDL Renderers
DreamShaper_v7_cyberpunk_woman_playing_video_games_modest_clot_0.jpg
This lesson is part of the course:

Game Dev with SDL3

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

Free, unlimited access

This course includes:

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

Images and Surface Blitting

Learn how to load images into our SDL2 application. Then, learn how to crop, resize, position and render them into our window.
aSDL9b.jpg
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved