1
UI Framework
minjaesong edited this page 2025-11-24 21:24:45 +09:00

UI Framework

Terrarum features a comprehensive UI framework built on a canvas-based architecture with event handling, animations, and a rich set of pre-built UI components.

Overview

The UI system provides:

  • UICanvas — Base class for all UI screens
  • UIItem — Individual UI elements (buttons, sliders, text fields)
  • UIHandler — Manages lifecycle and animations
  • Event system — Mouse, keyboard, and gamepad input
  • Layout management — Positioning and sizing
  • Built-in components — Buttons, lists, sliders, and more

Architecture

UICanvas

The base class for all UI screens and windows:

abstract class UICanvas(
    toggleKeyLiteral: String? = null,
    toggleButtonLiteral: String? = null,
    customPositioning: Boolean = false
) : Disposable {

    abstract var width: Int
    abstract var height: Int

    val handler: UIHandler
    val uiItems: List<UIItem>

    abstract fun updateImpl(delta: Float)
    abstract fun renderImpl(batch: SpriteBatch, camera: OrthographicCamera)
}

UIHandler

Manages the canvas lifecycle:

class UIHandler(
    val toggleKey: String?,
    val toggleButton: String?,
    val customPositioning: Boolean
) {
    var posX: Int
    var posY: Int
    var isOpened: Boolean
    var isOpening: Boolean
    var isClosing: Boolean
}

Creating a UI

Basic UICanvas Example

class MyUI : UICanvas() {

    override var width = 800
    override var height = 600

    init {
        // Position the UI
        handler.initialX = (App.scr.width - width) / 2
        handler.initialY = (App.scr.height - height) / 2

        // Add UI items
        addUIItem(
            UIItemTextButton(
                this,
                text = "Click Me",
                x = 50,
                y = 50,
                width = 200,
                clickOnceListener = { _, _ ->
                    println("Button clicked!")
                }
            )
        )
    }

    override fun updateImpl(delta: Float) {
        // Update logic
    }

    override fun renderImpl(batch: SpriteBatch, camera: OrthographicCamera) {
        // Custom rendering
        batch.color = Color.WHITE
        // Draw UI background, decorations, etc.
    }

    override fun dispose() {
        // Clean up resources
    }
}

UI Lifecycle

Show/Hide Timeline

The UI lifecycle follows this sequence:

User triggers open
    ↓
show()           -- Called once when opening starts
    ↓
doOpening()      -- Called every frame while opening
doOpening()
    ↓
endOpening()     -- Called once when fully opened
    ↓
(UI is open)
    ↓
User triggers close
    ↓
doClosing()      -- Called every frame while closing
doClosing()
    ↓
endClosing()     -- Called once when fully closed
    ↓
hide()           -- Called once when hidden

Override Points

override fun show() {
    super.show()
    // Initialize when opening
}

override fun hide() {
    super.hide()
    // Clean up when closing
}

override fun doOpening(delta: Float) {
    super.doOpening(delta)
    // Opening animation logic
}

override fun doClosing(delta: Float) {
    super.doClosing(delta)
    // Closing animation logic
}

override fun endOpening(delta: Float) {
    super.endOpening(delta)
    // Finalize opening
}

override fun endClosing(delta: Float) {
    super.endClosing(delta)
    // Finalize closing
}

UIItem Components

UIItems are individual UI elements added to a canvas.

Base UIItem

abstract class UIItem(
    val parentUI: UICanvas,
    var posX: Int,
    var posY: Int
) {
    abstract var width: Int
    abstract var height: Int

    var clickOnceListener: ((Int, Int) -> Unit)? = null
    var touchDraggedListener: ((Int, Int, Int, Int) -> Unit)? = null
    var keyDownListener: ((Int) -> Unit)? = null
    // ... more event listeners
}

Built-in Components

UIItemTextButton

Clickable button with text:

UIItemTextButton(
    parentUI = this,
    text = "OK",
    x = 100,
    y = 100,
    width = 150,
    clickOnceListener = { _, _ ->
        // Handle click
    }
)

UIItemImageButton

Button with an image:

