SDL School
SDL version Which one?

Make a button

SDL has no button type. A button is a rectangle that you draw, plus code that watches the mouse. This program has three buttons, and each one changes the background color.

Move the mouse over a button, then click it. Also try this: press a button, move away, and release. The color does not change.

The buttons have no labels yet. The lesson Draw text shows how to add them.

The full program

main.c
/* Three buttons change the background color. */
#define SDL_MAIN_USE_CALLBACKS
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>

#define WINDOW_WIDTH 640
#define WINDOW_HEIGHT 480
#define BUTTON_COUNT 3
#define NO_BUTTON -1

typedef struct {
    SDL_FRect rect;
    SDL_Color color;
} Button;

static SDL_Window *window = NULL;
static SDL_Renderer *renderer = NULL;

static const Button buttons[BUTTON_COUNT] = {
    { { 80.0f, 200.0f, 140.0f, 80.0f }, { 200, 70, 70, SDL_ALPHA_OPAQUE } },
    { { 250.0f, 200.0f, 140.0f, 80.0f }, { 70, 170, 110, SDL_ALPHA_OPAQUE } },
    { { 420.0f, 200.0f, 140.0f, 80.0f }, { 70, 120, 210, SDL_ALPHA_OPAQUE } }
};

static SDL_Color background = { 24, 26, 32, SDL_ALPHA_OPAQUE };
static int hovered = NO_BUTTON; /* The button under the mouse. */
static int pressed = NO_BUTTON; /* The button that the mouse pressed. */

static bool point_in_rect(float x, float y, const SDL_FRect *rect)
{
    return x >= rect->x && x < rect->x + rect->w &&
           y >= rect->y && y < rect->y + rect->h;
}

static int button_at(float x, float y)
{
    for (int i = 0; i < BUTTON_COUNT; i++) {
        if (point_in_rect(x, y, &buttons[i].rect)) {
            return i;
        }
    }
    return NO_BUTTON;
}

/* Add a value to a color channel, and stay in the range 0 to 255. */
static Uint8 shade(Uint8 channel, int amount)
{
    return (Uint8)SDL_clamp((int)channel + amount, 0, 255);
}

static void draw_button(int index)
{
    SDL_FRect rect = buttons[index].rect;
    SDL_Color color = buttons[index].color;
    int amount = 0;

    if (index == pressed && index == hovered) {
        amount = -40;
        rect.y += 3.0f;
    } else if (index == hovered) {
        amount = 30;
    }

    SDL_SetRenderDrawColor(renderer, shade(color.r, amount), shade(color.g, amount),
                           shade(color.b, amount), SDL_ALPHA_OPAQUE);
    SDL_RenderFillRect(renderer, &rect);
}

SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[])
{
    if (!SDL_Init(SDL_INIT_VIDEO)) {
        SDL_Log("SDL_Init failed: %s", SDL_GetError());
        return SDL_APP_FAILURE;
    }

    if (!SDL_CreateWindowAndRenderer("Make a button", WINDOW_WIDTH, WINDOW_HEIGHT, 0,
                                     &window, &renderer)) {
        SDL_Log("SDL_CreateWindowAndRenderer failed: %s", SDL_GetError());
        return SDL_APP_FAILURE;
    }

    SDL_SetRenderVSync(renderer, 1);
    return SDL_APP_CONTINUE;
}

SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event)
{
    if (event->type == SDL_EVENT_QUIT) {
        return SDL_APP_SUCCESS;
    }

    SDL_ConvertEventToRenderCoordinates(renderer, event);

    if (event->type == SDL_EVENT_MOUSE_MOTION) {
        hovered = button_at(event->motion.x, event->motion.y);
    } else if (event->type == SDL_EVENT_MOUSE_BUTTON_DOWN &&
               event->button.button == SDL_BUTTON_LEFT) {
        pressed = button_at(event->button.x, event->button.y);
    } else if (event->type == SDL_EVENT_MOUSE_BUTTON_UP &&
               event->button.button == SDL_BUTTON_LEFT) {
        /* A click needs a press and a release on the same button. */
        int released = button_at(event->button.x, event->button.y);

        if (released != NO_BUTTON && released == pressed) {
            background = buttons[released].color;
        }
        pressed = NO_BUTTON;
    }
    return SDL_APP_CONTINUE;
}

SDL_AppResult SDL_AppIterate(void *appstate)
{
    SDL_SetRenderDrawColor(renderer, background.r, background.g, background.b, SDL_ALPHA_OPAQUE);
    SDL_RenderClear(renderer);

    for (int i = 0; i < BUTTON_COUNT; i++) {
        draw_button(i);
    }

    SDL_RenderPresent(renderer);
    return SDL_APP_CONTINUE;
}

void SDL_AppQuit(void *appstate, SDL_AppResult result)
{
}
main.c
/* Three buttons change the background color. */
#include <SDL.h>
#include <stdbool.h>

#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#endif

#define WINDOW_WIDTH 640
#define WINDOW_HEIGHT 480
#define BUTTON_COUNT 3
#define NO_BUTTON -1

typedef struct {
    SDL_FRect rect;
    SDL_Color color;
} Button;

static SDL_Window *window = NULL;
static SDL_Renderer *renderer = NULL;

static const Button buttons[BUTTON_COUNT] = {
    { { 80.0f, 200.0f, 140.0f, 80.0f }, { 200, 70, 70, SDL_ALPHA_OPAQUE } },
    { { 250.0f, 200.0f, 140.0f, 80.0f }, { 70, 170, 110, SDL_ALPHA_OPAQUE } },
    { { 420.0f, 200.0f, 140.0f, 80.0f }, { 70, 120, 210, SDL_ALPHA_OPAQUE } }
};

