JelloUI Documentation

JelloUI public API documentation for Minecraft Fabric addon developers.

View the Project on GitHub yoimasama/jelloui-docs

Modules, settings and lifecycle

A module is the leaf unit the ClickGUI lists under a category. You describe a module with ModuleDefinition, JelloUI owns the live state.

ModuleDefinition

Build a definition through ModuleDefinition.Builder:

ModuleDefinition def = ModuleDefinition.builder(owner, JelloId.of("example", "auto_sprint"))
        .displayName("Auto Sprint")
        .description("Sprints automatically.")
        .category(JelloId.of("example", "movement"))
        .settings(List.of(settingA, settingB))
        .lifecycle(new MyLifecycle())
        .defaultEnabled(false)
        .build();
api.modules().register(def);

Keys of the definition:

showInActiveMods and aliases are retained in the public definition as metadata, but the current ClickGUI/profile bridges do not consume them. Do not rely on either field for visible behavior or migration.

A definition is immutable once registered. To change it, unregister and register a new definition.

Live state: ModuleState

JelloUI owns the live state. After registering, ask the module registry:

ModuleState state = api.modules().state(id).orElseThrow();

ModuleState gives you the enabled flag, the bound keybind and typed access to every setting handle:

if (state.isEnabled()) { /* ... */ }
state.setEnabled(false);
state.toggle();

Optional<JelloId> bound = state.keybindId();
state.bindKeybind(JelloId.of("example", "sprint_key"));

Optional<SettingHandle<Double>> speed = state.setting(id, Double.class);
Collection<SettingHandle<?>> all = state.settings();

The Class<T> overload is a compile-time convenience only; the current implementation does not validate the handle’s runtime value type. Keep setting ids and expected types paired in constants and definitions.

ModuleState is thread-confined to the client thread. Mutating it from another thread is not supported.

ModuleLifecycle

ModuleLifecycle is an interface with default no-op implementations:

Every callback runs on the client thread behind JelloUI’s error isolation guard. A failure is reported and swallowed so it cannot abort another module, tick or shutdown pass. A throwing onEnable() callback does not roll back the enabled flag; if partial initialization is unsafe, make the callback transactional or explicitly disable the state after your own cleanup.

For a module that only groups settings or is toggled by other modules, pass ModuleLifecycle.NONE instead of an empty anonymous class.

Settings

Settings are typed value definitions. JelloUI generates the matching widget for each one automatically. The value you store is always the stable form; the renderer is presentation.

Boolean

new BooleanSetting(id, "Enabled", "Master switch.", true);

Number (slider)

NumberSetting.builder(id, "Speed")
        .min(0).max(10).step(0.1).defaultValue(2.5)
        .unit("bps").decimals(1)
        .build();

NumberSetting stores a Double. For integer-only sliders, keep the step and bounds to integers and cast at read time.

The current ClickGUI bridge applies min, max and step. It does not consume unit, decimals or logarithmic; slider formatting is derived from the legacy slider’s step instead.

Mode (dropdown)

ModeSetting.builder(id, "Mode")
        .mode("auto", "Automatic")
        .mode("manual", "Manual")
        .defaultValue("auto")
        .build();

The stored value is the stable key, never the label. Renaming a label does not orphan stored values. The default must be one of the declared modes. The current ClickGUI bridge displays those stable keys and does not consume the separate human-readable labels.

Text

TextSetting.builder(id, "Label")
        .defaultValue("Hello")
        .maxLength(64)
        .placeholder("Type a label")
        .password(false)
        .build();

maxLength and an optional regex validation are enforced when an addon writes through SettingHandle.set. The current legacy text field does not consume maxLength, validation, placeholder or password: it does not mask password text and UI edits can bypass those API-side constraints.

Color

new ColorSetting(id, "Color", "ARGB color.", 0xFF3D7AFF,
        true /* rainbow */, true /* alpha */, false /* defaultRainbow */, null);

The stored value is an ARGB int. Rainbow derives the rendered hue from the system clock, matching the Sigma implementation. defaultRainbow is applied by the current bridge. rainbowSupported and alphaSupported are retained as metadata but are not used to configure the legacy picker, which always offers its rainbow toggle and has no API-controlled alpha mode.

Group

GroupSetting.of(id, "Advanced", "Collapsible.", true, List.of(childSetting));

A collapsible group whose children keep their own ids; the group does not prefix them. Child-id uniqueness is the addon’s responsibility and is not checked against top-level or sibling-group ids. The current ModuleState exposes the group handle but not its child handles, and child visibility predicates are not wired by the legacy bridge. Avoid groups for values that addon code must read or mutate through the public state API.

Action (button)

new ActionSetting(id, "Reset", "Resets the module.", this::reset,
        null /* icon */, true /* destructive */, null);

An action setting has no stored value; pressing it runs the Runnable behind the error isolation guard. The destructive flag changes the ClickGUI accent so it reads as a destructive action. The optional action icon is currently retained as metadata and is not drawn.

Custom (opaque value + renderer)

new CustomSetting<>(id, "Rating", "Rendered by the addon.", defaultValue,
        new MyRenderer(), null);

Custom settings let an addon draw a widget JelloUI has no built-in renderer for. The renderer receives a rectangle and a live handle, and failures are isolated. The current bridge forwards drawing but no generic mouse or keyboard input. The value is opaque and is not automatically serialized; persist it with your own ProfileSectionCodec when needed.

Visibility predicates

Every setting can carry a visibility predicate:

Predicate<SettingVisibilityContext> show =
        ctx -> Boolean.TRUE.equals(ctx.valueOf(masterToggleId, Boolean.class));

new BooleanSetting(id, "Conditional", "Shown when the toggle is on.", false, show);

SettingVisibilityContext resolves another setting’s current value by id. The context is re-evaluated every frame the panel is open, so visibility tracks live changes. An unknown id or a type mismatch returns null; write predicates defensively as in the example above.

Reading and writing values

You read and write a setting through SettingHandle<T>:

SettingHandle<Double> handle = state.setting(id, Double.class).orElseThrow();
double speed = handle.get();
handle.set(3.0);
handle.reset();
handle.isDefault();
AutoCloseable sub = handle.subscribe(value -> { /* on the client thread */ });

set validates through the definition. Invalid values are rejected with ApiUsageException. Subscribers are invoked on the client thread. The handle is thread-confined like the state.

StateAdapter

If your module would rather keep its own typed state view than read handles every frame, implement a StateAdapter<S>:

StateAdapter<MySnapshot> adapter = state -> Optional.of(new MySnapshot(
        state.isEnabled(),
        StateAdapter.value(state.setting(speedId, Double.class), 0.0)));

StateAdapter.value(handle, fallback) is a helper that unwraps an Optional<SettingHandle<T>> with a fallback. StateAdapter is not registered with or scheduled by the current runtime: your addon calls snapshot(state) when it wants a new view.

Keybinds bound to modules

You can bind a keybind to a module so the module toggles when the key fires:

state.bindKeybind(JelloId.of("example", "sprint_key"));
// pass null to clear:
state.bindKeybind(null);

The keybind must already be registered; an unknown id logs a warning and leaves the existing module key unchanged. See 04-surfaces. The current bridge copies the keybind’s raw key code into the legacy module when bindKeybind is called. A later KeybindRegistry.setBinding does not automatically recopy it, and keybindId() exposes a synthetic module-derived id rather than the original registered id. Treat the registry binding as authoritative and call bindKeybind again after rebinding when module-key synchronization matters.