UIItemImageButton(
    parentUI = this,
    image = texture,
    x = 50,
    y = 50,
    width = 64,
    height = 64,
    clickOnceListener = { _, _ ->
        // Handle click
    }
)

UIItemHorzSlider

Horizontal slider:

UIItemHorzSlider(
    parentUI = this,
    x = 50,
    y = 100,
    width = 200,
    min = 0.0,
    max = 100.0,
    initial = 50.0,
    changeListener = { newValue ->
        println("Value: $newValue")
    }
)

UIItemTextLineInput

Single-line text input:

UIItemTextLineInput(
    parentUI = this,
    x = 50,
    y = 50,
    width = 300,
    placeholder = "Enter text...",
    textCommitListener = { text ->
        println("Entered: $text")
    }
)

UIItemTextArea

Multi-line text area:

UIItemTextArea(
    parentUI = this,
    x = 50,
    y = 50,
    width = 400,
    height = 200,
    readOnly = false
)

UIItemList

Scrollable list:

UIItemList<String>(
    parentUI = this,
    x = 50,
    y = 50,
    width = 300,
    height = 400,
    items = listOf("Item 1", "Item 2", "Item 3"),
    itemRenderer = { item, y ->
        // Render item
    },
    clickListener = { item ->
        println("Selected: $item")
    }
)

UIItemToggleButton

Toggle button (on/off):

UIItemToggleButton(
    parentUI = this,
    x = 50,
    y = 50,
    width = 150,
    labelText = "Enable Feature",
    initialState = false,
    changeListener = { isOn ->
        println("Toggle: $isOn")
    }
)

Event Handling

Mouse Events

val button = UIItemTextButton(...)

button.clickOnceListener = { mouseX, mouseY ->
    // Single click
}

button.touchDownListener = { screenX, screenY, pointer, button ->
    // Mouse button pressed
}

button.touchUpListener = { screenX, screenY, pointer, button ->
    // Mouse button released
}

button.touchDraggedListener = { screenX, screenY, deltaX, deltaY ->
    // Mouse dragged while holding
}

Keyboard Events

item.keyDownListener = { keycode ->
    when (keycode) {
        Input.Keys.ENTER -> handleEnter()
        Input.Keys.ESCAPE -> handleEscape()
    }
}

item.keyTypedListener = { character ->
    handleTypedChar(character)
}

Scroll Events

item.scrolledListener = { amountX, amountY ->
    // Mouse wheel scrolled
    scrollOffset += amountY * scrollSpeed
}

Positioning

Absolute Positioning

// Position relative to canvas
uiItem.posX = 100
uiItem.posY = 50

Relative Positioning

// Center horizontally
uiItem.posX = (width - uiItem.width) / 2

// Align to right
uiItem.posX = width - uiItem.width - margin

// Align to bottom
uiItem.posY = height - uiItem.height - margin

Custom Positioning

override fun updateUI(delta: Float) {
    // Dynamic positioning
    followButton.posX = targetX - followButton.width / 2
    followButton.posY = targetY - followButton.height / 2
}

Sub-UIs

UICanvases can contain child UIs:

class ParentUI : UICanvas() {

    val childUI = ChildUI()

    init {
        addSubUI(childUI)
    }
}

Child UIs:

  • Have their own lifecycle
  • Can be opened/closed independently
  • Positioned relative to parent

Animations

Opening Animation

override fun doOpening(delta: Float) {
    // Fade in
    opacity = min(1.0f, opacity + delta * fadeSpeed)

    // Slide in from top
    handler.posY = lerp(startY, targetY, openProgress)

    super.doOpening(delta)
}

Closing Animation

override fun doClosing(delta: Float) {
    // Fade out
    opacity = max(0.0f, opacity - delta * fadeSpeed)

    // Slide out to bottom
    handler.posY = lerp(targetY, endY, closeProgress)

    super.doClosing(delta)
}

Mouse Control

UIs can be mouse-controlled:

interface MouseControlled {
    fun touchDragged(screenX: Int, screenY: Int, pointer: Int): Boolean
    fun touchDown(screenX: Int, screenY: Int, pointer: Int, button: Int): Boolean
    fun touchUp(screenX: Int, screenY: Int, pointer: Int, button: Int): Boolean
    fun scrolled(amountX: Float, amountY: Float): Boolean
}

