Skip to content

GUI API Guide

XYAT edited this page Oct 10, 2026 · 14 revisions

English | 简体中文

GUI API Guide

KineticCore's client GUI API lets an addon build screens without touching vanilla Screen, GuiGraphics or input callbacks. An addon describes pages; KineticCore hosts them and takes care of canvas scaling, 4K and GUI scale, clipping, tooltips, focus, context menus, dialogs, drafts, layers, scrollbars and input routing. The same addon GUI code runs on 1.20.1, 1.21.1 and 26.1.2: rendering and input changes between Minecraft versions are handled inside KineticCore.

All types live under dev.xyat.kineticcore.api.client.gui unless stated otherwise.

1. Rules for addon GUI code

  • Never import dev.xyat.kineticcore.internal.*; it is not part of the API and changes without notice.
  • GUI classes extend KineticPage, KineticContainerPage, KineticHudEditorPage, KineticCustomControl or KineticRowList — never Screen, AbstractContainerScreen or a vanilla widget.
  • Page code does not take GuiGraphics, Font, PoseStack or raw GLFW key/button numbers, and never uses a ResourceLocation as a texture handle. Use KineticGraphics, KineticText, KineticTexture and the input records. A ResourceLocation as a business or registry ID (recipe types, item IDs, config keys) is fine.
  • Player-visible text goes through KineticI18n.translatable(...), with keys in both zh_cn and en_us. Do not use Component.translatable(...).
  • Mixins are the one exception: code in mixin packages may use vanilla GuiGraphics, because it runs inside vanilla methods. It still must not import internal; take colours from KineticTheme.current().

The build can enforce these rules, see §17.

2. Pages

public final class MyPage extends KineticPage {
    private boolean enabled;

    public MyPage() {
        super(KineticI18n.translatable("gui.mymod.my_page.title"));
        setPausesGame(false);
    }

    @Override
    protected void build(KineticUi ui) {            // runs on open, resize, return from a child page and rebuild()
        ui.toggle(20, 40, 120)
                .value(enabled)
                .labels(KineticI18n.translatable("gui.mymod.on"), KineticI18n.translatable("gui.mymod.off"))
                .onChange(v -> enabled = v)
                .build();
        ui.button(20, 300, 80).text(KineticI18n.translatable("gui.mymod.back")).onClick(this::close).build();
    }

    @Override
    protected void renderBackground(KineticGraphics g, int mouseX, int mouseY, float partialTick) {
        KineticTheme.panel(g, 10, 10, width() - 20, height() - 20);
        g.centeredText(title(), width() / 2, 18, KineticTheme.current().text(), true);
    }
}

Open it with KineticGui.open(new MyPage()) from game code, or openChild(new MyPage()) from another page; closing a child returns to its parent automatically.

build must be repeatable. It runs again on resize, on returning from a child page and after rebuild(). Keep all state in fields and let build read it to create controls. A field that holds a control must be reassigned in every build.

Page members

Purpose Members
Size and title width(), height(), title()
Drawing renderBackground(g, mx, my, pt), renderForeground(g, mx, my, pt) (above controls)
Tooltips renderTooltips(mx, my), calling showTooltip(c), showTooltip(list[, maxWidth]), showFormattedTooltip(seq), showItemTooltip(stack)
Navigation openChild(page), close() (asks onCloseRequested first), navigateBack() (skips it), rebuild()
Pausing setPausesGame(boolean) in the constructor; pages pause by default
Focus focus(c), blur(c), clearFocus(), isFocused(c), focusedControl()
Overlays openContextMenu(x, y, items[, width]), closeContextMenu(), openDialog(title, message, yes, no, onYes, onNo), overlayBlocksInput()
Drafts (unsaved changes) configureDraft, configureStandaloneDraft, reserveStandaloneDraft, commitDraft, discardDraft, hasUnsavedEdits; may be called from the constructor
Clipping g.clipped(l, t, r, b, () -> …) or g.scissor(...) / g.endScissor(), in page coordinates
Entity preview zoom registerPreviewZoomArea(KineticEntityPreview, key, x, y, w, h)
Layout info layoutMetrics(), isCompactLayout(), isPortraitLayout(), isUltrawideLayout()

Use KineticGui.currentPage(MyPage.class) (nullable) to reach an open page from a network handler, and KineticGui.closeScreen() to close the GUI. KineticGui.captureNavigationParent() plus KineticGui.openChild(page, parent) opens a page after a server round-trip, with the back target captured when the request was sent.

