Home

Welcome to KotlinMCUI-Fork-26.1, a GUI framework powered by Kotlin DSL, ported and updated for Minecraft 26.1+.

Source Repositories:

Features

Use Cases


Start

Create a Project

Create a Kotlin project using your preferred loader (Fabric/NeoForge).

Add Maven Repository

Using Gradle Kotlin DSL (build.gradle.kts):

repositories {
    mavenCentral()
    maven("https://jitpack.io")
}

Add Dependencies

Add the KotlinMCUI dependency and the 26.1 Backend fork. Example for Fabric 26.1:

dependencies {
    implementation("com.github.2894638479:KotlinMCUI:v1.0.0-alpha.30")
    // Replace with the compiled artifact of the 26.1 backend fork
    modImplementation("io.github.u2894638479:KotlinMCUI-backend:1.0.0-alpha.1+fabric+26.1")
}

Enable Context Parameters

This framework relies on Kotlin's experimental context parameters. Ensure they are enabled in your build script:

val compileKotlin: KotlinCompile by tasks
compileKotlin.compilerOptions {
    freeCompilerArgs.set(listOf("-Xcontext-parameters"))
}

Run and Test

Run your mod and open the Mod Menu. You should see `KotlinMCUI` and `KotlinMCUI-backend`. The backend provides a configuration screen containing several built-in demo pages. Open it to verify that everything renders correctly.

Create a Demo

To display a screen using the framework, call:

dslBackend.showScreen {
    TestPage()
}

If you need the actual Screen object (e.g., to pass back to Minecraft):

val screen = dslBackend.createScreen {
    TestPage()
}.screen as Screen

Note: Do not call these during mod initialization, as the backend isn't fully ready. Trigger them via keybinds or mod menu entry points.

Your First Page

dslBackend.showScreen {
    Button(Modifier.size(60.scaled, 20.scaled)) {}
    DefaultBackground()
}

Entrypoint

KotlinMCUI provides a loader-agnostic entry point. Create a file at resources/META-INF/services/io.github.u2894638479.kotlinmcui.backend.DslEntryService and write the fully qualified name of your registration class inside.

The class must implement DslEntryService and have a no-argument constructor (do not use a Kotlin object).


Basic

Here is an example of a reactive counter button centered on the screen:

var i by remember(0)

Button(Modifier.size(Measure.AUTO_MIN, Measure.AUTO_MIN)) {
    TextFlatten(Modifier.padding(7.scaled)) { "counter: $i".emit() }
}.clickable { i++ }

DefaultBackground()

Layout

Layouts work similarly to Jetpack Compose. Components are resolved horizontally, then vertically (to allow text to wrap properly).

Measure

All sizes, margins, and font sizes use the Measure type (a zero-overhead inline value class wrapping a Double).

Special Values:

These are implemented by extracting special NaN bit patterns, meaning math operations involving them safely propagate the layout intent.

Modifier

Modifiers store layout constraints.

Modifier.padding(10.scaled)
Modifier.weight(2.0)
Modifier.align { left().middleY() }
Modifier.minSize(60.scaled, 40.scaled)
Modifier.size(40.scaled, 20.scaled)

If size is set to AUTO_MIN, the size resolves to the specified minSize (or 0 if unset). If set to AUTO, the parent container dictates the size (usually filling space based on weight).

Functions

UI components and decorators are built as functions requiring a DslContext parameter.

context(ctx: DslContext)
fun MyFunc(modifier: Modifier = Modifier, color: Color, text: String, id: Any) = Column(modifier, id = id) {
    ColorRect(color = color) {}
    TextFlatten { text.emit(color = color) }
}

ID

Every UI component needs an ID to preserve state across frames. Usually, passing a lambda allows KotlinMCUI to use lambda::class as an implicit ID. For loops, use forEachWithId to automatically append the loop element to the ID context.

Row {
    listOf("a","b","c").forEachWithId {
        TextFlatten { it.emit() }
    }
}

Property (State)

Use Kotlin property delegation to hold state that persists across frames.

var expanded by remember(true)
val size by autoAnimate(if(expanded) 50.px else 25.px)

var height by animatable(10.px)

To pass state references to components like Sliders or Text fields, extract the property using .property:

val stringProp by "".remember.property
EditableText(Modifier, stringProp)

Components

Image

Images are referenced via ImageHolder. The width/height provided to the holder define the texture's UV mapping scale, not its render size on screen.

Rendering Strategies: Use ImageStrategy to define cropping, repeating, stretching, or nineSlice behavior.

Text

Use TextFlatten for single-line text (ignores \n), TextFoldable for manual line breaks, or TextAutoFold to automatically wrap text to fill the parent width.

TextFlatten {
    "Red ".emit(color = Color.RED)
    "Italic".emit(style = DslCharStyle().italic)
}

Scroller

