Gui Helper
A Fabric 1.20.6 framework for building Minecraft GUIs from a single Gui object that holds every object and its data.
You describe a screen once, register it under an Identifier, and the server opens it by that id. Values come back the same way.
Quick start
// client — build and register
Gui shop = GuiRegistry.register(GuiHelper.id("shop"), new Gui()
.setTitle(Text.literal("Shop"))
.setBackground(new Texture(GuiHelper.id("textures/gui/shop.png"), 0, 0, 176, 166, 256, 256))
.setDarkBackground(true));
shop.addObject(new TextObject("price", new TextWithDetail(Text.literal("0"), ColorHelper.WHITE, 0.5f),
8, 20, 60, 10, Align.CENTER));
// server — open it
GuiNetwork.open(player, GuiHelper.id("shop"), new GuiData().set("price", 2000));
Every object takes a String id first. That id is the key for lookup, for server-sent data, and for values sent back — so it must be unique within a Gui (addObject throws otherwise).
Objects
All extend GuiObject. Coordinates are relative to the GUI's top-left corner.
| Object | Purpose | Value type |
|---|---|---|
TextObject |
Text, with Align / VerticalAlign inside a box |
— |
TextureObject |
A texture | — |
ItemRenderObject |
An ItemStack with count, optional vanilla item tooltip |
— |
ModelItemRenderObject |
An item's 3D model, spinning, with a fixed tilt | — |
EntityGuiObject |
A live entity, scaled to fit, optionally cursor-tracking | — |
ButtonObject |
Clickable, Runnable onPress |
— |
ToggleObject |
On/off, two textures | boolean |
RadioButtonObject |
One group, exclusive options | String |
StepperObject |
Arrows and ticks to pick a step | int |
DropdownObject |
Vector-drawn expanding list, wheel or draggable indicator, optional search | String |
TextFieldObject |
Text input | String |
IntegerFieldObject / DecimalFieldObject |
Validated numeric input, optional setRange(min, max) |
String (getIntValue() / getNumber()) |
PercentFieldObject |
Decimal 0–100, drawn as 65 % |
String (getNumber()) |
TextAreaObject |
Multi-line text input | String |
BoxObject |
Container, clips and scrolls its children | — |
ScrollBarObject |
Bar owned by a BoxObject |
— |
SlotGridObject |
Client proxy placing a server slot grid inside a BoxObject |
— |
Shared by every object
object.setLabel(new TextWithDetail(Text.literal("Name"), 0.5f)); // drawn above, left-aligned
object.setTooltip(Text.literal("Line one"), Text.literal("Line two"));
object.setShowWhen(toggle::getValue); // re-evaluated every frame
object.setDisabledWhen(() -> !toggle.getValue()); // 50% opacity, input blocked
setShowWhen / setDisabledWhen return GuiObject, so call them last in a chain.
Dropdown search
dropdown.setSearchable(true).setSearchPlaceholder(Text.literal("Search..."));
A search row filters options by label (case-insensitive contains); Enter picks the first match. Cleared on close.
Reading values
gui.getObject("sound", ToggleObject.class).getValue();
gui.collectValues(); // GuiData of every stateful object, boxes included
getObject recurses into boxes.
State
Values live on the object, not the widget, which decides when they reset:
| Event | Result |
|---|---|
| Window resize | Preserved — widgets rebuild and re-read the object |
| Reopening the GUI | Reset to defaults |
A Gui is a singleton once registered, so resetState() runs from the GuiRenderer constructor — one per screen instance, not per init(). Set your own baseline with setDefaultValue(...).
Data flow
GuiData is a String -> String map with typed accessors, used in both directions.
// server -> client, on open
GuiNetwork.open(player, id, new GuiData().set("price", 2000).set("sound", true));
// client -> server, on demand
ClientPlayNetworking.send(new GuiValuesPayload(id, gui.collectValues()));
An object picks up the entry matching its own id via applyData, and writes it back via collectData. ItemRenderObject and ModelItemRenderObject also read <id>.count.
Defaults are applied first, then server data — so an id absent from the payload leaves the default alone.
Receiving values
GuiValuesRegistry.register(GuiHelper.id("shop"), (player, values) -> {
// runs on the server thread
});
The receiver rejects a payload unless the player has that GUI open (GuiSessions) and a listener is registered. The wire format is capped at 64 entries, 64-char keys, 256-char values.
GuiData is untrusted. The server cannot validate ids or types — Gui is client-only — so re-check everything against server state.
Handler GUIs (inventories)
Slots are the sync protocol: index, order and count must match on both sides. So the layout is common (src/main) and the look is client (src/client), keyed by the same Identifier.
// common — onInitialize, both sides
GuiHandlerRegistry.register(GuiHelper.id("forge"), new GuiHandler()
.setTitle(Text.literal("Forge"))
.addSlotGrid("input", 0, 8, 18, 3, 3) // idPrefix, startIndex, x, y, columns, rows
.addSlot(new SlotObject("output", 9, 120, 35))
.setPlayerInventory(8, 84));
// client — a Gui under the same id
GuiRegistry.register(GuiHelper.id("forge"), buildForgeGui());
// server
GuiNetwork.openHandler(player, GuiHelper.id("forge"), data, inventory);
getInventorySize() is derived from the highest slot index, so size is never declared twice. To decorate slots, read their positions back rather than repeating them:
for (SlotObject slot : GuiHandlerRegistry.get(id).getSlots()) {
gui.addObject(new TextureObject(slot.getId(), Textures.TEXTURE_SLOT,
slot.getObjectStartX() - 1, slot.getObjectStartY() - 1));
}
Slots inside a box
// common: the grid's coordinates only define its shape
new GuiHandler().addSlotGrid("storage", 9, 0, 0, 3, 6);
// client: id = the grid prefix
box.addObject(new SlotGridObject("storage", GuiHelper.id("forge"), 0, 0));
The server keeps the slots and the sync; the client places them. Scrolling is smooth — partly visible slots are clipped and only their visible part takes hover and clicks (HandledScreenMixin + an access widener making Slot.x/y mutable). box.setScrollSnap(18) scrolls row by row instead.
One ExtendedScreenHandlerType<CustomScreenHandler, GuiOpenData> carries the id and data in the vanilla open packet, so handlers can be registered at any time.
Textures
new Texture(id, drawStartX, drawStartY, drawSizeX, drawSizeY, textureSizeX, textureSizeY)
drawStart + drawSize select a region of a sheet; textureSize is the file's dimensions. The region stretches to whatever size the object declares.
Nine-slice keeps corners crisp at any size — it works on sub-regions, unlike vanilla's GUI sprite atlas:
new Texture(...).setNineSlice(3); // all sides
new Texture(...).setNineSlice(6, 3, 6, 3); // left, top, right, bottom
Scaling
gui.setSizeMultiplicator(2) renders everything twice as big. Mouse coordinates are divided to match, and slot hit-testing follows. Tooltips stay at normal size at the real cursor.
Overlays
A DropdownObject's open list floats above everything and takes input first — clicks, wheel and ESC — so a widget underneath can't steal a click or show its tooltip. Implement OverlayRenderer for anything else that needs to escape draw order and clipping; BoxWidget forwards all five members with its scroll offset, so overlays work inside boxes.
Debug
GuiHelper.DEBUG follows FabricLoader.isDevelopmentEnvironment(), so these exist under runClient and not in a built jar:
| Command | Opens |
|---|---|
/testGui [id] |
a registered Gui as a plain screen (default debug) |
/testHandler [id] |
a registered GuiHandler (default debug_handler, tab-completes) |
Known limits
- A registered
Guiis a shared instance — one screen at a time on the client, which is fine, but it is not re-entrant. SlotObject.backgroundSpritegoes throughGuiSlot, so the identifier must be a sprite stitched into the block atlas, not a loosetextures/gui/*.png.- Shift-click can insert into slots scrolled out of a box;
setDisabledWhenon aSlotGridObjectonly dims it — lock slots server-side. SlotGridObjectonly works in a handler GUI, and only claims slots named<id>_<digits>.- The dropdown search text uses the first labelled option's style and needs rows at least 9px tall.
GuiObjectstill carriestexture/text/onPressfields that most subclasses leave null.

