Skip to content

CatFrame UI Components

dfdvdsf edited this page Jun 23, 2026 · 5 revisions

These are the UI building blocks provided by CatFrame — the library mod that CreateWorldUI (6.0.0+) depends on. If you're writing a custom tab or any screen that needs cycling buttons, panel rendering, or the tab system, this is your go-to reference.


GuiCyclableButton<T>

Package: decok.dfcdvadstf.catframe.ui.GuiCyclableButton

A generic cyclable button — similar to modern Minecraft's CyclingButtonWidget<T>. Click it or scroll your mouse wheel to cycle through a typed list of values. Left-click or scroll down goes forward; scroll up goes backward.

How it works

You build one with a Builder — give it a value-to-text function, a list of values, and an update callback. The button handles all the cycling internally; you just react to value changes.

Key pieces:

  • ValueToText<T> — converts your value to the display string. Gets called on every text refresh.
  • Values<T> — provides the list of values to cycle through (supports dynamic lists too).
  • UpdateCallback<T> — fires when the value changes: (button, newValue) -> { ... }
  • getValue() / setValue(T) — read or programmatically set the current value.

Example

import decok.dfcdvadstf.catframe.ui.GuiCyclableButton;
import net.minecraft.world.EnumDifficulty;
import net.minecraft.client.resources.I18n;

// A button that cycles through difficulties
GuiCyclableButton<EnumDifficulty> diffButton = GuiCyclableButton.<EnumDifficulty>builder(
        d -> I18n.format("options.difficulty") + ": " + I18n.format(d.getDifficultyResourceKey()))
    .values(EnumDifficulty.values())
    .initially(EnumDifficulty.NORMAL)
    .build(200, width / 2 - 104, height / 2, 208, 20, (button, difficulty) -> {
        // React to the new value — no manual index juggling needed
        yourDifficultySetter(difficulty);
    });
addButton(diffButton);

Need a label prefix? Pass it as the String label argument in build() — the display becomes "label: valueText" automatically:

// With a label — display shows "Difficulty: Easy" etc.
.build(200, x, y, w, h, I18n.format("options.difficulty"), callback);

// Without a label — your ValueToText controls the full string
.build(200, x, y, w, h, callback);

Boolean on/off toggle? There's a shortcut:

GuiCyclableButton<Boolean> cheatsBtn = GuiCyclableButton.onOffBuilder()
    .initially(false)
    .build(201, x, y, 208, 20, (button, value) -> setAllowCheats(value));

Tip: Call button.updateText() in your drawScreen() if the display depends on external state. And use setValue(T) to sync the button when something else changes the underlying value.


ContentPanelRenderer

Package: decok.dfcdvadstf.catframe.ui.ContentPanelRenderer

A shared renderer for content panels — gives you the header separator, a tiled panel background, and the footer separator. Use the pieces you want individually, or let drawContentPanel() do the whole thing in one shot.

What you get

Method What it draws
drawContentPanel(x, top, width, bottom) The whole thing — header line, tiled background, footer line.
drawHeaderSeparator(x, y, width) Just the top separator line (2px tall, tiled).
drawFooterSeparator(x, y, width) Just the bottom separator line (2px tall, tiled).
drawSeparator(x, y, width, texture) A separator line with your own 32×2 texture.
drawPanelBackground(x, y, width, height) Just the panel background (tiles the 16×16 texture).

Textures

Field Path Size
HEADER_SEPARATOR createworldui:textures/gui/header_separator.png 32×2
FOOTER_SEPARATOR createworldui:textures/gui/footer_separator.png 32×2
PANEL_BACKGROUND createworldui:textures/gui/panel_background.png 16×16

You can override these in your resource pack — or pass a custom ResourceLocation to drawSeparator() if you want a different style.

Quick Example

import decok.dfcdvadstf.catframe.ui.ContentPanelRenderer;

// Draw a full content panel in one call
ContentPanelRenderer.drawContentPanel(panelX, panelTop, panelWidth, panelBottom);

// Or draw pieces separately
ContentPanelRenderer.drawHeaderSeparator(panelX, panelTop, panelWidth);
ContentPanelRenderer.drawPanelBackground(panelX, panelTop + 2, panelWidth, panelHeight - 4);
ContentPanelRenderer.drawFooterSeparator(panelX, panelBottom - 2, panelWidth);

Note: The separator height is always 2 GUI pixels (SEPARATOR_HEIGHT = 2). When using drawContentPanel(), the background is automatically placed between the two separators.


Tab System

The tab system from CatFrame provides a clean way to build multi-tab screens. Each tab implements the Tab interface, and you can extend AbstractScreenTab for convenience.

Tab Interface

Package: decok.dfcdvadstf.catframe.ui.tab.Tab

The core contract for a tab:

