font
Font primitive — parse a font file ONCE into a baked, vectorized glyph format (ZFNT), then drive every text surface from it. A font is a general CPU resource addressed by name (the CPU counterpart of a renderer GPU resource), so a single registration is usable from UI text, 2D text, and true 3D glyph geometry.Authored fonts are .font assets whose onRegister hook calls font.register.
require("@builtin::modules.api.engine.font") -- font (also available as global 'font')
The font namespace: 36 functions.
globals/font/coverage
font.coverage(bytes: buffer | string) -> string?
The codepoints a font file maps to a glyph, as the coverage text
font.declare reads: comma-separated inclusive hexadecimal ranges.
Parameters
bytesbuffer | string— Raw font-file bytes (TTF / OTF).
Returns string? — Coverage text, or nil if the bytes don't parse as a font.
local cov = font.coverage(assert(vfs.read("/zero/source/fonts/MyCJK.otf")))
globals/font/declare
font.declare(name: string, coverage: string, opts: table) -> number
Declare an on-demand face: a face known by the codepoints it covers,
whose bytes are read only when a text surface first meets one of those
codepoints with no installed face that draws it. Until then the face costs
its coverage and nothing else, so a session that never shows its script
never reads its file. When a label, a text.* object, or font.prefetch
reaches a covered character, the engine requests the face: its file is read
and registered with font.register(name, bytes, opts), and every text
surface draws the character from it from then on. A .font asset whose
face.json lists onDemand chunks declares each of them through this call.
Parameters
namestring— The name the face is declared and registered under.coveragestring— The codepoints it covers: comma-separated inclusive hexadecimal ranges ("3040-30ff,4e00"), the textfont.coverage(bytes)writes.optstable—{ file: string, family: string?, role: string?, aliases: {string}? }naming the VFS path of the font file (TTF / OTF, or a baked ZFNT) and thefont.registeroptions it installs with.
Returns number — How many codepoints the coverage holds.
font.declare("MyCJK", assert(font.coverage(assert(vfs.read("/zero/source/fonts/MyCJK.otf")))), { file = "/zero/source/fonts/MyCJK.otf" })
globals/font/glyph
font.glyph(name: string, codepoint: number) -> any
Read one glyph's vectorized outline from a registered font, in font
units (resolution-independent — scale by fontSize / unitsPerEm).
Parameters
namestring— Registered family name.codepointnumber— Unicode codepoint (e.g.string.byte("A")).
Returns any — { advance, unitsPerEm, bbox = {xMin,yMin,xMax,yMax}, contours } where each contour is { start = {x,y}, segments = { {kind="line|quad|cubic", ...} } }, or nil if the font isn't registered.
local g = font.glyph("Inter", string.byte("A"))
globals/font/list
font.list() -> { string }
List every registered font family name.
Returns { string } — Array of family-name strings.
for _, fam in font.list() do print(fam) end
globals/font/observe
font.observe() -> { any }
What the text system is holding for fonts: one row per family the
shaper can resolve, with its face count, the numeric weights those faces
carry, whether any of them is slanted, and whether the family arrived
through a registration rather than from the platform. weights is what a
style's weight can name for that family. The same rows are fonts in
text.observe().
Returns { any } — Array of { family, faces, weights, italic, loaded }.
for _, f in ipairs(font.observe()) do print(f.family, #f.weights) end
globals/font/onDemand
font.onDemand() -> { any }
Every on-demand face font.declare declared, in declaration order, and
where each stands: declared (its file never read), requested (a
character it covers reached a text surface that could not draw it, and its
file is being read), installed (registered on every text surface; bytes
is the size of the file it installed from) or failed (error says why).
requestedBy is the character whose miss requested it, and family the
family group it installs into (opts.family of its font.declare, else
its own name); ui.listFonts() lists each face under that family's row.
Returns { any } — Array of { name, family, state, codepoints, requestedBy, bytes, error }.
for _, f in ipairs(font.onDemand()) do print(f.name, f.state, f.requestedBy) end
globals/font/parse
font.parse(bytes: buffer | string) -> string?
Parse a font file (TTF / OTF raw bytes) ONCE into the baked, vectorized
glyph format (ZFNT): per-glyph vector outlines + metrics + character map,
plus the original bytes. Heavy — run at import time (the .font assetType's
onCreate / the font importer), then store the result as the asset payload.
font.register loads it cheaply.
Parameters
bytesbuffer | string— Raw font-file bytes (binary-safe) — TTF / OTF.
Returns string? — Baked ZFNT payload (binary-safe string), or nil if the bytes don't parse as a font.
local zfnt = font.parse(assert(vfs.read("/zero/source/Inter.ttf")))
globals/font/prefetch
font.prefetch(text: string) -> number
Request, ahead of drawing it, every on-demand face covering a character
of text that no requested or installed face covers: a screen about to
show a script starts reading its faces before its first frame.
Parameters
textstring— Text whose characters the faces must cover.
Returns number — How many faces this call requested.
font.prefetch("日本語")
globals/font/ready
font.ready(text: string) -> boolean
Whether text draws without waiting on an on-demand face: no
character of it is covered by a face still declared or requested.
Call font.prefetch(text) first, then open the screen once this answers
true, and its first frame draws every glyph. True as well for a character
no declared face covers (ui.missingGlyphs() names it once drawn) and for
a face that failed (font.onDemand() says why).
Parameters
textstring— Text to be drawn.
Returns boolean — Whether every on-demand face the text needs is installed.
font.prefetch(title); task.spawn(function() while not font.ready(title) do task.wait() end; ui.showScreen("title") end)
globals/font/reconcile
font.reconcile() -> { any }
Every family the text shaper can resolve, held against what the shaper
does with it. family is the name, faces how many faces of it the font
database holds, weights the numeric weights those faces carry, loaded
whether it arrived through a registration rather than from the platform,
registered whether content registered the name, selectable whether some
style naming the family reaches it, matched whether fontFamily = family
on its own reaches it — the family name at the default weight over Latin
text — weight the weight it needs when the default is not it, shapedWith
the family the name selects, spelled the way the font file spells it, which
for a registration name is the family declared inside the file rather than
the name the asset carries, and reason why when the face that answered is
not that family. A
family is probed at its own weights and over content from several scripts,
so a family reachable only at one weight or covering only one script is
reported selectable, with matched false and weight naming what the style
must carry. Every probe object is destroyed again, so the live text-object
count is where it was.
Returns { any } — Array of { family, faces, weights, loaded, registered, selectable, matched, weight, shapedWith, reason }.
for _, f in ipairs(font.reconcile()) do if f.selectable and not f.matched then print(f.family, f.weight) end end
globals/font/register
font.register(name: string, zfnt: string, opts: table?) -> any
Register a baked font (ZFNT from font.parse) under name, making it
usable on every text surface via fontFamily = "<name>". Loads the
vectorized glyph data into the runtime store (for font.glyph /
font.textMesh) and feeds the embedded face to the 2D text and egui UI
systems. Raw font bytes (TTF / OTF) register too: their tables are read
and the glyph outlines are baked the first time font.glyph /
font.textMesh reads one. A name declared with font.declare is installed
by this call. Registering a name again replaces what the earlier call
installed under it: a name, group or slot the earlier call gave the face
and this one does not stops selecting it, and text laid out under a name
whose face changed is laid out again.
opts groups several weight/style faces under one CSS family and maps
web-font names onto it: opts.family is the shared group key, opts.role
is "regular" | "bold" | "italic" | "bolditalic", and opts.aliases is a
list of extra selectable names (web fonts + CSS generics like "Arial",
"sans-serif") that resolve to this group, matched case-insensitively.
With a group set, font-weight / font-style on a font-family pick the
real metric-compatible face instead of a synthesized one.
Parameters
namestring— Family name to register under.zfntstring— BakedZFNTpayload fromfont.parse(binary-safe string).optstable(optional) —{ family: string?, role: string?, aliases: {string}? }— group key, weight/style role, and case-insensitive selectable aliases.
Returns any — { family, faces, glyphCount } on success, or nil on failure.
local info = font.register("Inter", assert(font.parse(assert(vfs.read("/zero/source/Inter.ttf")))))
globals/font/textMesh
font.textMesh(name: string, text: string, opts: table?) -> any
Tessellate a string into renderable mesh geometry from a registered
font's glyph outlines — true 3D text, laid out left-to-right by advance
(newlines drop a line). Hand the result to renderer.mesh.create() (GPU)
or asset.create("mesh") (persistable).
Parameters
namestring— Registered family name.textstring— String to lay out.optstable(optional) —{ size?=1, depth?=0 (extrude, EM units), tolerance?=0.0015, letterSpacing?=0, lineHeight?=0 }.
Returns any — { positions, indices, normals, uvs } as flat float / u32 arrays, or nil if the font isn't registered or the string is all whitespace.
local geom = font.textMesh("Inter", "Hello", { size = 1, depth = 0.1 })
modules/font/coverage
coverage(bytes: buffer | string): string?
The codepoints a font file maps to a glyph, as the coverage text
font.declare reads: comma-separated inclusive hexadecimal ranges.
Parameters
bytesbuffer | string— Raw font-file bytes (TTF / OTF).
local cov = font.coverage(assert(vfs.read("/zero/source/fonts/MyCJK.otf")))
modules/font/declare
declare(name: string, coverage: string, opts: table): number
Declare an on-demand face: a face known by the codepoints it covers,
whose bytes are read only when a text surface first meets one of those
codepoints with no installed face that draws it. Until then the face costs
its coverage and nothing else, so a session that never shows its script
never reads its file. When a label, a text.* object, or font.prefetch
reaches a covered character, the engine requests the face: its file is read
and registered with font.register(name, bytes, opts), and every text
surface draws the character from it from then on. A .font asset whose
face.json lists onDemand chunks declares each of them through this call.
Parameters
namestring— The name the face is declared and registered under.coveragestring— The codepoints it covers: comma-separated inclusive hexadecimal ranges ("3040-30ff,4e00"), the textfont.coverage(bytes)writes.optstable—{ file: string, family: string?, role: string?, aliases: {string}? }naming the VFS path of the font file (TTF / OTF, or a baked ZFNT) and thefont.registeroptions it installs with.
font.declare("MyCJK", assert(font.coverage(assert(vfs.read("/zero/source/fonts/MyCJK.otf")))), { file = "/zero/source/fonts/MyCJK.otf" })
modules/font/glyph
glyph(name: string, codepoint: number): any
Read one glyph's vectorized outline from a registered font, in font
units (resolution-independent — scale by fontSize / unitsPerEm).
Parameters
namestring— Registered family name.codepointnumber— Unicode codepoint (e.g.string.byte("A")).
local g = font.glyph("Inter", string.byte("A"))
modules/font/list
list(): { string }
List every registered font family name.
for _, fam in font.list() do print(fam) end
modules/font/observe
observe(): { any }
What the text system is holding for fonts: one row per family the
shaper can resolve, with its face count, the numeric weights those faces
carry, whether any of them is slanted, and whether the family arrived
through a registration rather than from the platform. weights is what a
style's weight can name for that family. The same rows are fonts in
text.observe().
for _, f in ipairs(font.observe()) do print(f.family, #f.weights) end
modules/font/onDemand
onDemand(): { any }
Every on-demand face font.declare declared, in declaration order, and
where each stands: declared (its file never read), requested (a
character it covers reached a text surface that could not draw it, and its
file is being read), installed (registered on every text surface; bytes
is the size of the file it installed from) or failed (error says why).
requestedBy is the character whose miss requested it, and family the
family group it installs into (opts.family of its font.declare, else
its own name); ui.listFonts() lists each face under that family's row.
for _, f in ipairs(font.onDemand()) do print(f.name, f.state, f.requestedBy) end
modules/font/parse
parse(bytes: buffer | string): string?
Parse a font file (TTF / OTF raw bytes) ONCE into the baked, vectorized
glyph format (ZFNT): per-glyph vector outlines + metrics + character map,
plus the original bytes. Heavy — run at import time (the .font assetType's
onCreate / the font importer), then store the result as the asset payload.
font.register loads it cheaply.
Parameters
bytesbuffer | string— Raw font-file bytes (binary-safe) — TTF / OTF.
local zfnt = font.parse(assert(vfs.read("/zero/source/Inter.ttf")))
modules/font/prefetch
prefetch(text: string): number
Request, ahead of drawing it, every on-demand face covering a character
of text that no requested or installed face covers: a screen about to
show a script starts reading its faces before its first frame.
Parameters
textstring— Text whose characters the faces must cover.
font.prefetch("日本語")
modules/font/ready
ready(text: string): boolean
Whether text draws without waiting on an on-demand face: no
character of it is covered by a face still declared or requested.
Call font.prefetch(text) first, then open the screen once this answers
true, and its first frame draws every glyph. True as well for a character
no declared face covers (ui.missingGlyphs() names it once drawn) and for
a face that failed (font.onDemand() says why).
Parameters
textstring— Text to be drawn.
font.prefetch(title); task.spawn(function() while not font.ready(title) do task.wait() end; ui.showScreen("title") end)
modules/font/reconcile
reconcile(): { any }
Every family the text shaper can resolve, held against what the shaper
does with it. family is the name, faces how many faces of it the font
database holds, weights the numeric weights those faces carry, loaded
whether it arrived through a registration rather than from the platform,
registered whether content registered the name, selectable whether some
style naming the family reaches it, matched whether fontFamily = family
on its own reaches it — the family name at the default weight over Latin
text — weight the weight it needs when the default is not it, shapedWith
the family the name selects, spelled the way the font file spells it, which
for a registration name is the family declared inside the file rather than
the name the asset carries, and reason why when the face that answered is
not that family. A
family is probed at its own weights and over content from several scripts,
so a family reachable only at one weight or covering only one script is
reported selectable, with matched false and weight naming what the style
must carry. Every probe object is destroyed again, so the live text-object
count is where it was.
for _, f in ipairs(font.reconcile()) do if f.selectable and not f.matched then print(f.family, f.weight) end end
modules/font/register
register(name: string, zfnt: string, opts: table?): any
Register a baked font (ZFNT from font.parse) under name, making it
usable on every text surface via fontFamily = "<name>". Loads the
vectorized glyph data into the runtime store (for font.glyph /
font.textMesh) and feeds the embedded face to the 2D text and egui UI
systems. Raw font bytes (TTF / OTF) register too: their tables are read
and the glyph outlines are baked the first time font.glyph /
font.textMesh reads one. A name declared with font.declare is installed
by this call. Registering a name again replaces what the earlier call
installed under it: a name, group or slot the earlier call gave the face
and this one does not stops selecting it, and text laid out under a name
whose face changed is laid out again.
opts groups several weight/style faces under one CSS family and maps
web-font names onto it: opts.family is the shared group key, opts.role
is "regular" | "bold" | "italic" | "bolditalic", and opts.aliases is a
list of extra selectable names (web fonts + CSS generics like "Arial",
"sans-serif") that resolve to this group, matched case-insensitively.
With a group set, font-weight / font-style on a font-family pick the
real metric-compatible face instead of a synthesized one.
Parameters
namestring— Family name to register under.zfntstring— BakedZFNTpayload fromfont.parse(binary-safe string).optstable?(optional) —{ family: string?, role: string?, aliases: {string}? }— group key, weight/style role, and case-insensitive selectable aliases.
local info = font.register("Inter", assert(font.parse(assert(vfs.read("/zero/source/Inter.ttf")))))
modules/font/textMesh
textMesh(name: string, text: string, opts: table?): any
Tessellate a string into renderable mesh geometry from a registered
font's glyph outlines — true 3D text, laid out left-to-right by advance
(newlines drop a line). Hand the result to renderer.mesh.create() (GPU)
or asset.create("mesh") (persistable).
Parameters
namestring— Registered family name.textstring— String to lay out.optstable?(optional) —{ size?=1, depth?=0 (extrude, EM units), tolerance?=0.0015, letterSpacing?=0, lineHeight?=0 }.
local geom = font.textMesh("Inter", "Hello", { size = 1, depth = 0.1 })
typed/builtin//modules/api/engine/font/font/coverage
font.coverage(bytes: buffer | string) -> string?
The codepoints a font file maps to a glyph, as the coverage text
font.declare reads: comma-separated inclusive hexadecimal ranges.
Parameters
bytesbuffer | string— Raw font-file bytes (TTF / OTF).
Returns string? — Coverage text, or nil if the bytes don't parse as a font.
local cov = font.coverage(assert(vfs.read("/zero/source/fonts/MyCJK.otf")))
typed/builtin//modules/api/engine/font/font/declare
font.declare(name: string, coverage: string, opts: table) -> number
Declare an on-demand face: a face known by the codepoints it covers,
whose bytes are read only when a text surface first meets one of those
codepoints with no installed face that draws it. Until then the face costs
its coverage and nothing else, so a session that never shows its script
never reads its file. When a label, a text.* object, or font.prefetch
reaches a covered character, the engine requests the face: its file is read
and registered with font.register(name, bytes, opts), and every text
surface draws the character from it from then on. A .font asset whose
face.json lists onDemand chunks declares each of them through this call.
Parameters
namestring— The name the face is declared and registered under.coveragestring— The codepoints it covers: comma-separated inclusive hexadecimal ranges ("3040-30ff,4e00"), the textfont.coverage(bytes)writes.optstable—{ file: string, family: string?, role: string?, aliases: {string}? }naming the VFS path of the font file (TTF / OTF, or a baked ZFNT) and thefont.registeroptions it installs with.
Returns number — How many codepoints the coverage holds.
font.declare("MyCJK", assert(font.coverage(assert(vfs.read("/zero/source/fonts/MyCJK.otf")))), { file = "/zero/source/fonts/MyCJK.otf" })
typed/builtin//modules/api/engine/font/font/glyph
font.glyph(name: string, codepoint: number) -> any
Read one glyph's vectorized outline from a registered font, in font
units (resolution-independent — scale by fontSize / unitsPerEm).
Parameters
namestring— Registered family name.codepointnumber— Unicode codepoint (e.g.string.byte("A")).
Returns any — { advance, unitsPerEm, bbox = {xMin,yMin,xMax,yMax}, contours } where each contour is { start = {x,y}, segments = { {kind="line|quad|cubic", ...} } }, or nil if the font isn't registered.
local g = font.glyph("Inter", string.byte("A"))
typed/builtin//modules/api/engine/font/font/list
font.list() -> { string }
List every registered font family name.
Returns { string } — Array of family-name strings.
for _, fam in font.list() do print(fam) end
typed/builtin//modules/api/engine/font/font/observe
font.observe() -> { any }
What the text system is holding for fonts: one row per family the
shaper can resolve, with its face count, the numeric weights those faces
carry, whether any of them is slanted, and whether the family arrived
through a registration rather than from the platform. weights is what a
style's weight can name for that family. The same rows are fonts in
text.observe().
Returns { any } — Array of { family, faces, weights, italic, loaded }.
for _, f in ipairs(font.observe()) do print(f.family, #f.weights) end
typed/builtin//modules/api/engine/font/font/onDemand
font.onDemand() -> { any }
Every on-demand face font.declare declared, in declaration order, and
where each stands: declared (its file never read), requested (a
character it covers reached a text surface that could not draw it, and its
file is being read), installed (registered on every text surface; bytes
is the size of the file it installed from) or failed (error says why).
requestedBy is the character whose miss requested it, and family the
family group it installs into (opts.family of its font.declare, else
its own name); ui.listFonts() lists each face under that family's row.
Returns { any } — Array of { name, family, state, codepoints, requestedBy, bytes, error }.
for _, f in ipairs(font.onDemand()) do print(f.name, f.state, f.requestedBy) end
typed/builtin//modules/api/engine/font/font/parse
font.parse(bytes: buffer | string) -> string?
Parse a font file (TTF / OTF raw bytes) ONCE into the baked, vectorized
glyph format (ZFNT): per-glyph vector outlines + metrics + character map,
plus the original bytes. Heavy — run at import time (the .font assetType's
onCreate / the font importer), then store the result as the asset payload.
font.register loads it cheaply.
Parameters
bytesbuffer | string— Raw font-file bytes (binary-safe) — TTF / OTF.
Returns string? — Baked ZFNT payload (binary-safe string), or nil if the bytes don't parse as a font.
local zfnt = font.parse(assert(vfs.read("/zero/source/Inter.ttf")))
typed/builtin//modules/api/engine/font/font/prefetch
font.prefetch(text: string) -> number
Request, ahead of drawing it, every on-demand face covering a character
of text that no requested or installed face covers: a screen about to
show a script starts reading its faces before its first frame.
Parameters
textstring— Text whose characters the faces must cover.
Returns number — How many faces this call requested.
font.prefetch("日本語")
typed/builtin//modules/api/engine/font/font/ready
font.ready(text: string) -> boolean
Whether text draws without waiting on an on-demand face: no
character of it is covered by a face still declared or requested.
Call font.prefetch(text) first, then open the screen once this answers
true, and its first frame draws every glyph. True as well for a character
no declared face covers (ui.missingGlyphs() names it once drawn) and for
a face that failed (font.onDemand() says why).
Parameters
textstring— Text to be drawn.
Returns boolean — Whether every on-demand face the text needs is installed.
font.prefetch(title); task.spawn(function() while not font.ready(title) do task.wait() end; ui.showScreen("title") end)
typed/builtin//modules/api/engine/font/font/reconcile
font.reconcile() -> { any }
Every family the text shaper can resolve, held against what the shaper
does with it. family is the name, faces how many faces of it the font
database holds, weights the numeric weights those faces carry, loaded
whether it arrived through a registration rather than from the platform,
registered whether content registered the name, selectable whether some
style naming the family reaches it, matched whether fontFamily = family
on its own reaches it — the family name at the default weight over Latin
text — weight the weight it needs when the default is not it, shapedWith
the family the name selects, spelled the way the font file spells it, which
for a registration name is the family declared inside the file rather than
the name the asset carries, and reason why when the face that answered is
not that family. A
family is probed at its own weights and over content from several scripts,
so a family reachable only at one weight or covering only one script is
reported selectable, with matched false and weight naming what the style
must carry. Every probe object is destroyed again, so the live text-object
count is where it was.
Returns { any } — Array of { family, faces, weights, loaded, registered, selectable, matched, weight, shapedWith, reason }.
for _, f in ipairs(font.reconcile()) do if f.selectable and not f.matched then print(f.family, f.weight) end end
typed/builtin//modules/api/engine/font/font/register
font.register(name: string, zfnt: string, opts: table?) -> any
Register a baked font (ZFNT from font.parse) under name, making it
usable on every text surface via fontFamily = "<name>". Loads the
vectorized glyph data into the runtime store (for font.glyph /
font.textMesh) and feeds the embedded face to the 2D text and egui UI
systems. Raw font bytes (TTF / OTF) register too: their tables are read
and the glyph outlines are baked the first time font.glyph /
font.textMesh reads one. A name declared with font.declare is installed
by this call. Registering a name again replaces what the earlier call
installed under it: a name, group or slot the earlier call gave the face
and this one does not stops selecting it, and text laid out under a name
whose face changed is laid out again.
opts groups several weight/style faces under one CSS family and maps
web-font names onto it: opts.family is the shared group key, opts.role
is "regular" | "bold" | "italic" | "bolditalic", and opts.aliases is a
list of extra selectable names (web fonts + CSS generics like "Arial",
"sans-serif") that resolve to this group, matched case-insensitively.
With a group set, font-weight / font-style on a font-family pick the
real metric-compatible face instead of a synthesized one.
Parameters
namestring— Family name to register under.zfntstring— BakedZFNTpayload fromfont.parse(binary-safe string).optstable(optional) —{ family: string?, role: string?, aliases: {string}? }— group key, weight/style role, and case-insensitive selectable aliases.
Returns any — { family, faces, glyphCount } on success, or nil on failure.
local info = font.register("Inter", assert(font.parse(assert(vfs.read("/zero/source/Inter.ttf")))))
typed/builtin//modules/api/engine/font/font/textMesh
font.textMesh(name: string, text: string, opts: table?) -> any
Tessellate a string into renderable mesh geometry from a registered
font's glyph outlines — true 3D text, laid out left-to-right by advance
(newlines drop a line). Hand the result to renderer.mesh.create() (GPU)
or asset.create("mesh") (persistable).
Parameters
namestring— Registered family name.textstring— String to lay out.optstable(optional) —{ size?=1, depth?=0 (extrude, EM units), tolerance?=0.0015, letterSpacing?=0, lineHeight?=0 }.
Returns any — { positions, indices, normals, uvs } as flat float / u32 arrays, or nil if the font isn't registered or the string is all whitespace.
local geom = font.textMesh("Inter", "Hello", { size = 1, depth = 0.1 })