Text & Fonts

← Back to index

The text stack lives under Runtime/Text/. CSS text properties (the author surface) are documented on CSS Text; this page describes the engine.

Two-tier shaping: ATG primary, SDF fallback

Weva shapes text through a two-tier adapter (AtgPrimaryFallbackAdapter, Runtime/Text/Atg/):

  1. ATG (Advanced Text Generator) is tried first — Unity's FreeType-based hinted-bitmap glyph rasterizer, the same engine UI Toolkit uses for "Chrome-quality" small UI text. Glyph stems snap to whole pixels because the font's hinting bytecode is applied.
  2. SDF (signed distance field) is the fallback (Runtime/Text/Sdf/). It handles everything ATG can't: text-shadow blur, transforms/animation, large text, and font-fallback chains not yet wired ATG-side.

Both backends emit the same SdfGlyphQuad type, so the downstream renderer is unaware of the source. Shaping is Latin + simple scripts only — no bidi reordering, no complex-script shaping in v1 (TextShaper, Runtime/Text/TextCore/).

There is also a TextMeshPro adapter (Runtime/Text/Tmp/) and a low-level UnityFontEngineBackend over FontEngine.LowLevel; the project's v1 text path is the FontEngine-backed SDF baker, escaping to TextCore.Text.FontAsset only when forced.

Font resolution

FontResolver (Runtime/Text/TextCore/FontResolver.cs) maps a CSS font-family list to a loadable face:

  1. Each comma-separated family is tried in order.
  2. Each token matches (case-insensitive) registered families first (RegisterFont / @font-face), then the OS-default mapping (sans-serif, monospace, serif, system-ui).
  3. If nothing matches, it falls back to DefaultFamily (default sans-serif).

Custom fonts are registered explicitly (via @font-face or RegisterFont), which keeps headless tests deterministic.

Default-face policy

The default sans-serif face is Segoe UI, not Arial — a locked product decision. This intentionally diverges from Chrome on Windows: Segoe's line-height: normal is ≈1.36× the font size versus Arial's ≈1.15×. The resulting uniform vertical shifts relative to a Chrome baseline are accepted divergence, not bugs (see SAMPLE_AUDIT.md's font-drift rows). Only structural layout differences are treated as defects.

Font-pipeline gotcha

The ATG primary and the SDF/FontEngine fallback can resolve sans-serif to different faces. When ATG genuinely can't shape a run and it falls to the SDF path, the SDF fallback historically resolved sans-serif to the bundled Weva-Default.ttf (a different face than ATG's SegoeUI). This was the cause of intermittent "font-flap" and baseline bugs (e.g. the  /U+00A0 atlas-repack flap, now fixed by pre-rasterizing nbsp into the ATG atlas). If you see a run render in the "wrong" face, this fallback divergence is the usual suspect.

Glyph atlases

Glyphs are packed into shelf-packed atlases that grow then LRU-evict (GlyphAtlasPacker, AtlasRegistry, GlyphAtlas). The ASCII range plus U+00A0 is prepopulated so common runs shape without an on-demand repack. Shaped runs are cached in TextRunSnapshotCache (Runtime/Rendering/URP/) for the session.

Gradient text (background-clip: text)

background-clip: text with a non-opaque color fills the glyphs with the element's topmost background gradient. DrawTextCommand.TextFillGradient carries the gradient; SdfTextRendering samples it per glyph (a CSS-Images linear projection over the run bounds) and composites the text color over it:

v1 refinements left open: the gradient spans each run's bounds, so a multi-line clipped element restarts the gradient per line; radial/conic fall back to the gradient midpoint color; vertical gradients don't shade within the line height.


Next: Rendering