public interface Tab {
    void initGui(TabManager tabManager, int width, int height);
    void drawScreen(int mouseX, int mouseY, float partialTicks);
    void actionPerformed(GuiButton button);
    void mouseClicked(int mouseX, int mouseY, int mouseButton);
    void keyTyped(char typedChar, int keyCode);
    int getTabId();
    String getTabName();
    void setVisible(boolean visible);

    /** Default: {@code catframe:textures/gui/tabs.png} */
    default ResourceLocation getTabTexture();
}

AbstractScreenTab

Package: decok.dfcdvadstf.catframe.ui.tab.AbstractScreenTab

A base implementation that handles the boring stuff — button visibility, localization, button management, and tab texture. Extend this for your custom tabs.

Two constructors are available:

Constructor When to use
AbstractScreenTab(int tabId, String tabNameKey) Traditional I18n key (no colon) — automatically falls back to I18n.format(tabNameKey). If the key contains a colon (:), it's treated as a CatFrame-style domain:key and auto-wrapped in Text.translatable().
AbstractScreenTab(int tabId, Text tabTitle) Explicit Text title — gives you lazy translation via CatFrame's LocalizationManager. Use when you need namespace-based i18n.
import decok.dfcdvadstf.catframe.ui.Text;
import decok.dfcdvadstf.catframe.ui.tab.AbstractScreenTab;
import decok.dfcdvadstf.catframe.ui.tab.TabManager;

public class MyCustomTab extends AbstractScreenTab {

    // Option A: String key — no colon → I18n.format("mymod.tab.custom")
    public MyCustomTab() {
        super(103, "mymod.tab.custom");
    }

    // Option B: String key with colon → auto Text.translatable("mymod:tab.custom")
    // public MyCustomTab() {
    //     super(104, "mymod:tab.custom");
    // }

    // Option C: Explicit Text title
    // public MyCustomTab() {
    //     super(105, Text.translatable("mymod", "tab.custom"));
    // }

    @Override
    public void initGui(TabManager tabManager, int width, int height) {
        super.initGui(tabManager, width, height);
        // Add your buttons and fields here
    }
    // ... rest of the tab methods
}

Whichever way you go, the tab title shows up correctly in TabButton. The framework handles the translation routing for you.

Tip: Call setTabTexture(ResourceLocation) from the constructor or initGui() if you prefer a setter over overriding getTabTexture(). The default texture is catframe:textures/gui/tabs.png.

TabManager

Package: decok.dfcdvadstf.catframe.ui.tab.TabManager

TabManager handles tab switching, state persistence (survives window resizes), and gives you access to the parent screen.

Method What it does
addButton(GuiButton) Add a button to the screen's button list.
registerTab(Tab) Register a tab instance (internal — use TabRegistry instead).
switchToTab(int tabId) Switch to a tab by ID.
drawScreen(...) Delegate drawing to the current tab.
actionPerformed(GuiButton) Delegate button clicks to the current tab.
mouseClicked(...) Delegate mouse clicks to the current tab.
keyTyped(...) Delegate keyboard input to the current tab.
reinitializeTabs(width, height) Re-init all tabs on window resize (preserves current tab).
getScreen() Get the parent GuiScreen instance.
getCurrentTabId() Get the currently active tab ID.
getTabCount() Get the total number of registered tabs.
getTabBar() Get the optional TabBar associated with this manager, or null.

Two ways to construct a TabManager — both require a barId so it knows which bucket of tabs to load:

// Option 1: With a TabBar — barId is taken from bar.getBarId() automatically
TabBar bar = new MyModTabBar();
TabManager manager = new TabManager(screen, buttonList, width, height, bar);

// Option 2: With a bare barId string — no TabBar, no custom background
TabManager manager = new TabManager(screen, buttonList, width, height, "my_mod_bar");

Either way, TabManager only loads entries registered under that specific barId — tabs from other bars won't leak in.

TabState

Package: decok.dfcdvadstf.catframe.ui.tab.TabState

An enum that defines texture coordinates and text colors for the four tab interaction states — NORMAL, HOVER, SELECTED, SELECTED_HOVER. Used by the tab button renderer to pick the right texture slice and color.

State Texture (u, v) Text Color
NORMAL (0, 0) White (0xFFFFFF)
HOVER (0, 24) Yellow (0xFFFF55)
SELECTED (0, 48) White (0xFFFFFF)
SELECTED_HOVER (0, 72) Yellow (0xFFFF55)

TabButton

Package: decok.dfcdvadstf.catframe.ui.tab.TabButton

A tab button component managed by TabBar, rendered with nine-patch stretching across four state textures (normal / hovered / selected / selected+highlighted).

Text Colour Customisation

Each TabButton has three instance-level colour fields — no more hardcoded static final constants. External mods can set them individually on any button:

TabButton btn = new TabButton(myTab);