3. Coordinates, layouts and layers

  • PageLayout.CANVAS (default): a fixed virtual canvas of at most 640×360, scaled to the window. width()/height() return the canvas size; every hook receives canvas coordinates. Change the design size or safe margin with useCanvas(w, h, margin) in the constructor.
  • PageLayout.NATIVE (super(title, PageLayout.NATIVE)): GUI-scaled screen coordinates; width()/height() return the scaled screen size.
  • KineticContainerPage<M>: hooks receive UI coordinates; only renderScreenOverlay receives screen coordinates.
  • Constants: KineticPage.CANVAS_WIDTH (640), CANVAS_HEIGHT (360), SAFE_MARGIN (6), CONTROL_HEIGHT (16), ITEM_BUTTON_HEIGHT (38), CARD_BUTTON_HEIGHT (26).
  • The host converts every coordinate; pages never convert between canvas and screen space themselves.
  • Layers: builders that support .layer(n) (buttons, toggles, tab bars, every list and grid) draw the control above normal controls. To draw your own content above an item layer, call g.raise(n) inside g.isolated(...).

4. Input

Input arrives as immutable records from api.client.gui.input:

Record Fields and helpers
MouseInput x(), y(), button() (MouseButton: LEFT/RIGHT/MIDDLE/…), rawButton(), modifiers(), isLeft(), isRight(), isMiddle(), hasShift(), hasControl(), hasAlt(), inside(x, y, w, h)
MouseDragInput x(), y(), button(), rawButton(), deltaX(), deltaY()
ScrollInput x(), y(), deltaX(), deltaY(), inside(...)
KeyInput keyCode(), scanCode(), modifiers(), is(KineticKeyBindings.Key), isEscape(), isEnter() (keypad Enter too), hasShift(), hasControl(), hasAlt()
CharInput codePoint(), modifiers(), text()

Page hooks return true when they consumed the event:

Hook When it runs
onMouseClickCapture(MouseInput) Before controls; for things that must win over controls (drag handles, closing popups)
onMouseClick(MouseInput) After no control consumed the click
onMouseRelease(MouseInput) Always, before controls; the release still reaches the controls
onMouseDrag(MouseDragInput) Before controls; return true only while your own drag is active
onMouseScroll(ScrollInput) After controls
onMouseMove(x, y) Every move
onKeyPress, onKeyRelease, onCharTyped Before the focused control; return false for keys you don't handle
onTick() Every client tick
onRemoved() When the host screen is removed
onCloseRequested() Escape or close(); return true if handled (for example to show an "unsaved changes" dialog)

Test keys with input.isEnter(), input.isEscape() or input.is(KineticKeyBindings.Key.X) — never raw GLFW numbers. Some contexts outside pages still pass a raw int button (vanilla or third-party screen events, KineticClientEvents.MouseButtonContext, mixins); test it with KineticMouseButtons.isPrimary/isSecondary/isMiddle(button) or convert it once with MouseButton.of(button).

5. Controls

Controls are created only by the KineticUi passed to build. Every builder accepts tooltip(Component), tooltip(Supplier<Component>), enabled(boolean) and visible(boolean), and ends with .build(), which returns the control.

