SDL School
SDL version Which one?

Read a file

This program is a set of flash cards. Click to see the answer, and click again for the next card. The cards come from a text file, so you can change them with no new build.

The full program

main.c
/* Flash cards from a text file. Click to see the answer, and click to go on. */
#define SDL_MAIN_USE_CALLBACKS
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <SDL3_ttf/SDL_ttf.h>

#define WINDOW_WIDTH 640
#define WINDOW_HEIGHT 480
#define FONT_PATH "assets/DejaVuSans.ttf"
#define CARDS_PATH "assets/cards.txt"
#define TEXT_WIDTH 560

typedef struct {
    const char *question;
    const char *answer;
} Card;

static SDL_Window *window = NULL;
static SDL_Renderer *renderer = NULL;
static TTF_Font *font = NULL;
static SDL_Texture *question_text = NULL;
static SDL_Texture *answer_text = NULL;

static char *file_text = NULL; /* The cards point into this memory. */
static Card *cards = NULL;
static int card_count = 0;
static int current = 0;
static bool show_answer = false;

/* Each card needs one line, so the file has at most this many cards. */
static int count_lines(const char *text)
{
    int lines = 1;

    for (const char *p = text; *p != '\0'; p++) {
        if (*p == '\n') {
            lines++;
        }
    }
    return lines;
}

/* Remove a carriage return from the end of a line. Windows files have them. */
static void trim_line_end(char *line)
{
    size_t length = SDL_strlen(line);

    if (length > 0 && line[length - 1] == '\r') {
        line[length - 1] = '\0';
    }
}

/* Read one line into a card. Return false for a line that is not a card. */
static bool parse_line(char *line, Card *card, int line_number)
{
    char *bar;

    trim_line_end(line);

    if (line[0] == '\0' || line[0] == '#') {
        return false; /* A blank line or a comment. */
    }

    bar = SDL_strchr(line, '|');
    if (bar == NULL) {
        SDL_Log("Line %d has no | character, so the program skips it.", line_number);
        return false;
    }

    *bar = '\0'; /* End the question here. */
    card->question = line;
    card->answer = bar + 1;
    return true;
}

/* Break the text into lines in place, and fill the array. Return the count. */
static int parse_cards(char *text, Card *output)
{
    int count = 0;
    int line_number = 1;
    char *line = text;

    while (line != NULL) {
        char *newline = SDL_strchr(line, '\n');

        if (newline != NULL) {
            *newline = '\0';
        }

        if (parse_line(line, &output[count], line_number)) {
            count++;
        }

        line = (newline != NULL) ? newline + 1 : NULL;
        line_number++;
    }
    return count;
}

static bool load_cards(const char *path)
{
    file_text = SDL_LoadFile(path, NULL);
    if (file_text == NULL) {
        SDL_Log("SDL_LoadFile failed: %s", SDL_GetError());
        return false;
    }

    cards = SDL_malloc((size_t)count_lines(file_text) * sizeof(Card));
    if (cards == NULL) {
        SDL_Log("SDL_malloc failed: %s", SDL_GetError());
        return false;
    }

    card_count = parse_cards(file_text, cards);
    if (card_count == 0) {
        SDL_Log("%s has no cards.", path);
        return false;
    }
    return true;
}

static SDL_Texture *make_text(const char *text, SDL_Color color)
{
    SDL_Surface *surface = TTF_RenderText_Blended_Wrapped(font, text, 0, color, TEXT_WIDTH);
    SDL_Texture *texture;

    if (surface == NULL) {
        SDL_Log("TTF_RenderText_Blended_Wrapped failed: %s", SDL_GetError());
        return NULL;
    }

    texture = SDL_CreateTextureFromSurface(renderer, surface);
    SDL_DestroySurface(surface);
    return texture;
}

static void show_card(void)
{
    SDL_Color white = { 235, 236, 240, SDL_ALPHA_OPAQUE };
    SDL_Color orange = { 255, 157, 92, SDL_ALPHA_OPAQUE };

    SDL_DestroyTexture(question_text);
    SDL_DestroyTexture(answer_text);
    question_text = make_text(cards[current].question, white);
    answer_text = make_text(cards[current].answer, orange);
    show_answer = false;
}

static void handle_click(void)
{
    if (!show_answer) {
        show_answer = true;
    } else {
        current = (current + 1) % card_count;
        show_card();
    }
}

static void draw_text(SDL_Texture *texture, float x, float y)
{
    float width, height;

    if (texture == NULL) {
        return;
    }

    SDL_GetTextureSize(texture, &width, &height);

    SDL_FRect destination = { x, y, width, height };
    SDL_RenderTexture(renderer, texture, NULL, &destination);
}

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 (!TTF_Init()) {
        SDL_Log("TTF_Init failed: %s", SDL_GetError());
        return SDL_APP_FAILURE;
    }

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

    SDL_SetRenderVSync(renderer, 1);

    font = TTF_OpenFont(FONT_PATH, 26.0f);
    if (font == NULL) {
        SDL_Log("TTF_OpenFont failed: %s", SDL_GetError());
        return SDL_APP_FAILURE;
    }

    if (!load_cards(CARDS_PATH)) {
        return SDL_APP_FAILURE;
    }

    show_card();
    return SDL_APP_CONTINUE;
}

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

    if (event->type == SDL_EVENT_MOUSE_BUTTON_DOWN) {
        handle_click();
    }
    return SDL_APP_CONTINUE;
}