btn.setColorSelected(0xFFFFFF);  // White — selected tab
btn.setColorHovered(0xFFFF55);   // Yellow — mouse hover
btn.setColorNormal(0xA0A0A0);    // Grey — default

Defaults (same as before — purely informational, no dependency needed):

State Default Colour Customisation Method
Selected 0xFFFFFF (white) setColorSelected(int)
Hovered 0xFFFF55 (yellow) setColorHovered(int)
Normal 0xA0A0A0 (grey) setColorNormal(int)

Practical Example — Per-Bar Colour Scheme

If you have a custom TabBar, you can apply a uniform colour scheme to all its tab buttons after TabManager initialises them:

// Inside your screen's initGui()
TabBar bar = new MyModTabBar();
TabManager manager = new TabManager(this, buttonList, width, height, bar);

// Customise every tab button in this bar
for (TabRegistry.TabEntry entry : TabRegistry.getEntries(bar.getBarId())) {
    TabButton btn = bar.getTabButton(entry.tabId);  // retrieve the button
    if (btn != null) {
        btn.setColorSelected(0x55FF55);  // green for selected
        btn.setColorHovered(0xFFFF55);   // keep yellow for hover
        btn.setColorNormal(0xAAAAAA);    // lighter grey for normal
    }
}

Note: TabButton textures (the four state .png files) are still controlled via TabBar.setTabTexture() and individual Tab.getTabTexture(). The colour customisation above only affects the text colour drawn on top of those textures.

Getting a TabButton Reference

TabBar exposes tab buttons via:

// After arrangeNavElements() has been called
TabButton btn = tabBar.getTabButton(int tabId);

Returns null if no button exists for that ID.


TabBar

Package: decok.dfcdvadstf.catframe.ui.tab.TabBar

TabBar is an abstract container bar for tabs. It provides a common background for the tab area and holds the tabs that belong to it. Each TabBar has a unique ID, and subclasses customise the look (solid colour fill + optional tiled texture, defaults to solid black).

This is the piece you extend if you want a distinct visual style for your screen's tab area.

Properties

Property Type Default Description
barId String (required) Unique identifier for this bar, set via constructor.
backgroundColor int (optional) default: 0xFF000000 Solid background colour in ARGB format.
backgroundTexture ResourceLocation (optional) default: null Optional tiled background texture (16×16 tile).
tabTexture ResourceLocation (optional) default: catframe:textures/gui/tabs.png Default tab button texture for all tabs in this bar.

Key Methods

Method What it does
drawBackground(x, y, width, height) Draws the bar background — solid colour fill first, then optional tiled texture on top.
registerEntry(TabEntry) Register a TabRegistry.TabEntry into this bar. Called automatically by TabManager during construction.
getAllEntries() Get all registered entries (ordered by insertion).
getEntry(int tabId) Get an entry by its tab ID.
registerTab(Tab) Register a tab instance directly into this bar.
getAllTabs() Get all tab instances (ordered by creation).
getTab(int tabId) Get a tab by its ID.
containsTab(int tabId) Check if this bar contains a tab with the given ID.
getTabCount() Get the number of tab instances.
getBarId() Get the unique bar identifier.
getTabButton(int tabId) Get the TabButton for the given tab ID, or null if not found. After arrangeNavElements() has been called.

Subclasses may override drawTiledBackground(x, y, width, height) to customise how the tiled texture is rendered.

Custom TabBar Example (After 0.0.3)

import decok.dfcdvadstf.catframe.ui.tab.TabBar;
import net.minecraft.util.ResourceLocation;

public class MyModTabBar extends TabBar {

    public MyModTabBar() {
        super("my_mod_bar");           // Unique bar ID
        setBackgroundColor(0xFF2D2D2D); // Optional dark grey solid fill
        setBackgroundTexture(           // Optional tiled texture
            new ResourceLocation("mymod", "textures/gui/bar_bg.png"));
        setTabTexture(                  // Optional: override default tab texture
            new ResourceLocation("mymod", "textures/gui/tab_buttons.png"));
    }
}

Notes: Although except bar ID, rest of these are optional and can leave it unimplemented, this will end up with a soild black fill. Which means: public MyModTabBar() {super("my_mod_bar");} equals

public MyModTabBar() {
  super("my_mod_bar");
  setBackgroundColor("0XFF000000");
  setTabTexture(
  new ResourceLocation("catframe", "textures/gui/tabs/buttons.png"));
}

Usage with TabManager

// 1. Register tabs during preInit — specify which bar they belong to
TabRegistry.registerTab("my_mod_bar", MyCustomTab::new, 103, "mymod.tab.custom", 5);

// 2. Create your TabBar (its barId must match what you registered under)
TabBar bar = new MyModTabBar(); // super("my_mod_bar") inside