Control Builder Returns
Button ui.button(x, y, w).text(t).onClick(r); .compact(), .card(), .layer(1) KineticButton
Item button ui.itemButton(x, y, w, stack).text(t).onClick(r) KineticItemButton
Toggle ui.toggle(x, y, w).value(b).labels(on, off).validator(v).onChange(c) KineticToggle
Cycle button ui.cycleButton(x, y, w, options).index(i).onChange(c) KineticCycleButton
Colour button / swatch ui.colorButton(x, y, w, rgb).text(t).onClick(r) / ui.colorSwatch(x, y, rgb).onClick(r) KineticColorButton / KineticColorSwatch
Slider ui.slider(x, y, w).label(l).range(min, max, step).value(v).onChange(c) KineticSlider
Text field ui.textField(x, y, w).label(l).placeholder(p).value(v).maxLength(n).validator(v).onChange(c) KineticTextField
Text area ui.textArea(x, y, w, h).label(l).value(v).maxLength(n).onChange(c) KineticTextArea
Number field ui.numberField(x, y, w, NumberType.INT / LONG / DECIMAL).allowNegative(b).range(min, max).value(n).onChange(c) KineticNumberField
Auto-complete ui.autoComplete(x, y, w, dictionary).value(v).onChange(c).onSelect(s); numbers ui.numberAutoComplete(x, y, w, type, dictionary) KineticAutoCompleteField / KineticNumberAutoCompleteField
Dropdown ui.dropdown(x, y, w, options).selected(value).onChange(c) KineticDropdown
Tab bar ui.tabBar(x, y, w, labels).tooltips(t).selected(i).onSelect(c); .vertical() KineticTabBar
Scrollable tab strip ui.tabStrip(x, y, w, tabs).pinnedLeading(p).selected(i).arrows(prev, next).onSelect(c) KineticTabStrip
Lists ui.selectionList, itemSelectionList, actionList, itemActionList, multiActionList, toggleList, toggleActionList, multiToggleList — (x, y, w, h, items) + .selected(i), .scrollOffset(rows), .onSelect, .onAction, .onToggle Kinetic*List (api.client.gui.widget.list)
ID lists .textRows() on selectionList and toggleList draws plain striped 14 px rows instead of buttons, for biomes, damage types, attributes and other identifiers. The API keeps 2 px between rows; gaps do not select a row. Yellow outlines indicate the current choice; green outlines indicate chosen toggle rows. Selection preserves component text colors. same lists
Item grid ui.itemGrid(x, y, w, h, density, items).scrollOffset(rows).onClick(c) KineticItemGrid
Your own control ui.add(control) the control
Remove a control ui().remove(control)

Item grid spacing. ItemGridDensity.COMPACT uses 22 px slots, a 2 px gap between slots and 2 px of inner padding. Backgrounds reuse one complete 8×8 checker texture, with the same scale on both axes. The texture contains 8 rows and 8 columns of alternating pixels, not four large quadrants. Square slot interiors are filled without a black inset. Only whole tiles are drawn, with padding for any remainder in rectangular regions. Never stretch or crop the background to fill a rectangle. Keep the existing viewport size: its width determines the column count. A static item icon must fit completely inside its slot; reduce the number of rows or columns when necessary. A scrolling viewport may hide items at its edges. Native container and JEI recipe backgrounds keep their own slots.

Control method names. Control interfaces use names that never collide with vanilla widget methods, because the 1.20.1 production JAR renames colliding methods: controlX(), controlY(), controlWidth(), controlHeight(), moveControlX/Y, resizeControlWidth/Height, moveTo(x, y), controlVisible()/setControlVisible, controlHovered(), controlFocused(), contains(x, y); text fields use textValue()/setTextValue, onTextChange, limitTextLength, filterText, formatText, cursorIndex()/setCursorIndex; sliders setSliderValue. isEnabled/setEnabled/setActive and setTooltip(...) keep their usual names.

Selection colours. Yellow marks the current choice (setSelected). Green marks the chosen entries of a multi-select control: rows that are on in toggle lists, and buttons marked with KineticButton.setPicked(true), and checked MenuItem.toggle rows of context menus; use MenuItem.choice for a menu that picks one option, whose current option is yellow. The hovered row or control gets the blue hover outline.

Input text colours are applied by the core; do not draw your own:

State Colour
Empty and unfocused placeholder in pure white (colour styling and § codes in placeholders are ignored)
Value equal to the default cyan
Any other value green
Invalid value red
Custom rule whatever .valueColor(value -> …) returns (null falls back to the rows above)

Set the default with .defaultText(...) / .defaultValue(...), or .firstShownTextAsDefault() when the value is loaded after building. Without a default, any entered text counts as modified.

6. Drawing with KineticGraphics

KineticGraphics (api.client.gui.render) is the only drawing surface pages, custom controls, HUD renderers and render events receive.

