mirror of
https://github.com/yairm210/Unciv.git
synced 2025-08-04 00:59:41 +07:00
Add new universal crash handlers and error reporting screen. (#5804)
* Add new crash handler and error reporting screen. * Minor cleanup. * Word choice. * Rename `SafeCrashStage` to `CrashHandlingStage`. * Reviews. * Reference stack traces in comments for thread and postRunnable exceptions. * Remove excessive line breaks, superfluous .apply{}.
This commit is contained in:
@ -34,6 +34,17 @@ See your stats breakdown!\nEnter the Overview screen (top right corner) >\nClick
|
||||
Oh no! It looks like something went DISASTROUSLY wrong! This is ABSOLUTELY not supposed to happen! Please send me (yairm210@hotmail.com) an email with the game information (menu -> save game -> copy game info -> paste into email) and I'll try to fix it as fast as I can! =
|
||||
Oh no! It looks like something went DISASTROUSLY wrong! This is ABSOLUTELY not supposed to happen! Please send us an report and we'll try to fix it as fast as we can! =
|
||||
|
||||
# Crash screen
|
||||
|
||||
An unrecoverable error has occurred in Unciv: =
|
||||
If this keeps happening, you can try disabling mods. =
|
||||
You can also report this on the issue tracker. =
|
||||
Copy =
|
||||
Error report copied. =
|
||||
Open Issue Tracker =
|
||||
Please copy the error report first. =
|
||||
Close Unciv =
|
||||
|
||||
# Buildings
|
||||
|
||||
Unsellable =
|
||||
|
125
core/src/com/unciv/CrashHandlingStage.kt
Normal file
125
core/src/com/unciv/CrashHandlingStage.kt
Normal file
@ -0,0 +1,125 @@
|
||||
package com.unciv
|
||||
|
||||
import com.badlogic.gdx.graphics.g2d.Batch
|
||||
import com.badlogic.gdx.scenes.scene2d.Stage
|
||||
import com.badlogic.gdx.utils.viewport.Viewport
|
||||
import com.unciv.ui.utils.*
|
||||
|
||||
/** Stage that safely brings the game to a [CrashScreen] if any event handlers throw an exception or an error that doesn't get otherwise handled. */
|
||||
class CrashHandlingStage: Stage {
|
||||
constructor(): super()
|
||||
constructor(viewport: Viewport): super(viewport)
|
||||
constructor(viewport: Viewport, batch: Batch) : super(viewport, batch)
|
||||
|
||||
override fun draw() = { super.draw() }.wrapCrashHandlingUnit()()
|
||||
override fun act() = { super.act() }.wrapCrashHandlingUnit()()
|
||||
override fun act(delta: Float) = { super.act(delta) }.wrapCrashHandlingUnit()()
|
||||
|
||||
override fun touchDown(screenX: Int, screenY: Int, pointer: Int, button: Int)
|
||||
= { super.touchDown(screenX, screenY, pointer, button) }.wrapCrashHandling()() ?: true
|
||||
override fun touchDragged(screenX: Int, screenY: Int, pointer: Int)
|
||||
= { super.touchDragged(screenX, screenY, pointer) }.wrapCrashHandling()() ?: true
|
||||
override fun touchUp(screenX: Int, screenY: Int, pointer: Int, button: Int)
|
||||
= { super.touchUp(screenX, screenY, pointer, button) }.wrapCrashHandling()() ?: true
|
||||
override fun mouseMoved(screenX: Int, screenY: Int)
|
||||
= { super.mouseMoved(screenX, screenY) }.wrapCrashHandling()() ?: true
|
||||
override fun scrolled(amountX: Float, amountY: Float)
|
||||
= { super.scrolled(amountX, amountY) }.wrapCrashHandling()() ?: true
|
||||
override fun keyDown(keyCode: Int)
|
||||
= { super.keyDown(keyCode) }.wrapCrashHandling()() ?: true
|
||||
override fun keyUp(keyCode: Int)
|
||||
= { super.keyUp(keyCode) }.wrapCrashHandling()() ?: true
|
||||
override fun keyTyped(character: Char)
|
||||
= { super.keyTyped(character) }.wrapCrashHandling()() ?: true
|
||||
|
||||
}
|
||||
|
||||
// Example Stack traces from unhandled exceptions after a button click on Desktop and on Android are below.
|
||||
|
||||
// Another stack trace from an exception after setting TileInfo.naturalWonder to an invalid value is below that.
|
||||
|
||||
// Below that are another two exceptions from a lambda given to Gdx.app.postRunnable{} and another to thread{}.
|
||||
|
||||
// Stage()'s event handlers seem to be the most universal place to intercept exceptions from events.
|
||||
|
||||
// Events and the render loop are the main ways that code gets run with GDX, right? So if we wrap both of those in exception handling, it should hopefully gracefully catch most unhandled exceptions… Threads may be the exception, hence why I put the wrapping as extension functions that can be invoked on the lambdas passed to threads.
|
||||
|
||||
|
||||
// Button click (event):
|
||||
|
||||
/*
|
||||
Exception in thread "main" com.badlogic.gdx.utils.GdxRuntimeException: java.lang.Exception
|
||||
at com.badlogic.gdx.backends.lwjgl3.Lwjgl3Application.<init>(Lwjgl3Application.java:122)
|
||||
at com.unciv.app.desktop.DesktopLauncher.main(DesktopLauncher.kt:61)
|
||||
Caused by: java.lang.Exception
|
||||
at com.unciv.MainMenuScreen$newGameButton$1.invoke(MainMenuScreen.kt:107)
|
||||
at com.unciv.MainMenuScreen$newGameButton$1.invoke(MainMenuScreen.kt:106)
|
||||
at com.unciv.ui.utils.ExtensionFunctionsKt$onClick$1.invoke(ExtensionFunctions.kt:64)
|
||||
at com.unciv.ui.utils.ExtensionFunctionsKt$onClick$1.invoke(ExtensionFunctions.kt:64)
|
||||
at com.unciv.ui.utils.ExtensionFunctionsKt$onClickEvent$1.clicked(ExtensionFunctions.kt:57)
|
||||
at com.badlogic.gdx.scenes.scene2d.utils.ClickListener.touchUp(ClickListener.java:88)
|
||||
at com.badlogic.gdx.scenes.scene2d.InputListener.handle(InputListener.java:71)
|
||||
at com.badlogic.gdx.scenes.scene2d.Stage.touchUp(Stage.java:355)
|
||||
at com.badlogic.gdx.InputEventQueue.drain(InputEventQueue.java:70)
|
||||
at com.badlogic.gdx.backends.lwjgl3.DefaultLwjgl3Input.update(DefaultLwjgl3Input.java:189)
|
||||
at com.badlogic.gdx.backends.lwjgl3.Lwjgl3Window.update(Lwjgl3Window.java:394)
|
||||
at com.badlogic.gdx.backends.lwjgl3.Lwjgl3Application.loop(Lwjgl3Application.java:143)
|
||||
at com.badlogic.gdx.backends.lwjgl3.Lwjgl3Application.<init>(Lwjgl3Application.java:116)
|
||||
... 1 more
|
||||
|
||||
E/AndroidRuntime: FATAL EXCEPTION: GLThread 299
|
||||
Process: com.unciv.app, PID: 5910
|
||||
java.lang.Exception
|
||||
at com.unciv.MainMenuScreen$newGameButton$1.invoke(MainMenuScreen.kt:107)
|
||||
at com.unciv.MainMenuScreen$newGameButton$1.invoke(MainMenuScreen.kt:106)
|
||||
at com.unciv.ui.utils.ExtensionFunctionsKt$onClick$1.invoke(ExtensionFunctions.kt:64)
|
||||
at com.unciv.ui.utils.ExtensionFunctionsKt$onClick$1.invoke(ExtensionFunctions.kt:64)
|
||||
at com.unciv.ui.utils.ExtensionFunctionsKt$onClickEvent$1.clicked(ExtensionFunctions.kt:57)
|
||||
at com.badlogic.gdx.scenes.scene2d.utils.ClickListener.touchUp(ClickListener.java:88)
|
||||
at com.badlogic.gdx.scenes.scene2d.InputListener.handle(InputListener.java:71)
|
||||
at com.badlogic.gdx.scenes.scene2d.Stage.touchUp(Stage.java:355)
|
||||
at com.badlogic.gdx.backends.android.DefaultAndroidInput.processEvents(DefaultAndroidInput.java:425)
|
||||
at com.badlogic.gdx.backends.android.AndroidGraphics.onDrawFrame(AndroidGraphics.java:469)
|
||||
at android.opengl.GLSurfaceView$GLThread.guardedRun(GLSurfaceView.java:1522)
|
||||
at android.opengl.GLSurfaceView$GLThread.run(GLSurfaceView.java:1239)
|
||||
*/
|
||||
|
||||
// Invalid Natural Wonder (rendering):
|
||||
|
||||
/*
|
||||
Exception in thread "main" java.lang.NullPointerException
|
||||
at com.unciv.logic.map.TileInfo.getNaturalWonder(TileInfo.kt:149)
|
||||
at com.unciv.logic.map.TileInfo.getTileStats(TileInfo.kt:255)
|
||||
at com.unciv.logic.map.TileInfo.getTileStats(TileInfo.kt:240)
|
||||
at com.unciv.ui.worldscreen.bottombar.TileInfoTable.getStatsTable(TileInfoTable.kt:43)
|
||||
at com.unciv.ui.worldscreen.bottombar.TileInfoTable.updateTileTable$core(TileInfoTable.kt:25)
|
||||
at com.unciv.ui.worldscreen.WorldScreen.update(WorldScreen.kt:383)
|
||||
at com.unciv.ui.worldscreen.WorldScreen.render(WorldScreen.kt:828)
|
||||
at com.badlogic.gdx.Game.render(Game.java:46)
|
||||
at com.badlogic.gdx.backends.lwjgl3.Lwjgl3Window.update(Lwjgl3Window.java:403)
|
||||
at com.badlogic.gdx.backends.lwjgl3.Lwjgl3Application.loop(Lwjgl3Application.java:143)
|
||||
at com.badlogic.gdx.backends.lwjgl3.Lwjgl3Application.<init>(Lwjgl3Application.java:116)
|
||||
at com.unciv.app.desktop.DesktopLauncher.main(DesktopLauncher.kt:61)
|
||||
*/
|
||||
|
||||
// Thread:
|
||||
|
||||
/*
|
||||
Exception in thread "Thread-5" java.lang.Exception
|
||||
at com.unciv.MainMenuScreen$newGameButton$1$1.invoke(MainMenuScreen.kt:107)
|
||||
at com.unciv.MainMenuScreen$newGameButton$1$1.invoke(MainMenuScreen.kt:107)
|
||||
at kotlin.concurrent.ThreadsKt$thread$thread$1.run(Thread.kt:30)
|
||||
*/
|
||||
|
||||
// Gdx.app.postRunnable:
|
||||
|
||||
/*
|
||||
Exception in thread "main" com.badlogic.gdx.utils.GdxRuntimeException: java.lang.Exception
|
||||
at com.badlogic.gdx.backends.lwjgl3.Lwjgl3Application.<init>(Lwjgl3Application.java:122)
|
||||
at com.unciv.app.desktop.DesktopLauncher.main(DesktopLauncher.kt:61)
|
||||
Caused by: java.lang.Exception
|
||||
at com.unciv.MainMenuScreen$loadGameTable$1.invoke$lambda-0(MainMenuScreen.kt:112)
|
||||
at com.badlogic.gdx.backends.lwjgl3.Lwjgl3Application.loop(Lwjgl3Application.java:159)
|
||||
at com.badlogic.gdx.backends.lwjgl3.Lwjgl3Application.<init>(Lwjgl3Application.java:116)
|
||||
... 1 more
|
||||
*/
|
140
core/src/com/unciv/CrashScreen.kt
Normal file
140
core/src/com/unciv/CrashScreen.kt
Normal file
@ -0,0 +1,140 @@
|
||||
package com.unciv
|
||||
|
||||
import com.badlogic.gdx.Gdx
|
||||
import com.badlogic.gdx.graphics.Color
|
||||
import com.badlogic.gdx.scenes.scene2d.Actor
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.Label
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.Table
|
||||
import com.badlogic.gdx.utils.Align
|
||||
import com.unciv.models.ruleset.RulesetCache
|
||||
import com.unciv.ui.utils.*
|
||||
import java.io.PrintWriter
|
||||
import java.io.StringWriter
|
||||
|
||||
/** Screen to crash to when an otherwise unhandled exception or error is thrown. */
|
||||
class CrashScreen(message: String): BaseScreen() {
|
||||
constructor(exception: Throwable): this(exception.stringify())
|
||||
|
||||
private companion object {
|
||||
fun Throwable.stringify(): String {
|
||||
val out = StringWriter()
|
||||
this.printStackTrace(PrintWriter(out))
|
||||
return out.toString()
|
||||
}
|
||||
}
|
||||
|
||||
val text = generateReportHeader() + message
|
||||
var copied = false
|
||||
private set
|
||||
|
||||
fun generateReportHeader(): String {
|
||||
return """
|
||||
Platform: ${Gdx.app.type}
|
||||
Version: ${UncivGame.Current.version}
|
||||
Rulesets: ${RulesetCache.keys}
|
||||
|
||||
|
||||
""".trimIndent()
|
||||
}
|
||||
|
||||
init {
|
||||
println(text) // Also print to system terminal.
|
||||
stage.addActor(makeLayoutTable())
|
||||
}
|
||||
|
||||
/** @return A Table containing the layout of the whole screen. */
|
||||
private fun makeLayoutTable(): Table {
|
||||
val layoutTable = Table().also {
|
||||
it.width = stage.width
|
||||
it.height = stage.height
|
||||
}
|
||||
layoutTable.add(makeTitleLabel())
|
||||
.padBottom(15f)
|
||||
.width(stage.width)
|
||||
.row()
|
||||
layoutTable.add(makeErrorScroll())
|
||||
.maxWidth(stage.width * 0.7f)
|
||||
.maxHeight(stage.height * 0.5f)
|
||||
.minHeight(stage.height * 0.2f)
|
||||
.row()
|
||||
layoutTable.add(makeInstructionLabel())
|
||||
.padTop(15f)
|
||||
.width(stage.width)
|
||||
.row()
|
||||
layoutTable.add(makeActionButtonsTable())
|
||||
.padTop(10f)
|
||||
return layoutTable
|
||||
}
|
||||
|
||||
/** @return Label for title at top of screen. */
|
||||
private fun makeTitleLabel()
|
||||
= "An unrecoverable error has occurred in Unciv:".toLabel(fontSize = 24)
|
||||
.apply {
|
||||
wrap = true
|
||||
setAlignment(Align.center)
|
||||
}
|
||||
|
||||
/** @return Actor that displays a scrollable view of the error report text. */
|
||||
private fun makeErrorScroll(): Actor {
|
||||
val errorLabel = Label(text, skin).apply {
|
||||
setFontSize(15)
|
||||
}
|
||||
val errorTable = Table()
|
||||
errorTable.add(errorLabel)
|
||||
.pad(10f)
|
||||
return AutoScrollPane(errorTable)
|
||||
.addBorder(4f, Color.DARK_GRAY)
|
||||
}
|
||||
|
||||
/** @return Label to give the user more information and context below the error report. */
|
||||
private fun makeInstructionLabel()
|
||||
= "{If this keeps happening, you can try disabling mods.}\n{You can also report this on the issue tracker.}".toLabel()
|
||||
.apply {
|
||||
wrap = true
|
||||
setAlignment(Align.center)
|
||||
}
|
||||
|
||||
/** @return Table that displays decision buttons for the bottom of the screen. */
|
||||
private fun makeActionButtonsTable(): Table {
|
||||
val copyButton = "Copy".toButton()
|
||||
.onClick {
|
||||
Gdx.app.clipboard.contents = text
|
||||
copied = true
|
||||
ToastPopup(
|
||||
"Error report copied.",
|
||||
this@CrashScreen
|
||||
)
|
||||
}
|
||||
val reportButton = "Open Issue Tracker".toButton(icon = "OtherIcons/Link")
|
||||
.onClick {
|
||||
if (copied) {
|
||||
Gdx.net.openURI("https://github.com/yairm210/Unciv/issues")
|
||||
} else {
|
||||
ToastPopup(
|
||||
"Please copy the error report first.",
|
||||
this@CrashScreen
|
||||
)
|
||||
}
|
||||
}
|
||||
val closeButton = "Close Unciv".toButton()
|
||||
.onClick {
|
||||
Gdx.app.exit()
|
||||
}
|
||||
|
||||
val buttonsTable = Table()
|
||||
buttonsTable.add(copyButton)
|
||||
.pad(10f)
|
||||
buttonsTable.add(reportButton)
|
||||
.pad(10f)
|
||||
.also {
|
||||
if (isCrampedPortrait()) {
|
||||
it.row()
|
||||
buttonsTable.add()
|
||||
}
|
||||
}
|
||||
buttonsTable.add(closeButton)
|
||||
.pad(10f)
|
||||
|
||||
return buttonsTable
|
||||
}
|
||||
}
|
@ -22,6 +22,8 @@ import com.unciv.ui.worldscreen.WorldScreen
|
||||
import java.util.*
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
|
||||
|
||||
class UncivGame(parameters: UncivGameParameters) : Game() {
|
||||
// we need this secondary constructor because Java code for iOS can't handle Kotlin lambda parameters
|
||||
constructor(version: String) : this(UncivGameParameters(version, null))
|
||||
@ -62,6 +64,10 @@ class UncivGame(parameters: UncivGameParameters) : Game() {
|
||||
|
||||
var isInitialized = false
|
||||
|
||||
/** A wrapped render() method that crashes to [CrashScreen] on a unhandled exception or error. */
|
||||
private val wrappedCrashHandlingRender = { super.render() }.wrapCrashHandlingUnit()
|
||||
// Stored here because I imagine that might be slightly faster than allocating for a new lambda every time, and the render loop is possibly one of the only places where that could have a significant impact.
|
||||
|
||||
|
||||
val translations = Translations()
|
||||
|
||||
@ -167,6 +173,8 @@ class UncivGame(parameters: UncivGameParameters) : Game() {
|
||||
screen.resize(width, height)
|
||||
}
|
||||
|
||||
override fun render() = wrappedCrashHandlingRender()
|
||||
|
||||
override fun dispose() {
|
||||
cancelDiscordEvent?.invoke()
|
||||
Sounds.clearCache()
|
||||
|
@ -11,6 +11,7 @@ import com.badlogic.gdx.scenes.scene2d.ui.*
|
||||
import com.badlogic.gdx.scenes.scene2d.utils.Drawable
|
||||
import com.badlogic.gdx.utils.viewport.ExtendViewport
|
||||
import com.unciv.MainMenuScreen
|
||||
import com.unciv.CrashHandlingStage
|
||||
import com.unciv.UncivGame
|
||||
import com.unciv.models.Tutorial
|
||||
import com.unciv.ui.tutorials.TutorialController
|
||||
@ -32,7 +33,7 @@ open class BaseScreen : Screen {
|
||||
val height = resolutions[1]
|
||||
|
||||
/** The ExtendViewport sets the _minimum_(!) world size - the actual world size will be larger, fitted to screen/window aspect ratio. */
|
||||
stage = Stage(ExtendViewport(height, height), SpriteBatch())
|
||||
stage = CrashHandlingStage(ExtendViewport(height, height), SpriteBatch())
|
||||
|
||||
if (enableSceneDebug) {
|
||||
stage.setDebugUnderMouse(true)
|
||||
|
@ -1,13 +1,13 @@
|
||||
package com.unciv.ui.utils
|
||||
|
||||
import com.badlogic.gdx.Gdx
|
||||
import com.badlogic.gdx.graphics.Color
|
||||
import com.badlogic.gdx.scenes.scene2d.Actor
|
||||
import com.badlogic.gdx.scenes.scene2d.InputEvent
|
||||
import com.badlogic.gdx.scenes.scene2d.Stage
|
||||
import com.badlogic.gdx.scenes.scene2d.Touchable
|
||||
import com.badlogic.gdx.scenes.scene2d.*
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.*
|
||||
import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener
|
||||
import com.badlogic.gdx.scenes.scene2d.utils.ClickListener
|
||||
import com.unciv.CrashScreen
|
||||
import com.unciv.UncivGame
|
||||
import com.unciv.models.UncivSound
|
||||
import com.unciv.models.translations.tr
|
||||
import java.text.SimpleDateFormat
|
||||
@ -93,6 +93,19 @@ fun Actor.addBorder(size:Float, color: Color, expandCell:Boolean = false): Table
|
||||
return table
|
||||
}
|
||||
|
||||
/** Wrap an [Actor] in a [Group] of a given size */
|
||||
fun Actor.sizeWrapped(x: Float, y: Float): Group {
|
||||
val wrapper = Group().apply {
|
||||
isTransform = false // performance helper - nothing here is rotated or scaled
|
||||
setSize(x, y)
|
||||
}
|
||||
setSize(x, y)
|
||||
center(wrapper)
|
||||
wrapper.addActor(this)
|
||||
return wrapper
|
||||
}
|
||||
|
||||
|
||||
/** get background Image for a new separator */
|
||||
private fun getSeparatorImage(color: Color) = ImageGetter.getDot(
|
||||
if (color.a != 0f) color else BaseScreen.skin.get("color", Color::class.java) //0x334d80
|
||||
@ -184,6 +197,21 @@ fun Float.toPercent() = 1 + this/100
|
||||
/** Translate a [String] and make a [TextButton] widget from it */
|
||||
fun String.toTextButton() = TextButton(this.tr(), BaseScreen.skin)
|
||||
|
||||
/** Translate a [String] and make a [Button] widget from it, with control over font size, font colour, and an optional icon. */
|
||||
fun String.toButton(fontColor: Color = Color.WHITE, fontSize: Int = 24, icon: String? = null): Button {
|
||||
val button = Button(BaseScreen.skin)
|
||||
if (icon != null) {
|
||||
val size = fontSize.toFloat()
|
||||
button.add(
|
||||
ImageGetter.getImage(icon).sizeWrapped(size, size)
|
||||
).padRight(size / 3)
|
||||
}
|
||||
button.add(
|
||||
this.toLabel(fontColor, fontSize)
|
||||
)
|
||||
return button
|
||||
}
|
||||
|
||||
/** Translate a [String] and make a [Label] widget from it */
|
||||
fun String.toLabel() = Label(this.tr(), BaseScreen.skin)
|
||||
/** Make a [Label] widget containing this [Int] as text */
|
||||
@ -284,3 +312,43 @@ object UncivDateFormat {
|
||||
*/
|
||||
fun String.parseDate(): Date = utcFormat.parse(this)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns a wrapped version of a function that safely crashes the game to [CrashScreen] if an exception or error is thrown.
|
||||
*
|
||||
* In case an exception or error is thrown, the return will be null. Therefore the return type is always nullable.
|
||||
*
|
||||
* @param postToMainThread Whether the [CrashScreen] should be opened by posting a runnable to the main thread, instead of directly. Set this to true if the function is going to run on any thread other than the main loop.
|
||||
* @return Result from the function, or null if an exception is thrown.
|
||||
* */
|
||||
fun <R> (() -> R).wrapCrashHandling(
|
||||
postToMainThread: Boolean = false
|
||||
): () -> R?
|
||||
= {
|
||||
try {
|
||||
this()
|
||||
} catch (e: Throwable) {
|
||||
if (postToMainThread) {
|
||||
Gdx.app.postRunnable {
|
||||
UncivGame.Current.setScreen(CrashScreen(e))
|
||||
}
|
||||
} else {
|
||||
UncivGame.Current.setScreen(CrashScreen(e))
|
||||
}
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a wrapped a version of a Unit-returning function which safely crashes the game to [CrashScreen] if an exception or error is thrown.
|
||||
*
|
||||
* @param postToMainThread Whether the [CrashScreen] should be opened by posting a runnable to the main thread, instead of directly. Set this to true if the function is going to run on any thread other than the main loop.
|
||||
* */
|
||||
fun (() -> Unit).wrapCrashHandlingUnit(
|
||||
postToMainThread: Boolean = false
|
||||
): () -> Unit {
|
||||
val wrappedReturning = this.wrapCrashHandling(postToMainThread)
|
||||
// Don't instantiate a new lambda every time.
|
||||
return { wrappedReturning() ?: Unit }
|
||||
}
|
||||
|
@ -238,15 +238,8 @@ class TabbedPager(
|
||||
name = caption // enable finding pages by untranslated caption without needing our own field
|
||||
if (icon != null) {
|
||||
if (iconSize != 0f) {
|
||||
val wrapper = Group().apply {
|
||||
isTransform =
|
||||
false // performance helper - nothing here is rotated or scaled
|
||||
setSize(iconSize, iconSize)
|
||||
icon.setSize(iconSize, iconSize)
|
||||
icon.center(this)
|
||||
addActor(icon)
|
||||
}
|
||||
add(wrapper).padRight(headerPadding * 0.5f)
|
||||
add(icon.sizeWrapped(iconSize, iconSize))
|
||||
.padRight(headerPadding * 0.5f)
|
||||
} else {
|
||||
add(icon)
|
||||
}
|
||||
|
Reference in New Issue
Block a user