Drawing Text

Introduction

Drawing text isn’t that different from drawing sprites. However, since text can be understood as a group of sprites, where each character is a sprite, cerlib’s API provides us some extra functionality. In this chapter, we’ll go through all functions one by one.

First up, unsurprisingly, we need a font so that we can draw some text. Fortunately, cerlib comes with an embedded font that is directly available via the cer::Font::built_in function. Alternatively, you may add a cer::Font variable to your game and load it in load_content using the cer::load_font function. Because it won’t make much of a difference in this chapter, we can stick to the built-in font.

The central function is cer::draw_string, which takes a cer::Font, a size and the text to draw as a standard string. The string is expected to be UTF8-encoded.

In your game’s draw method, simply call cer::draw_string using the font object and give it some text:

void draw(const cer::Window& window) override
{
  const cer::Font font = cer::Font::built_in();
  
  // Draw some text at position {100, 100}
  cer::draw_string("Hello World!", font, /*font_size: */ 48, {100, 100});
  
  // Draw another text at position {100, 200}, and with color red
  cer::draw_string("Hello World 2!", font, /*font_size: */ 32, {100, 200}, cer::yellow);
}

This will draw some text as expected:

Text decorations

For cases when text should be highlighted or otherwise hint at certain information, cer::draw_string provides a way to decorate text, namely using the cer::TextDecoration variant.

We can for example draw strikethrough or underlined text using the respective cer::TextStrikethrough and cer::TextUnderline types. Let’s see how that would look:

const cer::Font font        = cer::Font::built_in();
const std::string_view text = "Hello World!";
 
// Draw text with a strikethrough line
cer::draw_string(text, font, 48, {100, 100}, cer::white, cer::TextStrikethrough{});
 
// Draw text with a strikethrough line, but specify a custom color and thickness
cer::draw_string(text, font, 48, {400, 100}, cer::white,
                 cer::TextStrikethrough{
                     .color = cer::red,
                     .thickness = 10.0f,
                 });
 
// Same for an underline.
cer::draw_string(text, font, 48, {100, 200}, cer::yellow, cer::TextUnderline{});
 
cer::draw_string(text, font, 48, {400, 200}, cer::yellow,
                 cer::TextUnderline{
                     .color = cer::red,
                     .thickness = 10.0f,
                 });

The result:

Text information

While drawing text is enough for many cases, there are some cases where you want to gain some information about text you’re about to draw. Think of user interfaces where text elements are neighbors of each other. This requires some kind of layouting algorithm, which in turn relies on the size of text elements so that it can properly figure out where to put each element. For such cases, cer::Font provides methods such as measureline_height and for_each_glyph.

We’ll first talk about measure. It allows us to get the exact size of a text, in pixels, given a font and a size. Modify our draw method as follows:

void draw(const cer::Window& window) override
{
  const cer::Font font        = cer::Font::built_in();
  const std::string_view text = "Hello World!\nThis is line two.";
  const uint32_t font_size    = 48;
  
  // Measure the text we're going to draw.
  const cer::Vector2 text_size = font.measure(text, font_size);
  
  const cer::Vector2 text_position{100, 100};
  
  // Draw a rectangle first to show that we know the text size correctly.
  cer::fill_rectangle({text_position, text_size}, cer::yellow);
  
  cer::draw_string(text, font, font_size, text_position, cer::black);
}

This should give us a yellow rectangle with black text on top:

Imagine we had two draw multiple independent text objects next to one another, including a margin between each of them. With this information we can now implement such a case, just like this:

void draw(const cer::Window& window) override
{
  // Use the same font for all texts.
  const cer::Font font = cer::Font::built_in();
 
  // These are the texts we have to draw.
  const std::array texts{
    std::make_tuple("Hello World!\nThis is line two.", 32, cer::white),
    std::make_tuple("This is text 2", 48, cer::yellow),
    std::make_tuple("... and this is text 3!", 64, cer::black),
  };
 
  // Spacing between each text element.
  constexpr float spacing = 30.0f;
 
  // This will be our "pen" that defines where we start drawing text.
  float position_x = 50.0f;
 
  // For this example, we'll just draw text at a fixed Y-position.
  constexpr float position_y = 100.0f;
 
  for (const auto& [text, size, color] : texts)
  {
    const cer::Vector2 text_size = font.measure(text, size);
 
    cer::draw_string(text, font, size, {position_x, position_y}, color);
 
    // Advance our pen position
    position_x += text_size.x + spacing;
  }
}

This will greet us with all texts neatly lined up:

Next up is the line_height method. It gives you the fixed height of the font itself, in pixels. This is for example used to align text objects next to each other vertically (“line by line”). Let’s see an example:

