Package dev.xichil.mvui
package dev.xichil.mvui
The API of Xi's Multiversion UI Lib (
xis_mvui): declarative, version-independent menus for Minecraft mods.
A mod describes its configuration screens once, with the plain Java widgets of this package, and registers the
first screen with MultiversionUi.register. At runtime the library
picks the backend for the running Minecraft version and loader (1.7.10 – 26.x; Fabric, Quilt, Legacy Fabric, Forge,
NeoForge) and draws the screens with that version's own vanilla GUI classes. This package contains no Minecraft
classes and depends on nothing but the JDK (Java 8), so a mod compiles against it once for every version.
Quick start
// 1. Describe the menu. create() runs every time the player opens it.
final class MyMenu implements ScreenFactory {
private final MyConfig config;
MyMenu(MyConfig config) {
this.config = config;
}
public Text buttonLabel() {
return Text.translatable("mymod.menu", "My Mod...");
}
public Screen create(UiHost host) {
return MenuScreen.builder("main")
.title(Text.translatable("mymod.title", "My Mod"))
.add(Toggle.builder("sounds")
.label(Text.translatable("mymod.sounds", "Sounds"))
.value(() -> config.sounds) // read on every refresh
.onChange(v -> config.sounds = v) // the value lives in the mod
.build())
.add(Slider.builder("volume")
.label(Text.translatable("mymod.volume", "Volume"))
.range(0, 100).step(5)
.value(() -> config.volume)
.onChange(v -> config.volume = (int) v)
.enabled(() -> config.sounds) // greyed out while sounds are off
.build())
.footer(Button.builder("done")
.label(UiTexts.DONE)
.onPress(() -> { config.save(); host.back(); })
.build())
.build();
}
}
// 2. Register it once, on the client, while the game starts:
// Fabric / Quilt / Legacy Fabric: in the client entrypoint; Forge / NeoForge: in the @Mod constructor.
MultiversionUi.register("mymod", Text.translatable("mymod.name", "My Mod"), new MyMenu(config));
The player then finds the menu behind a button on the Options screen and, depending on the loader, behind the mod
list's Config button (see MultiversionUi).
Core concepts
ScreenFactory- What a mod registers. Builds the menu's first
Screeneach time the menu opens. Screen:MenuScreenandConfirmDialog- Immutable descriptions of one screen. A
MenuScreenis a title, a vertical scrolling list ofentriesand a footer row of buttons; aConfirmDialogis a yes/no question. Entry- One widget. The widget set is closed; each has a builder (
Button.builder("id")…build()):- Actions and text:
Button,Label,StatusLine. - Booleans:
Toggle("label: ON"),Checkbox. - One of several values:
Cycle(few values),Dropdown(many values),SelectList(a scrolling list of rows). - Numbers:
Slider,NumberField. - Text:
TextInput(one line),MultiLineInput,StringListEditor(a list of strings). - Other:
Keybind(withKey),ColorPicker. - Grouping:
Section, optionally collapsible.
- Actions and text:
UiHost- Handed to
ScreenFactory.create(UiHost); opens and closes screens (open,back,close), refreshes the current one, and runs work on the main thread or in the background (runOnMainThread,runAsync). Keep it to use from the screen's actions. Text- Every user-visible string: a translation key with an English fallback
(
Text.translatable(String, String, Object...)), orliteraltext.UiTextshas ready-made texts such asDONEandCANCEL.
How values and refreshes work
- The mod owns the state. Widgets such as
Toggle,SliderorCyclehold no value: avaluesupplier reads it from the mod and anonChangecallback writes it back. The widget shows what the supplier returns, so anonChangethat ignores the new value leaves the widget unchanged. - Exception: typed text.
TextInput,NumberFieldandMultiLineInputkeep what the player typed in the entry itself, so invalid text survives refreshes;onChangereceives only valid text. - Suppliers are re-read on refresh. Labels, values,
enabled,visible, tooltips and list rows are suppliers. The backend reads them when a screen opens, after every entry action (press, toggle, edit…), after arunAsynccallback, and when the mod callsUiHost.refresh(). Callrefresh()yourself only when state changes outside an entry action, e.g. in arunOnMainThreadtask. - Static or dynamic. Every supplier setter has a constant overload:
.label(Text)for fixed text,.label(() -> …)for text that changes. - Screens are cheap. Build screens fresh in
createand in button actions (host.open(buildDetailsScreen())); don't cache them across menu openings, as aTextInput's typed text and aSection's expanded state live in the built objects.
Ids
Every screen and entry has an id matching[A-Za-z0-9_.-]+. Entry ids must be unique within one
MenuScreen (entries, section children and footer together); building a screen with a
duplicate throws. Ids are used by backends and by tests (FakeHost.click("save")), never shown to players.
Composite widgets derive ids for their parts from their own id: <id>.list, <id>.input, ….
Threads
Builders may run on any thread. Everything a backend calls on a built screen (suppliers,onChange,
onPress, dialog answers, runAsync callbacks) runs on the game's main (client) thread, as do
UiHost methods other than runOnMainThread. Long or blocking work (file IO, network)
belongs in UiHost.runAsync(Callable, BiConsumer), which shows a "Working…" state and calls back on the main
thread.
Failures
The library isolates mods from each other (Failsafe):
- If a mod's
ScreenFactoryor one of its entries' suppliers throws while a screen is built or refreshed, only that mod's menu turns off for the session (logged once, naming the mod); other mods' menus keep working. - If an action (
onPress,onChange, dialog answer,runOnMainThreadtask,runAsynccallback) throws, the error is logged and the menu stays open. - If the library's own backend fails (e.g. a Minecraft update renamed something), every menu turns off; the game and the mods keep running.
build(),
with a message naming the widget and field, so it shows up in development.
Testing menus without Minecraft
The API's test fixtures (testImplementation(testFixtures("dev.xichil.mvui:xis-mvui-api:<version>")))
contain dev.xichil.mvui.fake.FakeHost, a UiHost that renders the current screen as
plain English text and drives entries by id:
FakeHost host = new FakeHost();
host.open(new MyMenu(config).create(host));
host.click("sounds"); // flips the toggle
host.slide("volume", 40);
assertTrue(host.render().contains("Volume: 40"));
For backend authors
UiBackend, BackendDescriptor, BackendRegistry,
UiPlatform, EntryVisitor, MenuWorkers,
TextAreaModel, ForcedFailure and most of
Failsafe are the contract between this API and the library's per-version backends. Mods
don't need them. Entry methods that backends call (such as Toggle.set(boolean),
Slider.setPosition(double) or Keybind.capture(Key)) are public so backends and
tests can drive widgets; a mod rarely calls them.-
ClassDescriptionWhat one backend serves (TODO.md 0.6, 5.3): per platform, the DataVersion range it was checked against, plus the runtime names it is remapped to.The backends a jar bundles, and which one serves a running game (TODO.md 5.3).A push button, drawn as the version's vanilla button.Builds a
Button; fromButton.builder(String).A check box with its label to the right, like vanilla'sCheckbox(1.16+; older versions get an equivalent drawing).Builds aCheckbox; fromCheckbox.builder(String).A color setting: a button "label: #RRGGBB" with a swatch of the color on it.Builds aColorPicker; fromColorPicker.builder(String).A yes/no question on its own screen, drawn as the version's vanilla confirm screen (like "Delete world?").Builds aConfirmDialog; fromConfirmDialog.builder(String).Cycle<T>A button that steps through a fixed list of values, shown as "label: value" like vanilla's option buttons (e.g.Builds aCycle; fromCycle.builder(String).Dropdown<T>Picks one of a list of values, for lists too long to step through with aCycle: languages, biomes, profiles.Builds aDropdown; fromDropdown.builder(String).Entry.Builder<B extends Entry.Builder<B>>The options every entry builder has.EntryVisitor<R>One method per entry type, for code that must handle every widget: a backend implements it to create the vanilla widget for each entry, and the test fixture'sFakeHostto render and drive them.The fail-safe around every backend entry point (D9, TODO.md 9.1): Options screen hooks, opening the menu, the mod list's config screen, a menu screen's init and drawing, andGameBridgecalls.Test switch for the fail-safe paths (D9): with-Dxis_mvui.forceError=trueevery backend hook that callsForcedFailure.check(String)fails as if Minecraft had changed under it, so integration tests can show that the game and sync keep working without the menu.The running game's options file, for menus that save or restore it (TODO.md §3.5): e.g.A keyboard key or mouse button that aKeybindcan be set to, the same in every Minecraft version.Whether aKeyis a keyboard key or a mouse button.A key binding button, as on vanilla's Controls screen: "label: key".Builds aKeybind; fromKeybind.builder(String).Read-only text on its own row: an explanation, a heading, or a value to show.Builds aLabel; fromLabel.builder(String).The main screen type: a title, a vertical, scrolling list ofentries, and a row of footer entries at the bottom (usually "Done").Builds aMenuScreen; fromMenuScreen.builder(String).Where backends runUiHost.runAsync(Callable, BiConsumer)tasks: one daemon thread per mod, so a mod whose task hangs (store IO waiting on another instance's file lock) holds up only its own menu.A text box of several lines, for notes, descriptions or lists the player types freely.Builds aMultiLineInput; fromMultiLineInput.builder(String).Where mods register their menus with Xi's Multiversion UI Lib, and the static entry point of the API.One successfulMultiversionUi.register(String, Text, ScreenFactory)call: a mod id, its display name and its guarded menu.WhatMultiversionUineeds from the library's runtime (core, which implements it): the part that knows the running game and its backend.NumberField<N extends Number>ATextInputfor a number, optionally between a minimum and a maximum.NumberField.Builder<N extends Number>WhatGameBridge.reloadOptions()could not apply in the running game: the options that take effect only after a restart.Something aUiHostcanopen: exactly one ofMenuScreen(a list of widgets) orConfirmDialog(a yes/no question).A mod's menu: builds the menu's first screen each time the player opens it.A titled group of entries on aMenuScreen.Builds aSection; fromSection.builder(String).A scrolling list of rows of text, at most one of them selected, like vanilla's world or server lists.Builds aSelectList; fromSelectList.builder(String).One row of aSelectList: an id the mod knows it by, and the text shown.A vanilla slider over a range of numbers, showing "label: value" on the slider itself, like vanilla's FOV or volume sliders.Builds aSlider; fromSlider.builder(String).A line the screen's actions write their results to, e.g.Builds aStatusLine; fromStatusLine.builder(String).The severity of a message; backends pick its color from it (e.g.Edits a list of unique strings, such as server addresses, item ids or ignored player names: a list of the items, a text field for a new item, and Add and Remove buttons.Builds aStringListEditor; fromStringListEditor.builder(String).A piece of user-visible text, independent of Minecraft's text classes (which differ between versions).The editing logic of aMultiLineInputbox, for backends of versions without vanilla'sMultiLineEditBox(before 1.19.3, TODO.md Phase 11): text, cursor, word wrap, scrolling and the keys, so such a backend only draws the box and feeds it input.One line as drawn:text().substring(start, end), without its line break.A one-line text field with a label above it and an error line below it, like the name field of vanilla's world-creation screen.Builds aTextInput; fromTextInput.builder(String).An on/off setting, drawn as a vanilla button reading "label: ON" or "label: OFF" (UiTexts.ON,UiTexts.OFF), like the toggles on vanilla's options screens.Builds aToggle; fromToggle.builder(String).A backend's entry point: the classBackendDescriptor.entryClassName()names, with a public no-argument constructor.Shows a menu's screens and runs work for them.The mod loader a backend serves (TODO.md 0.6).Ready-made texts: the ones the widgets themselves need, and common button labels mods can reuse.