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