mirror of
https://github.com/curioustorvald/Terrarum.git
synced 2026-03-07 12:21:52 +09:00
Page:
UI Framework
Pages
Actor Values Reference
Actors
Animation Description Language
Asset Archiving
Audio Engine Internals
Audio System
Autotiling In Depth
Blocks
Building the App
Developer Portal
Fixtures
Glossary
Inventory
Items
Keyboard Layout and IME
Languages
Lighting Engine
Modules
Modules:Codex Systems
Modules:Setup
OpenGL Considerations
Physics Engine
Rendering Pipeline
Save and Load
Tile Atlas System
UI Framework
World Time and Calendar
World
Clone
1
UI Framework
minjaesong edited this page 2025-11-24 21:24:45 +09:00
Table of Contents
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
- Dispose resources — Always implement dispose() properly
- Cache calculations — Don't recalculate positions every frame
- Use event listeners — Don't poll mouse state manually
- Batch rendering — Group draw calls efficiently
- Handle edge cases — Check bounds, null items, empty lists
- Provide keyboard navigation — Accessibility and convenience
- Test at different resolutions — Ensure UI scales properly
Performance Considerations
- Minimize UIItem count — Too many items slow updates
- Cull off-screen items — Skip rendering invisible elements
- Pool objects — Reuse UIItems when possible
- Optimize render calls — Batch similar draw operations
- 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}")