Need Call
Rectangles and lines g.fill(x1, y1, x2, y2, argb), g.fillGradient(...), g.outline(x, y, w, h, argb), g.hLine, g.vLine
Text g.text(text, x, y, argb[, shadow]) (no shadow without the flag), g.centeredText(...), g.wrappedText(text, x, y, maxW, argb), g.scrollingText / scrollingTextCentered / scrollingTextRight(...)
Items g.item(stack, x, y[, alpha]), g.fakeItem(stack, x, y), g.itemDecorations(stack, x, y[, text])
Textures static final KineticTexture TEX = KineticTexture.of(ns, path[, texW, texH]); then g.texture(TEX, x, y, u, v, w, h[, argb]) or the scaled g.texture(TEX, x, y, w, h, u, v, rw, rh)
Effect icons g.effectIcon(effect, x, y, size)
Transforms g.push() / g.pop(), g.isolated(() -> …), g.translate(x, y), g.scale(sx, sy), g.rotate(deg), g.raise(n)
Clipping g.clipped(l, t, r, b, runnable), g.scissor(...) / g.endScissor()
Measuring g.textWidth(x), g.lineHeight(); outside rendering KineticText.width / lineHeight / trim / ellipsize / wrap / wrappedHeight / scrollOffset
Entity previews KineticEntityPreview.create(), then preview.render(g, entity or id, key, x, y, w, h, hovered)
HUD size KineticClientRuntime.guiScaledWidth() / guiScaledHeight()

RenderSystem is not used in addon GUI code; every primitive sets up its own state.

Text with limited room

English text is often much wider than Chinese, so any text that sits in a fixed space (a label before an input, a row cell, a card title, a header next to buttons) must be drawn with a width limit:

  • g.scrollingText(text, x, y, maxW, argb, shadow): draws normally when it fits. When it is wider, it is clipped to maxW and scrolls back and forth at a steady speed. scrollingTextCentered and scrollingTextRight do the same centered or right-aligned.
  • maxW is the room up to the next element, not the text's own width: for a label before a control it is controlX - labelX - gap.
  • Text that may span several lines uses g.wrappedText and reserves the height from KineticText.wrappedHeight.
  • Self-drawn mixed content (text plus icons) shifts by KineticText.scrollOffset(contentW, viewportW) inside g.clipped(...), with the same timing.
  • Plain g.text is only for text whose length you control (numbers, short fixed symbols) or that has the whole row to itself.

Buttons, text fields, tabs, lists and dialogs from ui() already do this for their own labels.

KineticTheme draws themed parts: panel, panelAlt, surface, stateSurface, button(g, x, y, w, h, text, hovered, active, error), separator, gridFrame, itemSlot, itemGrid, selectionFlash, stateOutline, indicatorOutline/Fill/Color, colorSwatch, checkerboard, shadow, alphaText, hovering(...), muted(text). Palette colours come from KineticTheme.current(): text(), mutedText(), danger(), background(), ….

7. Lists you draw yourself: KineticRowList

KineticRowList<T> handles smooth scrolling, the themed scrollbar with its hover hint, middle-click jump with selection flash, the wheel, thumb dragging, clipping, zebra rows and up/down key selection. You draw only the row content.

private final class TypeList extends KineticRowList<Row> {
    TypeList() { super(16, 68, 612, 240, 24); }              // x, y, width, height, rowHeight

    @Override
    protected void renderRow(KineticGraphics g, Row row, int index, int x, int y, int w, int h,
                             boolean hovered, boolean selected) {
        g.item(row.icon(), x + 5, y + 4);
        g.text(KineticText.trim(row.name(), 320), x + 28, y + 8, KineticTheme.current().text());
    }

    @Override
    protected boolean onRowClick(Row row, int index, MouseInput input) {
        if (!input.isLeft()) return false;
        select(index);                                          // fires setOnSelect and keeps the row visible
        return true;
    }

    @Override protected Component rowTooltip(Row row, int index) { return row.tooltip(); }
    @Override protected Component emptyText() { return KineticI18n.translatable("gui.mymod.empty"); }
}
// in build():  list = ui.add(new TypeList());  list.setItems(rows);  list.setOnSelect(i -> …);

Other members: items(), setItems, selectedIndex(), selectedItem(), setSelectedIndex(i) (no callback), select(i) (with callback), scrollOffset(), setScrollOffset(rows), scrollTo(i), visibleRows(), rowAt(mx, my), rowsWidth(), rowTop(index), mouseX()/mouseY(); override renderRowBackground to change the zebra background.

Buttons inside rows are painted with KineticTheme.button(...) in renderRow (hover from mouseX()/mouseY()) and handled in onRowClick using rowTop(index) and input.inside(...). If every row is "label + toggle + action", use the built-in toggleActionList / actionList / multiActionList instead.