Scrollable areas and scrollbars are separated. Link them using a shared Scroller property.

val scrollerProp by Scroller.empty.remember.property
LazyColumn(Modifier, scrollerProp) { /* content */ }
ScrollBarVertical(Modifier.width(20.scaled), scrollerProp) {}

Mouse Tip & Tooltip

MouseTip { ... } renders arbitrary UI components anchored to the cursor's location.

For standard Minecraft tooltips, declare a Tooltip {} somewhere in your layout, and then attach tooltips to individual components using the .tooltip decorator.

Tooltip {} // Enable tooltips in this view

Button(Modifier) {}
    .clickable { }
    .tooltip {
        Column {
            TextFlatten { "Hover Text".emit() }
        }.tooltipBackground()
    }

Slider

Supports Int and Double. Fully keyboard-accessible (use Tab/Arrow keys to select, Enter to focus, Arrow keys to slide).

val valueProp by remember(30.0).property
SliderHorizontal(Modifier.height(20.scaled), 0.0..100.0, valueProp) {
    TextFlatten { "value: ${valueProp.value}".emit() }
}

LateBox

LateBox delays building its children until its own vertical layout phase. This allows you to read the container's final width and height to create highly responsive UIs (e.g., stacking side-by-side vs top-to-bottom based on screen space).

Caveat: Because children are added late, they cannot stretch the `LateBox` itself. The box must have its size determined externally.


Advanced

Custom Components

You can create low-level components by implementing or delegating to DslComponent.

context(ctx: DslContext)
fun MyComponent(modifier: Modifier = Modifier, id: Any) = collect(
    object: DslComponent by DslComponentImpl(newChildId(id), modifier) {
        context(eventModifier: EventModifier, mouse: Position)
        override fun mouseDown(mouseButton: MouseButton): Boolean {
            // handle click
            return true
        }
    }
)

The instance variable inside a DslComponent points to the final, fully-decorated object, while delegate or super calls the underlying layer.

Events

Keyboard and mouse events traverse downwards. If a function like mouseDown or charTyped returns true, the event is consumed and stops propagating.

Scroll events pass down remaining scroll amounts and return unconsumed remainders. Returning 0.0 stops propagation.

Create a Backend (Permanent Warnings for 26.1 Implementers)

If you are writing or maintaining the backend bridge for 26.1 (the kotlinmcuibackend mod), pay extremely close attention to the following architectural changes required by newer Minecraft versions. The 1.20.1 implementation strategies will fail or crash if used directly in 26.1.

1. Lifecycle Shift: extractRenderState replaces render

Minecraft 26.1 separates the tick loop from rendering. UI drawing no longer happens in render(...). You must override extractRenderState(guiGraphics: GuiGraphicsExtractor, ...) instead.

Crucial: Because you are injecting into a state extraction phase, you must manage matrix stacks correctly: use guiGraphics.pose().pushMatrix() and .popMatrix() to wrap the DSL render call, applying your 1 / guiScale downscale there.

2. Sprite-Based Rendering (No more manual 9-slicing)

Buttons, slots, and containers no longer use a manual 9-slice UV map targeting generic textures like inventory.png. You must use renderParam.blitSprite() with RenderPipelines.GUI_TEXTURED.

  • Buttons: Identifier.parse("minecraft:widget/button") (handle disabled/highlighted states accordingly).
  • Containers: Identifier.parse("minecraft:popup/background").
  • Slots: Identifier.parse("minecraft:container/slot/slot").

3. The ItemStack Bootstrap NPE

If your UI renders an Item early in the game launch cycle (e.g., on the title screen configuration), accessing BuiltInRegistries.ITEM.getOptional(...).get().defaultInstance can throw a NullPointerException: "Components not bound yet".

This happens because extractRenderState executes before the registry bootstrap fully binds item components. Wrap item generation in a try-catch block and gracefully degrade to a missing image (e.g., a "missing" ImageHolder) until the game finishes loading.

4. Character Input Guard (Android/Custom Launcher Bug)

In your Screen implementation, the charTyped method must actively filter out ISO control characters:

val charVal = event.codepoint().toChar()
if (Character.isISOControl(charVal)) return true

Some custom launchers/Android bridges (like Zalith Launcher) pass raw keystrokes like \n (Enter) down the character-typed pipeline instead of as proper KeyEvents. Without this guard, single-line editable text fields will ingest newlines and break the layout.

5. Scissor Coordinates and Double-Scaling

When implementing withScissor, realize that renderParam.enableScissor() in 26.1 automatically assumes logical GUI coordinates and handles physical pixel math internally.

If you divide your clipping rectangle by guiScale before passing it to enableScissor (which was required in 1.20.1), you will double-scale the clip box, causing UI elements to prematurely vanish when the GUI scale is greater than 1x. Pass the raw, integer-casted Rect coordinates directly.