ellipog

The client toolkit

Minecraft 1.21.1Fabric + NeoForgeArmature 0.2.0

Armature's other half is a UI toolkit, and it is built on one decision: every draw call goes through one interface. GuiRenderer knows about fills, text, item icons, player faces, blur and scissor rectangles — and the only game types it names are the item stack and the texture id a caller already holds. render/GuiGraphicsRenderer is its 1.21.1 implementation, and no drawing call in the toolkit names the graphics context: the controls are ordinary AbstractWidgets, for the input and narration the game already gives them, and what they draw they draw through the seam.

That is what makes the toolkit testable at all: the layout engine, the text models and the shapes are arithmetic over rectangles and strings, so they are asserted in milliseconds without a client, a font or a window. It is also the version seamSeamThe one boundary where two halves of a system meet so neither has to know the other. Armature has two: the loader seam, which is the platform layer over Fabric and NeoForge, and the renderer seam, which keeps the whole toolkit free of game classes. — the 26.x port swaps the implementation and the toolkit above it does not move.

protected void renderContent(GuiRenderer r, int mouseX, int mouseY, float partialTick) {
    r.fill(x, y, right, bottom, ArmatureTheme.panel());
    r.text("Hello", x + 8, y + 6, ArmatureTheme.body());
    r.styledText(List.of(new GuiRenderer.StyledRun("bold", true, false, false)), x + 8, y + 22, ink);
    r.icon(stack, iconX, iconY, 16);
}

Drawing, and the one thing to know about it#

GuiRenderer has fill, text, shadowedText (at a scale), styledText (runs of bold/italic/underline, each at a scale) with styledWidth, centredText, textWidth, lineHeight, icon(ItemStack, …) and face(UUID, …) (each answering whether it drew), texture/scaled/sprite stamps with textureSize for a file's own dimensions (textureStamp when only the handle is wanted), turned (a rotated region, closed like a clip), blur, and clip(left, top, right, bottom) — which returns a Scoped that closes the clip again, so a scissor cannot be left open by a return taken early. clip also takes a layout Slot or a Viewport, for the region a caller already holds.

The other half of the seam is batched(...): a region of drawing that submits once instead of once per fill, because the unmanaged screen path flushes per fill. It also has flush() for the caller that has to draw something in the middle.

Every colour a widget asks for comes from ArmatureTheme.current() — a button does not know what a theme is, it asks for body and gets whatever the current theme says. Themes is the page about that.

The packages#

PackageWhat it holdsPage
ui.kitLayout and scrolling, the text models, and the drawing vocabularyLayout and scrolling, Text models
ui.shapeShape: an outline as spans per row, so drawing and hit-testing agreeShapes
ui (themes)Theme, the theme files, Look, and the static paletteThemes
widgetsArmatureButton, the text-field widgetsWidgetA control the game's own machinery can focus, narrate, tab to and hover. Armature's buttons and text fields are ordinary widgets that draw through the renderer seam.See also Seam, ArmatureScreen, ArmatureLiveWidgets and screens
ui.inspectThe machinery behind a property editor, and ui.party's roster modelProperty panels

The smaller pieces in ui.kit, each written once so a screen does not carry its own copy:

RoundedRectA rounded rectangle as a span table — the horizontal extent of each row.
NineSliceOne source image drawn into a box of any size without its corners stretching.
OutlineA collapsible tree of keyed nodes — each with a parent and a depth — and which rows that makes visible.
ColourARGB arithmetic: blending, alpha scaling, taking a colour apart. Eight digits, always.
Easing, Tween, MotionThe curve of an animation over time; one value moving towards a target; and whether this client animates at all.
HoverWhich of a set of things the pointer is over, eased — exactly one at a time.
InsetsSpace around something: padding when inside, margin when outside.
MeasureThe only thing a layout needs to know about a font.
TextWrap, Codepoints, TextHistoryWrapping a paragraph to a column; what counts as one character to a caret; and a field's undo history.
DeclaredPathsHow a folder that declares its own children resolves them — one bare segment, resolved inward.
Support the workko-fi.com/ellipog