void draw(const cer::Window& window) override
{
  const cer::Font font = cer::Font::built_in();
 
  const std::array texts{
    std::make_tuple("Hello World! This is line 1.", 48, cer::white),
    std::make_tuple("This is line 2, and tiny", 24, cer::yellow),
    std::make_tuple("... and this is line 3!", 48, cer::black),
  };
 
  // X and Y are swapped this time, since we're incrementally
  // drawing text downwards.
  constexpr float position_x = 100.0f;
 
  float position_y = 50.0f;
 
  for (const auto& [text, size, color] : texts)
  {
    const float line_height = font.line_height(size);
 
    cer::draw_string(text, font, size, {position_x, position_y}, color);
 
    // Advance our pen position
    position_y += line_height;
  }
}

This gives us perfectly aligned text:

And lastly, the most important of all: for_each_glyph. This is the method all others are based on. Given a text, size and an std::function, it allows you to iterate each glyph of a string and perform an arbitrary action. For each iteration, your function gets information about the glyph such as codepoint and position as parameters. Let’s look at its signature:

void for_each_glyph(std::string_view text,
                    uint32_t size,
                    const std::function<bool(uint32_t codepoint, Rectangle rect)>& action) const;

We see that we have to give it a function that takes the glyph’s Unicode codepoint and its occupied area within the text, in pixels. Additionally, it has to return a bool value to indicate whether to keep going. If it returns false, the iteration stops after that glyph. for_each_glyph itself returns nothing, as it’s just a forward iteration method.

With this, we could even implement a custom text rendering function. But for this example we’ll keep it simple and print glyph information to the console, just to show that it works:

void draw(const cer::Window& window) override
{
  const cer::Font font        = cer::Font::built_in();
  const uint32_t font_size    = 32;
  const std::string_view text = "Hello\nWorld!";
 
  font.for_each_glyph(text, font_size, [](uint32_t codepoint, cer::Rectangle rect)
  {
    cer::log_info("Got glyph {} at {}", char(codepoint), rect);
    return true; // Keep going
  });
}

Which prints the following to the console:

Got glyph H at [ x=0; y=4.5167785; width=16; height=21 ]
Got glyph e at [ x=20.67114; y=9.516779; width=15; height=17 ]
Got glyph l at [ x=37.583893; y=4.5167785; width=4; height=21 ]
Got glyph l at [ x=45.221478; y=4.5167785; width=4; height=21 ]
Got glyph o at [ x=52.859062; y=9.516779; width=15; height=17 ]
Got glyph W at [ x=0; y=36.516777; width=27; height=21 ]
Got glyph o at [ x=25.570469; y=41.516777; width=15; height=17 ]
Got glyph r at [ x=42.389263; y=41.516777; width=10; height=16 ]
Got glyph l at [ x=53.691277; y=36.516777; width=4; height=21 ]
Got glyph d at [ x=61.328857; y=36.516777; width=14; height=22 ]
Got glyph ! at [ x=78.77853; y=36.516777; width=3; height=21 ]

We can observe that the X-position increments steadily until the W is encountered, which starts at a new line. X is therefore reset, and Y is now offset. X increments steadily while Y remains the same until the end of the string.

There is one use case where for_each_glyph really shines, which is hit detection. When implementing UI controls such as text boxes, the correct position for the caret must be determined. This is only possible when we have information about each character’s location inside such a text box. Whenever a user clicks somewhere inside the text box, we can iterate its text and compare the clicked position to each glyphs area.

Conversely, when the caret’s position is already known and the user presses the left or right key to go to the previous or next glyph, we can correctly move the caret, since we know the exact positions of each glyph.

Text samplers

It’s important to note that the active sampler (set by cer::set_sampler) also affects how text is drawn. This is by design, since it allows you to draw pixelated fonts easily. Just set a nearest-neighbor sampler (e.g. cer::Sampler::point_clamp()) and then draw your text as usual. Example:

// Disable interpolation.
cer::set_sampler(cer::Sampler::point_clamp());
cer::draw_string(...);
cer::draw_string(...);
// ...
 
// Enable interpolation again.
cer::set_sampler(cer::Sampler::linear_repeat());
// ...

DPI-aware text

The text size we specify in text-related functions is given in pixels. However, different displays might have different pixel densities (DPI). cerlib doesn’t handle drawing at different densities itself, but gives you the information so that you can handle it. When working with text, you should obtain the current DPI scaling factor using cer::Window::pixel_ratio of the main/current window. This gives you a floating-point value by which you have to scale the font size, so that you get the “real” pixel size for that window’s display.

An example:

uint32_t font_size   = 32;
float pixel_ratio    = my_window.pixel_ratio();
float real_font_size = uint32_t(float(font_size) * pixel_ratio);
 
cer::draw_string("Hello World!", my_font, real_font_size);

The same is true for other text-related functions such as cer::Font::measure and cer::Font::for_each_glyph.

Next: Handling Input