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 Screen each time the menu opens.
Screen: MenuScreen and ConfirmDialog
Immutable descriptions of one screen. A MenuScreen is a title, a vertical scrolling list of entries and a footer row of buttons; a ConfirmDialog is a yes/no question.
Entry
One widget. The widget set is closed; each has a builder (Button.builder("id")…build()):
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...)), or literal text. UiTexts has ready-made texts such as DONE and CANCEL.

How values and refreshes work

  • The mod owns the state. Widgets such as Toggle, Slider or Cycle hold no value: a value supplier reads it from the mod and an onChange callback writes it back. The widget shows what the supplier returns, so an onChange that ignores the new value leaves the widget unchanged.
  • Exception: typed text. TextInput, NumberField and MultiLineInput keep what the player typed in the entry itself, so invalid text survives refreshes; onChange receives 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 a runAsync callback, and when the mod calls UiHost.refresh(). Call refresh() yourself only when state changes outside an entry action, e.g. in a runOnMainThread task.
  • 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 create and in button actions (host.open(buildDetailsScreen())); don't cache them across menu openings, as a TextInput's typed text and a Section'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 ScreenFactory or 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, runOnMainThread task, runAsync callback) 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.
Builder misuse (a missing required field, a bad id, an invalid range) throws immediately from 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.