Scene2D debug tool (#9579)

This commit is contained in:
SomeTroglodyte 2023-06-14 07:20:34 +02:00 committed by GitHub
parent c56644cd6d
commit b0a1eed872
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 222 additions and 5 deletions

View File

@ -19,17 +19,22 @@ import com.unciv.UncivGame
import com.unciv.models.TutorialTrigger
import com.unciv.models.skins.SkinStrings
import com.unciv.ui.components.Fonts
import com.unciv.ui.components.extensions.isNarrowerThan4to3
import com.unciv.ui.components.input.KeyShortcutDispatcher
import com.unciv.ui.components.input.KeyShortcutDispatcherVeto
import com.unciv.ui.components.input.DispatcherVetoer
import com.unciv.ui.components.input.installShortcutDispatcher
import com.unciv.ui.components.input.keyShortcuts
import com.unciv.ui.components.extensions.isNarrowerThan4to3
import com.unciv.ui.components.input.KeyShortcutDispatcherVeto
import com.unciv.ui.crashhandling.CrashScreen
import com.unciv.ui.images.ImageGetter
import com.unciv.ui.popups.Popup
import com.unciv.ui.popups.activePopup
import com.unciv.ui.popups.options.OptionsPopup
// Both `this is CrashScreen` and `this::createPopupBasedDispatcherVetoer` are flagged.
// First - not a leak; second - passes out a pure function
@Suppress("LeakingThis")
abstract class BaseScreen : Screen {
val game: UncivGame = UncivGame.Current
@ -50,10 +55,11 @@ abstract class BaseScreen : Screen {
/** The ExtendViewport sets the _minimum_(!) world size - the actual world size will be larger, fitted to screen/window aspect ratio. */
stage = UncivStage(ExtendViewport(height, height))
if (enableSceneDebug) {
if (enableSceneDebug && this !is CrashScreen) {
stage.setDebugUnderMouse(true)
stage.setDebugTableUnderMouse(true)
stage.setDebugParentUnderMouse(true)
stage.mouseOverDebug = true
}
@Suppress("LeakingThis")

View File

@ -0,0 +1,202 @@
package com.unciv.ui.screens.basescreen
import com.badlogic.gdx.Gdx
import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.graphics.GL20
import com.badlogic.gdx.graphics.g2d.Batch
import com.badlogic.gdx.graphics.glutils.ShapeRenderer
import com.badlogic.gdx.math.Vector2
import com.badlogic.gdx.scenes.scene2d.Actor
import com.badlogic.gdx.scenes.scene2d.Group
import com.badlogic.gdx.scenes.scene2d.Stage
import com.badlogic.gdx.scenes.scene2d.ui.Label
import com.badlogic.gdx.scenes.scene2d.ui.TextButton
import com.badlogic.gdx.utils.Align
import com.unciv.ui.components.Fonts
import com.unciv.ui.images.ImageGetter
private typealias AddToStringBuilderFactory = (sb: StringBuilder) -> Unit
/**
* A debug helper drawing mouse-over info and world coordinate axes onto a Stage.
*
* Usage: save an instance, and in your `Stage.draw` override, call [draw] (yourStage) *after* `super.draw()`.
*
* Implementation notes:
* * Uses the stage's [Batch], but its own [ShapeRenderer]
* * Tries to avoid any memory allocation in [draw], hence for building nice Actor names,
* the reusable StringBuilder is filled using those lambdas, and those are built trying
* to use as few closures as possible.
*/
internal class StageMouseOverDebug {
private val label: Label
private val mouseCoords = Vector2()
private lateinit var shapeRenderer: ShapeRenderer
private val axisColor = Color.RED.cpy().apply { a = overlayAlpha }
private val sb = StringBuilder(160)
companion object {
private const val padding = 3f
private const val overlayAlpha = 0.8f
private const val axisInterval = 20
private const val axisTickLength = 6f
private const val axisTickWidth = 1.5f
private const val maxChildScan = 10
private const val maxTextLength = 20
}
init {
val style = Label.LabelStyle(Fonts.font, Color.WHITE)
style.background = ImageGetter.getWhiteDotDrawable().tint(Color.DARK_GRAY).apply {
leftWidth = padding
rightWidth = padding
topHeight = padding
bottomHeight = padding
}
style.fontColor = Color.GOLDENROD
label = Label("", style)
label.setAlignment(Align.center)
}
fun draw(stage: Stage) {
mouseCoords.set(Gdx.input.x.toFloat(), Gdx.input.y.toFloat())
stage.screenToStageCoordinates(mouseCoords)
sb.clear()
sb.append(mouseCoords.x.toInt())
sb.append(" / ")
sb.append(mouseCoords.y.toInt())
sb.append(" (")
sb.append(Gdx.graphics.framesPerSecond)
sb.append(")\n")
addActorLabel(stage.hit(mouseCoords.x, mouseCoords.y, false))
label.setText(sb)
layoutLabel(stage)
val batch = stage.batch
batch.projectionMatrix = stage.camera.combined
batch.begin()
label.draw(batch, overlayAlpha)
batch.end()
stage.drawAxes()
}
private fun addActorLabel(actor: Actor?) {
if (actor == null) return
// For this actor, see if it has a descriptive name
val actorBuilder = getActorDescriptiveName(actor)
var parentBuilder: AddToStringBuilderFactory? = null
var childBuilder: AddToStringBuilderFactory? = null
// If there's no descriptive name for this actor, look for parent or children
if (actorBuilder == null) {
// Try to get a descriptive name from parent
if (actor.parent != null)
parentBuilder = getActorDescriptiveName(actor.parent)
// If that failed, try to get a descriptive name from first few children
if (parentBuilder == null && actor is Group)
childBuilder = actor.children.asSequence()
.take(maxChildScan)
.map { getActorDescriptiveName(it) }
.firstOrNull { it != null }
}
// assemble name parts with fallback to plain class names for parent and actor
if (parentBuilder != null) {
parentBuilder(sb)
sb.append('.')
} else if (actor.parent != null) {
sb.append((actor.parent)::class.java.simpleName)
sb.append('.')
}
if (actorBuilder != null)
actorBuilder(sb)
else
sb.append(actor::class.java.simpleName)
if (childBuilder != null) {
sb.append('(')
childBuilder(sb)
sb.append(')')
}
}
private fun getActorDescriptiveName(actor: Actor): AddToStringBuilderFactory? {
if (actor.name != null) {
val className = actor::class.java.simpleName
if (actor.name.startsWith(className))
return { sb -> sb.append(actor.name) }
return { sb ->
sb.append(className)
sb.append(':')
sb.append(actor.name)
}
}
if (actor is Label && actor.text.isNotBlank()) return { sb ->
sb.append("Label\"")
sb.appendLimited(actor.text)
sb.append('\"')
}
if (actor is TextButton && actor.text.isNotBlank()) return { sb ->
sb.append("TextButton\"")
sb.appendLimited(actor.text)
sb.append('\"')
}
return null
}
private fun StringBuilder.appendLimited(text: CharSequence) {
val lf = text.indexOf('\n') + 1
val len = (if (lf == 0) text.length else lf).coerceAtMost(maxTextLength)
if (len == text.length) {
append(text)
return
}
append(text, 0, len)
append('‥') // '…' is taken
}
private fun layoutLabel(stage: Stage) {
if (!label.needsLayout()) return
val width = label.prefWidth + 2 * padding
label.setSize(width, label.prefHeight + 2 * padding)
label.setPosition(stage.width - width, 0f)
label.validate()
}
private fun Stage.drawAxes() {
if (!::shapeRenderer.isInitialized) {
shapeRenderer = ShapeRenderer()
shapeRenderer.setAutoShapeType(true)
}
val sr = shapeRenderer
Gdx.gl.glEnable(GL20.GL_BLEND)
sr.projectionMatrix = viewport.camera.combined
sr.begin()
sr.set(ShapeRenderer.ShapeType.Filled)
for (x in 0..width.toInt() step axisInterval) {
val xf = x.toFloat()
sr.rectLine(xf, 0f, xf, axisTickLength, axisTickWidth, axisColor, axisColor)
}
val x2 = width
val x1 = x2 - axisTickLength
for (y in 0..height.toInt() step axisInterval) {
val yf = y.toFloat()
sr.rectLine(x1, yf, x2, yf, axisTickWidth, axisColor, axisColor)
}
sr.end()
Gdx.gl.glDisable(GL20.GL_BLEND)
}
}

View File

@ -29,6 +29,13 @@ class UncivStage(viewport: Viewport) : Stage(viewport, getBatch()) {
var lastKnownVisibleArea: Rectangle
private set
var mouseOverDebug: Boolean
get() = mouseOverDebugImpl != null
set(value) {
mouseOverDebugImpl = if (value) StageMouseOverDebug() else null
}
private var mouseOverDebugImpl: StageMouseOverDebug? = null
private val events = EventBus.EventReceiver()
init {
@ -49,8 +56,10 @@ class UncivStage(viewport: Viewport) : Stage(viewport, getBatch()) {
super.act()
}
override fun draw() =
{ super.draw() }.wrapCrashHandlingUnit()()
override fun draw() {
{ super.draw() }.wrapCrashHandlingUnit()()
mouseOverDebugImpl?.draw(this)
}
/** libGDX has no built-in way to disable/enable pointer enter/exit events. It is simply being done in [Stage.act]. So to disable this, we have
* to replicate the [Stage.act] method without the code for pointer enter/exit events. This is of course inherently brittle, but the only way. */