LavaUI docs

class UIFont

Swift

final class UIFont

Swift-facing typeface handle. Wraps canvas::Font (FreeType + HarfBuzz).

Identity for local cache keys is the path, face index, size and hinting. Note that this is not the identity the renderer uses — that one is the file's contents, computed on the far side of registerWithEngine, because only the process that opens the file can hash it. The two agree on everything except the path, and disagree about the path on purpose: this side needs a name to look up, that side needs a fact to trust.

Sources/LavaUI/Font.swift:105

Constructors

init?(path: String, pixelSize: Float = 16, faceIndex: UInt32 = 0, raster: FontRasterFlags = .default)

Not documented.

Sources/LavaUI/Font.swift:151

Properties

var ascent: Float { get }

Not documented.

Sources/LavaUI/Font.swift:125

var cellAdvance: Float?

Snap substituted glyphs onto a fixed advance, for a caller drawing on a character grid.

A terminal emits a whole run of cells as one DrawList.text and trusts the face's advance to keep the columns. A fallback glyph does not have that advance — Iosevka is a 0.5em face and JetBrains Mono a 0.6em one, so every substituted character pulled the rest of the line 1.5px left at 15px. One symbol in a line is invisible; a run of twenty braille cells ended 30px short of where the row above it ended.

Set to the cell width and each substituted glyph is rounded to a whole number of cells — never zero — and centred in them. Rounding rather than clamping is what keeps a double-width glyph two cells wide. nil, the default, leaves shaping alone, which is right for proportional text.

Sources/LavaUI/Font.swift:399

var descent: Float { get }

Not documented.

Sources/LavaUI/Font.swift:126

var engineId: UInt32 { get }

Id of this face in the engine's font registry. Stamped into every GlyphInstance so the renderer resolves face-relative glyph ids against the face they were actually shaped with.

Sources/LavaUI/Font.swift:131

let faceIndex: UInt32

Which face inside a .ttc/.otc collection. 0 for a plain font file.

Sources/LavaUI/Font.swift:109

var fallbacks: [UIFont] { get }

Faces consulted, in order, for characters this one cannot draw.

No single face is enough. The packed OpenSans is a Latin subset (883 glyphs — no arrows, no geometric shapes, no ⚠), and even DejaVu at ~6000 lacks U+23F8. Picking a "better" primary only moves the holes, so the fix has to be a chain. Set by FontStore; empty is fine and means the old behaviour.

Sources/LavaUI/Font.swift:372

let identity: String

Not documented.

Sources/LavaUI/Font.swift:111

var lineHeight: Float { get }

Not documented.

Sources/LavaUI/Font.swift:124

nonisolated(unsafe) static var missingGlyphWarnings: Bool

Set LAVAUI_FONT_WARNINGS=0 to silence.

Sources/LavaUI/Font.swift:714

let path: String

Not documented.

Sources/LavaUI/Font.swift:106

let pixelSize: Float

Not documented.

Sources/LavaUI/Font.swift:107

var pixelSize26_6: UInt32 { get }

The size as FreeType and HarfBuzz both count it: pixels times 64.

Rounded, not truncated. A Float size cannot be compared for equality across two processes and cannot be handed to FreeType unquantised, so this is the number that actually travels and the number the renderer keys on.

Sources/LavaUI/Font.swift:119

let raster: FontRasterFlags

Not documented.

Sources/LavaUI/Font.swift:110

nonisolated(unsafe) static var tabColumns: Int

Shapes with this face, then re-shapes any .notdef stretch with the first fallback that can actually draw it.

Substitution happens per cluster range, not per glyph: HarfBuzz's cluster is a byte offset into the source, so a failed run maps back to real substring boundaries. Re-shaping that substring separately is the same trade the word-wrapper already makes (wrapLinesImpl shapes each word alone) — no cross-run kerning, which does not exist across a script change anyway.

The common case costs one extra scan of the glyph array and nothing else: text that shapes cleanly never touches the fallback path. Columns a tab advances. Four, matching what Tab inserts.

nonisolated(unsafe) for the same reason the bridges in WindowControls are: set once at startup, read from the main thread thereafter.

Sources/LavaUI/Font.swift:507

Methods

func clearShapeCache()

Drops cached shaped runs. Call if the cache grows unbounded; entries are keyed by string only, since a UIFont is one face at one size.

Sources/LavaUI/Font.swift:743

static func loadSymbols(assetsRoot: String?, pixelSize: Float = 16) -> UIFont?

Symbol / icon face for media glyphs (▶ ⏸ etc.).

Prefers Noto Sans Symbols 2 (has Geometric Shapes + media controls). Plain "Noto Sans Symbols" does not include U+25B6 / U+23F8 — that is why the second file is required for play/pause.

Sources/LavaUI/Font.swift:190

static func loadSystemFallback(pixelSize: Float = 16) -> UIFont?

