-
Notifications
You must be signed in to change notification settings - Fork 1
CatFrame UI Components
This guide is aimed at mod developers: you want to build GUIs with CatFrame's UI library in your own mod — read this. All classes live in the
decok.dfcdvadstf.catframe.uipackage (and its subpackages).
In one sentence: it brings the modern (1.20+) UI programming model back to 1.7.10. You write screens the same way you would in a modern Minecraft mod:
- Extend
ui.screens.Screen(the modern-version counterpart ofGuiScreen), then register widgets withaddRenderableWidget(...)insideinit(); - The component tree is built from
AbstractComponent/GuiEventListener, with rendering funneled throughextractRenderState; - It ships with a full set of facilities: layout system (Linear/Grid/Frame/HeaderFooter), tab system, toasts, overlays, and nine-patch texture stretching (
TextureStretching).
Package map at a glance:
| Package | Contents |
|---|---|
ui.screens |
Screen base class (modern-version counterpart of GuiScreen) |
ui.components |
Concrete widgets: Button, CyclingButton, Checkbox, ToggleButton, ImageWidget, StringWidget, SimpleEditBox, WaitingPanel, TabButton, etc. |
ui.components.tab |
Tab system: Tab, TabRegistry, TabManager, TabBar, AbstractScreenTab, GridLayoutTab, LoadingTab
|
ui.components.toast |
Toast system: ToastManager, SimpleToast, SystemToast, ItemToast, BaseToast
|
ui.layouts |
Layouts: LinearLayout, GridLayout, FrameLayout, HeaderFooterLayout, EqualSpacingLayout, SimpleLayout, etc. |
ui.overlay |
Overlays: OverlayManager, Overlay, ScreenAnchor
|
ui.util |
Utilities: TextureStretching (nine-patch/tiling), TextureStretchingMetadata (mcmeta-driven) |
ui root |
Text/Style text components, GuiGraphicsExtractor (GUI item rendering), GuiDrawing (rect drawing), LoadingDotsText, ContentPanelRenderer (panel backgrounds), ActionBar/Title (HUD text facades) |
Three steps: extend Screen → add widgets in init() → open it.
public class MyScreen extends Screen {
public MyScreen() {
super(Text.literal("My Screen")); // title is required
}
@Override
protected void init() {
// Build the widget tree here (called on every open / resize)
addRenderableWidget(Button.builder(Text.literal("Click me"),
btn -> System.out.println("Clicked!"))
.pos(100, 100)
.build());
}
@Override
public void tick() {
// Per-tick logic (optional)
}
@Override
public void removed() {
// Cleanup when the screen closes (optional)
}
}Open the screen (from any client-side code):
Minecraft.getMinecraft().displayGuiScreen(new MyScreen());| Method | Rendered | Events/Focus | Use for |
|---|---|---|---|
addRenderableWidget(w) |
✅ | ✅ | Most widgets |
addWidget(w) |
❌ | ✅ | Widget renders itself elsewhere (e.g. managed by a layout) |
addRenderableOnly(w) |
✅ | ❌ | Pure decoration, no interaction |
-
initGui()is the vanilla hook (fires on first open and every resize); the base class already implements it: clear widgets → callinit()→ set initial focus. Subclasses always overrideinit(), neverinitGui(). -
updateScreen()→tick();onGuiClosed()→removed(). Same semantics as the modern version. - The base class also provides
getMinecraft()andgetFont()convenience getters.
All widget labels use ui.Text (counterpart of the modern Component); styled text uses ui.Style.
// Literal
Text.literal("Hello");
Text.literal("Red text", Style.EMPTY.withColor(0xFF5555).withBold(true));
// Translatable
Text.translatable("my.mod.gui.title");
Text.translatable("my.mod.gui.count", 42); // with %s arguments
// Raw JSON text component (interoperable with vanilla chat components)
Text.fromJson("{\"text\":\"Hi\",\"color\":\"gold\"}");
// Concatenation (siblings)
Text.literal("A ").append(Text.translatable("key"));Common Style chainable methods: withColor(int), withBold(boolean), withItalic(boolean), withClickEvent(...), etc. Get the final string with text.getString() (it concatenates all siblings).
Every widget extends AbstractComponent (→ GuiEventListener) and inherits a common set of properties:
comp.setX(x); comp.setY(y); comp.setSize(w, h);
comp.setVisible(false); // hide
comp.setActive(false); // disable interaction (no events)
comp.setAlpha(0.5F); // transparency
comp.setTooltip(Tooltip.create("Hover tooltip")); // built-in tooltip
comp.setTooltipDelay(500); // delay in ms, default 0Button.builder(Text.literal("OK"), btn -> { /* click callback */ })
.pos(10, 20) // optional, default (0,0)
.width(100) // optional, default DEFAULT_WIDTH=150
.height(20) // optional, default DEFAULT_HEIGHT=20
.useVanillaTexture() // optional: use vanilla widgets.png texture
.build();- Width constants:
Button.SMALL_WIDTH(120) /DEFAULT_WIDTH(150) /BIG_WIDTH(200). - Default texture is CatFrame's tri-state stretched texture (normal/hover/pressed, auto nine-patch);
useVanillaTexture()falls back to the vanilla look. - Text color constants:
TEXT_COLOR_ENABLED(0xE0E0E0),TEXT_COLOR_DISABLED(0xA0A0A0),TEXT_COLOR_HOVER(0xFFFFA0). Pressing plays the vanillagui.button.presssound.
CyclingButton<Boolean> onOff = CyclingButton.onOffBuilder()
.initially(true)
.build(10, 20, 80, 20, (btn, value) -> {
// value-change callback
});
CyclingButton<String> mode = CyclingButton.builder(value -> Text.literal("Mode: " + value))
.values("Easy", "Normal", "Hard")
.initially("Normal")
.label(Text.literal("Difficulty")) // optional: fixed text left of the button
.build(10, 50, 120, 20, (btn, value) -> { });- Interaction: click advances to the next value; mouse wheel up = previous, down = next (matching the modern version).
- Feed values via
values(T...)/values(Collection<T>)/values(Values<T>);Values<T>is a dynamic value-source interface (getCurrent()/getDefaults()) for value lists that change at runtime. - Read the current value:
btn.getValue()/btn.setValue(v)/btn.getDisplayText().
Checkbox.builder(Text.literal("Accept terms"))
.pos(10, 80)
.maxWidth(200) // optional, text wrap limit
.selected(true) // initially checked
.onValueChange(cb -> { }) // change callback
.tooltip(Tooltip.create("Required to continue"))
.build();Internally a 16×16 four-state texture (normal/hover/checked/checked-hover), with the label laid out to the right automatically. At runtime: isSelected() / setSelected(boolean).
new ToggleButton(10, 110, 32, 16, Text.literal("Auto"), true,
(btn, selected) -> { /* selected is the new state */ });A 32×16 toggle switch (16×8 source texture scaled 2×), label drawn to the right. Texture: toggle_swticher (note: that is CatFrame's actual spelling).
// Variant A: texture — scales by integer multiples only (1x/2x/3x...);
// violating this throws IllegalArgumentException
addRenderableWidget(ImageWidget.texture(32, 32,
new ResourceLocation("my_mod", "textures/gui/icon.png"), 16, 16));
// Variant B: sprite — stretches freely to the target size
addRenderableWidget(ImageWidget.sprite(40, 25, mySprite));-
ImageWidget.texture(w, h, tex, texW, texH):texW/texHis the logical source size inside the texture; target w/h must be an integer multiple of it, otherwise it throws (to prevent blur). -
ImageWidget.sprite(w, h, sprite): pass aTextureAtlasSprite, stretch it to any size. -
updateResource(...)swaps the texture at runtime. NoteisActive()is alwaysfalse(pure display widget).
StringWidget label = new StringWidget(Text.literal("HP: 100"), 0xFFFFFF);
label.setColor(0xFF5555); // change color
label.setShadow(false); // disable shadow
label.setText(Text.translatable("my.key"));Renders one line of text, receives no events. For multi-line, place several or embed \n in the Text.
SimpleEditBox box = new SimpleEditBox(10, 140, 150, 20);
box.setHint("Enter a name"); // grey hint when empty
box.setMaxLength(32); // max character count
box.setText("default"); // read back with getText()-
SimpleEditBoxuses the vanilla texture by default (setUseVanillaTexture(true)is already called in its constructor). - The underlying
AbstractEditBoxalso supportssetForceVerticalCursor(boolean)(cursor rendering style) andsetMessage(Text).
WaitingPanel panel = new WaitingPanel(Text.literal("Loading..."));
// or new WaitingPanel("string form works too")
addRenderableWidget(panel);A full-screen semi-transparent overlay + centered panel + three-dot animation (LoadingDotsText). It swallows all mouse clicks, keyboard input and scrolling (isMouseOver is always true) — effectively a modal blocker. Great for world switching or resource loading. Once your tick() condition is met, setVisible(false) or remove it from the Screen.
Tooltip tip = Tooltip.create("A tooltip string");
// Styled variant:
Tooltip.create("Tooltip", Optional.of(myTooltipComponent), new ResourceLocation("my_mod", "textures/gui/tooltip.png"));- Usually you don't construct these yourself — just attach one via
comp.setTooltip(tip)to anyAbstractComponent(rendering and delay are handled internally). -
getLines(mc)caches per-language line breaking (MAX_WIDTH=170).
Extend AbstractComponent and override renderWidget (the new rendering hook):
public class MyWidget extends AbstractComponent {
public MyWidget(int x, int y, int w, int h) {
this.x = x; this.y = y; this.width = w; this.height = h;
}
@Override
protected void renderWidget(GuiGraphicsExtractor gui, int mouseX, int mouseY, float partialTicks) {
GuiDrawing.drawRect(x, y, x + width, y + height, 0xFF00FF00);
}
}-
renderWidgetis the recommended entry point; the legacyrender(...)hook is deprecated, and the base class bridges to it by default — writerenderWidgetin new code. - State fields are all there:
visible/active/alpha/isHovered/focused; the baseextractRenderStatetemplate method already handles "skip when invisible + update hover state", so you only draw. - For a widget that greys out with an inactive message when disabled, extend the inner class
AbstractComponent.WithInactiveMessage(providesdefaultInactiveMessage).
Layouts live in the ui.layouts package. A layout is not a widget — it receives no events. You add widgets to a layout, the layout computes positions, and finally you register the layout itself as a child of the Screen (layouts implement ILayout, and the layout renders its children internally).
layout.setPadding(6); // padding, default 4
layout.setSpacing(4); // child spacing, default 2
layout.add(child); // default settings
layout.add(child, LayoutSettings.defaults().padding(5).align(0.5F, 0.5F));
layout.add(child, s -> s.paddingHorizontal(8).alignHorizontallyCenter()); // lambda form
layout.getChildren(); // read-only list
layout.clear(); // clear allLayoutSettings (one per child) supports:
-
padding(...):padding(5)/padding(h, v)/padding(l, t, r, b)/paddingLeft()etc. -
align(x, y): floats from 0.0 to 1.0; plus semantic shortcuts likealignHorizontallyCenter(),alignVerticallyMiddle() -
copy()for an independent copy
LinearLayout list = new LinearLayout(); // vertical by default
LinearLayout row = new LinearLayout(LinearLayout.Axis.HORIZONTAL);
LinearLayout col = new LinearLayout(LinearLayout.Axis.VERTICAL, LinearLayout.Alignment.CENTER);
row.addChild(btn1);
row.addChild(btn2, s -> s.alignVerticallyMiddle());
row.addChild(new SpacerElement.width(20)); // spacer to push things apart-
Axis:HORIZONTAL/VERTICAL;Alignment:START/CENTER/END/FILL(on the axis perpendicular to the layout direction). - Two convenience subclasses:
HorizontalLayout(horizontal + vertical centering by default) andVerticalLayout(vertical + horizontal centering by default). - There is also
EqualSpacingLayout: instead of a fixed gap, it distributes children evenly across the available width/height (new EqualSpacingLayout()horizontal,new EqualSpacingLayout(Axis.VERTICAL)vertical).
GridLayout grid = new GridLayout();
grid.addChild(btn1, 0, 0); // place at (row 0, column 0)
grid.addChild(btn2, 0, 1);
grid.addChild(bigBtn, 1, 0, 2, 2); // span 2 rows and 2 columns
grid.addChild(btn3); // no coords → fill sequentially
grid.setRowSpacing(4); grid.setColumnSpacing(4);
grid.setSpacing(4); // set both at once
// Row-wise fill mode
GridLayout.RowHelper helper = grid.createRowHelper(3); // 3 per row
helper.addChild(a); helper.addChild(b); helper.addChild(c); // auto wrapsFull addChild signature: addChild(child, row, column) or addChild(child, row, column, rows, columns), with an optional trailing LayoutSettings/lambda.
FrameLayout frame = new FrameLayout(200, 100); // minimum size
frame.addChild(background); // centered by default (0.5, 0.5)
frame.addChild(icon, s -> s.align(0.0F, 0.0F)); // top-left
frame.addChild(title, s -> s.alignHorizontallyCenter().alignVerticallyTop());- All children stack in the same area, each positioned by its own
align. - Two very handy static methods (no layout instance needed):
FrameLayout.centerInRectangle(widget, x, y, w, h); // center FrameLayout.alignInRectangle(widget, x, y, w, h, 0.5F, 0.16666667F); // custom ratio
HeaderFooterLayout hfl = new HeaderFooterLayout(); // default header height 30
HeaderFooterLayout hfl2 = new HeaderFooterLayout(40, 20); // header 40 / footer 20
HeaderFooterLayout hfl3 = new HeaderFooterLayout(false); // don't draw the panel background
hfl.setHeader(headerWidget); // returns the child, chainable
hfl.setContent(contentWidget);
hfl.setFooter(footerWidget);
hfl.addToHeader(...); hfl.addToContents(...); hfl.addToFooter(...);Note: draw() is currently a no-op (pure layout container) — backgrounds/separators are up to you, using ContentPanelRenderer or GuiDrawing (see section 8).
SimpleLayout sl = new SimpleLayout(8); // computes bounding box + padding only, no rearrangement
sl.add(widget1); // you set widget1's position yourselfFor "I position every child myself" scenarios. Also, SpacerElement (new SpacerElement(w, h) / SpacerElement.width(w) / SpacerElement.height(h)) is a pure placeholder for reserving gaps inside layouts.
CatFrame's tab system mirrors the modern TabNavigationBar flow: register (TabRegistry) → assemble (TabManager) → render the nav bar (TabBar). Tabs manage their own content; TabManager adds/removes widgets on switch.
// At some client-init stage (e.g. in FMLInitializationEvent):
TabRegistry.registerTab("my_mod_bars", // barId: unique id of the tab bar
() -> new MySettingsTab(), // Tab factory (Supplier)
103, // tabId: unique within the bar
"my_mod.tab.settings"); // title: lang key or TextOverloads include registerTab(barId, factory, tabId, Text) and an int priority variant (sort weight, smaller = earlier), 4 in total.
Rules:
- Duplicate
tabIdwithin the samebarIdthrowsIllegalArgumentException. -
The registration window is finite: the first
TabManagerconstructed for a bar "freezes" it; registering into a frozen bar throwsIllegalStateException. So put registration code before any TabManager is created. -
tabIdis conventionally expected to start at 103 (0–102 is reserved by vanilla/usage).
The easiest path is extending AbstractScreenTab:
public class MySettingsTab extends AbstractScreenTab {
public MySettingsTab() {
super(103, "my_mod.tab.settings"); // (tabId, lang key) or (tabId, Text)
}
@Override
public void initGui(TabManager tabManager, int width, int height) {
// Add your widgets here (rebuilt on every switch to this tab)
addButton(new GuiButton(0, 10, 10, "vanilla buttons work too")); // vanilla GuiButton
addWidget(new MyCatFrameWidget()); // CatFrame widget
addComponent(someGuiEventListener); // any GuiEventListener
}
}- Three add methods:
addButton(GuiButton)(vanilla buttons, handled uniformly by TabManager),addWidget(Object),addComponent(GuiEventListener). - You may also override
drawScreen/actionPerformed/mouseClicked/keyTypedfor custom behavior. - Custom tab texture:
setTabTexture(ResourceLocation)(single texture) orsetTabTextures(TabTextures)(four states: normal/hovered/selected/selected-hovered). Unset → defaultcatframe:textures/gui/tabs/tab.png.
Even simpler: GridLayoutTab — plug into a grid layout without hand-writing the initGui registration:
public class MyGridTab extends GridLayoutTab {
public MyGridTab() {
super(104, "my_mod.tab.grid");
// GridLayoutTab carries a protected final GridLayout layout field;
// add things in the constructor (initGui auto-registers and centers it)
layout.addChild(Button.builder(Text.literal("Button"), b -> { }).build());
}
}LoadingTab: a placeholder page while loading.
new LoadingTab(105, "my_mod.tab.loading",
Text.literal("Loading"), Text.literal("Preparing data..."));// Construct the TabManager (freezes the bar's registration window, loads your tabs in order)
TabManager tabManager = new TabManager("my_mod_bars", this /* Screen */);
// Construct and bind the TabBar (TabBar is abstract; pass your implementation to build)
TabBar tabBar = TabBar.builder(tabManager, 200) // width 200
.addTabs() // fetch all tabs from the registry
.build(new MyTabBar("my_mod_bars"));TabManager has 5 constructors: (TabBar, Screen) / (barId, Screen) / (Consumer<TabBar>, Screen) / (Consumer<TabBar>, Screen, Component) / (barId, Screen, Component) — pick as needed. Core methods:
tabManager.switchToTab(103); // switch page
tabManager.setCurrentTab(tab, true); // programmatically set the current tab (true = relayout now)
tabManager.setTabArea(new ScreenRectangle(x, y, w, h)); // content area rect
tabManager.setTabCallbacks(onSwitch, onInit); // switch/init callbacks
tabManager.reinitializeTabs(); // rebuild all
tabManager.getCurrentTabId(); tabManager.getCurrentTab();
tabManager.getTabCount(); tabManager.getAllTabs(); tabManager.getSortedTabIds();Note: if a registered tabId doesn't match the value passed into the TabManager constructor, it throws TabUncorrespondException (registration and loading must agree).
TabBar is abstract — subclass it (reference: create in initGui, draw nav with drawNavButtons, handle input via mouseClickedNav/keyPressedNav):
public class MyTabBar extends TabBar {
public MyTabBar(String barId) {
super(barId);
setBackgroundColor(0xFF000000); // pure black by default
setBackgroundTexture(new ResourceLocation("my_mod", "textures/gui/bar.png")); // or a texture (16×16 tiled)
setTabTexture(new ResourceLocation("my_mod", "textures/gui/tab.png"));
}
}- Layout constants:
NAV_HEIGHT(24),MAX_TABS_WIDTH(400),MARGIN(14);setNavWidth(int)changes the nav area width. -
TabBar.builder(tabManager, width)chains:.addTabs()(add specified tabs) /.addAllFromManager()(add all) →.build(yourTabBar). -
Keyboard navigation is built in:
Ctrl+Tabnext,Ctrl+Shift+Tabprevious,Ctrl+numberjumps to the Nth tab (LWJGL keycodes 2–11). -
TabButtonis the nav button implementation (four-state texture:tab.png/tab_highlighted/tab_selected/tab_selected_highlighted, 130×24 source with 2px edges). Normally you don't create these yourself —TabBar.arrangeNavElements()does it; to customize, override that or useTabButton'ssetStateTexture(...)/setColorSelected(0xFFFFFF)/setColorHovered(0xFFFF55)/setColorNormal(0xA0A0A0)/setOnPress(Runnable). - Texture priority:
Tab.getTabTextures()(four states) >Tab.getTabTexture()(single texture) > instance-level colors and texture.
The modern ToastComponent system: notification boxes that slide in from a screen corner, auto-queue, auto-stack and auto-dismiss. All in ui.components.toast.
ToastManager toastManager = new ToastManager(mc); // usually a field, one per Screen
toastManager.addToast(new SimpleToast("Saved", "Your progress was written to disk"));
toastManager.update(); // call every tick (drives animation and queue)
toastManager.render(mouseX, mouseY, partialTicks); // render (auto-skipped when F1 hides the UI)- Each corner has 5 independent slots (
MAX_SLOT_COUNT); animation takes 600ms (ANIMATION_DURATION_MS). - Slot usage is computed from toast height; toasts in the same corner stack without colliding.
-
getToast(clazz, token)finds a visible toast by type+token (useful with SystemToast dedup);clear()empties everything.
SimpleToast — text notification, size adapts (min width 80):
// Static factories ship icon prefixes and colors
toastManager.addToast(SimpleToast.success("Quest complete", "Reward granted"));
toastManager.addToast(SimpleToast.warning("Heads up", "Inventory almost full"));
toastManager.addToast(SimpleToast.error("Failed", "Not enough materials"));
toastManager.addToast(SimpleToast.info("Tip", "Press E to open your inventory"));
// Or just new: new SimpleToast(title, description[, displayTimeMs])SystemToast — system notifications, auto-wrapped text (180px wide), deduplicated by id:
SystemToastId id = new SystemToastId(); // default 5000ms
SystemToast.add(toastManager, id, "New version", "Update found");
SystemToast.addOrUpdate(toastManager, id, "New version", "Update revised"); // same id: reset content and extend time
id.forceHide(); // immediately hide the toast with this idItemToast — notification with an item icon (180×48):
toastManager.addToast(new ItemToast(itemStack, "Obtained", "Diamond x3"));Extend BaseToast and implement renderContent(FontRenderer, long); everything else is configuration:
public class MyToast extends BaseToast {
public MyToast() {
super(3000); // display time in ms
setBackgroundTexture(new ResourceLocation("my_mod", "textures/gui/toast/my.png"));
// null = default texture catframe:textures/gui/toast/default.png;
// unset → falls back to solid colors (0xCC000000 bg + 0xFF555555 border)
setShowSound(new SoundEvent(...)); // optional: slide-in sound
setHideSound(new SoundEvent(...)); // optional: slide-out sound
setCorner(ToastCorner.BOTTOM_LEFT); // default TOP_RIGHT (matches vanilla)
}
@Override
public void renderContent(FontRenderer font, long time) {
font.drawString("Custom content", 8, 8, 0xFFFFFF);
}
}ToastCorner: TOP_LEFT / TOP_RIGHT / BOTTOM_LEFT / BOTTOM_RIGHT — top corners stack downward, bottom corners upward, and each corner's slot pool is independent.
Persistent/temporary HUD-level layers (minimaps, status bars, hint boxes) that don't need a Screen. All in ui.overlay.
// Register an overlay
OverlayManager.INSTANCE.register(myOverlay);
// Remove / clear
OverlayManager.INSTANCE.unregister(myOverlay);
OverlayManager.INSTANCE.clearAll();Implement the Overlay interface (it extends GuiEventListener, so size and event methods are inherited):
public class MyHudBadge implements Overlay {
@Override
public ScreenAnchor getAnchor() {
return ScreenAnchor.TOP_RIGHT; // which screen point to anchor to
}
@Override
public int getOffsetX() { return -8; } // offset from the anchor
@Override
public int getOffsetY() { return 8; }
@Override
public int getStackPriority() { return 1; } // smaller = closer to the anchor; same anchor auto-stacks (2px gap)
@Override
public boolean isBlocking() { return false; } // true = swallows mouse/keyboard events (modal)
@Override
public boolean isPausingGame() { return false; } // SCREEN context only
@Override
public OverlayContext getContext() { return OverlayContext.SCREEN; } // SCREEN / HUD / BOTH
@Override
public void update() { /* per-tick logic */ }
}-
ScreenAnchornine anchors:TOP_LEFT/TOP_CENTER/TOP_RIGHT,CENTER_LEFT/CENTER/CENTER_RIGHT,BOTTOM_LEFT/BOTTOM_CENTER/BOTTOM_RIGHT. Bottom anchors stack upward, the rest downward. -
Contexts:
SCREEN= shown only while a screen is open;HUD= shown only in the game HUD (rendered with -1,-1);BOTH= always. - Rendering: SCREEN/BOTH contexts are driven by
renderAll(mouseX, mouseY, partialTicks), HUD context byrenderHud(partialTicks)— CatFrame hooks call these automatically, you never call them yourself. -
Gotcha: an overlay in the HUD context with
isPausingGame() == truethrowsIllegalStateException(pausing only makes sense for screens).
GuiDrawing.drawRect(left, top, right, bottom, 0xCC000000); // ARGB colorBlend/texture/Tessellator state is handled internally — a one-liner for solid rects.
// Grab the text each frame; 200ms, three-frame loop: "o O o" → "o o O" → "O o o"
font.drawString(LoadingDotsText.get(System.currentTimeMillis()), x, y, 0xFFFFFF);The deferred-rendering pipeline counterpart of modern GuiGraphics.item(). Use it to render items that have CatFrame-registered models:
GuiGraphicsExtractor gui = GuiGraphicsExtractor.getInstance();
gui.item(stack, x, y); // collects state only, doesn't draw immediately
gui.extractDeferredElements(); // end-of-frame flush (called automatically by CatFrame hooks)- Only items registered in
ModelRegistry(hasItemModel) take this path; everything else goes through the vanilla pipeline. - Items with
oversized_in_guiuse a separate PiP channel, letting model geometry overflow the 16×16 slot. - The modelview matrix is snapshotted at the call site and restored at flush, so the call-site transform is reproduced exactly.
ContentPanelRenderer.drawContentPanel(x, topY, width, bottomY); // full panel background
ContentPanelRenderer.drawHeaderSeparator(x, y, width); // 2px header separator
ContentPanelRenderer.drawFooterSeparator(x, y, width); // 2px footer separator
ContentPanelRenderer.drawSeparator(tex, x, y, width); // custom separator texture
ContentPanelRenderer.drawPanelBackground(x, y, width, height); // background only- Textures live under
catframe:textures/gui/seperator/(note the directory is "seperator", not "separator"):header_separator.png(32×2),footer_separator.png(32×2),panel_background.png(16×16). - Internally delegates to
TextureStretching.drawTiled, seamlessly tiling 16×16 units.
Nine-patch/tiling stretch utility (ui.util) — GUI buttons and panel backgrounds all rely on it. Two modes: direct calls or mcmeta data-driven.
// Nine-patch: 4 corners fixed, 4 edges tiled, center tiled
TextureStretching.drawNinePatch(tex, x, y, w, h,
edgeL, edgeT, edgeR, edgeB, texW, texH);
// Fixed ends + repeated middle (three-patch): left edgeL fixed, right edgeR fixed, middle repeats tileW
TextureStretching.drawFixedEndRepeat(tex, x, y, w, h,
edgeL, edgeR, tileW, texW, texH);
// Plain tiling (what ContentPanelRenderer/TabBar backgrounds use)
TextureStretching.drawTiled(tex, x, y, w, h, tileW, tileH);
// Static: stretch as-is; target must be an integer multiple of the source,
// otherwise IllegalArgumentException
TextureStretching.drawStatic(tex, x, y, w, h, texW, texH[, alpha]);Give the texture a xxx.png.mcmeta and use the drawAuto family — zero parameters in code:
TextureStretching.drawAuto(tex, x, y, w, h,
fallbackType, fallbackW, fallbackH, fallbackL, fallbackT, fallbackR, fallbackB);
// uses mcmeta if present, falls back to the fallback params otherwise
TextureStretching.drawAutoNinePatch(tex, x, y, w, h, edgeL, edgeT, edgeR, edgeB);
TextureStretching.drawAutoThreePatch(tex, x, y, w, h, edgeL, edgeR);xxx.png.mcmeta format (the key must be stretching):
{
"stretching": {
"type": "three_patch",
"default": { "width": 16, "height": 16 },
"edge": { "left": 2, "right": 2, "top": 2, "bottom": 2 },
"tileWidth": 6
}
}| Key | Description |
|---|---|
type |
nine_patch (default) / three_patch / tile / static
|
default |
default logical size, defaults to 32×32 (negative values throw WrongMetadataError) |
edge |
edge width. Object (left/top/right/bottom) or integer shorthand (all four equal). nine_patch defaults to 4, three_patch to 2 |
tileWidth |
three_patch only: middle repeat unit width; defaults to default.width − left − right |
Metadata is cached globally (load() caches; parse failures return null without crashing); TextureStretchingMetadata.clearCache() clears it. Unknown type throws WrongMetadataError.
Don't want to build a UI layout, just want a line of text on the HUD? Use these two static facades.
// Action bar (floating text above the hotbar)
ActionBar.show(Text.translatable("my.mod.saved")); // plain white
ActionBar.show(Text.literal("Now playing"), true); // record-style HSV rainbow animation
ActionBar.clear();
// Title / subtitle (large centered text, mirrors the /title command)
Title.show(Text.literal("Chapter I"));
Title.show("Chapter I", "The Overture"); // title + subtitle
Title.show(Text.literal("Boss", Style.EMPTY.withColor(0xFF5555).withBold(true)));
Title.showJson("{\"text\":\"Victory\",\"color\":\"gold\"}"); // raw JSON
Title.times(10, 70, 20); // fade-in/stay/fade-out ticks
Title.subtitle(Text.literal("Subtitle"));
Title.actionbar(Text.translatable("my.key")); // actionbar subcommand
Title.clear(); Title.reset();- Must be called on the client thread; rendering happens on the HUD via
ClientOverlayHandler/ClientActionBarHandler. - Times set by
Title.timesare session state: they persist across saves and servers until a client restart orreset()restores the 10/70/20 tick defaults.
Tying it all together — a settings screen with a nav bar, two tabs, and toast capability:
// ── 1. Register two tabs (in FMLInitializationEvent or equivalent, before any TabManager) ──
TabRegistry.registerTab("my_mod_bars", () -> new GeneralTab(), 103, "my_mod.tab.general");
TabRegistry.registerTab("my_mod_bars", () -> new AdvancedTab(), 104, "my_mod.tab.advanced");
// ── 2. Tab classes ──
public class GeneralTab extends GridLayoutTab {
public GeneralTab() {
super(103, "my_mod.tab.general");
layout.addChild(Button.builder(Text.translatable("my_mod.btn.apply"), b -> {
// apply settings…
ActionBar.show(Text.translatable("my_mod.saved"));
}).width(Button.SMALL_WIDTH).build());
layout.addChild(Checkbox.builder(Text.literal("Enable experimental features"))
.selected(false).build());
}
}
public class AdvancedTab extends AbstractScreenTab {
public AdvancedTab() {
super(104, "my_mod.tab.advanced");
}
@Override
public void initGui(TabManager tabManager, int width, int height) {
addWidget(new SimpleEditBox(20, 20, 150, 20));
}
@Override
public void drawScreen(int mouseX, int mouseY, float partialTicks) {
super.drawScreen(mouseX, mouseY, partialTicks);
GuiDrawing.drawRect(20, 50, 170, 52, 0xFFFFFFFF); // custom drawing
}
}
// ── 3. Screen assembly ──
public class MySettingsScreen extends Screen {
private final ToastManager toastManager = new ToastManager(mc);
private TabManager tabManager;
public MySettingsScreen() {
super(Text.translatable("my_mod.screen.settings"));
}
@Override
protected void init() {
this.tabManager = new TabManager("my_mod_bars", this);
TabBar tabBar = TabBar.builder(tabManager, width)
.addAllFromManager()
.build(new MyTabBar("my_mod_bars")); // see section 5.4
addRenderableWidget(tabBar);
addRenderableWidget(tabManager); // TabManager is itself a widget
}
@Override
public void tick() {
toastManager.update();
}
@Override
public void drawScreen(int mouseX, int mouseY, float partialTicks) {
super.drawScreen(mouseX, mouseY, partialTicks);
toastManager.render(mouseX, mouseY, partialTicks);
}
}-
Package names: it's
ui.components(notui.componet) andui.components.tab(notui.tab) — don't copy the misspellings from old docs. -
Texture paths: the directory
catframe:textures/gui/seperator/is spelled "seperator"; the toggle texture istoggle_swticher— CatFrame's own historical spellings, quote them verbatim. -
HeaderFooterLayout doesn't draw a background:
draw()is a no-op; draw the background yourself. - Tab registration timing: must happen before any TabManager is constructed; tabIds within a bar must be unique and match the value passed to the Tab constructor.
-
ImageWidget.texture integer-multiple constraint: non-integer-multiple target sizes throw immediately; use the
spritevariant for free stretching. -
drawStatic has the same constraint: target must be an integer multiple of the source, else
IllegalArgumentException. -
HUD-context overlays can't pause the game:
isPausingGame() == trueis only allowed in the SCREEN context. -
Write
renderWidgetfor new rendering hooks: the oldrenderis deprecated — don't carry it into new code.