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
/* 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();
}/* 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_RenderPresentEach 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:
- A blank line or a comment is not a card, so the program skips it.
- A line with no bar is a mistake. The program logs the line number and continues.
- Windows ends a line with two characters,
\r\n. The parser removes the extra\r. - The last line can have no newline after it. The loop handles this with
newline != NULL. - A file with no cards stops the program with a message, because
% 0is a crash.
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
- Add your own cards to
cards.txt, and run the program with no new build. - Add a line with no bar, and read the message in the log.
- Show the card number, such as "Card 2 of 6".
- 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
- A file keeps data apart from code, so you can change it with no new build.
SDL_LoadFilereads a file into one string, and you must free it.- Writing a null character splits a string in place, with no copying.
- A file can contain anything, so a parser must handle bad input.