// 3. Pass it to TabManager — only "my_mod_bar" entries get loaded
TabManager manager = new TabManager(screen, buttonList, width, height, bar);

TabRegistry

Package: decok.dfcdvadstf.catframe.ui.tab.TabRegistry

The static registry for external mods to register custom tabs. Entries are bucketed by barId — so a statistics screen's tabs won't pollute the create-world screen, and vice versa. Register your tabs during mod init with the target barId, and when the corresponding screen opens, TabManager freezes that bucket and creates only its own tabs.

Key Methods

Method What it does
registerTab(String, Supplier<Tab>, int, String, int) Register with a String name key + priority.
registerTab(String, Supplier<Tab>, int, String) Register with a String name key (priority defaults to tabId).
registerTab(String, Supplier<Tab>, int, Text, int) Register with a Text title + priority.
registerTab(String, Supplier<Tab>, int, Text) Register with a Text title (priority defaults to tabId).
getEntries(String barId) Get all entries for that bar, sorted by priority (unmodifiable).
isFrozen(String barId) Check if a specific bar's bucket is frozen.
freeze(String barId) Freeze a bar's bucket. Called internally by TabManager.
clear(String barId) Clear a specific bar's bucket (testing/reset).
clearAll() Clear everything (testing/reset).

Parameters

Parameter Type What it means
barId String Which bar this tab belongs to. Must match the TabBar's barId (or the string you pass to TabManager).
factory Supplier<Tab> A factory that creates your Tab instance. Called once when TabManager is constructed.
tabId int Unique tab ID within a bar. Built-in tabs use 100–102, so start from 103.
nameKey / nameText String or Text Tab title. Use String for simple I18n keys (no colon → I18n.format), or Text for namespace-based lazy translation via CatFrame's LocalizationManager. Internally stored as both nameKey (raw string, always present) and nameText (Text, may be null if registered with String).
priority int Sort order — lower comes first. Defaults to tabId if not specified. Built-in tabs use 0, 1, 2.

Entries are sorted by priority ascending, then tabId ascending as a tiebreaker. The uniqueness check for tabId only applies within the same barId bucket — two different bars can safely share the same numeric ID.

Registration Example

String key (classic I18n):

import cpw.mods.fml.common.Mod;
import cpw.mods.fml.common.event.FMLPreInitializationEvent;
import decok.dfcdvadstf.catframe.ui.tab.AbstractScreenTab;
import decok.dfcdvadstf.catframe.ui.tab.Tab;
import decok.dfcdvadstf.catframe.ui.tab.TabManager;
import decok.dfcdvadstf.catframe.ui.tab.TabRegistry;
import net.minecraft.client.gui.GuiButton;

@Mod(modid = "mymod", name = "My Mod", version = "1.0")
public class MyMod {

    @Mod.EventHandler
    public void preInit(FMLPreInitializationEvent event) {
        TabRegistry.registerTab(
            "create_world",          // Target bar ID
            MyCustomTab::new,        // Supplier<Tab> factory
            103,                     // Unique tab ID (start from 103)
            "mymod.tab.custom",      // Localization key (no colon → I18n.format)
            5                        // Sort priority (lower = earlier)
        );
    }
}
// Tab constructor — String key, no colon, auto-fallback to I18n.format
public MyCustomTab() {
    super(103, "mymod.tab.custom");
}

And don't forget the localization in your lang file:

mymod.tab.custom=My Tab

Text key (CatFrame namespace-based):

import decok.dfcdvadstf.catframe.ui.Text;
// ... same imports as above

public class MyMod {

    @Mod.EventHandler
    public void preInit(FMLPreInitializationEvent event) {
        TabRegistry.registerTab(
            "create_world",
            MyCustomTab::new,
            104,
            Text.translatable("mymod", "tab.fancy"),  // Text title
            5
        );
    }
}
// Tab constructor — explicit Text, lazy translation via LocalizationManager
public MyCustomTab() {
    super(104, Text.translatable("mymod", "tab.fancy"));
}

Or use the colon shorthand (auto-wraps in Text.translatable):

TabRegistry.registerTab("create_world", MyCustomTab::new, 105, "mymod:tab.quick");
// Tab constructor
public MyCustomTab() {
    super(105, "mymod:tab.quick");  // colon → auto Text.translatable
}

Things to Keep in Mind

  • Register early — like FMLPreInitializationEvent. Once TabManager is constructed for a given bar, that bar's bucket freezes; registering to it after that throws an IllegalStateException.
  • Tab IDs must be unique within a bar — duplicate IDs in the same bucket throw an IllegalArgumentException. Different bars can reuse the same numeric IDs without conflict.
  • barId must match — the string you pass to registerTab() must be the same as the TabBar's barId (or the string passed directly to TabManager). Typos mean your tab silently won't appear.
  • AbstractScreenTab constructor tabId must match registrationTabManager now enforces this at init time. If tab.getTabId() doesn't equal the entry's tabId, a TabUncorrespondException is thrown with a clear message. No more silent mismatches.
  • String vs Text — both registerTab(... String nameKey) and registerTab(... Text nameText) overloads exist. Pick whichever matches your tab's constructor. The String overload sets entry.nameText = null; the Text overload sets entry.nameKey = text.getRaw() and keeps entry.nameText for display.
  • initGui() gets called by TabManager — on init and on resize. Always call super.initGui().

