UI: Fix options popup "spilling" in cramped screen conditions (#11235)

* Remove unused "Closing page" feature from TabbedPager

* Minor linting

* Ensure Popup's innerTable doesn't exceed its Cell within Popup

* Change a few defaults and remove a misguided comment

* Move OptionPopup's close button to top right - another Red X
This commit is contained in:
SomeTroglodyte 2024-03-03 19:02:31 +01:00 committed by GitHub
parent b577f5ee9a
commit d843ac0c68
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 51 additions and 44 deletions

View File

@ -266,7 +266,7 @@ fun String.toImageButton(iconSize: Float, circleSize: Float, circleColor: Color,
* Automatically binds the BACK key to the [action]. * Automatically binds the BACK key to the [action].
*/ */
fun getCloseButton( fun getCloseButton(
size: Float, size: Float = 50f,
iconSize: Float = size - 20f, iconSize: Float = size - 20f,
circleColor: Color = BaseScreen.skinStrings.skinConfig.baseColor, circleColor: Color = BaseScreen.skinStrings.skinConfig.baseColor,
overColor: Color = Color.RED, overColor: Color = Color.RED,

View File

@ -55,6 +55,15 @@ import com.unciv.ui.screens.basescreen.BaseScreen
* maximum is not specified, that coordinate will grow with content up to screen size, and layout * maximum is not specified, that coordinate will grow with content up to screen size, and layout
* max-W/H will always report the same as pref-W/H. * max-W/H will always report the same as pref-W/H.
*/ */
/*
Notes - this is implemented as 2x3 Table with 4 Cells.
First row is (scrolling header, fixed decoration) - second cell empty and size 0 until you use decorateHeader.
Second row / third Cell is "fixed" content, colspan 2. Used for pages that implement IPageExtensions.getFixedContent, otherwise size zero.
Third row / fourth Cell is scrolling content, colspan 2. Can scroll in both axes, while "fixed" only scrolls horizontally -
horizontal scroll is synchronized (for that to look good, both content parts must match in size and visual layout).
*/
//region Fields //region Fields
@Suppress("MemberVisibilityCanBePrivate", "unused") // All members are part of our API @Suppress("MemberVisibilityCanBePrivate", "unused") // All members are part of our API
open class TabbedPager( open class TabbedPager(
@ -290,12 +299,6 @@ open class TabbedPager(
} }
} }
private class EmptyClosePage(private val action: ()->Unit) : Actor(), IPageExtensions {
override fun activated(index: Int, caption: String, pager: TabbedPager) {
action()
}
}
//endregion //endregion
//region Initialization //region Initialization
@ -582,18 +585,6 @@ open class TabbedPager(
return addAndShowPage(page, insertBefore) return addAndShowPage(page, insertBefore)
} }
/**
* Add a "Close" button tho the Tab headers, with empty content which will invoke [action] when clicked
*/
fun addClosePage(
insertBefore: Int = -1,
color: Color = Color(0.75f, 0.1f, 0.1f, 1f),
action: ()->Unit
) {
val index = addPage(Constants.close, EmptyClosePage(action), insertBefore = insertBefore)
pages[index].button.color = color
}
/** /**
* Activate any [secret][addPage] pages by asking for the password. * Activate any [secret][addPage] pages by asking for the password.
* *
@ -633,14 +624,13 @@ open class TabbedPager(
* Must be called _after_ all pages are final, otherwise effects not guaranteed. * Must be called _after_ all pages are final, otherwise effects not guaranteed.
* *
* Notes: * Notes:
* * Using [fixed] will make the widths of content and header Scrollpanes different. Synchronized scrolling will look off.
* * [fixed] decorations have predefined cells, thus decorateHeader will replace any previous decoration. Non-[fixed] are cumulative and cannot be removed. * * [fixed] decorations have predefined cells, thus decorateHeader will replace any previous decoration. Non-[fixed] are cumulative and cannot be removed.
* * [leftSide] and [fixed] both true is not implemented. * * [leftSide] and [fixed] both true is not implemented.
* *
* @param leftSide If `true` then [actor] is inserted on the left, otherwise on the right of the page buttons. * @param leftSide If `true` then [actor] is inserted on the left, otherwise on the right of the page buttons.
* @param fixed If `true` [actor] is outside the header ScrollPane and thus always shown. * @param fixed If `true` [actor] is outside the header ScrollPane and thus always shown.
*/ */
fun decorateHeader(actor: Actor, leftSide: Boolean, fixed: Boolean = false) { fun decorateHeader(actor: Actor, leftSide: Boolean = false, fixed: Boolean = true) {
if (fixed) headerDecorationRightCell.pad(headerPadding).setActor(actor) if (fixed) headerDecorationRightCell.pad(headerPadding).setActor(actor)
else insertHeaderCellAt(if (leftSide) 0 else -1).setActor(actor) else insertHeaderCellAt(if (leftSide) 0 else -1).setActor(actor)
invalidate() invalidate()

View File

@ -43,7 +43,8 @@ import com.unciv.ui.screens.basescreen.UncivStage
* *
* @property stageToShowOn The stage that will be used for [open], measurements or finding other instances * @property stageToShowOn The stage that will be used for [open], measurements or finding other instances
* @param scrollable Controls how content can scroll if too large - see [Scrollability] * @param scrollable Controls how content can scroll if too large - see [Scrollability]
* @param maxSizePercentage Causes [topTable] to limit its height - useful if `scrollable` is on. Will be multiplied by stageToShowOn.height. * @param maxSizePercentage Causes [topTable] to limit its width/height - useful if `scrollable` is on. Will be multiplied by stageToShowOn sizes.
* Note this can be overridden if a larger minWidth is reported by [innerTable].
*/ */
@Suppress("MemberVisibilityCanBePrivate") @Suppress("MemberVisibilityCanBePrivate")
open class Popup( open class Popup(
@ -63,7 +64,7 @@ open class Popup(
* With scrolling enabled, the ScrollPane can be accessed via [getScrollPane]. * With scrolling enabled, the ScrollPane can be accessed via [getScrollPane].
* @property None No scrolling * @property None No scrolling
* @property All Entire content wrapped in an [AutoScrollPane] so it can scroll if larger than maximum dimensions * @property All Entire content wrapped in an [AutoScrollPane] so it can scroll if larger than maximum dimensions
* @property WithoutButtons content separated into scrollable upper part and static lower part containing the buttons * @property WithoutButtons Content separated into scrollable upper part and static lower part containing the buttons
*/ */
enum class Scrollability { None, All, WithoutButtons } enum class Scrollability { None, All, WithoutButtons }
@ -75,6 +76,8 @@ open class Popup(
* *
* Note you seldom need to interact directly with it, Popup has many Table method proxies * Note you seldom need to interact directly with it, Popup has many Table method proxies
* that pass through, like [add], [row], [defaults], [addSeparator] or [clear]. * that pass through, like [add], [row], [defaults], [addSeparator] or [clear].
*
* For [Scrollability.WithoutButtons], this wraps [topTable] and [bottomTable], otherwise both are aliases.
*/ */
/* Hierarchy: /* Hierarchy:
Scrollability.None: Scrollability.None:
@ -98,11 +101,14 @@ open class Popup(
*/ */
protected val innerTable = Table(BaseScreen.skin) protected val innerTable = Table(BaseScreen.skin)
/** This contains most of the Popup content (except the closing buttons which go in [bottomTable]) */ /** This contains most of the Popup content.
* For [Scrollability.WithoutButtons], that excludes the closing buttons which go in [bottomTable].
* In other modes, this is an alias for [innerTable]. */
private val topTable: Table private val topTable: Table
private val topTableCell: Cell<WidgetGroup> private val topTableCell: Cell<WidgetGroup>
/** This contains the bottom row buttons and does not participate in scrolling */ /** This contains the bottom row buttons and does not participate in scrolling
* (for [Scrollability.WithoutButtons], otherwise alias for [innerTable]). */
protected val bottomTable: Table protected val bottomTable: Table
/** Callbacks that will be called whenever this Popup is shown */ /** Callbacks that will be called whenever this Popup is shown */
@ -180,6 +186,13 @@ open class Popup(
innerTable.invalidate() innerTable.invalidate()
} }
private fun recalculateInnerTableMaxWidth() {
val minWidth = innerTable.minWidth.coerceAtMost(stageToShowOn.width)
if (minWidth < maxPopupWidth) return
topTableCell.maxWidth(minWidth)
innerTable.invalidate()
}
/** /**
* Displays the Popup on the screen. If another popup is already open, this one will display after the other has * Displays the Popup on the screen. If another popup is already open, this one will display after the other has
@ -189,6 +202,7 @@ open class Popup(
stageToShowOn.addActor(this) stageToShowOn.addActor(this)
recalculateInnerTableMaxHeight() recalculateInnerTableMaxHeight()
innerTable.pack() innerTable.pack()
recalculateInnerTableMaxWidth()
pack() pack()
center(stageToShowOn) center(stageToShowOn)
events.receive(UncivStage.VisibleAreaChanged::class) { events.receive(UncivStage.VisibleAreaChanged::class) {
@ -240,18 +254,18 @@ open class Popup(
Note the Kdoc mentions innerTable when under Scrollability.WithoutButtons it's actually topTable, Note the Kdoc mentions innerTable when under Scrollability.WithoutButtons it's actually topTable,
but metioning that distinction seems overkill. innerTable has the clearer Kdoc for "where the Actors go". but metioning that distinction seems overkill. innerTable has the clearer Kdoc for "where the Actors go".
*/ */
/** Popup proxy redirects [add][com.badlogic.gdx.scenes.scene2d.ui.Table.add] to [innerTable] */ /** Popup proxy redirects [add][com.badlogic.gdx.scenes.scene2d.ui.Table.add] to [topTable] */
final override fun <T : Actor?> add(actor: T): Cell<T> = topTable.add(actor) final override fun <T : Actor?> add(actor: T): Cell<T> = topTable.add(actor)
/** Popup proxy redirects [add][com.badlogic.gdx.scenes.scene2d.ui.Table.add] to [innerTable] */ /** Popup proxy redirects [add][com.badlogic.gdx.scenes.scene2d.ui.Table.add] to [topTable] */
final override fun add(): Cell<Actor?> = topTable.add() final override fun add(): Cell<Actor?> = topTable.add()
/** Popup proxy redirects [row][com.badlogic.gdx.scenes.scene2d.ui.Table.row] to [innerTable] */ /** Popup proxy redirects [row][com.badlogic.gdx.scenes.scene2d.ui.Table.row] to [topTable] */
override fun row(): Cell<Actor> = topTable.row() override fun row(): Cell<Actor> = topTable.row()
/** Popup proxy redirects [defaults][com.badlogic.gdx.scenes.scene2d.ui.Table.defaults] to [innerTable] */ /** Popup proxy redirects [defaults][com.badlogic.gdx.scenes.scene2d.ui.Table.defaults] to [topTable] */
override fun defaults(): Cell<Actor> = topTable.defaults() override fun defaults(): Cell<Actor> = topTable.defaults()
/** Popup proxy redirects [addSeparator][com.unciv.ui.components.extensions.addSeparator] to [innerTable] */ /** Popup proxy redirects [addSeparator][com.unciv.ui.components.extensions.addSeparator] to [topTable] */
fun addSeparator(color: Color = Color.WHITE, colSpan: Int = 0, height: Float = 2f) = fun addSeparator(color: Color = Color.WHITE, colSpan: Int = 0, height: Float = 2f) =
topTable.addSeparator(color, colSpan, height) topTable.addSeparator(color, colSpan, height)
/** Proxy redirects [add][com.badlogic.gdx.scenes.scene2d.ui.Table.clear] to clear content: /** Proxy redirects [clear][com.badlogic.gdx.scenes.scene2d.ui.Table.clear] to clear all content:
* [innerTable] or if [Scrollability.WithoutButtons] was used [topTable] and [bottomTable] */ * [innerTable] or if [Scrollability.WithoutButtons] was used [topTable] and [bottomTable] */
override fun clear() { override fun clear() {
topTable.clear() topTable.clear()

View File

@ -90,7 +90,7 @@ private fun addCutoutCheckbox(table: Table, optionsPopup: OptionsPopup) {
{ {
optionsPopup.settings.androidCutout = it optionsPopup.settings.androidCutout = it
Display.setCutout(it) Display.setCutout(it)
optionsPopup.reopenAfterDiplayLayoutChange() optionsPopup.reopenAfterDisplayLayoutChange()
} }
} }
@ -99,7 +99,7 @@ private fun addHideSystemUiCheckbox(table: Table, optionsPopup: OptionsPopup) {
{ {
optionsPopup.settings.androidHideSystemUi = it optionsPopup.settings.androidHideSystemUi = it
Display.setSystemUiVisibility(hide = it) Display.setSystemUiVisibility(hide = it)
optionsPopup.reopenAfterDiplayLayoutChange() optionsPopup.reopenAfterDisplayLayoutChange()
} }
} }
@ -116,7 +116,7 @@ private fun addOrientationSelectBox(table: Table, optionsPopup: OptionsPopup) {
val orientation = selectBox.selected val orientation = selectBox.selected
settings.displayOrientation = orientation settings.displayOrientation = orientation
Display.setOrientation(orientation) Display.setOrientation(orientation)
optionsPopup.reopenAfterDiplayLayoutChange() optionsPopup.reopenAfterDisplayLayoutChange()
} }
table.add(selectBox).minWidth(optionsPopup.selectBoxMinWidth).pad(10f).row() table.add(selectBox).minWidth(optionsPopup.selectBoxMinWidth).pad(10f).row()

View File

@ -9,6 +9,7 @@ import com.unciv.models.metadata.BaseRuleset
import com.unciv.models.ruleset.RulesetCache import com.unciv.models.ruleset.RulesetCache
import com.unciv.ui.components.extensions.areSecretKeysPressed import com.unciv.ui.components.extensions.areSecretKeysPressed
import com.unciv.ui.components.extensions.center import com.unciv.ui.components.extensions.center
import com.unciv.ui.components.extensions.getCloseButton
import com.unciv.ui.components.extensions.toCheckBox import com.unciv.ui.components.extensions.toCheckBox
import com.unciv.ui.components.widgets.TabbedPager import com.unciv.ui.components.widgets.TabbedPager
import com.unciv.ui.images.ImageGetter import com.unciv.ui.images.ImageGetter
@ -65,7 +66,7 @@ class OptionsPopup(
selectBoxMinWidth = if (stage.width < 600f) 200f else 240f selectBoxMinWidth = if (stage.width < 600f) 200f else 240f
tabMaxWidth = if (isPortrait()) stage.width - 10f else 0.8f * stage.width tabMaxWidth = if (isPortrait()) stage.width - 10f else 0.8f * stage.width
tabMinWidth = 0.6f * stage.width tabMinWidth = 0.6f * stage.width
tabMaxHeight = (if (isPortrait()) 0.7f else 0.8f) * stage.height tabMaxHeight = 0.8f * stage.height
} }
tabs = TabbedPager( tabs = TabbedPager(
tabMinWidth, tabMaxWidth, 0f, tabMaxHeight, tabMinWidth, tabMaxWidth, 0f, tabMaxHeight,
@ -123,13 +124,13 @@ class OptionsPopup(
tabs.addPage("Debug", debugTab(this), ImageGetter.getImage("OtherIcons/SecretOptions"), 24f) tabs.addPage("Debug", debugTab(this), ImageGetter.getImage("OtherIcons/SecretOptions"), 24f)
} }
addCloseButton { tabs.decorateHeader(getCloseButton {
screen.game.musicController.onChange(null) screen.game.musicController.onChange(null)
center(screen.stage) center(screen.stage)
keyBindingsTab?.save() keyBindingsTab?.save()
settings.save() settings.save()
onClose() onClose()
}.padBottom(10f) })
if (GUI.keyboardAvailable) { if (GUI.keyboardAvailable) {
showOrHideKeyBindings() // Do this late because it looks for the page to insert before showOrHideKeyBindings() // Do this late because it looks for the page to insert before
@ -172,7 +173,7 @@ class OptionsPopup(
* Does nothing if any Popup (which can only be this one) is still open after a short delay and context yield. * Does nothing if any Popup (which can only be this one) is still open after a short delay and context yield.
* Reason: A resize might relaunch the parent screen ([MainMenuScreen] is [RecreateOnResize]) and thus close this Popup. * Reason: A resize might relaunch the parent screen ([MainMenuScreen] is [RecreateOnResize]) and thus close this Popup.
*/ */
fun reopenAfterDiplayLayoutChange() { internal fun reopenAfterDisplayLayoutChange() {
Concurrency.run("Reload from options") { Concurrency.run("Reload from options") {
delay(100) delay(100)
withGLContext { withGLContext {
@ -183,7 +184,7 @@ class OptionsPopup(
} }
} }
fun addCheckbox(table: Table, text: String, initialState: Boolean, updateWorld: Boolean = false, newRow: Boolean = true, action: ((Boolean) -> Unit)) { internal fun addCheckbox(table: Table, text: String, initialState: Boolean, updateWorld: Boolean = false, newRow: Boolean = true, action: ((Boolean) -> Unit)) {
val checkbox = text.toCheckBox(initialState) { val checkbox = text.toCheckBox(initialState) {
action(it) action(it)
val worldScreen = GUI.getWorldScreenIfActive() val worldScreen = GUI.getWorldScreenIfActive()
@ -193,7 +194,7 @@ class OptionsPopup(
else table.add(checkbox).left() else table.add(checkbox).left()
} }
fun addCheckbox( internal fun addCheckbox(
table: Table, table: Table,
text: String, text: String,
settingsProperty: KMutableProperty0<Boolean>, settingsProperty: KMutableProperty0<Boolean>,

View File

@ -71,8 +71,8 @@ class EmpireOverviewScreen(
} }
} }
val closeButton = getCloseButton(50f) { game.popScreen() } val closeButton = getCloseButton { game.popScreen() }
tabbedPager.decorateHeader(closeButton, leftSide = false, fixed = true) tabbedPager.decorateHeader(closeButton)
tabbedPager.setFillParent(true) tabbedPager.setFillParent(true)
stage.addActor(tabbedPager) stage.addActor(tabbedPager)

View File

@ -135,10 +135,12 @@ class VictoryScreen(
val difficultyLabel = "{Difficulty}: {${gameInfo.difficulty}}".toLabel() val difficultyLabel = "{Difficulty}: {${gameInfo.difficulty}}".toLabel()
val neededSpace = topRightPanel.width.coerceAtLeast(difficultyLabel.width) * 2 + tabs.getHeaderPrefWidth() val neededSpace = topRightPanel.width.coerceAtLeast(difficultyLabel.width) * 2 + tabs.getHeaderPrefWidth()
if (neededSpace > stage.width) { if (neededSpace > stage.width) {
tabs.decorateHeader(difficultyLabel, true) // Let additions take part in TabbedPager's header scrolling
tabs.decorateHeader(topRightPanel, false) tabs.decorateHeader(difficultyLabel, leftSide = true, fixed = false)
tabs.decorateHeader(topRightPanel, leftSide = false, fixed = false)
tabs.headerScroll.fadeScrollBars = false tabs.headerScroll.fadeScrollBars = false
} else { } else {
// Let additions float in the corners
val panelY = stage.height - tabs.getRowHeight(0) * 0.5f val panelY = stage.height - tabs.getRowHeight(0) * 0.5f
stage.addActor(topRightPanel) stage.addActor(topRightPanel)
topRightPanel.setPosition(stage.width - 10f, panelY, Align.right) topRightPanel.setPosition(stage.width - 10f, panelY, Align.right)