SDL_AppResult SDL_AppIterate(void *appstate)
{
    SDL_SetRenderDrawColor(renderer, 24, 26, 32, SDL_ALPHA_OPAQUE);
    SDL_RenderClear(renderer);

    draw_text(question_text, 40.0f, 80.0f);
    if (show_answer) {
        draw_text(answer_text, 40.0f, 260.0f);
    }

    SDL_RenderPresent(renderer);
    return SDL_APP_CONTINUE;
}

void SDL_AppQuit(void *appstate, SDL_AppResult result)
{
    SDL_DestroyTexture(question_text);
    SDL_DestroyTexture(answer_text);
    SDL_free(cards);
    SDL_free(file_text);
    TTF_CloseFont(font);
    TTF_Quit();
}
main.c
/* Flash cards from a text file. Click to see the answer, and click to go on. */
#include <SDL.h>
#include <SDL_ttf.h>
#include <stdbool.h>

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

#define WINDOW_WIDTH 640
#define WINDOW_HEIGHT 480
#define FONT_PATH "assets/DejaVuSans.ttf"
#define CARDS_PATH "assets/cards.txt"
#define TEXT_WIDTH 560

typedef struct {
    const char *question;
    const char *answer;
} Card;

static SDL_Window *window = NULL;
static SDL_Renderer *renderer = NULL;
static TTF_Font *font = NULL;
static SDL_Texture *question_text = NULL;
static SDL_Texture *answer_text = NULL;

static char *file_text = NULL; /* The cards point into this memory. */
static Card *cards = NULL;
static int card_count = 0;
static int current = 0;
static bool show_answer = false;
static bool running = true;

/* Each card needs one line, so the file has at most this many cards. */
static int count_lines(const char *text)
{
    int lines = 1;

    for (const char *p = text; *p != '\0'; p++) {
        if (*p == '\n') {
            lines++;
        }
    }
    return lines;
}

/* Remove a carriage return from the end of a line. Windows files have them. */
static void trim_line_end(char *line)
{
    size_t length = SDL_strlen(line);

    if (length > 0 && line[length - 1] == '\r') {
        line[length - 1] = '\0';
    }
}

/* Read one line into a card. Return false for a line that is not a card. */
static bool parse_line(char *line, Card *card, int line_number)
{
    char *bar;

    trim_line_end(line);

    if (line[0] == '\0' || line[0] == '#') {
        return false; /* A blank line or a comment. */
    }

    bar = SDL_strchr(line, '|');
    if (bar == NULL) {
        SDL_Log("Line %d has no | character, so the program skips it.", line_number);
        return false;
    }

    *bar = '\0'; /* End the question here. */
    card->question = line;
    card->answer = bar + 1;
    return true;
}

/* Break the text into lines in place, and fill the array. Return the count. */
static int parse_cards(char *text, Card *output)
{
    int count = 0;
    int line_number = 1;
    char *line = text;

    while (line != NULL) {
        char *newline = SDL_strchr(line, '\n');

        if (newline != NULL) {
            *newline = '\0';
        }

        if (parse_line(line, &output[count], line_number)) {
            count++;
        }

        line = (newline != NULL) ? newline + 1 : NULL;
        line_number++;
    }
    return count;
}

static bool load_cards(const char *path)
{
    file_text = SDL_LoadFile(path, NULL);
    if (file_text == NULL) {
        SDL_Log("SDL_LoadFile failed: %s", SDL_GetError());
        return false;
    }

    cards = SDL_malloc((size_t)count_lines(file_text) * sizeof(Card));
    if (cards == NULL) {
        SDL_Log("SDL_malloc failed: %s", SDL_GetError());
        return false;
    }

    card_count = parse_cards(file_text, cards);
    if (card_count == 0) {
        SDL_Log("%s has no cards.", path);
        return false;
    }
    return true;
}

static SDL_Texture *make_text(const char *text, SDL_Color color)
{
    SDL_Surface *surface = TTF_RenderUTF8_Blended_Wrapped(font, text, color, TEXT_WIDTH);
    SDL_Texture *texture;

    if (surface == NULL) {
        SDL_Log("TTF_RenderUTF8_Blended_Wrapped failed: %s", TTF_GetError());
        return NULL;
    }

    texture = SDL_CreateTextureFromSurface(renderer, surface);
    SDL_FreeSurface(surface);
    return texture;
}