Layouts

Package: decok.dfcdvadstf.catframe.ui.layouts

A layout system for arranging UI elements — think of it as flexible containers that automatically compute child positions so you don't have to hardcode coordinates everywhere. Every layout is itself an ILayout, so you can nest layouts inside other layouts.

The idea is simple: you add children (any ILayout), set a few properties like padding and spacing, call recalculate(), and bam — everything falls into place. You can even switch layouts at runtime without touching child positions.

The Hierarchy

ILayout                          ← base: x, y, width, height
  └── Layout                     ← adds children, padding, spacing, recalculate, draw
        └── AbstractLayout       ← base impl: manages children, auto-recalculates
              ├── SimpleLayout   ← bounding-box container (no auto-arrangement)
              ├── LinearLayout   ← single row/column (HORIZONTAL / VERTICAL)
              │     ├── HorizontalLayout  ← preset horizontal + centre
              │     └── VerticalLayout    ← preset vertical + centre
              ├── GridLayout     ← fixed-column grid
              ├── EqualSpacingLayout     ← evenly distributed children
              └── HeaderFooterLayout     ← three zones, draws ContentPanel background

All layouts live under decok.dfcdvadstf.catframe.ui.layouts.


Common Properties

Every layout inherits these from AbstractLayout:

Property Type Default Description
padding int 4 Space around the content area (applied on all sides). Changing it triggers recalculate().
spacing int 2 Gap between children. Changing it triggers recalculate().
children List<ILayout> (empty) Managed via add(), remove(), clear().
x, y int 0 Top-left position of the layout.
width, height int 0 Calculated automatically by recalculate(), or set externally.
// Chaining works because add() returns the layout itself
layout.add(child1).add(child2).setPadding(8).setSpacing(4);

And there's a batch variant:

layout.addAll(header, content, footer);

SimpleLayout

A general-purpose container. It doesn't rearrange children at all — they keep whatever position they already have. recalculate() just computes the bounding box of all children plus padding, so the layout knows its own size.

Use this when you want to group elements but handle positioning yourself.

SimpleLayout group = new SimpleLayout(6); // padding = 6
// Manually position each child relative to the group
group.add(someWidget);
someWidget.setPosition(group.getX() + 8, group.getY() + 8);

Why would you use this? Sometimes you just need a container for drawing or event-handling purposes, without any automatic arrangement. SimpleLayout gives you that without imposing a layout strategy.


LinearLayout / HorizontalLayout / VerticalLayout

These arrange children in a single line — either left-to-right or top-to-bottom. HorizontalLayout and VerticalLayout are convenience wrappers with sensible defaults.

Axis & Alignment

LinearLayout has two enums:

  • Axis: HORIZONTAL or VERTICAL — the primary arrangement direction.
  • Alignment: controls how children sit on the perpendicular axis:
    • START — clump at the left/top edge.
    • CENTER — centred.
    • END — clump at the right/bottom edge.
    • FILL — stretch to fill the available space.
// Vertical column, children centred horizontally
LinearLayout column = new LinearLayout(Axis.VERTICAL, Alignment.CENTER);
column.setSpacing(6).setPadding(10);
column.add(button1).add(button2).add(button3);
column.setPosition(20, 30);
column.recalculate();

Or grab the convenience versions:

// Horizontal row, children vertically centred by default
HorizontalLayout row = new HorizontalLayout();
row.setSpacing(8).add(icon).add(label).add(button);

// Vertical column, centred horizontally
VerticalLayout col = new VerticalLayout();
col.addAll(title, description, actionRow);

Both convenience layouts default to CENTER alignment on the perpendicular axis — so HorizontalLayout centres children vertically, VerticalLayout centres them horizontally. Need a different alignment? Pass it to the constructor or call setAlignment().


GridLayout

Arranges children in a fixed-column grid, left-to-right then top-to-bottom. Each cell can be auto-sized from the largest child, or you can fix a cell size.

Children are centred within their cell — so mismatched sizes still look tidy.

// Three columns, auto-sized cells (largest child dictates cell size)
GridLayout grid = new GridLayout(3);
grid.setSpacing(4).setPadding(6);
grid.add(btn1).add(btn2).add(btn3).add(btn4);
grid.recalculate();

// Or use fixed 60x20 cells
GridLayout fixed = new GridLayout(2, 60, 20);

