diff --git a/build.gradle.kts b/build.gradle.kts index 6cb83107a4..629cf0eb7e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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") diff --git a/docs/Developers/UI-development.md b/docs/Developers/UI-development.md new file mode 100644 index 0000000000..4e831e6e87 --- /dev/null +++ b/docs/Developers/UI-development.md @@ -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. diff --git a/tests/src/com/unciv/dev/FasterUIDevelopment.kt b/tests/src/com/unciv/dev/FasterUIDevelopment.kt new file mode 100644 index 0000000000..5b026911ee --- /dev/null +++ b/tests/src/com/unciv/dev/FasterUIDevelopment.kt @@ -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) { + 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() + 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 { + return sequenceOf(FontFamilyData(Fonts.DEFAULT_FONT_FAMILY)) + } +}