Hand-written scroll areas use KineticScrollController: update(total, visible) or updateRange(max, total, visible), render(g, …), beginDrag(mx, my, input.button(), x, y, w, h, minThumb), drag, release(input.button()), scroll(deltaY), scrollTo(offset). The thumb hint and middle-click jump need a target:

scroll.bindSelection(() -> selectedIndex >= 0 ? selectedIndex : lastClicked,
        index -> index / COLUMNS - VISIBLE_ROWS / 2);            // item index → scroll offset
// after drawing each item:
scroll.renderSelectionFlash(g, index, cellX, cellY, CELL, CELL);

Real controls that scroll together use a viewport (CANVAS pages only); controls outside it are clipped and get no input, and changing the number of rows needs rebuild():

KineticUi rows = ui.scrollViewport(LIST_X, LIST_Y, LIST_X + LIST_W, LIST_Y + LIST_H, () -> scroll.smoothOffset() * ROW_H);
for (int i = 0; i < entries.size(); i++) {
    int index = i;
    rows.button(x, LIST_Y + i * ROW_H + 6, 30).text(UP).onClick(() -> move(index, -1)).build();
}

8. Custom controls

public final class TrashButton extends KineticCustomControl {
    private static final KineticTexture ICON = KineticTexture.of("mymod", "textures/gui/trash.png", 16, 16);
    private final Runnable action;

    public TrashButton(int x, int y, Runnable action) { super(x, y, 18, 18); this.action = action; }

    @Override
    protected void render(KineticGraphics g, int mx, int my, float pt) {
        KineticTheme.stateSurface(g, controlX(), controlY(), controlWidth(), controlHeight(), KineticTheme.Surface.PANEL, false, controlHovered(), false);
        g.texture(ICON, controlX() + 1, controlY() + 1, 0, 0, 16, 16);
    }

    @Override
    protected boolean onMouseClick(MouseInput in) {
        if (!in.isLeft()) return false;
        playClickSound();
        action.run();
        return true;
    }
}
// page:  ui.add(new TrashButton(10, 10, this::clear)).setTooltip(KineticI18n.translatable("…"));

Hooks: render (required), onMouseClick (inside the bounds), onMouseRelease, onMouseDrag, onMouseScroll, onKeyPress/onCharTyped (while focused; return true from isFocusable()), onFocusChanged, onTick, narration.

Panels and tab modules that make up part of a page receive the page's KineticUi (and the page, for showTooltip, openChild, rebuild) and create their controls inside the page's build. The host draws and routes every control; never render or forward events for controls by hand.

9. Container pages

public final class DummyPage extends KineticContainerPage<DummyMenu> {
    public DummyPage(DummyMenu menu, Component title) { super(menu, title); setImageSize(176, 222); }
    @Override protected void build(KineticUi ui) { … }
    @Override protected void renderContainerBackground(KineticGraphics g, int mx, int my, float pt) { … }
}
// client setup:
KineticClientMenus.register(MyMenus.DUMMY, DummyPage::new);

Container pages add menu(), hoveredSlot(), leftPos(), topPos(), imageWidth(), imageHeight(), setImageSize(w, h), setTitleLabelPosition(x, y) and setInventoryLabelPosition(x, y). By default renderTooltips shows the hovered slot's tooltip; call super.renderTooltips(mx, my) when overriding it. Menu types are registered with KineticMenuTypes.

10. HUD position editors

Extend KineticHudEditorPage, a NATIVE page that does not pause the game and builds the Save/Reset/Cancel buttons itself:

public final class WalletHudEditorPage extends KineticHudEditorPage {
    public WalletHudEditorPage() { super(KineticI18n.translatable("screen.mymod.hud_editor")); }
    @Override protected int elementWidth()  { return Hud.width(); }
    @Override protected int elementHeight() { return Hud.height(); }
    @Override protected HudLayout initialLayout(int sw, int sh) { return new HudLayout(Config.x(), Config.y(), Config.scale()); }
    @Override protected HudLayout defaultLayout(int sw, int sh) { return new HudLayout(sw - elementWidth() - 2, sh - elementHeight() - 2, 1.0D); }
    @Override protected void renderElement(KineticGraphics g, int x, int y, int mx, int my) { Hud.render(g, x, y); }
    @Override protected void save(HudLayout layout) { Config.setLayout(layout.x(), layout.y(), layout.scale()); KTConfigApi.refreshOpenScreens(); }
}