Properties you can tweak:

Method Description
setColumns(int) Change column count (min 1).
setCellSize(w, h) Fix cell dimensions. Set to 0 to auto-size again.
getColumns() / getCellWidth() / getCellHeight() Read current values.

EqualSpacingLayout

Distributes children evenly across the available space. Instead of a fixed gap (like LinearLayout), it calculates the gap automatically so that children span the full content width or height.

This is your go-to for things like evenly spaced toolbar buttons or a row of action icons.

EqualSpacingLayout toolbar = new EqualSpacingLayout(EqualSpacingLayout.Axis.HORIZONTAL);
toolbar.add(undoBtn).add(redoBtn).add(saveBtn).add(deleteBtn);
// Children are now evenly spread across the toolbar width
toolbar.setPosition(10, 10);
toolbar.recalculate();

If you set a fixed width/height beforehand, children are distributed within that space. If not, the layout auto-sizes from its children's total width plus the calculated gaps.


HeaderFooterLayout

A three-zone layout: header at the top, content in the middle, footer at the bottom. By default it draws itself using ContentPanelRenderer.drawContentPanel() — the tiled background with header/footer separator lines, no extra effort needed.

The header and footer are centred horizontally; the content area stretches to fill whatever vertical space remains.

HeaderFooterLayout panel = new HeaderFooterLayout();
panel.setHeader(titleWidget);
panel.setContent(scrollableList);
panel.setFooter(doneButton);

// Position and size — the recalculate(availableWidth, availableHeight)
// variant is handy when the parent knows the exact space
panel.setPosition(10, 10);
panel.recalculate(panelWidth, panelHeight);

// Drawing includes ContentPanel background automatically
// Call this in your drawScreen()
panel.draw(mouseX, mouseY, partialTicks);

Want to skip the background rendering? new HeaderFooterLayout(false) or setDrawPanel(false).

Method Returns Description
setHeader(ILayout) Layout Sets the header slot (index 0).
setContent(ILayout) Layout Sets the content slot (index 1).
setFooter(ILayout) Layout Sets the footer slot (index 2).
getHeader() ILayout / null Returns the header child.
getContent() ILayout / null Returns the content child.
getFooter() ILayout / null Returns the footer child.
recalculate(w, h) Recalculate with explicit dimensions.

Drawing

The draw(mouseX, mouseY, partialTicks) method is a no-op by default, but HeaderFooterLayout overrides it to render the ContentPanelRenderer background. If you're making a custom layout that draws something, just override draw() in your subclass.


Nesting layouts

Since every layout is an ILayout, you can mix and match freely — a GridLayout inside a VerticalLayout inside a HeaderFooterLayout, for instance. Each child gets positioned by its parent layout, then positions its own children in turn.

HeaderFooterLayout screen = new HeaderFooterLayout();
screen.setHeader(new TextWidget("Settings"));

VerticalLayout body = new VerticalLayout();
body.add(new SomeWidget());

GridLayout grid = new GridLayout(2);
grid.add(opt1).add(opt2).add(opt3).add(opt4);
body.add(grid);

screen.setContent(body);
screen.setFooter(new TextWidget("v1.0"));
screen.recalculate(width, height);

One thing to keep in mind: always call recalculate() on the outermost layout after you're done adding children — it cascades down because each layout's add() already triggers its own recalculation.


GridLayoutTab

Package: decok.dfcdvadstf.catframe.ui.tab.GridLayoutTab

An abstract tab backed by a GridLayout — arranges children in a fixed-column grid and centres the whole thing within the screen. Subclasses only need to override buildLayout() to populate content; no coordinate juggling required.

Constructors

Constructor Description
GridLayoutTab(int tabId, String tabNameKey) Default 2 columns, auto-sized cells
GridLayoutTab(int tabId, String tabNameKey, int columns) Custom column count
GridLayoutTab(int tabId, String tabNameKey, int columns, int cellW, int cellH) Custom columns + fixed cell size

Abstract Method

protected abstract void buildLayout(GridLayout layout);

Called once during initGui(), before the layout is arranged. Subclasses add children to layout here — the framework handles positioning and centring.

Lifecycle

initGui() flow:

  1. Clear the layout (layout.clear())
  2. Call buildLayout(layout) — subclass fills in children
  3. Call arrangeAndCenter(width, height) — arrange and centre the grid
  4. Walk children and auto-register any GuiButton with TabManager

Centring rules — matching 26.1's alignInRectangle(layout, rect, 0.5F, 0.1666F):

  • Horizontal: centred (0.5)
  • Vertical: roughly one-sixth from the top (0.16666667)

Full Example

import decok.dfcdvadstf.catframe.ui.tab.GridLayoutTab;
import decok.dfcdvadstf.catframe.ui.tab.TabRegistry;
import decok.dfcdvadstf.catframe.ui.layouts.GridLayout;
import net.minecraft.client.gui.GuiButton;

