Repository navigation
GUI API Guide
English | 简体中文
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.
- Never import
dev.xyat.kineticcore.internal.*; it is not part of the API and changes without notice. - GUI classes extend
KineticPage,KineticContainerPage,KineticHudEditorPage,KineticCustomControlorKineticRowList— neverScreen,AbstractContainerScreenor a vanilla widget. - Page code does not take
GuiGraphics,Font,PoseStackor raw GLFW key/button numbers, and never uses aResourceLocationas a texture handle. UseKineticGraphics,KineticText,KineticTextureand the input records. AResourceLocationas a business or registry ID (recipe types, item IDs, config keys) is fine. - Player-visible text goes through
KineticI18n.translatable(...), with keys in bothzh_cnanden_us. Do not useComponent.translatable(...). - Mixins are the one exception: code in
mixinpackages may use vanillaGuiGraphics, because it runs inside vanilla methods. It still must not importinternal; take colours fromKineticTheme.current().
The build can enforce these rules, see §17.
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.
| 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.
-
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 withuseCanvas(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; onlyrenderScreenOverlayreceives 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, callg.raise(n)insideg.isolated(...).
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).
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.
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.
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 tomaxWand scrolls back and forth at a steady speed.scrollingTextCenteredandscrollingTextRightdo the same centered or right-aligned. -
maxWis the room up to the next element, not the text's own width: for a label before a control it iscontrolX - labelX - gap. - Text that may span several lines uses
g.wrappedTextand reserves the height fromKineticText.wrappedHeight. - Self-drawn mixed content (text plus icons) shifts by
KineticText.scrollOffset(contentW, viewportW)insideg.clipped(...), with the same timing. - Plain
g.textis 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(), ….
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();
}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.
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.
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) -> …).
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).
addTextFieldjoins 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),addListenerandremoveListenerexist only to adjust widgets the host screen already owns. Anything you add goes throughaddButton/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.
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.
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.problemreturnsnullwhen the text can be saved, otherwise the error to show; blank text saves as"". -
KineticCommandListEditor.open(getter, setter, pageId, entryId, text);KineticCommandListEditor.action(...)gives aRunnablefor 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.
| 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.
-
KineticClientEvents.onHudRender(stage, (KineticGraphics g, float pt) -> …)draws on the HUD. -
onScreenRenderBefore/After((screen, g, mx, my, pt) -> …)draws around any screen.Screenappears in event signatures only to identify vanilla screens; to test for your own GUI useKineticGui.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(...))whereMyComponent implements KineticTooltipComponent(height(),width(),render(KineticGraphics, x, y)). - Labels that follow world positions: inside
KineticClientEvents.onLevelRender(...),KineticWorldRender.beginScreenOverlay(context)givesproject(worldPos)andgraphics(). - 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.
-
Shadows.
g.text(text, x, y, color)draws no shadow; passtrueas the fifth argument where vanilla text would have one. -
Click order.
onMouseClickruns after controls had their chance; work that must happen before them goes inonMouseClickCapture. -
Structural changes. Adding or removing rows that own real controls needs
rebuild(). -
Pausing. Pages pause by default; editors opened in game usually want
setPausesGame(false). -
Escape. "Unsaved changes" dialogs go in
onCloseRequested()(returntrue). A Back button that should ask too callsclose(), notnavigateBack(). -
Scrolling. Use
in.deltaY()fromScrollInput. - Business rules stay in the addon. The GUI decides how things are shown and operated, never defaults, validation, save timing, permissions or packet formats.
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.