Skip to content

CatFrame UI Components

dfdvdsf edited this page Aug 10, 2026 · 5 revisions

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.ui package (and its subpackages).

0. What This Library Does

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 of GuiScreen), then register widgets with addRenderableWidget(...) inside init();
  • The component tree is built from AbstractComponent / GuiEventListener, with rendering funneled through extractRenderState;
  • 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)

1. Quick Start: Your First Screen

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());

1.1 Three Registration Methods — Don't Mix Them Up

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

1.2 Lifecycle Mapping

  • initGui() is the vanilla hook (fires on first open and every resize); the base class already implements it: clear widgets → call init() → set initial focus. Subclasses always override init(), never initGui().
  • updateScreen()tick(); onGuiClosed()removed(). Same semantics as the modern version.
  • The base class also provides getMinecraft() and getFont() convenience getters.

2. Text and Style: Text / Style

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).


3. Widget Catalog

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 0

3.1 Button

Button.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 vanilla gui.button.press sound.

3.2 CyclingButton — Cycle Selection

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().

3.3 Checkbox

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).

3.4 ToggleButton — Switch

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).

3.5 ImageWidget — Image

// 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/texH is 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 a TextureAtlasSprite, stretch it to any size.
  • updateResource(...) swaps the texture at runtime. Note isActive() is always false (pure display widget).

3.6 StringWidget — Single Line of Text

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.

3.7 SimpleEditBox — Text Input

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()
  • SimpleEditBox uses the vanilla texture by default (setUseVanillaTexture(true) is already called in its constructor).
  • The underlying AbstractEditBox also supports setForceVerticalCursor(boolean) (cursor rendering style) and setMessage(Text).

3.8 WaitingPanel — Blocking Wait Panel

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.

3.9 Tooltip

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 any AbstractComponent (rendering and delay are handled internally).
  • getLines(mc) caches per-language line breaking (MAX_WIDTH=170).

3.10 Custom Widgets

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);
    }
}
  • renderWidget is the recommended entry point; the legacy render(...) hook is deprecated, and the base class bridges to it by default — write renderWidget in new code.
  • State fields are all there: visible/active/alpha/isHovered/focused; the base extractRenderState template 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 (provides defaultInactiveMessage).

4. Layout System

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).

4.1 Common Capabilities (AbstractLayout)

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 all

LayoutSettings (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 like alignHorizontallyCenter(), alignVerticallyMiddle()
  • copy() for an independent copy

4.2 LinearLayout — Linear Arrangement

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) and VerticalLayout (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).

4.3 GridLayout — Grid

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 wraps

Full addChild signature: addChild(child, row, column) or addChild(child, row, column, rows, columns), with an optional trailing LayoutSettings/lambda.

4.4 FrameLayout — Stacked Alignment

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

4.5 HeaderFooterLayout — Header / Content / Footer

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).

4.6 SimpleLayout — Manual Positioning Container

SimpleLayout sl = new SimpleLayout(8);   // computes bounding box + padding only, no rearrangement
sl.add(widget1);  // you set widget1's position yourself

For "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.


5. Tab System

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.

5.1 Step 1: Register Your Tabs

// 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 Text

Overloads include registerTab(barId, factory, tabId, Text) and an int priority variant (sort weight, smaller = earlier), 4 in total.

Rules:

  • Duplicate tabId within the same barId throws IllegalArgumentException.
  • The registration window is finite: the first TabManager constructed for a bar "freezes" it; registering into a frozen bar throws IllegalStateException. So put registration code before any TabManager is created.
  • tabId is conventionally expected to start at 103 (0–102 is reserved by vanilla/usage).

5.2 Step 2: Write a Tab Class

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 / keyTyped for custom behavior.
  • Custom tab texture: setTabTexture(ResourceLocation) (single texture) or setTabTextures(TabTextures) (four states: normal/hovered/selected/selected-hovered). Unset → default catframe: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..."));

5.3 Step 3: Assemble TabManager + TabBar

// 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).

5.4 TabBar — The Nav Bar Itself

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+Tab next, Ctrl+Shift+Tab previous, Ctrl+number jumps to the Nth tab (LWJGL keycodes 2–11).
  • TabButton is 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 use TabButton's setStateTexture(...) / setColorSelected(0xFFFFFF) / setColorHovered(0xFFFF55) / setColorNormal(0xA0A0A0) / setOnPress(Runnable).
  • Texture priority: Tab.getTabTextures() (four states) > Tab.getTabTexture() (single texture) > instance-level colors and texture.

6. Toasts

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.

6.1 The Manager

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.

6.2 Ready-Made Flavors

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 id

ItemToast — notification with an item icon (180×48):

toastManager.addToast(new ItemToast(itemStack, "Obtained", "Diamond x3"));

6.3 Custom BaseToast

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.


7. Overlays

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();

7.1 Writing an Overlay

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 */ }
}
  • ScreenAnchor nine 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 by renderHud(partialTicks) — CatFrame hooks call these automatically, you never call them yourself.
  • Gotcha: an overlay in the HUD context with isPausingGame() == true throws IllegalStateException (pausing only makes sense for screens).

8. Rendering & Drawing

8.1 GuiDrawing — Draw a Rect

GuiDrawing.drawRect(left, top, right, bottom, 0xCC000000);   // ARGB color

Blend/texture/Tessellator state is handled internally — a one-liner for solid rects.

8.2 LoadingDotsText — Loading Dot Animation

// 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);

8.3 GuiGraphicsExtractor — GUI Item Rendering

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_gui use 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.

8.4 ContentPanelRenderer — Panel Backgrounds & Separators

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.

9. Texture Stretching: TextureStretching

Nine-patch/tiling stretch utility (ui.util) — GUI buttons and panel backgrounds all rely on it. Two modes: direct calls or mcmeta data-driven.

9.1 Direct Calls

// 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]);

9.2 mcmeta Data-Driven (Recommended)

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.


10. HUD Text Facades: ActionBar / Title

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.times are session state: they persist across saves and servers until a client restart or reset() restores the 10/70/20 tick defaults.

11. Complete Example: A Screen With Tabs

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);
    }
}

Appendix: Pitfalls Cheat Sheet

  1. Package names: it's ui.components (not ui.componet) and ui.components.tab (not ui.tab) — don't copy the misspellings from old docs.
  2. Texture paths: the directory catframe:textures/gui/seperator/ is spelled "seperator"; the toggle texture is toggle_swticher — CatFrame's own historical spellings, quote them verbatim.
  3. HeaderFooterLayout doesn't draw a background: draw() is a no-op; draw the background yourself.
  4. 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.
  5. ImageWidget.texture integer-multiple constraint: non-integer-multiple target sizes throw immediately; use the sprite variant for free stretching.
  6. drawStatic has the same constraint: target must be an integer multiple of the source, else IllegalArgumentException.
  7. HUD-context overlays can't pause the game: isPausingGame() == true is only allowed in the SCREEN context.
  8. Write renderWidget for new rendering hooks: the old render is deprecated — don't carry it into new code.

Clone this wiki locally