public class OptionsTab extends GridLayoutTab {

    public OptionsTab() {
        super(103, "mymod.tab.options", 2); // 2 columns
    }

    @Override
    protected void buildLayout(GridLayout layout) {
        layout.add(new GuiButton(1, 0, 0, "Option A"));
        layout.add(new GuiButton(2, 0, 0, "Option B"));
        layout.add(new GuiButton(3, 0, 0, "Option C"));
        layout.add(new GuiButton(4, 0, 0, "Option D"));
    }

    @Override
    public void actionPerformed(GuiButton button) {
        switch (button.id) {
            case 1: /* handle A */ break;
            case 2: /* handle B */ break;
        }
    }
}

// Register during preInit
TabRegistry.registerTab("create_world", OptionsTab::new, 103, "mymod.tab.options", 5);

API Reference

Method Description
buildLayout(GridLayout) Abstract — subclass fills in children here
getLayout() Direct access to the underlying GridLayout (advanced usage)
arrangeAndCenter(int, int) Re-arrange and centre the grid (called automatically on resize)

Component System

CatFrame provides a unified component system inspired by higher-version Minecraft's LayoutElement + Renderable + GuiEventListener. All UI controls implement the Component interface.

Component Interface

Package: decok.dfcdvadstf.catframe.ui.components.Component

public interface Component extends ILayout {
    int getX(); void setX(int x);
    int getY(); void setY(int y);
    int getWidth(); int getHeight();
    boolean isVisible(); void setVisible(boolean visible);
    boolean isActive(); void setActive(boolean active);

    default void render(int mouseX, int mouseY, float partialTicks) {}
    default void mouseClicked(int mouseX, int mouseY, int mouseButton) {}
    default void keyTyped(char typedChar, int keyCode) {}
    default void mouseScrolled(int delta) {}
    default void mouseDrag(int mouseX, int mouseY, int mouseButton, long timeSinceLastClick) {}
    default void mouseReleased(int mouseX, int mouseY, int mouseButton) {}
    default boolean isMouseOver(int mouseX, int mouseY) { ... }
}

Component Hierarchy

Component (interface)
  └── AbstractComponent (base impl: position, visibility, drawing utils)
        ├── AbstractButton (press/hover/sound logic)
        │     ├── Button (simple text button)
        │     └── CyclingButton<T> (generic cycling button)
        ├── CyclingArea<T> (cycling with textured background)
        ├── EditBox (text input field)
        ├── StringWidget (text display)
        ├── WaitingPanel (loading spinner)
        ├── GuiButtonAdapter (wraps vanilla GuiButton as Component)
        └── Toast (notification system)
              ├── BaseToast
              │     ├── SimpleToast
              │     ├── SystemToast
              │     └── ItemToast
              └── ...

CyclingButton<T>

Package: decok.dfcdvadstf.catframe.ui.components.CyclingButton

A generic cycling button, extending AbstractButton with unified texture backgrounds. Supports click and scroll wheel cycling.

Differences from GuiCyclableButton

GuiCyclableButton<T> CyclingButton<T>
Base class GuiButton (vanilla) AbstractButton (Component)
Texture Vanilla button texture CatFrame custom texture
Builder .build(id, x, y, w, h, callback) .build(x, y, w, h, callback)
Event dispatch actionPerformed(GuiButton) mouseClicked() / render()

New code should use CyclingButton.

Usage

CyclingButton<EnumDifficulty> diffBtn = CyclingButton.<EnumDifficulty>builder(
        d -> I18n.format(d.getDifficultyResourceKey()))
    .values(EnumDifficulty.values())
    .initially(EnumDifficulty.NORMAL)
    .label("Difficulty:")
    .build(x, y, 200, 20, (button, value) -> {
        setDifficulty(value);
    });

Boolean on/off convenience:

CyclingButton<Boolean> cheatsBtn = CyclingButton.onOffBuilder()
    .initially(false)
    .build(x, y, 200, 20, (button, value) -> setAllowCheats(value));

CyclingArea<T>

Package: decok.dfcdvadstf.catframe.ui.components.CyclingArea

A generic cycling selection area, extending AbstractComponent. Supports textured backgrounds (9-slice stretching), click and scroll wheel cycling, and label text display. Integrates with the unified component system.

CyclingArea<EnumDifficulty> area = CyclingArea.<EnumDifficulty>builder(
        d -> I18n.format(d.getDifficultyResourceKey()))
    .values(EnumDifficulty.values())
    .initially(EnumDifficulty.NORMAL)
    .build(x, y, 200, 20, (area, value) -> { ... });

EditBox

Package: decok.dfcdvadstf.catframe.ui.components.EditBox