static SDL_Color background = { 24, 26, 32, SDL_ALPHA_OPAQUE };
static int hovered = NO_BUTTON; /* The button under the mouse. */
static int pressed = NO_BUTTON; /* The button that the mouse pressed. */
static bool running = true;

static bool point_in_rect(float x, float y, const SDL_FRect *rect)
{
    return x >= rect->x && x < rect->x + rect->w &&
           y >= rect->y && y < rect->y + rect->h;
}

static int button_at(float x, float y)
{
    for (int i = 0; i < BUTTON_COUNT; i++) {
        if (point_in_rect(x, y, &buttons[i].rect)) {
            return i;
        }
    }
    return NO_BUTTON;
}

/* Add a value to a color channel, and stay in the range 0 to 255. */
static Uint8 shade(Uint8 channel, int amount)
{
    return (Uint8)SDL_clamp((int)channel + amount, 0, 255);
}

static void draw_button(int index)
{
    SDL_FRect rect = buttons[index].rect;
    SDL_Color color = buttons[index].color;
    int amount = 0;

    if (index == pressed && index == hovered) {
        amount = -40;
        rect.y += 3.0f;
    } else if (index == hovered) {
        amount = 30;
    }

    SDL_SetRenderDrawColor(renderer, shade(color.r, amount), shade(color.g, amount),
                           shade(color.b, amount), SDL_ALPHA_OPAQUE);
    SDL_RenderFillRectF(renderer, &rect);
}

static void handle_event(const SDL_Event *event)
{
    if (event->type == SDL_QUIT) {
        running = false;
    } else if (event->type == SDL_MOUSEMOTION) {
        hovered = button_at((float)event->motion.x, (float)event->motion.y);
    } else if (event->type == SDL_MOUSEBUTTONDOWN &&
               event->button.button == SDL_BUTTON_LEFT) {
        pressed = button_at((float)event->button.x, (float)event->button.y);
    } else if (event->type == SDL_MOUSEBUTTONUP &&
               event->button.button == SDL_BUTTON_LEFT) {
        /* A click needs a press and a release on the same button. */
        int released = button_at((float)event->button.x, (float)event->button.y);

        if (released != NO_BUTTON && released == pressed) {
            background = buttons[released].color;
        }
        pressed = NO_BUTTON;
    }
}

static void frame(void)
{
    SDL_Event event;

    while (SDL_PollEvent(&event)) {
        handle_event(&event);
    }

    SDL_SetRenderDrawColor(renderer, background.r, background.g, background.b, SDL_ALPHA_OPAQUE);
    SDL_RenderClear(renderer);

    for (int i = 0; i < BUTTON_COUNT; i++) {
        draw_button(i);
    }

    SDL_RenderPresent(renderer);
}

int main(int argc, char *argv[])
{
    if (SDL_Init(SDL_INIT_VIDEO) != 0) {
        SDL_Log("SDL_Init failed: %s", SDL_GetError());
        return 1;
    }

    if (SDL_CreateWindowAndRenderer(WINDOW_WIDTH, WINDOW_HEIGHT, 0, &window, &renderer) != 0) {
        SDL_Log("SDL_CreateWindowAndRenderer failed: %s", SDL_GetError());
        SDL_Quit();
        return 1;
    }

    SDL_SetWindowTitle(window, "Make a button");
    SDL_RenderSetVSync(renderer, 1);

#ifdef __EMSCRIPTEN__
    emscripten_set_main_loop(frame, 0, 1);
#else
    while (running) {
        frame();
    }
#endif

    SDL_DestroyRenderer(renderer);
    SDL_DestroyWindow(window);
    SDL_Quit();
    return 0;
}

Describe a button

typedef struct {
    SDL_FRect rect;
    SDL_Color color;
} Button;

A struct keeps the data for one button together. The program holds the buttons in an array, so one loop can draw them and one loop can test them.

Find the button under the mouse

static bool point_in_rect(float x, float y, const SDL_FRect *rect)
{
    return x >= rect->x && x < rect->x + rect->w &&
           y >= rect->y && y < rect->y + rect->h;
}

This is the overlap test from the collisions lesson, for a point in place of a second rectangle. SDL has this test too, but your own version shows that there is no magic in it.

The function button_at tests each button and returns its index. If no button is there, it returns NO_BUTTON, which is -1. A valid index is never negative, so -1 is a safe special value.

What is a click?

A click is two events: a press and a release, on the same button. The program needs both, because people change their minds. If you press a button and then move away, the release does nothing. Most buttons on a computer work this way.

The program uses two variables:

static int hovered = NO_BUTTON; /* The button under the mouse. */
static int pressed = NO_BUTTON; /* The button that the mouse pressed. */

The press records the button. The release does the action only on a match:

        int released = button_at(event->button.x, event->button.y);

        if (released != NO_BUTTON && released == pressed) {
            background = buttons[released].color;
        }
        pressed = NO_BUTTON;
        int released = button_at((float)event->button.x, (float)event->button.y);

        if (released != NO_BUTTON && released == pressed) {
            background = buttons[released].color;
        }
        pressed = NO_BUTTON;

Show the state

A button must show that it responds. The draw_button function makes three changes:

The shade function adds a value to one color channel. A channel holds 0 to 255, so SDL_clamp keeps the result in that range. Without the clamp, the cast to Uint8 wraps around. A value of 300 becomes 44, and a bright color turns dark.

Try this

  1. Add a fourth button. Change BUTTON_COUNT, and move the others to make room.
  2. Make the button that matches the current background show a white border. Use the rectangle outline function from the mouse lesson.
  3. Give each button a function pointer, and call it on a click. This is how most button code works.

What you learned