Home
Welcome to KotlinMCUI-Fork-26.1, a GUI framework powered by Kotlin DSL, ported and updated for Minecraft 26.1+.
Source Repositories:
Features
- Broad Binary Compatibility:
- Does not directly depend on Fabric/Forge/NeoForge APIs.
- Does not directly depend on Minecraft code (the Backend handles the bridge).
- Provides cross-platform entry points.
- Declarative & Reactive Layout (Kotlin DSL):
- Vanilla Minecraft styling.
- Global and local UI scaling.
- Full keyboard navigation support.
- Text Rendering: Translation, colors, sizing, auto-wrapping, editable text fields, styles (underline, shadow, etc.).
- Image Rendering: Resource locations, local files, cropping strategies, NineSlice.
- Focus management, hover highlights, and accessibility narration.
- Real-time state updates using local variables and interpolated animations.
- Built-in Components:
Button,Slider,Container,Slot,Item,EditableText,Row,Column,LazyColumn, and more.
Use Cases
- Standalone UI Services: (e.g., local file managers, network queries). KotlinMCUI provides a binary-compatible layer so your UI code survives version migrations untouched.
- Minecraft Logic UIs: Simplifies complex UI creation using a beautiful Jetpack Compose-like DSL, while rendering in Immediate Mode (perfect for games).
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).
- Box: Stacks items on top of each other. Later items render above earlier ones.
- Row: Arranges items horizontally, sharing available space.
- Column: Arranges items vertically, sharing available space.
Measure
All sizes, margins, and font sizes use the Measure type (a zero-overhead inline value class wrapping a Double).
10.px: 10 literal pixels.10.scaled: 10 pixels scaled by Minecraft's current GUI scale. (Recommended for almost everything)
Special Values:
Measure.AUTOMeasure.AUTO_MIN
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)
remember: Keeps a value steady across redraws.animatable: A mutable property that automatically interpolates changes over time.autoAnimate: A read-only property that smoothly interpolates based on another target value.
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.
- Resource Images:
imageResource("minecraft:textures/block/dirt.png", 16.px, 16.px) - Local Files:
imageFile(file)orbackend.loadLocalImage(file). This is async and returns an empty holder immediately, re-rendering once loaded.
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.