Skip to content

Config menus

VanillaConfigMenu turns a config screen into a few lines: declare a title + ordered list of ConfigEntry, pass an onChange callback, call open(). The framework compiles a vanilla-themed screen, routes every control click and text submit, applies the standard mutation, fires your callback with the updated entry, and re-issues the screen.

Built on the Vanilla GUI kit — same gray-bevel panel, white shadow text, black-outlined buttons.

  • You need a settings / config / options menu in-world.
  • You want toggles, cycles, steppers, text fields, and “Apply” buttons without hand-wiring each click handler.
  • You don’t need pixel-precise custom layout. (Use the kit’s widgets directly if you do.)
import me.zlex.conduit.screen.ScreenId;
import me.zlex.conduit.screen.placement.Placements;
import me.zlex.conduit.screen.ui.config.ConfigEntry;
import me.zlex.conduit.screen.ui.config.VanillaConfigMenu;
int screenId = ScreenId.of("mymod", "settings");
var place = Placements.inFrontOf(player, 2.6f, 1.3f); // farther + raised
VanillaConfigMenu.open(player, screenId, place, 4.0f, 4.0f, "Settings",
List.of(
new ConfigEntry.Toggle ("pvp", "PvP", cfg.pvp),
new ConfigEntry.Cycle ("difficulty", "Difficulty", List.of("Easy", "Normal", "Hard"), 1),
new ConfigEntry.Stepper("max_players", "Max players", cfg.maxPlayers, 2, 16, 1),
new ConfigEntry.Text ("name", "Server name", cfg.name, "Enter a name", 32),
new ConfigEntry.Action ("reset", "Reset to defaults")),
(p, changed) -> {
switch (changed) {
case ConfigEntry.Toggle t -> cfg.pvp = t.value();
case ConfigEntry.Cycle c -> cfg.difficultyIdx = c.selectedIndex();
case ConfigEntry.Stepper s -> cfg.maxPlayers = s.value();
case ConfigEntry.Text tx -> cfg.name = tx.value();
case ConfigEntry.Action a -> { /* "reset" — re-open with defaults */ }
}
});

Done hides the screen and unregisters the menu. Persistence is the consumer’s job.

VariantFieldsClick mutation
Togglekey, label, valueFlips value
Cyclekey, label, options[], selectedIndexAdvances selectedIndex (wraps)
Stepperkey, label, value, min, max, stepAdjusts value via [−]/[+], clamped
Sliderkey, label, value, min, max, stepSets value from click position along the track, snapped to step
Actionkey, labelFires onChange; no auto-reshow
Textkey, label, value, placeholder, maxLengthOpens vanilla text-input popup

Records are immutable. The framework calls flipped / advanced / decremented / incremented / withValue / withFraction for you; consumer code only sees the result in the onChange callback.

Slider and Stepper both edit a clamped, step-snapped int — pick by feel: Stepper for small discrete ranges where exact ±step nudges matter (2–16 players), Slider for wide ranges where you want to point at a target (0–100 volume). The slider is click-to-position, not drag: an in-world screen emits a click rather than mouse-move, so the handle jumps to wherever you click and snaps to the nearest step. Routing goes through ServerScreenManager.onButtonAt (the positional handler carrying the click’s screen-UV) — the framework wires this for you.

PAGE_SIZE = 6. Menus with more entries paginate with [< Prev] Page x/y [Next >] above the Done button. Directional buttons hide when they’d be dead (first / last page). Up to 6 entries render as a single page with no nav, identical to the basic case.

For multi-category configs (the vanilla Options → Video / Audio / Controls pattern), use VanillaConfigMenu.openTabbed(...) with a List<ConfigTab> instead of a flat entry list:

VanillaConfigMenu.openTabbed(player, screenId, place, 4.0f, 4.0f, "Settings",
List.of(
new ConfigTab("Rules", List.of(/* entries */)),
new ConfigTab("World", List.of(/* entries */))),
(p, changed) -> { /* same callback */ });

A tab strip renders across the top; the active tab shows pressed-in with a white underline, its entries below. Pagination still applies within each tab. The flat open(...) is just the single-tab case.

Unlike stateful entries, Action doesn’t auto-reshow — it fires onChange and lets the handler decide what’s next (close the menu, re-open with a fresh entry list, navigate to another menu). The canonical use is a “Reset to defaults” button that re-opens with new entries.

Run the pillars mod’s dev client and /pillars debugconfig — opens a tabbed demo (Rules / World / Server) exercising every entry type, including a Music slider on the World tab, plus pagination.

  • Always uses Theme.vanilla(). Wrap inside a ThemedGroupWidget if you need a vanilla config sub-panel inside a non-vanilla screen.
  • One open menu per (player, screenId). Re-opening replaces the registry entry — safe to call from any handler.
  • Available since engine 0.10.0+mc26.1.2.