349. A face an application ships is found after the bundled ones
Date: 2026-09-17
Status
Accepted. Closes docs/gaps.md G39. Extends
ADR-0066 and
ADR-0323.
Context
An application’s landing page sets display text in Forum and running text in
Golos Text. Its desktop client should use the same two faces. A stylesheet could
name only the faces BundledFont enumerates, so font-family: Forum resolved to
nothing and the title was drawn in Inter.
A canvas could open the bytes with Font.of(byte[], size) and draw a title
itself. That title would not select, wrap or follow font-size like every other
label, which makes it a second text stack. A face has to reach four places
together: the cascade, Paragraph layout, the text-input caret and the glyph
cache. All four already go through one Fonts book, so the book is where a face
has to be added.
Decision
FontSource describes a shipped face. Application.fonts() lists them, and
Fonts.bundled(List<FontSource>) searches them after the bundled faces.
assets.Faceis a sealed interface overBundledFontandFontSource: a family, one of the two weights, and upright or italic.Face.matchis the matching ruleBundledFont.ofalready had (family, then style, then weight, then the family’s upright regular), moved so that both kinds share it. With two copies, a shipped family would fall back differently from Inter the first time one of them changed.- The matrix stays closed. A shipped face is one of the two weights and one of the two styles. §1.4 ships two weights, and Principle 3 applies to an application’s faces too.
- Bundled first.
Fonts.of(Typography)asksBundledFont.ofand only then the shipped list, so a file an application callsInteris never drawn. It is logged when the book is opened, rather than refused, because the stylesheet still gets the face it named. Two sources for the same corner are refused, since one of them could never be drawn and nothing could say which. - Lazy, and forgiving at draw time. The bytes are a
Supplier<byte[]>, read the first time the face is asked for.FontSource.resource(…)reads a resource beside a class, the wayStylesheet.resourcedoes. A source that cannot be read or parsed is logged once, remembered as unusable and drawn in the UI face. That happens inside a render pass, where a thrown exception would mean a window with no text. - Read once. The launcher opens its book with
application.fonts()beforestart, like the stylesheets.Offscreen.fonts(List<FontSource>)does the same for a render, so a render test paints what the window paints.
Consequences
font-family: Forumworks everywhere a bundled family does: in labels, fields, the caret,canvaspainters (throughCanvasStyle.font()),markdown-viewand offscreen renders.BundledFont.ofkeeps its signature and its answers.BundledFontTest,ItalicFaceTestandFontsTestpass unchanged.- There is no markup form. Faces belong to the application, like its stylesheets.
- Font licences are the application’s to ship. The toolkit’s
THIRD-PARTY-NOTICES.mdcovers only what the toolkit bundles.