A broad-coverage system face, for glyphs the packed ones lack.

Deliberately the last tier and never the primary. Packed faces are what make text metrics identical on every machine — they feed Yoga, and layout that varies by host turns a rendering bug into one that reproduces on your box and not mine. This only ever adds glyphs that would otherwise be tofu, so a machine without any of these degrades to exactly the previous behaviour.

Ordered by measured coverage of what this codebase actually draws: DejaVu has ~6000 glyphs including arrows, geometric shapes and ⚠; Liberation and Noto have progressively less.

Sources/LavaUI/Font.swift:234

static func loadUI(assetsRoot: String, pixelSize: Float = 16) -> UIFont?

Prefer OpenSans, fall back to LiberationSerif.

Looks under assetsRoot/fonts/, then legacy assets/, then the root itself — SPM packs defaults as Resources/fonts → bundle fonts/.

Sources/LavaUI/Font.swift:177

func measure(_ text: String) -> (width: Float, height: Float)

Not documented.

Sources/LavaUI/Font.swift:354

func measure(_ text: String, availWidth: Float, mode: Int) -> (width: Float, height: Float)

mode: 0=Undefined, 1=Exactly, 2=AtMost (YGMeasureMode).

Sources/LavaUI/Font.swift:360

static func monospaceFallbacks(assetsRoot: String? = LavaResources.root, pixelSize: Float = 16) -> [FontFallback]

A fallback chain for a monospace primary, in the order it should be consulted. Names faces; useFallbacks attaches it and loading happens on the first character that needs one.

An app that loads its own face gets no chain: FontStore builds one for the UI face and nothing built one for anybody else, so LavaTerm drew tofu for every character outside its terminal face. That face is chosen for column alignment, which is exactly the property that makes it narrow — JetBrains Mono Nerd Font has 11,792 codepoints and not one of them is braille, so every spinner frame in Claude Code was a box, along with the ✻ ✽ ✢ ⏺ ⎿ ⧉ it prints on most lines.

Broadest fixed-width face first: a substituted glyph brings its own advance, and a monospace donor keeps the column grid closer to intact than a proportional one. NotoSansSymbols2 ships with LavaUI, so the braille and dingbat holes close even on a machine with nothing else.

Deliberately absent: NotoColorEmoji, a CBDT colour-bitmap font the R8 glyph atlas cannot store. Emoji stay tofu until the atlas grows a colour path, and the missing-glyph warning says so.

Sources/LavaUI/Font.swift:292

@discardableResult func registerWithEngine(_ editor: Editor) -> Bool

Registers this face with whoever owns the glyph atlas and records the returned id. Must happen before any glyph from this face reaches the draw list — otherwise the renderer resolves its ids against the wrong face.

Through editor.resources rather than the editor itself, because the atlas is not always here: under a shared renderer the id has to be the compositor's, and a locally-invented one indexes a face that process never loaded. See GPUResourceHost.

Sources/LavaUI/Font.swift:727

func shape(_ text: String) -> [ShapedGlyph]

Shaped run for one line, cached per string. Positions are relative to the run origin (pen at the baseline); the caller offsets them.

This is what the draw list paints, and the renderer never calls HarfBuzz itself. It is not what Yoga measured: layout goes through measure, which is C++ Font::measure over this face alone. So a run carrying characters this face lacks is wider here than the box layout derived for it — see shapeWithFallbacks, which re-shapes them through fallbacks. Do not use a shaped width to second-guess a laid-out box: the two disagree by a fallback glyph's advance, every time.

Sources/LavaUI/Font.swift:450

func shapedRun(_ line: String) -> ShapedRun

Shapes line (cached) and wraps it with the cursor mappings.

Sources/LavaUI/ShapedRun.swift:151

static func symbolsPath(assetsRoot: String?) -> String?

Where loadSymbols would load from, without loading it. Splitting the two is what lets a fallback chain be named before anything is read — see FontFallback.

Sources/LavaUI/Font.swift:198

static func systemFallbackPath() -> String?

Where loadSystemFallback would load from, without loading it.

Sources/LavaUI/Font.swift:240

func useFallbacks(_ chain: [FontFallback], into editor: Editor?)

Point this face at chain for characters it cannot draw.

Nothing is read here. Each entry is loaded and registered by fallbackFace when a character reaches it, which for most runs is never — see FontFallback for what that saves.

The editor is taken here rather than at load time because registration and use have to stay together: a substituted glyph carries its own face id into the draw list, and an unregistered face has id 0 — which resolves to the primary and draws whatever glyph happens to sit at that index. Handing over the editor once is what lets fallbackFace refuse to hand out a face it could not register.

Sources/LavaUI/Font.swift:413

func wrapLines(_ text: String, availWidth: Float) -> [String]

Lines matching measure(..., AtMost/Exactly) wrap breaks.

Sources/LavaUI/Font.swift:780