Optional: minimumScale(), instruction(), positionText(HudLayout) (for configs that store offsets from a corner), renderBackdrop, scalable() (fixed-scale elements). Helpers: currentLayout(), scaledElementWidth(), scaledElementHeight(), resetLayout(), and KineticHudEditorPage.renderInventoryReference(g, width(), height(), mx, my) for editors that place something inside the inventory. Draw the HUD itself with KineticClientEvents.onHudRender(stage, (g, pt) -> …).

11. Adding controls to vanilla or third-party screens

KineticClientEvents.onScreenInitAfter(ctx -> {
    if (!(ctx.screen() instanceof InventoryScreen)) return;
    ctx.addButton(x, y, 20, KineticI18n.translatable("…"), action);   // KineticButton
    ctx.addControl(new TrashButton(x, y, action));                       // any KineticCustomControl
    KineticTextField box = ctx.addTextField(x, y, 80);                  // editable text field
});
  • Buttons and custom controls added this way are overlays: drawn after the host screen, they get mouse events before it and never take keyboard focus (safe on the chat screen). addTextField joins the screen normally, because typing needs focus.
  • They are cleared whenever the screen re-initialises; your handler adds them again. ctx.removeControl(control) removes one.
  • Custom controls here get no tick or key hooks; keep their state in sync from KineticClientEvents.onScreenRenderBefore.
  • findExistingListener(type), removeFirstExistingListener(type), addListener and removeListener exist only to adjust widgets the host screen already owns. Anything you add goes through addButton / addTextField / addControl.
  • Use Mixins only to change vanilla behaviour. A Mixin that must draw Kinetic visuals wraps its graphics with KineticGraphicsInterop.wrap(graphics); KineticGraphicsInterop.unwrap(g) hands a vanilla graphics object to third-party APIs that require one.

12. Command input with suggestions

private KineticCommandAssist assist;

@Override protected void build(KineticUi ui) {
    KineticTextField input = ui.textField(44, 278, 552).maxLength(2048).value("/").build();
    assist = KineticCommandAssist.attach(input, width(), height(), false, 10, value -> { /* on change */ });
    focus(input);
}
@Override protected void renderForeground(KineticGraphics g, int mx, int my, float pt) { assist.render(g, mx, my); }
@Override protected boolean onKeyPress(KeyInput in)            { return assist.keyPress(in); }
@Override protected boolean onMouseClickCapture(MouseInput in) { return assist.mouseClick(in); }
@Override protected boolean onMouseScroll(ScrollInput in)      { return assist.mouseScroll(in); }

attach takes over the field's change listener, so pass your own handler as the last argument.

13. Selectors and editors

KineticSelectors opens shared pickers on top of the current page or screen, which becomes their parent:

  • openItemSelector(cb) / openItemSelector(preset, cb) / openItemSelectorWithOptions(...)
  • openEntitySelector(...), openItemListEditor(...), openNbtEditor(initial, onSave), openColorPicker(title, rgb, onApply), openPalette(...)
  • openNbtEditor(initialText, problem, onSave) opens the same editor for text your own rule checks, for example 1.20.5+ item components ([damage=5]), so the screen matches the NBT editor on Forge. problem returns null when the text can be saved, otherwise the error to show; blank text saves as "".
  • KineticCommandListEditor.open(getter, setter, pageId, entryId, text); KineticCommandListEditor.action(...) gives a Runnable for config buttons
  • KineticSelectors.registerInventorySource(...) adds item sources (for example accessory slots from another mod) to the item selector's inventory tab

The default item selector keeps its original viewport: 19 columns and 12 visible rows, with scrolling for the remaining items. The column count follows that viewport rather than expanding the panel. Forge 1.20.1, NeoForge 1.21.1 and NeoForge 26.1.2 use the same layout.

Callbacks run while the picker closes; build runs again on the parent, so update state fields in the callback and let build recreate the controls.

14. Config GUI

Need Call
Open the config index / a page / a mod's pages KTConfigApi.openIndex(), openPage(pageId), openPage(page), openOwner(modId)
Refresh open config screens after a change KTConfigApi.refreshOpenScreens()
A config button that opens your page KTConfigApi.pageAction(XPage::new); another config page KTConfigApi.configPageAction(() -> page)
The mod-list "Config" button KTConfigApi.installConfigHub(modId) or KTConfigApi.installConfigPage(modId, XPage::new)

