Skip to content

Guide

Text and fonts #

Knots draws text from the glyph outlines on the GPU. It does not make bitmaps of glyphs. Text stays sharp at all sizes and all display scales.

Show text #

Use the Text component:

try frame.e(Text{
    .key = .src(@src()),
    .content = "A long paragraph that must wrap at the width of its parent.",
    .style = &.{ .width = .grow(), .wrap = true, .font_size = .md, .foreground = .dimmed },
});
  • content is UTF-8. Knots does not copy it. It must stay valid until the frame ends. frame.arena() is a good place for text that you make.
  • Text is selectable by default. The user can select it with the pointer and copy it. Set .selectable = false for labels and titles.
  • font, font_size, and foreground are inherited from the parent when you do not set them.

Wrapping #

Text does not wrap by default. Each \n starts a new line. Set wrap = true to break lines at the element width. Knots uses greedy word wrap: it puts as many words on a line as fit.

A wrapping text element must get its width from its parent, for example with .width = .grow() or .fixed. A .fit() width is the width of the text on one line, so it does not wrap. Read How layout runs for the order of the passes.

Fonts #

Knots includes one font, named "default". It contains Roboto Regular and Material Icons. To use other fonts, load TrueType data and give each font a name.

At startup #

var app = try knots.App.init(io, gpa, .{
    .window = .{ .width = 800, .height = 600, .title = "App" },
    .ui = .{ .fonts = &.{
        .{ .name = "default", .data = @embedFile("fonts/Inter-Regular.ttf") },
        .{ .name = "mono", .data = @embedFile("fonts/JetBrainsMono.ttf") },
    } },
});

The first font in the list is the default font. When you set fonts, the list replaces the built-in font.

After startup #

try app.main_viewport.ui_ctx.ui.font.addFace("mono", @embedFile("fonts/JetBrainsMono.ttf"));

Select a font #

.style = &.{ .font = "mono", .font_size = .{ .px = 13 } },

The name data and font data must stay valid while the UI exists. @embedFile data is always valid. A UI can have a maximum of 256 fonts. Two fonts cannot have the same name. If a style names a font that does not exist, the frame returns error.UnknownFont.

How text is rendered #

Text goes through these stages:

  1. Parse. Knots reads the font with a pure-Zig TrueType parser. There is no FreeType or HarfBuzz dependency.
  2. Shape. Knots decodes the UTF-8 text into codepoints. It finds one glyph for each codepoint and adds the advance widths. At this stage it also wraps lines.
  3. Build glyphs. The first time a glyph is used, Knots converts its outline to quadratic Bézier curves. It splits each cubic curve into eight quadratic curves. It writes the curves into a curve texture. Then it divides the glyph into a maximum of 16 horizontal and 16 vertical bands. For each band, it writes a list of the curves that cross it into a band texture.
  4. Draw. Each glyph is one instanced quad. The fragment shader finds the band of the pixel, reads only the curves in that band, and calculates the exact coverage of the pixel. This is the Slug algorithm by Eric Lengyel.

This method has these results:

  • One glyph works for all sizes. Knots does not keep a copy of each glyph for each size.
  • Text is sharp on high-density displays, and when it scales or animates.
  • Edges are anti-aliased from the real curves, not from a signed-distance field, so small text and sharp corners stay correct.

Caches and uploads #

  • The curve and band textures are 4096 texels wide. When new glyphs are added, the frame output marks which rows changed. The renderer uploads only those rows.
  • Knots keeps shaped and wrapped text in a cache for each frame. It removes text that was not used for two frames.
  • A glyph atlas has an ID and a revision number. A renderer that misses a revision uploads the full atlas. A custom renderer does not need to acknowledge uploads.

Limits #

  • A single text value can have a maximum of 1 MiB of UTF-8.
  • TextInput and TextArea accept a maximum of bytes_max bytes. The default is 1 MiB.
  • There is no complex shaping. Scripts that need glyph substitution or reordering, for example Arabic or Devanagari, do not show correctly.
  • There is no bidirectional text, no ligatures, and no kerning.
  • There is no font fallback. A codepoint that is not in the selected font shows the missing glyph of that font.
  • There is no IME composition for text input.