Text input field inspired by higher-version Minecraft's EditBox. Supports textured backgrounds or vanilla black/white style, and vanilla-style cursor.

EditBox seedBox = new EditBox(x, y, 200, 20);
seedBox.setHint("Enter seed...");
seedBox.setMaxLength(64);
seedBox.setUseVanillaTexture(false); // Use CatFrame texture

Toast System

CatFrame provides a higher-version-style toast notification system with queuing, animations, and multi-slot display.

Toast Interface

Package: decok.dfcdvadstf.catframe.ui.components.toast.Toast

public interface Toast extends Component {
    int DEFAULT_WIDTH = 160;
    int SLOT_HEIGHT = 32;

    Visibility getWantedVisibility();
    void update(ToastManager manager, long fullyVisibleForMs);
    void render(int mouseX, int mouseY, float partialTicks);
    default Object getToken() { return this; }
    default int occupiedSlotCount() { ... }
    default float xPos(int screenWidth, float visiblePortion) { ... }
    default float yPos(int firstSlotIndex) { ... }
}

ToastManager

ToastManager toastManager = new ToastManager(mc);

// Called in tick
toastManager.update();

// Called in render
toastManager.render();
  • Maximum 5 slots displayed simultaneously
  • Slide-in/slide-out animation: 600ms
  • Automatic queue management

SimpleToast

Simple text notification with adaptive sizing:

// Basic usage
toastManager.addToast(new SimpleToast("Hello World"));
toastManager.addToast(new SimpleToast("Title", "Description"));

// Convenience factories
SimpleToast.success("Saved!");     // Green ✓
SimpleToast.warning("Low space");  // Yellow ⚠
SimpleToast.error("Failed!");      // Red ✗
SimpleToast.info("Tip: ...");      // Cyan ℹ

SystemToast

System notification with icon and automatic word wrap:

SystemToast.SystemToastId id = new SystemToast.SystemToastId(8000L);

// Add new
SystemToast.add(toastManager, id, "Update Available", "Version 2.0 is out!");

// Update existing Toast (resets content if exists)
SystemToast.addOrUpdate(toastManager, id, "Downloading...", "50% complete");

ItemToast

Toast with an item icon:

toastManager.addToast(new ItemToast(
    new ItemStack(Items.diamond),
    "Achievement Get!",
    "Diamonds!"
));

Custom Toast

Extend BaseToast to create custom toasts:

public class MyToast extends BaseToast {
    public MyToast() {
        setBackgroundTexture(new ResourceLocation("mymod", "textures/gui/toast.png"));
        // Or null for solid color fallback
    }

    @Override
    protected void renderContent(FontRenderer font, long fullyVisibleForMs) {
        font.drawString("Custom!", 10, 10, 0xFFFFFF);
    }

    @Override
    public void update(ToastManager manager, long fullyVisibleForMs) {
        wantedVisibility = fullyVisibleForMs < 5000L ? Visibility.SHOW : Visibility.HIDE;
    }
}

Style

Package: decok.dfcdvadstf.catframe.ui.Style

Text style class inspired by higher-version Minecraft's net.minecraft.network.chat.Style. Controls color, formatting, click/hover events, etc.

Style style = Style.EMPTY
    .withColor(TextColor.fromRgb(0xFF5555))
    .withBold(true)
    .withItalic(true)
    .withClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, "https://example.com"))
    .withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, Text.literal("Click me!")));

Text styled = Text.literal("Hello", style);

Supported formatting:

Attribute Method
Color withColor(TextColor)
Bold withBold(boolean)
Italic withItalic(boolean)
Underlined withUnderlined(boolean)
Strikethrough withStrikethrough(boolean)
Obfuscated withObfuscated(boolean)
Click event withClickEvent(ClickEvent)
Hover event withHoverEvent(HoverEvent)

FrameLayout

Package: decok.dfcdvadstf.catframe.ui.layouts.FrameLayout

A stacking layout — children are placed in layers, with each child aligned within the frame according to LayoutSettings. The frame size is determined by the largest child (including padding).

FrameLayout frame = new FrameLayout(200, 100);

// Center placement
frame.addChild(someWidget);

// Custom alignment (0.0=left/top, 0.5=center, 1.0=right/bottom)
frame.addChild(otherWidget, settings -> settings.align(0.0F, 1.0F));

frame.recalculate();

Static utility methods:

// Center within a rectangle
FrameLayout.centerInRectangle(widget, x, y, width, height);

// Custom alignment within a rectangle
FrameLayout.alignInRectangle(widget, x, y, width, height, 0.5F, 0.1666F);

Dependency Setup

To use CatFrame as a dependency, add this to your build.gradle:

dependencies {
    implementation 'com.github.song682:CatFrame:Tag'
}

Replace Tag with the version you want from the repository.

Clone this wiki locally