static void show_card(void)
{
    SDL_Color white = { 235, 236, 240, SDL_ALPHA_OPAQUE };
    SDL_Color orange = { 255, 157, 92, SDL_ALPHA_OPAQUE };

    SDL_DestroyTexture(question_text);
    SDL_DestroyTexture(answer_text);
    question_text = make_text(cards[current].question, white);
    answer_text = make_text(cards[current].answer, orange);
    show_answer = false;
}

static void handle_click(void)
{
    if (!show_answer) {
        show_answer = true;
    } else {
        current = (current + 1) % card_count;
        show_card();
    }
}

static void draw_text(SDL_Texture *texture, float x, float y)
{
    int width, height;

    if (texture == NULL) {
        return;
    }

    SDL_QueryTexture(texture, NULL, NULL, &width, &height);

    SDL_FRect destination = { x, y, (float)width, (float)height };
    SDL_RenderCopyF(renderer, texture, NULL, &destination);
}

static void frame(void)
{
    SDL_Event event;

    while (SDL_PollEvent(&event)) {
        if (event.type == SDL_QUIT) {
            running = false;
        }

        if (event.type == SDL_MOUSEBUTTONDOWN) {
            handle_click();
        }
    }

    SDL_SetRenderDrawColor(renderer, 24, 26, 32, SDL_ALPHA_OPAQUE);
    SDL_RenderClear(renderer);

    draw_text(question_text, 40.0f, 80.0f);
    if (show_answer) {
        draw_text(answer_text, 40.0f, 260.0f);
    }

    SDL_RenderPresent(renderer);
}

static void clean_up(void)
{
    SDL_DestroyTexture(question_text);
    SDL_DestroyTexture(answer_text);
    SDL_free(cards);
    SDL_free(file_text);
    TTF_CloseFont(font);
    TTF_Quit();
    SDL_DestroyRenderer(renderer);
    SDL_DestroyWindow(window);
    SDL_Quit();
}

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

    if (TTF_Init() != 0) {
        SDL_Log("TTF_Init failed: %s", TTF_GetError());
        SDL_Quit();
        return 1;
    }

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

    SDL_SetWindowTitle(window, "Read a file");
    SDL_RenderSetVSync(renderer, 1);

    font = TTF_OpenFont(FONT_PATH, 26);
    if (font == NULL) {
        SDL_Log("TTF_OpenFont failed: %s", TTF_GetError());
        clean_up();
        return 1;
    }

    if (!load_cards(CARDS_PATH)) {
        clean_up();
        return 1;
    }

    show_card();

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

    clean_up();
    return 0;
}

To build this program, use the setup from Draw text. Save cards.txt and DejaVuSans.ttf in your assets folder.

Keep data out of the code

The quiz project keeps its questions in a C array, so each change needs a new build. A file is better. A person who cannot program can edit it, and one program can load many different sets of questions, levels, or settings.

This is the file format:

# Lines that start with a hash are comments.
What is one picture on the screen?|A frame
Which call shows the hidden picture?|SDL_RenderPresent

Each line is one card, and a bar character separates the question from the answer. You choose the format, so keep it easy to read and easy to split.

Load the file

    file_text = SDL_LoadFile(path, NULL);

SDL_LoadFile reads the full file into memory that it allocates, and it adds a null character at the end. The result is a normal C string. The NULL means that the program does not need the size in bytes.

This function works the same way on the desktop and in the browser. In a browser build, --preload-file assets packs the file with the program.

Make room for the cards

The program does not know how many cards the file has, so it allocates the array at run time:

    cards = SDL_malloc((size_t)count_lines(file_text) * sizeof(Card));

Each card needs a line, so the number of lines is a safe maximum. Comments and blank lines make the array a little too large, and this does no harm.

Split the text in place

A C string ends at a null character. So to split a line, the program writes a null character over the separator. This turns one long string into many short strings, with no copying:

    *bar = '\0'; /* End the question here. */
    card->question = line;
    card->answer = bar + 1;

Before this code, the line is one string. After it, question and answer are two strings in the same memory.

The cards hold pointers into file_text, and not copies of the text. So the program must keep file_text until it finishes with the cards. The order of the frees does not matter, but freeing file_text early makes every card point at invalid memory.

Handle bad input

A file comes from outside your program, so it can contain anything. The parser handles the cases that really happen:

Test these cases. A parser that works only on perfect input breaks on the first file that somebody else writes.

Free what you allocated

    SDL_free(cards);
    SDL_free(file_text);

Memory from SDL_malloc and SDL_LoadFile goes back with SDL_free. Use the SDL function, because SDL can use a different allocator from your C library.

Try this

  1. Add your own cards to cards.txt, and run the program with no new build.
  2. Add a line with no bar, and read the message in the log.
  3. Show the card number, such as "Card 2 of 6".
  4. Load the quiz questions from a file. Use one line for each question, with the correct answer first, and shuffle the answers.

What you learned