Make CityScreen's top-right widget use an ExpanderTab (#13186)

* Make CityScreen's top-right widget use an ExpanderTab

* No nasty tricks please
This commit is contained in:
SomeTroglodyte 2025-04-17 22:03:03 +02:00 committed by GitHub
parent adfaacb0f6
commit a5a148cc51
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 117 additions and 80 deletions

View File

@ -5,13 +5,17 @@ import com.badlogic.gdx.math.Interpolation
import com.badlogic.gdx.scenes.scene2d.Actor import com.badlogic.gdx.scenes.scene2d.Actor
import com.badlogic.gdx.scenes.scene2d.Touchable import com.badlogic.gdx.scenes.scene2d.Touchable
import com.badlogic.gdx.scenes.scene2d.actions.FloatAction import com.badlogic.gdx.scenes.scene2d.actions.FloatAction
import com.badlogic.gdx.scenes.scene2d.ui.Cell
import com.badlogic.gdx.scenes.scene2d.ui.ScrollPane import com.badlogic.gdx.scenes.scene2d.ui.ScrollPane
import com.badlogic.gdx.scenes.scene2d.ui.Table import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.badlogic.gdx.scenes.scene2d.ui.Value
import com.badlogic.gdx.utils.Align import com.badlogic.gdx.utils.Align
import com.unciv.Constants import com.unciv.Constants
import com.unciv.UncivGame import com.unciv.UncivGame
import com.unciv.ui.components.extensions.toLabel import com.unciv.ui.components.extensions.toLabel
import com.unciv.ui.components.input.ActivationTypes
import com.unciv.ui.components.input.KeyboardBinding import com.unciv.ui.components.input.KeyboardBinding
import com.unciv.ui.components.input.clearActivationActions
import com.unciv.ui.components.input.keyShortcuts import com.unciv.ui.components.input.keyShortcuts
import com.unciv.ui.components.input.onActivation import com.unciv.ui.components.input.onActivation
import com.unciv.ui.images.IconCircleGroup import com.unciv.ui.images.IconCircleGroup
@ -25,6 +29,7 @@ import com.unciv.ui.screens.basescreen.BaseScreen
* @param fontSize Size applied to header text (only) * @param fontSize Size applied to header text (only)
* @param icon Optional icon - please use [Image][com.badlogic.gdx.scenes.scene2d.ui.Image] or [IconCircleGroup] * @param icon Optional icon - please use [Image][com.badlogic.gdx.scenes.scene2d.ui.Image] or [IconCircleGroup]
* @param defaultPad Padding between content and wrapper. * @param defaultPad Padding between content and wrapper.
* @param topPad Padding between content top and wrapper.
* @param headerPad Default padding for the header Table. * @param headerPad Default padding for the header Table.
* @param expanderWidth If set initializes header width * @param expanderWidth If set initializes header width
* @param expanderHeight If set initializes header height * @param expanderHeight If set initializes header height
@ -38,6 +43,7 @@ class ExpanderTab(
icon: Actor? = null, icon: Actor? = null,
startsOutOpened: Boolean = true, startsOutOpened: Boolean = true,
defaultPad: Float = 10f, defaultPad: Float = 10f,
topPad: Float = defaultPad,
headerPad: Float = 10f, headerPad: Float = 10f,
expanderWidth: Float = 0f, expanderWidth: Float = 0f,
expanderHeight: Float = 0f, expanderHeight: Float = 0f,
@ -46,13 +52,15 @@ class ExpanderTab(
private val onChange: (() -> Unit)? = null, private val onChange: (() -> Unit)? = null,
initContent: ((Table) -> Unit)? = null initContent: ((Table) -> Unit)? = null
): Table(BaseScreen.skin) { ): Table(BaseScreen.skin) {
private companion object { companion object {
const val arrowSize = 18f private const val arrowSize = 18f
const val arrowImage = "OtherIcons/BackArrow" private const val arrowImage = "OtherIcons/BackArrow"
val arrowColor = Color(1f,0.96f,0.75f,1f) private val arrowColor = Color(1f,0.96f,0.75f,1f)
const val animationDuration = 0.2f private const val animationDuration = 0.2f
val persistedStates = HashMap<String, Boolean>() private val persistedStates = HashMap<String, Boolean>()
fun wasOpen(persistenceID: String) = persistedStates[persistenceID]
} }
/** Header with label, [headerContent] and icon, touchable to show/hide. /** Header with label, [headerContent] and icon, touchable to show/hide.
@ -64,7 +72,7 @@ class ExpanderTab(
val headerContent = Table() val headerContent = Table()
private val headerLabel = title.toLabel(fontSize = fontSize, hideIcons = true) private val headerLabel = title.toLabel(fontSize = fontSize, hideIcons = true)
private val headerIcon = ImageGetter.getImage(arrowImage) val headerIcon = ImageGetter.getImage(arrowImage)
private val contentWrapper = Table() // Wrapper for innerTable, this is what will be shown/hidden private val contentWrapper = Table() // Wrapper for innerTable, this is what will be shown/hidden
/** The container where the client should add the content to toggle */ /** The container where the client should add the content to toggle */
@ -107,7 +115,7 @@ class ExpanderTab(
if (expanderWidth != 0f) if (expanderWidth != 0f)
defaults().minWidth(expanderWidth) defaults().minWidth(expanderWidth)
defaults().growX() defaults().growX()
contentWrapper.defaults().growX().pad(defaultPad) contentWrapper.defaults().growX().pad(topPad, defaultPad, defaultPad, defaultPad)
innerTable.defaults().growX() innerTable.defaults().growX()
add(header).fill().row() add(header).fill().row()
add(contentWrapper) add(contentWrapper)
@ -189,4 +197,24 @@ class ExpanderTab(
fun setText(text: String) { fun setText(text: String) {
headerLabel.setText(text) headerLabel.setText(text)
} }
fun toggleOnIconOnly() {
header.clearActivationActions(ActivationTypes.Tap)
headerIcon.onActivation { toggle() }
}
private fun Cell<Actor>.resetFixedSize(): Cell<Actor> {
minHeight(Value.minHeight)
prefHeight(Value.prefHeight)
maxHeight(Value.maxHeight)
minWidth(Value.minWidth)
prefWidth(Value.prefHeight)
maxWidth(Value.maxWidth)
return this
}
fun setDynamicHeaderSize() {
header.cells[0]!!.resetFixedSize().center()
header.cells[1]!!.resetFixedSize().center()
}
} }

View File

@ -2,9 +2,10 @@ package com.unciv.ui.screens.cityscreen
import com.badlogic.gdx.graphics.Color import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.scenes.scene2d.Actor import com.badlogic.gdx.scenes.scene2d.Actor
import com.badlogic.gdx.scenes.scene2d.Touchable import com.badlogic.gdx.scenes.scene2d.ui.Cell
import com.badlogic.gdx.scenes.scene2d.ui.Label import com.badlogic.gdx.scenes.scene2d.ui.Label
import com.badlogic.gdx.scenes.scene2d.ui.Table import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.badlogic.gdx.scenes.scene2d.ui.Value
import com.badlogic.gdx.utils.Align import com.badlogic.gdx.utils.Align
import com.unciv.Constants import com.unciv.Constants
import com.unciv.logic.city.* import com.unciv.logic.city.*
@ -16,29 +17,26 @@ import com.unciv.models.stats.Stat
import com.unciv.models.translations.tr import com.unciv.models.translations.tr
import com.unciv.ui.components.extensions.* import com.unciv.ui.components.extensions.*
import com.unciv.ui.components.fonts.Fonts import com.unciv.ui.components.fonts.Fonts
import com.unciv.ui.components.input.ActivationTypes
import com.unciv.ui.components.input.KeyboardBinding import com.unciv.ui.components.input.KeyboardBinding
import com.unciv.ui.components.input.clearActivationActions
import com.unciv.ui.components.input.onActivation import com.unciv.ui.components.input.onActivation
import com.unciv.ui.components.input.onClick import com.unciv.ui.components.input.onClick
import com.unciv.ui.components.widgets.AutoScrollPane
import com.unciv.ui.components.widgets.ExpanderTab import com.unciv.ui.components.widgets.ExpanderTab
import com.unciv.ui.images.ImageGetter import com.unciv.ui.images.ImageGetter
import com.unciv.ui.screens.basescreen.BaseScreen import com.unciv.ui.screens.basescreen.BaseScreen
import kotlin.math.ceil import kotlin.math.ceil
import kotlin.math.round import kotlin.math.round
import com.unciv.ui.components.widgets.AutoScrollPane as ScrollPane
class CityStatsTable(private val cityScreen: CityScreen) : Table() { class CityStatsTable(private val cityScreen: CityScreen) : Table() {
private val innerTable = Table() // table within this Table. Slightly smaller creates border
private val upperTable = Table() // fixed position table
private val lowerTable = Table() // table that will be in the ScrollPane
private val lowerPane: ScrollPane
private val city = cityScreen.city private val city = cityScreen.city
private val headerIcon = ImageGetter.getImage("OtherIcons/BackArrow").apply { private val expander: ExpanderTab
setSize(18f, 18f) // table within this Table. Slightly smaller creates border
setOrigin(Align.center) private val miniStatsTable = MiniStatsTable(ExpanderTab.wasOpen("CityStatsTable"))
rotation = 90f private val lowerTable = Table() // table that will be in the ScrollPane
} private val lowerPane = AutoScrollPane(lowerTable)
private var headerIconClickArea = Table() private var lowerCell: Cell<AutoScrollPane>? = null
private var isOpen = !cityScreen.isCrampedPortrait()
private val detailedStatsButton = "Stats".toTextButton().apply { private val detailedStatsButton = "Stats".toTextButton().apply {
labelCell.pad(10f) labelCell.pad(10f)
@ -54,61 +52,43 @@ class CityStatsTable(private val cityScreen: CityScreen) : Table() {
tintColor = colorFromRGB(194, 180, 131) tintColor = colorFromRGB(194, 180, 131)
) )
innerTable.pad(5f) expander = ExpanderTab("",
innerTable.background = BaseScreen.skinStrings.getUiBackground( startsOutOpened = !cityScreen.isCrampedPortrait(),
persistenceID = "CityStatsTable",
defaultPad = 7f,
headerPad = if (cityScreen.isCrampedPortrait()) 7f else 6f,
topPad = 0f, // remove space between miniStatsTable and detailedStatsButton
expanderWidth = miniStatsTable.width,
expanderHeight = miniStatsTable.height,
onChange = {
cityScreen.updateWithoutConstructionAndMap()
}
) {
lowerCell = it.add(lowerPane).grow()
}
expander.headerContent.add(miniStatsTable).growX()
expander.background = BaseScreen.skinStrings.getUiBackground(
"CityScreen/CityStatsTable/InnerTable", "CityScreen/CityStatsTable/InnerTable",
tintColor = ImageGetter.CHARCOAL.cpy().apply { a = 0.8f } tintColor = ImageGetter.CHARCOAL.cpy().apply { a = 0.8f }
) )
expander.header.background = null // Make header transparent
/** Without this, the expander will keep initial header height, even when the
* row count of miniStatsTable changes when opening/closing in portrait mode */
expander.setDynamicHeaderSize()
// Don't toggle expander when clicking the stats icons
expander.toggleOnIconOnly()
upperTable.defaults().pad(2f)
lowerTable.defaults().pad(2f)
lowerPane = ScrollPane(lowerTable)
lowerPane.setOverscroll(false, false) lowerPane.setOverscroll(false, false)
lowerPane.setScrollingDisabled(true, false) lowerPane.setScrollingDisabled(x = true, y = false)
lowerTable.defaults().space(4f)
add(innerTable).growX() add(expander).growX()
// collapse icon with larger click area
headerIconClickArea.add(headerIcon).size(headerIcon.width).pad(6f+2f, 12f, 6f, 2f )
headerIconClickArea.touchable = Touchable.enabled
headerIconClickArea.onClick {
isOpen = !isOpen
cityScreen.updateWithoutConstructionAndMap()
}
} }
fun update(height: Float) { fun update(height: Float) {
upperTable.clear() miniStatsTable.update()
lowerTable.clear()
val miniStatsTable = Table() lowerTable.clear()
val selected = BaseScreen.skin.getColor("selection")
for ((stat, amount) in city.cityStats.currentCityStats) {
if (stat == Stat.Faith && !city.civ.gameInfo.isReligionEnabled()) continue
val icon = Table()
val focus = CityFocus.safeValueOf(stat)
val toggledFocus = if (focus == city.getCityFocus()) {
icon.add(ImageGetter.getStatIcon(stat.name).surroundWithCircle(27f, false, color = selected))
CityFocus.NoFocus
} else {
icon.add(ImageGetter.getStatIcon(stat.name).surroundWithCircle(27f, false, color = Color.CLEAR))
focus
}
if (cityScreen.canCityBeChanged()) {
icon.onActivation(binding = toggledFocus.binding) {
city.setCityFocus(toggledFocus)
city.reassignPopulation()
cityScreen.update()
}
}
miniStatsTable.add(icon).size(27f).padRight(3f)
val valueToDisplay = if (stat == Stat.Happiness) city.cityStats.happinessList.values.sum() else amount
miniStatsTable.add(round(valueToDisplay).toInt().toLabel()).padRight(5f)
if (cityScreen.isCrampedPortrait() && !isOpen && stat == Stat.Gold) {
miniStatsTable.row()
}
}
upperTable.add(miniStatsTable).expandX()
lowerTable.add(detailedStatsButton).row() lowerTable.add(detailedStatsButton).row()
addText() addText()
@ -124,22 +104,12 @@ class CityStatsTable(private val cityScreen: CityScreen) : Table() {
addBuildingsInfo() addBuildingsInfo()
headerIcon.rotation = if(isOpen) 90f else 0f
innerTable.clear()
innerTable.add(upperTable).expandX()
innerTable.add(headerIconClickArea).row()
val lowerCell = if (isOpen) {
innerTable.add(lowerPane).colspan(2)
} else null
upperTable.pack()
lowerTable.pack() lowerTable.pack()
lowerPane.layout() lowerPane.layout()
lowerPane.updateVisualScroll() lowerPane.updateVisualScroll()
lowerCell?.maxHeight(height - upperTable.height - 8f) // 2 on each side of each cell in innerTable lowerCell?.maxHeight(height - expander.header.height - 8f) // 2 on each side of each cell in expander
innerTable.pack() // update innerTable expander.pack() // update expander
pack() // update self last pack() // update self last
} }
@ -417,4 +387,43 @@ class CityStatsTable(private val cityScreen: CityScreen) : Table() {
lowerTable.addCategory("Great People", greatPeopleTable, KeyboardBinding.GreatPeopleDetail) lowerTable.addCategory("Great People", greatPeopleTable, KeyboardBinding.GreatPeopleDetail)
} }
private inner class MiniStatsTable(wasOpen: Boolean?) : Table() {
// Challenge: we want this measured before instantating the ExpanderTab.
// Ergo: update() must not access expander until _after_ init
init {
update(wasOpen)
pack()
}
fun update() = update(expander.isOpen)
private fun update(expanderIsOpen: Boolean?) {
clear()
val selected = BaseScreen.skin.getColor("selection")
for ((stat, amount) in city.cityStats.currentCityStats) {
if (stat == Stat.Faith && !city.civ.gameInfo.isReligionEnabled()) continue
val icon = Table()
val focus = CityFocus.safeValueOf(stat)
val toggledFocus = if (focus == city.getCityFocus()) {
icon.add(ImageGetter.getStatIcon(stat.name).surroundWithCircle(27f, false, color = selected))
CityFocus.NoFocus
} else {
icon.add(ImageGetter.getStatIcon(stat.name).surroundWithCircle(27f, false, color = Color.CLEAR))
focus
}
if (cityScreen.canCityBeChanged()) {
icon.onActivation(binding = toggledFocus.binding) {
city.setCityFocus(toggledFocus)
city.reassignPopulation()
cityScreen.update()
}
}
add(icon).size(27f).padRight(3f)
val valueToDisplay = if (stat == Stat.Happiness) city.cityStats.happinessList.values.sum() else amount
add(round(valueToDisplay).toInt().toLabel()).padRight(5f)
if (cityScreen.isCrampedPortrait() && (expanderIsOpen == null || !expanderIsOpen) && stat == Stat.Gold) {
row()
}
}
}
}
} }