MIT License NuGet

Home / Docs / Text and fonts

Text and fonts

There is one glyph pipeline on every backend, and it is MSDF. Glyphs are rasterised into multi-channel signed distance fields at a fixed pixel range of 4 and packed into the engine's own atlas, so Scale is a free parameter rather than a quality setting and no per-size atlas is ever rebuilt.

Loading a font

Fonts are registered once during startup. Several can be resident at the same time, and glyph metrics are cached per font and code point.

var font = await Season.Fonts.Font.CreateAsync("Fonts/NotoSans-Regular.ttf", 48f);

The size argument is the rasterisation size, which affects atlas footprint and the smallest legible detail — not the size text renders at. That is Scale on the control, and it is independent.

Drawing text

title = new Texts
{
    Content = "Settings",
    Color = Colors.DarkRed,
    Scale = Vector2.One * 1.2f,
    OnClick = () => settingPanel.Alpha = 1f
};
AddControl(title);

body = new Texts
{
    Content = longParagraph,
    WidthRequest = 360,          // wrapping box; null means one line
    LineHeight = 34
};
AddControl(body);
MemberNotes
ContentThe string. Assigning it triggers layout. ContentOrigin keeps the untranslated original.
TranslateTrue by default: the string goes through localisation before layout. Set it false for names, numbers and log lines.
ScaleA Vector2, so text can be stretched on one axis. Costs nothing in sharpness.
LineHeightLine advance in design units. 40 by default.
WidthRequest, HeightRequestThe wrapping box. Leave both null and the text is a single line.
WordsSpace, EmptySpaceExtra tracking, and the width of a space in Latin runs. EmptySpace is 10 by default.
OriginWidth, OriginHeightRead back the measured size after layout, for positioning something next to it.
TextsTypeImmediately or FadeIn, for text that appears rather than pops.
Append(string)Appends a fragment and lays out only the tail. See below.
Translate is on by default

Every Content assignment is looked up before layout. That is what you want for UI labels and emphatically not what you want for a player's name or a file path, so set Translate = false on those controls before assigning content.

Inline colour

A small span markup is understood inside Content, which is enough to highlight one word without splitting a paragraph into three controls.

log.Content = "Build <span style='color:Red;font-weight:bold;'>failed</span> in 2.4s";

That is the whole markup surface. There is no rich text document model, no stylesheet and no data binding — deliberately, because the moment a text control accepts a document you own a layout engine.

Streaming text

Append exists because token streams exist. It lays out only the new tail against a checkpoint of the layout state at the start of the current line, so an incremental append matches a full relayout pixel for pixel — including whole-word lookahead, which is the part a naive tail append gets wrong.

// A model streams tokens; the paragraph is not rebuilt sixty times a second.
await foreach (var token in stream)
    answer.Append(token);
Span markup disables incremental append

A colour scope can straddle a chunk boundary and span indices are absolute within the whole string, so the tail cannot be parsed on its own. Once span markup has appeared in Content, appends fall back to full relayout until the next whole-content assignment. Streaming and inline colour are each supported; combined, you pay for the colour.

Line breaking

Breaking is script-aware rather than character-count based. CJK breaks between characters, including the punctuation that lives outside the main unified block and must not begin a line; Latin breaks on whole words, with lookahead to measure the word before committing to the line.

Both rules run in the same pass, so a mixed CJK and Latin paragraph wraps correctly without you selecting a mode. What the engine does not do is hyphenation, bidirectional text or complex shaping for Indic and Arabic scripts.

Text and post-processing

Overlay text is composited after tone mapping, with inverse-ACES compensation applied so the colour you asked for is the colour on screen rather than the tonemapped version of it. That is also why a HUD left in RenderDomain.Scene looks wrong: it gets graded, bloomed and TAA-resolved along with the world, and thin glyph edges are exactly what temporal accumulation smears.

See 2D controls for the domain table, and 2D, text and UI for the wider feature picture including the known gaps.