SDL School
SDL version Which one?

Draw text

This program shows a title, a paragraph, and a counter. Click the demo, and the counter goes up. You learn how a font file becomes pixels, and how to handle text that changes.

The full program

main.c
/* Draw text from a font file. Remake the text only when it changes. */
#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 PARAGRAPH_WIDTH 560

static SDL_Window *window = NULL;
static SDL_Renderer *renderer = NULL;
static TTF_Font *title_font = NULL;
static TTF_Font *body_font = NULL;
static SDL_Texture *title = NULL;
static SDL_Texture *paragraph = NULL;
static SDL_Texture *counter = NULL;
static int clicks = 0;

/* Make a texture that shows the text. A wrap width of 0 means no wrapping. */
static SDL_Texture *make_text(TTF_Font *font, const char *text, int wrap_width)
{
    SDL_Color color = { 235, 236, 240, SDL_ALPHA_OPAQUE };
    SDL_Surface *surface = TTF_RenderText_Blended_Wrapped(font, text, 0, color, wrap_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;
}

/* Draw a text texture at its real size, so that the letters stay sharp. */
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);
}

/* Text that changes needs a new texture. */
static void update_counter(void)
{
    char text[64];

    SDL_snprintf(text, sizeof(text), "Clicks: %d", clicks);
    SDL_DestroyTexture(counter);
    counter = make_text(body_font, text, 0);
}

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("Draw text", WINDOW_WIDTH, WINDOW_HEIGHT, 0,
                                     &window, &renderer)) {
        SDL_Log("SDL_CreateWindowAndRenderer failed: %s", SDL_GetError());
        return SDL_APP_FAILURE;
    }

    SDL_SetRenderVSync(renderer, 1);

    title_font = TTF_OpenFont(FONT_PATH, 40.0f);
    body_font = TTF_OpenFont(FONT_PATH, 22.0f);
    if (title_font == NULL || body_font == NULL) {
        SDL_Log("TTF_OpenFont failed: %s", SDL_GetError());
        return SDL_APP_FAILURE;
    }

    title = make_text(title_font, "Draw text", 0);
    paragraph = make_text(body_font,
                          "A font file holds the shape of each letter. SDL_ttf turns "
                          "your words into an image, and this paragraph wraps at a "
                          "width that you choose. It can show accents too: café, señor.",
                          PARAGRAPH_WIDTH);
    update_counter();
    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) {
        clicks++;
        update_counter();
    }
    return SDL_APP_CONTINUE;
}

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

    draw_text(title, 40.0f, 36.0f);
    draw_text(paragraph, 40.0f, 110.0f);
    draw_text(counter, 40.0f, 320.0f);

    /* Debug text needs no font, but it has one small size. */
    SDL_SetRenderDrawColor(renderer, 255, 157, 92, SDL_ALPHA_OPAQUE);
    SDL_RenderDebugText(renderer, 40.0f, 440.0f, "This line is SDL_RenderDebugText.");

    SDL_RenderPresent(renderer);
    return SDL_APP_CONTINUE;
}

void SDL_AppQuit(void *appstate, SDL_AppResult result)
{
    SDL_DestroyTexture(title);
    SDL_DestroyTexture(paragraph);
    SDL_DestroyTexture(counter);
    TTF_CloseFont(title_font);
    TTF_CloseFont(body_font);
    TTF_Quit();
}
main.c
/* Draw text from a font file. Remake the text only when it changes. */
#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 PARAGRAPH_WIDTH 560

static SDL_Window *window = NULL;
static SDL_Renderer *renderer = NULL;
static TTF_Font *title_font = NULL;
static TTF_Font *body_font = NULL;
static SDL_Texture *title = NULL;
static SDL_Texture *paragraph = NULL;
static SDL_Texture *counter = NULL;
static int clicks = 0;
static bool running = true;

/* Make a texture that shows the text. A wrap width of 0 means no wrapping. */
static SDL_Texture *make_text(TTF_Font *font, const char *text, int wrap_width)
{
    SDL_Color color = { 235, 236, 240, SDL_ALPHA_OPAQUE };
    SDL_Surface *surface = TTF_RenderUTF8_Blended_Wrapped(font, text, color, wrap_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;
}

/* Draw a text texture at its real size, so that the letters stay sharp. */
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);
}

