Add a simple GDX application to help with developing UI components (#7411)

* Add a simple GDX application to help with developing UI components

* Add UI development docs
This commit is contained in:
Timo T 2022-07-13 20:58:43 +02:00 committed by GitHub
parent 749ba406d0
commit 83d5d1ab36
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 207 additions and 0 deletions

View File

@ -133,9 +133,13 @@ project(":core") {
dependencies {
"implementation"(project(":core"))
"implementation"("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.1")
"implementation"("junit:junit:4.13.1")
"implementation"("org.mockito:mockito-all:1.10.19")
"implementation"("com.badlogicgames.gdx:gdx-backend-lwjgl3:${gdxVersion}")
"implementation"("com.badlogicgames.gdx:gdx-platform:$gdxVersion:natives-desktop")
"implementation"("com.badlogicgames.gdx:gdx-backend-headless:$gdxVersion")
"implementation"("com.badlogicgames.gdx:gdx:$gdxVersion")

View File

@ -0,0 +1,29 @@
# UI Development
Unciv is backed by [GDX's scene2d](https://libgdx.com/wiki/graphics/2d/scene2d/scene2d) for the UI, so check out [their official documentation](https://libgdx.com/wiki/graphics/2d/scene2d/scene2d) for more info about that.
We mainly use the [`Table` class](https://libgdx.com/wiki/graphics/2d/scene2d/table) of scene2d, because it offers nice flexibility in laying out all the user interface.
## The `FasterUIDevelopment` class
This class is basically just a small helper GDX application to help develop UI components faster.
It sets up the very basics of Unciv, so that you can then show one single UI component instantly. This gives you much faster response times for when you change something, so that you can immediately see the changes you made, without having to restart the game, load a bunch of stuff and navigate to where your UI component would actually be.
To use it, you change the `DevElement` class within the `FasterUIDevelopment.kt` file so that the `actor` field is set to the UI element you want to develop. A very basic usage is there by default, just showing a label, but you can put any UI element there instead.
```kotlin
class DevElement(
val screen: UIDevScreen
) {
lateinit var actor: Actor
fun createDevElement() {
actor = "This could be your UI element in development!".toLabel()
}
fun afterAdd() {
}
}
```
You can then simply run the `main` method of `FasterUIDevelopment` to show your UI element.

View File

@ -0,0 +1,174 @@
package com.unciv.dev
import com.badlogic.gdx.Game
import com.badlogic.gdx.Gdx
import com.badlogic.gdx.backends.lwjgl3.Lwjgl3Application
import com.badlogic.gdx.backends.lwjgl3.Lwjgl3ApplicationConfiguration
import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.graphics.Pixmap
import com.badlogic.gdx.math.Vector2
import com.badlogic.gdx.scenes.scene2d.Actor
import com.badlogic.gdx.scenes.scene2d.InputEvent
import com.badlogic.gdx.scenes.scene2d.InputListener
import com.unciv.UncivGame
import com.unciv.UncivGameParameters
import com.unciv.logic.UncivFiles
import com.unciv.logic.multiplayer.throttle
import com.unciv.ui.images.ImageGetter
import com.unciv.ui.images.ImageWithCustomSize
import com.unciv.ui.utils.BaseScreen
import com.unciv.ui.utils.FontFamilyData
import com.unciv.ui.utils.Fonts
import com.unciv.ui.utils.NativeFontImplementation
import com.unciv.ui.utils.extensions.center
import com.unciv.ui.utils.extensions.toLabel
import com.unciv.utils.concurrency.Concurrency
import java.awt.Font
import java.awt.RenderingHints
import java.awt.image.BufferedImage
import java.time.Duration
import java.time.Instant
import java.util.concurrent.atomic.AtomicReference
/** Creates a basic GDX application that mimics [UncivGame] as closely as possible, starts up fast and shows one UI element, to be returned by [DevElement.createDevElement] */
object FasterUIDevelopment {
class DevElement(
val screen: UIDevScreen
) {
lateinit var actor: Actor
fun createDevElement() {
actor = "This could be your UI element in development!".toLabel()
}
fun afterAdd() {
}
}
@JvmStatic
fun main(arg: Array<String>) {
System.setProperty("org.lwjgl.opengl.Display.allowSoftwareOpenGL", "true")
System.setProperty("org.lwjgl.system.stackSize", "384")
val config = Lwjgl3ApplicationConfiguration()
val settings = UncivFiles.getSettingsForPlatformLaunchers()
if (!settings.isFreshlyCreated) {
config.setWindowedMode(settings.windowState.width.coerceAtLeast(120), settings.windowState.height.coerceAtLeast(80))
}
Lwjgl3Application(UIDevGame(), config)
}
class UIDevGame : Game() {
val game = UncivGame(UncivGameParameters(
fontImplementation = NativeFontDesktop()
))
override fun create() {
UncivGame.Current = game
UncivGame.Current.files = UncivFiles(Gdx.files)
game.settings = UncivGame.Current.files.getGeneralSettings()
ImageGetter.resetAtlases()
ImageGetter.setNewRuleset(ImageGetter.ruleset)
BaseScreen.setSkin()
game.pushScreen(UIDevScreen())
Gdx.graphics.requestRendering()
}
override fun render() {
game.render()
}
}
class UIDevScreen : BaseScreen() {
val devElement = DevElement(this)
init {
devElement.createDevElement()
val actor = devElement.actor
actor.center(stage)
addBorder(actor, Color.ORANGE)
actor.zIndex = Int.MAX_VALUE
stage.addActor(actor)
devElement.afterAdd()
stage.addListener(object : InputListener() {
val lastPrint = AtomicReference<Instant?>()
override fun mouseMoved(event: InputEvent?, x: Float, y: Float): Boolean {
Concurrency.run {
throttle(lastPrint, Duration.ofMillis(500), {}) {
println(String.format("x: %.1f\ty: %.1f", x, y))
}
}
return false
}
})
}
private var curBorderZ = 0
fun addBorder(actor: Actor, color: Color) {
val border = ImageWithCustomSize(ImageGetter.getBackground(color))
border.zIndex = curBorderZ++
val stageCoords = actor.localToStageCoordinates(Vector2(0f, 0f))
border.x = stageCoords.x - 1
border.y = stageCoords.y - 1
border.width = actor.width + 2
border.height = actor.height + 2
stage.addActor(border)
val background = ImageWithCustomSize(ImageGetter.getBackground(clearColor))
background.zIndex = curBorderZ++
background.x = stageCoords.x
background.y = stageCoords.y
background.width = actor.width
background.height = actor.height
stage.addActor(background)
}
}
}
class NativeFontDesktop : NativeFontImplementation {
private val font by lazy {
Font(Fonts.DEFAULT_FONT_FAMILY, Font.PLAIN, Fonts.ORIGINAL_FONT_SIZE.toInt())
}
private val metric by lazy {
val bi = BufferedImage(1, 1, BufferedImage.TYPE_4BYTE_ABGR)
val g = bi.createGraphics()
g.font = font
val fontMetrics = g.fontMetrics
g.dispose()
fontMetrics
}
override fun getFontSize(): Int {
return Fonts.ORIGINAL_FONT_SIZE.toInt()
}
override fun getCharPixmap(char: Char): Pixmap {
var width = metric.charWidth(char)
var height = metric.ascent + metric.descent
if (width == 0) {
height = Fonts.ORIGINAL_FONT_SIZE.toInt()
width = height
}
val bi = BufferedImage(width, height, BufferedImage.TYPE_4BYTE_ABGR)
val g = bi.createGraphics()
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
g.font = font
g.color = java.awt.Color.WHITE
g.drawString(char.toString(), 0, metric.ascent)
val pixmap = Pixmap(bi.width, bi.height, Pixmap.Format.RGBA8888)
val data = bi.getRGB(0, 0, bi.width, bi.height, null, 0, bi.width)
for (i in 0 until bi.width) {
for (j in 0 until bi.height) {
pixmap.setColor(Integer.reverseBytes(data[i + (j * bi.width)]))
pixmap.drawPixel(i, j)
}
}
g.dispose()
return pixmap
}
override fun getAvailableFontFamilies(): Sequence<FontFamilyData> {
return sequenceOf(FontFamilyData(Fonts.DEFAULT_FONT_FAMILY))
}
}