Config pages themselves are described with KTConfigPage.builder(...) and registered with KTConfigApi.register(...); server-owned values use KTServerConfigSpec.

For a STRING_LIST row, call builder.listEditor(entryId, (title, initial, apply) -> ...) after adding the row. The editor receives an immutable initial draft; call apply.accept(updatedList) only on Apply, and leave it untouched on Cancel. The config page owns final saving, validation and server permissions. The hook works in both openPage(...) and openOwner(...), from KineticCore 26.10.10.1.

15. Events, tooltips and world labels

  • KineticClientEvents.onHudRender(stage, (KineticGraphics g, float pt) -> …) draws on the HUD.
  • onScreenRenderBefore/After((screen, g, mx, my, pt) -> …) draws around any screen. Screen appears in event signatures only to identify vanilla screens; to test for your own GUI use KineticGui.currentPage(MyPage.class).
  • ClientHooks.ResourceReloadUi.render(KineticGraphics, w, h) draws during resource reloads.
  • Item tooltips with icons or custom rows: KineticItemTooltips.registerComponentFactory(type, data -> new MyComponent(...)) where MyComponent implements KineticTooltipComponent (height(), width(), render(KineticGraphics, x, y)).
  • Labels that follow world positions: inside KineticClientEvents.onLevelRender(...), KineticWorldRender.beginScreenOverlay(context) gives project(worldPos) and graphics().
  • The item under the mouse in another mod's list (for copy-item): KineticHoveredItems.register(...).
  • Inventory screens that show the compact status effects implement EffectAreaProvider, so other code can stay clear of the effect area.

16. Common pitfalls

  1. Shadows. g.text(text, x, y, color) draws no shadow; pass true as the fifth argument where vanilla text would have one.
  2. Click order. onMouseClick runs after controls had their chance; work that must happen before them goes in onMouseClickCapture.
  3. Structural changes. Adding or removing rows that own real controls needs rebuild().
  4. Pausing. Pages pause by default; editors opened in game usually want setPausesGame(false).
  5. Escape. "Unsaved changes" dialogs go in onCloseRequested() (return true). A Back button that should ask too calls close(), not navigateBack().
  6. Scrolling. Use in.deltaY() from ScrollInput.
  7. Business rules stay in the addon. The GUI decides how things are shown and operated, never defaults, validation, save timing, permissions or packet formats.

17. Build checks and testing

Architecture check. Copy gradle/kinetic-addon-architecture.gradle from the KineticCore repository and add apply from: 'gradle/kinetic-addon-architecture.gradle' to the addon's build.gradle. Before compileJava it scans the sources and fails on every rule in §1 outside mixins: internal/feature imports, extending vanilla screens or widgets, Button.builder, new EditBox, new ConfirmScreen, Tooltip.create, addRenderableWidget/clearWidgets/this.init(), setScreen/MenuScreens.register/NetworkHooks.openScreen, imports of vanilla GUI types that have a Kinetic replacement, Screen.hasShiftDown()-style modifier checks (use KineticClientRuntime.shiftModifierDown() and friends), Component.translatable and texture ResourceLocations. Each failure names the file, line, rule and the API to use. A genuine exception — such as a JEI category that must draw with vanilla graphics — is declared per file and rule after the apply from line: ext.kineticArchitectureAllow = ['src/main/java/.../ExampleCategory.java': ['vanilla-gui-type']]; an entry that no longer matches anything fails the check.

Final-JAR check (1.20.1). The 1.20.1 production JAR renames methods that collide with vanilla names, so a passing compileJava is not enough. Copy gradle/KineticReobfCheck.java and gradle/kinetic-addon-reobf-verification.gradle, apply the script before the dependencies block, and add a kineticCoreReobf dependency on the same KineticCore JAR as the core dependency. gradlew build then runs checkKineticReobfRefs: every reference from the addon JAR into KineticCore must resolve in the released core JAR (-Pkineticcore_jar=<path> tests against a local build). Manual run: java gradle/KineticReobfCheck.java addon <kineticcore.jar> <addon.jar>.

In-game test for each page: open and close with Escape and Back; resize the window (tests that build is repeatable); open a child page or selector and return; scroll lists with the wheel and by dragging the thumb; hover the thumb for half a second (hint) and middle-click it (jump and flash); tooltips, context menus and dialogs; GUI scale 1–4 and a 4K window.

Clone this wiki locally