/* Text that changes needs a new texture. */
static void update_counter(void)
{
    char text[64];

    SDL_snprintf(text, sizeof(text), "Clicks: %d", clicks);
    SDL_DestroyTexture(counter);
    counter = make_text(body_font, text, 0);
}

static void frame(void)
{
    SDL_Event event;

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

        if (event.type == SDL_MOUSEBUTTONDOWN) {
            clicks++;
            update_counter();
        }
    }

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

    draw_text(title, 40.0f, 36.0f);
    draw_text(paragraph, 40.0f, 110.0f);
    draw_text(counter, 40.0f, 320.0f);

    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 (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());
        TTF_Quit();
        SDL_Quit();
        return 1;
    }

    SDL_SetWindowTitle(window, "Draw text");
    SDL_RenderSetVSync(renderer, 1);

    title_font = TTF_OpenFont(FONT_PATH, 40);
    body_font = TTF_OpenFont(FONT_PATH, 22);
    if (title_font == NULL || body_font == NULL) {
        SDL_Log("TTF_OpenFont failed: %s", TTF_GetError());
        TTF_Quit();
        SDL_Quit();
        return 1;
    }

    title = make_text(title_font, "Draw text", 0);
    paragraph = make_text(body_font,
                          "A font file holds the shape of each letter. SDL_ttf turns "
                          "your words into an image, and this paragraph wraps at a "
                          "width that you choose. It can show accents too: café, señor.",
                          PARAGRAPH_WIDTH);
    update_counter();

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

    SDL_DestroyTexture(title);
    SDL_DestroyTexture(paragraph);
    SDL_DestroyTexture(counter);
    TTF_CloseFont(title_font);
    TTF_CloseFont(body_font);
    TTF_Quit();
    SDL_DestroyRenderer(renderer);
    SDL_DestroyWindow(window);
    SDL_Quit();
    return 0;
}

SDL does not draw real text

SDL3 has one built-in text function, SDL_RenderDebugText. It needs no font file, but it has one tiny size and shows only ASCII characters. It is good for a quick number while you debug. The orange line in the demo uses it.

    SDL_RenderDebugText(renderer, 40.0f, 440.0f, "This line is SDL_RenderDebugText.");

For text that people read, you use SDL_ttf. This is a separate library from the SDL project. It reads a TrueType font file, the same kind of file that your computer uses for its fonts.

SDL2 has no text function. For text, you use SDL_ttf. This is a separate library from the SDL project. It reads a TrueType font file, the same kind of file that your computer uses for its fonts.

Add SDL_ttf to your project

Linux

On Ubuntu or Debian, you do not need to install anything new. If there is no SDL3_ttf package, CMake downloads and builds it for you.

On Fedora:

sudo dnf install SDL3_ttf-devel

On Arch:

sudo pacman -S sdl3_ttf

macOS

brew install sdl3_ttf

Windows

pacman -S mingw-w64-ucrt-x86_64-sdl3-ttf

Then replace your CMakeLists.txt file with this one:

CMakeLists.txt
cmake_minimum_required(VERSION 3.24)
project(hello C)

# Use installed libraries if there are any. If not, download and build them.
include(FetchContent)
FetchContent_Declare(
    SDL3
    GIT_REPOSITORY https://github.com/libsdl-org/SDL.git
    GIT_TAG release-3.4.18
    GIT_SHALLOW TRUE
    FIND_PACKAGE_ARGS 3.4 CONFIG
)

# These settings apply only when CMake must build SDL_ttf.
set(SDLTTF_VENDORED ON)
set(SDLTTF_HARFBUZZ OFF)
set(SDLTTF_PLUTOSVG OFF)
FetchContent_Declare(
    SDL3_ttf
    GIT_REPOSITORY https://github.com/libsdl-org/SDL_ttf.git
    GIT_TAG release-3.2.2
    GIT_SHALLOW TRUE
    GIT_SUBMODULES external/freetype
    FIND_PACKAGE_ARGS CONFIG
)
FetchContent_MakeAvailable(SDL3 SDL3_ttf)

add_executable(hello WIN32 main.c)
target_link_libraries(hello PRIVATE SDL3_ttf::SDL3_ttf SDL3::SDL3)