Keyboard Control

UIs can be keyboard-navigable:

interface KeyControlled {
    fun keyDown(keycode: Int): Boolean
    fun keyUp(keycode: Int): Boolean
    fun keyTyped(character: Char): Boolean
}

Tooltips

UIItems can display tooltips on hover:

override fun getTooltipText(mouseX: Int, mouseY: Int): String? {
    return if (mouseOver) {
        "This is a helpful tooltip"
    } else {
        null
    }
}

The tooltip system automatically displays text near the cursor.

Toolkit Utilities

The Toolkit object provides UI utilities:

object Toolkit {
    val drawWidth: Int       // UI render area width
    val drawHeight: Int      // UI render area height

    fun drawBoxBorder(batch: SpriteBatch, x: Int, y: Int, w: Int, h: Int)
    fun drawBoxFilled(batch: SpriteBatch, x: Int, y: Int, w: Int, h: Int)
}

Common Patterns

Modal Dialog

class ConfirmDialog(
    val message: String,
    val onConfirm: () -> Unit
) : UICanvas() {

    override var width = 400
    override var height = 200

    init {
        // Center on screen
        handler.initialX = (App.scr.width - width) / 2
        handler.initialY = (App.scr.height - height) / 2

        // Yes button
        addUIItem(UIItemTextButton(
            this, "Yes",
            x = 50, y = 150, width = 100,
            clickOnceListener = { _, _ ->
                onConfirm()
                handler.setAsClose()
            }
        ))

        // No button
        addUIItem(UIItemTextButton(
            this, "No",
            x = 250, y = 150, width = 100,
            clickOnceListener = { _, _ ->
                handler.setAsClose()
            }
        ))
    }

    override fun renderImpl(batch: SpriteBatch, camera: OrthographicCamera) {
        // Draw background
        Toolkit.drawBoxFilled(batch, posX, posY, width, height)

        // Draw message
        App.fontGame.draw(batch, message, posX + 50f, posY + 50f)
    }
}

Settings Screen

class SettingsUI : UICanvas() {

    val volumeSlider = UIItemHorzSlider(...)
    val fullscreenToggle = UIItemToggleButton(...)

    init {
        addUIItem(volumeSlider)
        addUIItem(fullscreenToggle)

        // Apply button
        addUIItem(UIItemTextButton(
            this, "Apply",
            clickOnceListener = { _, _ ->
                applySettings()
            }
        ))
    }

    private fun applySettings() {
        App.audioMixer.masterVolume = volumeSlider.value.toFloat()
        setFullscreen(fullscreenToggle.isOn)
    }
}

Inventory Grid

class InventoryUI : UICanvas() {

    val inventoryGrid = UIItemInventoryItemGrid(
        parentUI = this,
        inventory = player.inventory,
        x = 50,
        y = 50,
        columns = 10,
        rows = 5,
        cellSize = 48
    )

    init {
        addUIItem(inventoryGrid)
    }
}

Best Practises

  1. Dispose resources — Always implement dispose() properly
  2. Cache calculations — Don't recalculate positions every frame
  3. Use event listeners — Don't poll mouse state manually
  4. Batch rendering — Group draw calls efficiently
  5. Handle edge cases — Check bounds, null items, empty lists
  6. Provide keyboard navigation — Accessibility and convenience
  7. Test at different resolutions — Ensure UI scales properly

Performance Considerations

  1. Minimize UIItem count — Too many items slow updates
  2. Cull off-screen items — Skip rendering invisible elements
  3. Pool objects — Reuse UIItems when possible
  4. Optimize render calls — Batch similar draw operations
  5. Lazy initialization — Create heavy UIs only when needed

Debugging

Debug Overlays

if (App.IS_DEVELOPMENT_BUILD) {
    // Draw UIItem bounds
    shapeRenderer.rect(posX, posY, width, height)

    // Show mouse position
    println("Mouse: ($mouseX, $mouseY)")
}

UI State Logging

println("UI opened: ${handler.isOpened}")
println("UI opening: ${handler.isOpening}")
println("UI closing: ${handler.isClosing}")

See Also