Brodgar client

Text rendering and fonts

Foundries, the named surfaces that bake them, and the custom-font paths.

What Where
Foundry (font+size+colour+aa) Text.Foundry; renderwrap builds a RichText.Foundry
Global default Text.std = new Foundry(sans,10) (public static final); Text.render(…) statics; Label default
Built-in fonts Text.sans/serif/mono/fraktur. Only two of the four name an AWT logical family. serif and mono ask for "Serif"/"Monospaced", which exist; sans asks for "Sans", which does not, so it resolves to Dialog and getFamily() on it is indistinguishable from a Dialog font’s. getName() keeps the string the Font was constructed with and survives every deriveFont, so it, not the family, is what tells the four apart in a font some site has already derived. fraktur is a resource font and answers its own family
Window titles Window.DefaultDeco.cf/ncf = new Text.Foundry(Text.fraktur,15).aa(true)
Speech bubbles Speaking — cached Text (ctor + update), frame measured from text.sz() in draw; stock = Text.std. draw blits the finished raster under g.chcolor(Color.BLACK), so any colour baked into it — the Foundry.fixcol a rule sets, or the Color passed to render — is multiplied to black on the way to the screen. A colour cannot reach this surface without moving that chcolor
Floating kin names NOT in the fork — published code in the ui/obj/buddy resource (haven.KinInfo is gone; OCache.OD_BUDDY commented -- Removed). Adopted with get-code → src/haven/res/ui/obj/buddy/{Buddy,Info,InfoPart}.java: shared foundry InfoPart.fnd + rendertext, composed Tex invalidated by Info.dirty()
Per-run markup ($font) RichText $font tag — resolves by AWT family name (TextAttribute.FAMILY); a custom TTF needs GraphicsEnvironment.registerFont at load
DPI sizing UI.scale(float) — every produced size passes through it
~81 baked Foundry sites across 36 files (Label, ChatUI, SListMenu, CharWnd, Button, FlowerMenu, …) — route per slice, never all at once
Custom-TTF load Font.createFont(TRUETYPE_FONT, file) (built-ins from Text.*); Resource.Font is the resource-backed path
Text → GPU Text.tex() lazily wraps the BufferedImage in a TexI and memoises it; Text.dispose() forwards to it. A Text is therefore two allocations: the AWT raster (kept) and the texture
The immediate-mode blit GOut.atext = Text.render → tex() → aimage → dispose(), all four per call, every frame; GOut.text is atext(…,0,0). Drop the dispose() and you have the Label pattern
TexI GL side st() uploads lazily on first render (so building a TexI is free of GL); dispose() drops the ColorTex and nulls it — but st() would silently re-upload, so a double-free shows up as a slow leak, never a crash. Size is Tex.nextp2-rounded (tdim): bytes = 4·nextp2(w)·nextp2(h), not 4·w·h
A surface that draws what the USER TYPED Fonts.enterTyped(scope) — enter plus a mark on that frame, and display refuses a marked frame outright, an entry under Fonts.EVERY included. It exists for the one site where the two halves want different answers: the chat’s quick line (ChatUI.drawsmall) is styled as chat, so it cannot declare textentry the way TextEntry.draw and ConsoleHost.drawcmd do. The mark is per frame — exit() takes it down with the scope it belongs to, and a scope entered inside a marked one is not itself marked
The composition scope Fonts.enter(scope)/Fonts.exit() push a per-thread stack a routed site declares itself with; Fonts.scope() is the innermost one else "default" (what the generic Text.render/RichText.render statics resolve), and Fonts.dynamic() is the innermost one, else "default" while a per-widget frame is open and null otherwise, which is the form Text.Foundry.resolved() and its RichText twin ask — that is how a foundry the fork cannot route, a .res’s own private one, still resolves, and how a per-widget frame reaches it with no second mechanism. enter early-outs while Fonts is inactive AND the stack is empty, so the skip can only be taken outermost and an unmatched exit() finds an empty stack
What a string is DISPLAYED as Fonts.display(scope, text), called from Text.Foundry.render(String, Color) after resolved() has delegated — the last thing that happens to a string before g.drawString. It walks a volatile Fonts.Catalogue[] (top first), so Text.text is the string that was drawn, not the one the site was handed. Fonts.isDisplayScope/displayScopes are which scopes may be keyed; Fonts.EVERY is "*"
…and its rich twin RichText.Foundry.render(Document, int), the same way and in the same place — what a RichText.Foundry site (chat, both tooltip flavours) goes through. What it asks about is Document.text, i.e. the marked-up source, because a run is a fragment nothing could be keyed on; so RichText.text is the document that was drawn. RichText.Foundry.nodisplay turns it off for one foundry and rides along to the twin resolved() derives
The wrapped path is displayed one level up Text.Foundry.renderwrap calls Fonts.display itself, before RichText.Parser.quote and before it wraps the string in $col[…]{…} — and marks its private wfnd nodisplay, since what that foundry would be asked about is the caption already quoted and wrapped. So a wrapped caption is keyed on the caption
Which sites declare a scope The Fonts.enter/exit pair, one per routed text surface: Window.DefaultDeco.checkcap (window.title), CharWnd.Heading + GridList.Group.rname + QuestWnd (heading, through Fonts.render(scope, furnace, text)), Widget.PaginaTip.get + Widget.KeyboundTip.get + UILoop’s plain-string tip + ItemInfo.longtip/shorttip/buildinfo + MenuGrid.PagButton.rendertt + Buff.shorttip + CharWnd.AttrWdg.info’s appended Name + Equipory.etttip + Fightsess.tooltip’s three + BAttrWnd.GlutMeter.tooltip + Makewindow.Spec.longtip’s pagina row + MiniMap.tooltip’s two + OptWnd.kbtt + SkillWnd.Skill/Credo/Experience.tooltip (tooltip — every tooltip in the client, which is what an item tip needs: its rows come from all over), FlowerMenu.Petal.render (menu), ChatUI.Channel.RenderedMessage.text (the line’s own Message.scope()), ChatUI.Selector.DarkChannel.rname and ChatUI.drawsmall’s quick line (chat, the last through enterTyped), Speaking.render (world.speech), res/ui/obj/buddy/InfoPart.rendertext (world.nick), TextEntry.draw and ConsoleHost.drawcmd (textentry, which display refuses), Button.render (button), Label.mktext (its own)
The scope is the SITE’s, not the foundry’s display reads Fonts.scope(), so a site that hands its foundry to someone else — a heading’s is a Supplier<Text.Furnace> four classes ask for — owes the pair even though its foundry already knows its scope. Fonts.render(scope, furnace, text) is that pair written once for the one-call case, and Fonts.render(scope, text, Color) the twin that resolves the foundry as well — the shape nearly every plain tooltip wants. Resolving a foundry at a scope is not declaring it: a site that did only the first drew in the right font and offered its string under default, which is the drift this pair of helpers exists to stop
The scope vocabulary Fonts.SCOPES — the names a stylesheet key may be a site key for, default being the selector *’s twin; Fonts.isScope is the test. Some (button, label, textentry, chat, menu) are also Selector.WIDGET_ROLES; the rest name a render site nothing is ever classified as (Selector.SITE_ROLES); and the widget roles window/inventory have no scope, so a bare window key is a tree key
What a site declares its own look to be Fonts.stock(scope, prop, value…) writes into the private Fonts.stocks map; Fonts.stockOf(scope) reads one site and Fonts.stocked() lists every scope that has declared anything, in SCOPES order. The property is spelled as a rule spells it ("font", "bg", "border", "padding", …) and the value is what the site holds — a Text.Foundry, a Color, a pair of Coords, one or more Fonts.Pieces. Idempotent: an unchanged declaration compares equal and writes nothing, so a site may declare from inside its own draw
Wrapping RichText.Foundry.render(text, width), in device pixels, and 0 means do not wrap — Text.Foundry.render lays a string on one line and nothing else, so a width forces the rich path (Text.Foundry.renderwrap reaches for the same one). layout splits any Part that overruns and TextPart.split keeps at least one character doing it (// addon:), because a head that is empty leaves an identical tail behind and layout never advances — an infinite loop that hangs the client on a width narrower than the first glyph, and on an $img wider than the box, where Part.split hands itself straight back. layout therefore splits each part once per line and lays an unsplittable one out overrunning, so a raster can come back wider than the width it was given. The two paths also measure a line differently — the stock one takes FontMetrics.getHeight(), the rich one the glyph bounds — so a wrapped line is a couple of pixels shorter than the same string unwrapped
Where COLOUR enters a render Three different layers — see the gotcha below. Text.Foundry.defcol (the default), the Color argument of render(text, c)/renderwrap (baked into the raster by g.setColor(c)), and a per-render TextAttribute.FOREGROUND extra on a RichText.Foundry.render(…) call

The furnace stack

A Text.Furnace is “render this string”; Text.Forge narrows it to one that also answers height(), strsize() and a Slug. Text.Foundry is the only leaf that owns a Font; everything else is a decorator — Text.OffsetForge wraps a backing Forge and post-processes the raster, reporting the room it took as tloff()/broff().

Decorator Does Costs
PUtils.TexFurn(bk, BufferedImage) PUtils.tilemod — tiles the image through the glyph raster in place, so the mask is what you see and the foundry’s colour is gone no growth (tloff/broff are Coord.z)
PUtils.BlurFurn(bk, grad, brad, Color) PUtils.blurmask2 — a coloured halo behind the glyphs grows the raster by grad + brad on every side, which is tloff/broff and moves what the site lays out around it
Utils.outline(img, col) / Utils.outline2(img, col) a hard one-pixel edge: outline answers the edge alone, a four-neighbour dilation of the mask alpha >= 250 painted in col; outline2 draws the source back over it at (1, 1). Neither is a Furnace — they take a finished BufferedImage, so a caller wraps its own Text grows the raster by 1 on every side (imgsz.add(2, 2)); no tloff/broff exists to report it, so the caller owns the shift

The two are always stacked the same way — BlurFurn(TexFurn(foundry, tex), …) — at four places, and each rebuilds its statics on a Fonts.gen() compare. The blur’s grad/brad/col are per site and none of them is shared:

Site Stock foundry Texture Blur
Window.DefaultDeco.checktitlefont → cf, ncf DefaultDeco.titlefnd (fraktur 15) Window.ctex = Resource.loadsimg("gfx/hud/fonttex") UI.rscale(0.75), UI.rscale(1.0); Color(96,96,0) focused, Color.BLACK not
CharWnd.checkcapfont → bcatf, bfailf CharWnd.capfnd (fraktur 25) Window.ctex, and gfx/hud/fontred for the failed twin UI.scale(3), UI.scale(2), Color(96,48,0)
GridList.dcatfont → bdcatf GridList.dcatfnd (fraktur 18) Window.ctex 2, 1, Color(96,48,0) — unscaled literals, unlike the three above
Button.checkfont → bnf Button.tf (bold serif 12) Window.ctex UI.rscale(0.75) twice, Color(80,40,0)

Charlist, Fightsess, MapView and QuestWnd build furnaces of their own from the same two classes and are not routed.

Gotchas