Linux

On Ubuntu or Debian:

sudo apt install libsdl2-ttf-dev

On Fedora:

sudo dnf install SDL2_ttf-devel

On Arch:

sudo pacman -S sdl2_ttf

macOS

brew install sdl2_ttf

Windows

pacman -S mingw-w64-ucrt-x86_64-SDL2_ttf

Then replace your CMakeLists.txt file with this one:

CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(hello C)

find_package(SDL2 REQUIRED CONFIG)
find_package(SDL2_ttf REQUIRED CONFIG)

add_executable(hello WIN32 main.c)

# Windows needs SDL2main. The other systems do not have this library.
if(TARGET SDL2::SDL2main)
    target_link_libraries(hello PRIVATE SDL2::SDL2main)
endif()
target_link_libraries(hello PRIVATE SDL2::SDL2 SDL2_ttf::SDL2_ttf)

Next, add a font to your assets folder. This lesson uses DejaVuSans.ttf, a free font. Its license lets you copy it and use it in your own programs.

If you use a different font, read its license first. A font file is software, and many fonts have limits on how you can share them.

From a font to the screen

Text follows the same path as an image in the last lesson. SDL_ttf draws the words into a surface. You make a texture from the surface, and you draw the texture.

    SDL_Surface *surface = TTF_RenderText_Blended_Wrapped(font, text, 0, color, wrap_width);

The 0 after text tells the function that the string ends at its null character.

    SDL_Surface *surface = TTF_RenderUTF8_Blended_Wrapped(font, text, color, wrap_width);

The word Blended means that the edges of each letter are smooth. The word Wrapped means that the text breaks onto a new line at wrap_width pixels. A value of 0 breaks lines only at a newline character.

One font object for each size

    title_font = TTF_OpenFont(FONT_PATH, 40.0f);
    body_font = TTF_OpenFont(FONT_PATH, 22.0f);
    title_font = TTF_OpenFont(FONT_PATH, 40);
    body_font = TTF_OpenFont(FONT_PATH, 22);

A font object holds one font at one size. For two sizes, the program opens the file two times.

Text that changes

The title and the paragraph never change, so the program makes each texture one time, at the start. The counter changes, so it needs a new texture:

    char text[64];

    SDL_snprintf(text, sizeof(text), "Clicks: %d", clicks);
    SDL_DestroyTexture(counter);
    counter = make_text(body_font, text, 0);

A C string is an array of characters, so the program needs an array for it. SDL_snprintf writes into the array, and it stops at the size that you give it. This protects the memory after the array.

The old texture must go first. Without SDL_DestroyTexture, each click leaks one texture.

The program calls this function only after a click. Making a texture is much slower than drawing one, so do not remake text in each frame unless it changes in each frame.

Keep text sharp

    draw_text(title, 40.0f, 36.0f);

The draw_text function draws each texture at its real size. If you scale a text texture up, the letters go blurry. For larger text, use a larger font size.

Accents and other characters

SDL_ttf reads strings as UTF-8, the standard way to store text in many languages. This is why café works in the C source. A font holds only the characters that its designer drew, so a character outside the font appears as an empty box.

Clean up

The program destroys the textures, closes the fonts, then calls TTF_Quit. SDL calls SDL_Quit for you after SDL_AppQuit.

The program destroys the textures, closes the fonts, then calls TTF_Quit. SDL_Quit comes last, because SDL_ttf uses SDL.

Build for the browser

The port for SDL_ttf comes with Emscripten, and --preload-file packs the font:

emcc main.c -sUSE_SDL=3 -sUSE_SDL_TTF=3 --preload-file assets -o index.html
emcc main.c -sUSE_SDL=2 -sUSE_SDL_TTF=2 --preload-file assets -o index.html

The font and the SDL_ttf code make the download larger than the earlier demos. The first build also takes longer, because Emscripten compiles FreeType, the library that reads fonts.

Try this

  1. Center the title. Use the texture width and WINDOW_WIDTH to work out x.
  2. Show the number of seconds since the program started. Remake that texture only after the number changes.
  3. Add text labels to the buttons from Make a button.
  4. Replace the score squares in Pong with numbers.

What you learned