Align ruleset icons in text to font metrics (#10233)

* Try to respect actual font layout, so fontSizeMultiplier works for ruleset icons too

* Replace font-based nation symbol in top bar with statically sized actor

* Reuse getReadonlyPixmap in extractPixmapFromTextureRegion

* Tweak topbar selected civ vertical align to be more pleasing to the human eye

* FasterUIDevelopment missing implementation of FontMetricsCommon

* Address hardcoded pixel coordinates comment

* Readability and comment cleanup

* More readability changes
This commit is contained in:
SomeTroglodyte
2023-10-07 21:00:30 +02:00
committed by GitHub
parent 5db8489bcb
commit 49e2979427
7 changed files with 296 additions and 99 deletions

View File

@ -14,10 +14,12 @@ import com.badlogic.gdx.Gdx
import com.badlogic.gdx.graphics.Pixmap
import com.unciv.ui.components.FontFamilyData
import com.unciv.ui.components.FontImplementation
import com.unciv.ui.components.FontMetricsCommon
import com.unciv.ui.components.Fonts
import com.unciv.utils.Log
import java.util.Locale
import kotlin.math.abs
import kotlin.math.ceil
class AndroidFont : FontImplementation {
@ -105,21 +107,22 @@ class AndroidFont : FontImplementation {
override fun getCharPixmap(char: Char): Pixmap {
val metric = paint.fontMetrics
var width = paint.measureText(char.toString()).toInt()
var height = (metric.descent - metric.ascent).toInt()
var height = ceil(metric.bottom - metric.top).toInt()
if (width == 0) {
height = getFontSize()
width = height
}
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
canvas.drawText(char.toString(), 0f, -metric.ascent, paint)
canvas.drawText(char.toString(), 0f, -metric.top, paint)
val pixmap = Pixmap(width, height, Pixmap.Format.RGBA8888)
val data = IntArray(width * height)
bitmap.getPixels(data, 0, width, 0, 0, width, height)
for (i in 0 until width) {
for (j in 0 until height) {
pixmap.setColor(Integer.rotateLeft(data[i + (j * width)], 8))
pixmap.drawPixel(i, j)
bitmap.getPixels(data, 0, width, 0, 0, width, height) // faster than bitmap[x, y]
for (x in 0 until width) {
for (y in 0 until height) {
pixmap.drawPixel(x, y, Integer.rotateLeft(data[x + (y * width)], 8))
}
}
bitmap.recycle()
@ -157,4 +160,11 @@ class AndroidFont : FontImplementation {
}.distinct()
.map { FontFamilyData(it, it) }
}
override fun getMetrics() = FontMetricsCommon(
ascent = -paint.fontMetrics.ascent,
descent = paint.fontMetrics.descent,
height = paint.fontMetrics.bottom - paint.fontMetrics.top,
leading = paint.fontMetrics.ascent - paint.fontMetrics.top
)
}

View File

@ -13,7 +13,9 @@ import com.badlogic.gdx.graphics.g2d.PixmapPacker
import com.badlogic.gdx.graphics.g2d.SpriteBatch
import com.badlogic.gdx.graphics.g2d.TextureRegion
import com.badlogic.gdx.graphics.glutils.FrameBuffer
import com.badlogic.gdx.math.Matrix4
import com.badlogic.gdx.scenes.scene2d.Actor
import com.badlogic.gdx.scenes.scene2d.Group
import com.badlogic.gdx.utils.Array
import com.badlogic.gdx.utils.Disposable
import com.unciv.Constants
@ -21,10 +23,45 @@ import com.unciv.GUI
import com.unciv.UncivGame
import com.unciv.models.ruleset.Ruleset
import com.unciv.models.translations.tr
import com.unciv.ui.components.extensions.getReadonlyPixmap
import com.unciv.ui.components.extensions.setSize
import com.unciv.ui.images.ImageGetter
import com.unciv.ui.images.Portrait
import kotlin.math.ceil
import kotlin.math.roundToInt
// See https://en.wikipedia.org/wiki/Private_Use_Areas
// char encodings 57344 to 63743 (U+E000-U+F8FF) are not assigned
private const val UNUSED_CHARACTER_CODES_START = 57344
private const val UNUSED_CHARACTER_CODES_END = 63743
/** Implementations of FontImplementation will use different FontMetrics - AWT or Android.Paint,
* both have a class of that name, no other common point: thus we create an abstraction.
*
* This is used by [Fonts.getPixmapFromActor] for vertical positioning.
*/
class FontMetricsCommon(
/** (positive) distance from the baseline up to the recommended top of normal text */
val ascent: Float,
/** (positive) distance from the baseline down to the recommended bottom of normal text */
val descent: Float,
/** (positive) maximum distance from top to bottom of any text,
* including potentially empty space above ascent or below descent */
val height: Float,
/** (positive) distance from the bounding box top (as defined by [height])
* to the highest possible top of any text */
// Note: This is NOT what typographical leading actually is, but redefined as extra empty space
// on top, to make it easier to sync desktop and android. AWT has some leading but no measures
// outside ascent+descent+leading, while Android has its leading always 0 but typically top
// above ascent and bottom below descent.
// I chose to map AWT's spacing to the top as I found the calculations easier to visualize.
/** Space from the bounding box top to the top of the ascenders - includes line spacing and
* room for unusually high ascenders, as [ascent] is only a recommendation. */
val leading: Float
)
interface FontImplementation {
fun setFontFamily(fontFamilyData: FontFamilyData, size: Int)
fun getFontSize(): Int
@ -37,6 +74,8 @@ interface FontImplementation {
font.setOwnsTexture(true)
return font
}
fun getMetrics(): FontMetricsCommon
}
// If save in `GameSettings` need use invariantFamily.
@ -79,6 +118,23 @@ class NativeBitmapFontData(
private val filter = Texture.TextureFilter.Linear
private companion object {
/** How to get the alpha channel in a Pixmap.getPixel return value (Int) - it's the LSB */
const val alphaChannelMask = 255
/** Where to test circle for transparency */
// The center of a squared circle's corner wedge would be at (1-PI/4)/2 ≈ 0.1073
const val nearCornerRelativeOffset = 0.1f
/** Where to test circle for opacity */
// arbitrary choice just off-center
const val nearCenterRelativeOffset = 0.4f
/** Width multiplier to get extra advance after a ruleset icon, empiric */
const val relativeAdvanceExtra = 0.039f
/** Multiplier to get default kerning between a ruleset icon and 'open' characters */
const val relativeKerning = -0.055f
/** Which follower characters receive how much kerning relative to [relativeKerning] */
val kerningMap = mapOf('A' to 1f, 'T' to 0.6f, 'V' to 1f, 'Y' to 1.2f)
}
init {
// set general font data
flipped = false
@ -105,40 +161,77 @@ class NativeBitmapFontData(
setScale(Constants.defaultFontSize / Fonts.ORIGINAL_FONT_SIZE)
}
override fun getGlyph(ch: Char): Glyph {
var glyph: Glyph? = super.getGlyph(ch)
if (glyph == null) {
val charPixmap = getPixmapFromChar(ch)
override fun getGlyph(ch: Char): Glyph = super.getGlyph(ch) ?: createAndCacheGlyph(ch)
glyph = Glyph()
glyph.id = ch.code
glyph.width = charPixmap.width
glyph.height = charPixmap.height
glyph.xadvance = glyph.width
private fun createAndCacheGlyph(ch: Char): Glyph {
val charPixmap = getPixmapFromChar(ch)
val rect = packer.pack(charPixmap)
charPixmap.dispose()
glyph.page = packer.pages.size - 1 // Glyph is always packed into the last page for now.
glyph.srcX = rect.x.toInt()
glyph.srcY = rect.y.toInt()
val glyph = Glyph()
glyph.id = ch.code
glyph.width = charPixmap.width
glyph.height = charPixmap.height
glyph.xadvance = glyph.width
// If a page was added, create a new texture region for the incrementally added glyph.
if (regions.size <= glyph.page)
packer.updateTextureRegions(regions, filter, filter, false)
// Check alpha to guess whether this is a round icon
// Needs to be done before disposing charPixmap, and we want to do that soon
val assumeRoundIcon = charPixmap.guessIsRoundSurroundedByTransparency()
val rect = packer.pack(charPixmap)
charPixmap.dispose()
glyph.page = packer.pages.size - 1 // Glyph is always packed into the last page for now.
glyph.srcX = rect.x.toInt()
glyph.srcY = rect.y.toInt()
if (ch.code >= UNUSED_CHARACTER_CODES_START)
glyph.setRulesetIconGeometry(assumeRoundIcon)
// If a page was added, create a new texture region for the incrementally added glyph.
if (regions.size <= glyph.page)
packer.updateTextureRegions(regions, filter, filter, false)
setGlyphRegion(glyph, regions.get(glyph.page))
setGlyph(ch.code, glyph)
dirty = true
setGlyphRegion(glyph, regions.get(glyph.page))
setGlyph(ch.code, glyph)
dirty = true
}
return glyph
}
private fun getPixmap(fileName:String) = Fonts.extractPixmapFromTextureRegion(ImageGetter.getDrawable(fileName).region)
private fun Pixmap.guessIsRoundSurroundedByTransparency(): Boolean {
// If a pixel near the center is opaque...
val nearCenterOffset = (width * nearCenterRelativeOffset).toInt()
if ((getPixel(nearCenterOffset, nearCenterOffset) and alphaChannelMask) == 0) return false
// ... and one near a corner is transparent ...
val nearCornerOffset = (width * nearCornerRelativeOffset).toInt()
return (getPixel(nearCornerOffset, nearCornerOffset) and alphaChannelMask) == 0
// ... then assume it's a circular icon surrounded by transparency - for kerning
}
private fun Glyph.setRulesetIconGeometry(assumeRoundIcon: Boolean) {
// This is a Ruleset object icon - first avoid "glue"'ing them to the next char..
// ends up 2px for default font scale, 1px for min, 3px for max
xadvance += (width * relativeAdvanceExtra).roundToInt()
if (!assumeRoundIcon) return
// Now, if we guessed it's round, do some kerning, only for the most conspicuous combos.
// Will look ugly for very unusual Fonts - should we limit this to only default fonts?
// Kerning is a sparse 2D array of up to 2^16 hints, each stored as byte, so this is
// costly: kerningMap.size * Fonts.charToRulesetImageActor.size * 512 bytes
// Which is 1.76MB for vanilla G&K rules.
// Ends up -3px for default font scale, -2px for minimum, -4px for max
val defaultKerning = (width * relativeKerning)
for ((char, kerning) in kerningMap)
setKerning(char.code, (defaultKerning * kerning).roundToInt())
}
private fun getPixmapForTextureName(regionName: String) =
Fonts.extractPixmapFromTextureRegion(ImageGetter.getDrawable(regionName).region)
private fun getPixmapFromChar(ch: Char): Pixmap {
// Images must be 50*50px so they're rendered at the same height as the text - see Fonts.ORIGINAL_FONT_SIZE
return when (ch) {
in Fonts.allSymbols -> getPixmap(Fonts.allSymbols[ch]!!)
in Fonts.allSymbols -> getPixmapForTextureName(Fonts.allSymbols[ch]!!)
in Fonts.charToRulesetImageActor ->
try {
// This sometimes fails with a "Frame buffer couldn't be constructed: incomplete attachment" error, unclear why
@ -201,39 +294,47 @@ object Fonts {
*/
// From https://stackoverflow.com/questions/29451787/libgdx-textureregion-to-pixmap
fun extractPixmapFromTextureRegion(textureRegion: TextureRegion): Pixmap {
val metrics = fontImplementation.getMetrics()
val boxHeight = ceil(metrics.height).toInt()
val boxWidth = ceil(metrics.ascent * textureRegion.regionWidth / textureRegion.regionHeight).toInt()
// In case the region's aspect isn't 1:1, scale the rounded-up width back to a height with unrounded aspect ratio
val drawHeight = textureRegion.regionHeight * (boxWidth / textureRegion.regionWidth)
// place region from top of bounding box down
// Adding half the descent is empiric - should theoretically be leading only
val drawY = ceil(metrics.leading + metrics.descent * 0.5f).toInt()
val textureData = textureRegion.texture.textureData
if (!textureData.isPrepared) {
textureData.prepare()
}
val pixmap = Pixmap(
textureRegion.regionWidth,
textureRegion.regionHeight,
textureData.format
)
val textureDataPixmap = textureData.consumePixmap()
val textureDataPixmap = textureData.getReadonlyPixmap()
val pixmap = Pixmap(boxWidth, boxHeight, textureData.format)
// We're using the scaling drawPixmap so pixmap.filter is relevant - it defaults to BiLinear
pixmap.drawPixmap(
textureDataPixmap, // The other Pixmap
0, // The target x-coordinate (top left corner)
0, // The target y-coordinate (top left corner)
textureRegion.regionX, // The source x-coordinate (top left corner)
textureRegion.regionY, // The source y-coordinate (top left corner)
textureRegion.regionWidth, // The width of the area from the other Pixmap in pixels
textureRegion.regionHeight // The height of the area from the other Pixmap in pixels
textureDataPixmap, // The source Pixmap
textureRegion.regionX, // The source x-coordinate (top left corner)
textureRegion.regionY, // The source y-coordinate (top left corner)
textureRegion.regionWidth, // The width of the area from the other Pixmap in pixels
textureRegion.regionHeight, // The height of the area from the other Pixmap in pixels
0, // The target x-coordinate (top left corner)
drawY, // The target y-coordinate (top left corner)
boxWidth, // The target width
drawHeight, // The target height
)
textureDataPixmap.dispose() // Prevent memory leak.
return pixmap
}
val rulesetObjectNameToChar =HashMap<String, Char>()
val charToRulesetImageActor = HashMap<Char, Actor>()
// See https://en.wikipedia.org/wiki/Private_Use_Areas - char encodings 57344 63743 are not assigned
private var nextUnusedCharacterNumber = 57344
private var nextUnusedCharacterNumber = UNUSED_CHARACTER_CODES_START
fun addRulesetImages(ruleset: Ruleset) {
rulesetObjectNameToChar.clear()
charToRulesetImageActor.clear()
nextUnusedCharacterNumber = 57344
nextUnusedCharacterNumber = UNUSED_CHARACTER_CODES_START
fun addChar(objectName: String, objectActor: Actor) {
if (nextUnusedCharacterNumber > UNUSED_CHARACTER_CODES_END) return
val char = Char(nextUnusedCharacterNumber)
nextUnusedCharacterNumber++
rulesetObjectNameToChar[objectName] = char
@ -273,10 +374,27 @@ object Fonts {
}
}
private val frameBuffer by lazy { FrameBuffer(Pixmap.Format.RGBA8888, Gdx.graphics.width, Gdx.graphics.height, false) }
private val frameBuffer by lazy {
// Size here is way too big, but it's hard to know in advance how big it needs to be.
// Gdx world coords, not pixels.
FrameBuffer(Pixmap.Format.RGBA8888, Gdx.graphics.width, Gdx.graphics.height, false)
}
private val spriteBatch by lazy { SpriteBatch() }
private val transform = Matrix4() // for repeated reuse without reallocation
/** Get a Pixmap for a "show ruleset icons as part of text" actor.
*
* Draws onto an offscreen frame buffer and copies the pixels.
* Caller becomes owner of the returned Pixmap and is responsible for disposing it.
*
* Size is such that the actor's height is mapped to the font's ascent (close to
* ORIGINAL_FONT_SIZE * GameSettings.fontSizeMultiplier), the actor is placed like a letter into
* the total height as given by the font's metrics, and width scaled to maintain aspect ratio.
*/
fun getPixmapFromActor(actor: Actor): Pixmap {
val (boxWidth, boxHeight) = scaleAndPositionActor(actor)
val pixmap = Pixmap(boxWidth, boxHeight, Pixmap.Format.RGBA8888)
frameBuffer.begin()
@ -286,27 +404,52 @@ object Fonts {
spriteBatch.begin()
actor.draw(spriteBatch, 1f)
spriteBatch.end()
val w = actor.width.toInt()
val h = actor.height.toInt()
val pixmap = Pixmap(w, h, Pixmap.Format.RGBA8888)
Gdx.gl.glReadPixels(0, 0, w, h, GL20.GL_RGBA, GL20.GL_UNSIGNED_BYTE, pixmap.pixels)
Gdx.gl.glReadPixels(0, 0, boxWidth, boxHeight, GL20.GL_RGBA, GL20.GL_UNSIGNED_BYTE, pixmap.pixels)
frameBuffer.end()
// Pixmap is now *upside down* so we need to flip it around the y axis
pixmap.blending = Pixmap.Blending.None
for (i in 0..w)
for (j in 0..h/2) {
val topPixel = pixmap.getPixel(i,j)
val bottomPixel = pixmap.getPixel(i, h-j)
pixmap.drawPixel(i,j,bottomPixel)
pixmap.drawPixel(i,h-j,topPixel)
}
return pixmap
}
/** Does the Actor scaling and positioning using metrics for [getPixmapFromActor]
* @return boxWidth to boxHeight
*/
private fun scaleAndPositionActor(actor: Actor): Pair<Int, Int> {
// We want our - mostly circular - icon to match a typical large uppercase letter in height
// The drawing bounding box should have room, however for the font's leading and descent
val metrics = fontImplementation.getMetrics()
// Empiric slight size reduction - "correctly calculated" they just look a bit too big
val scaledActorHeight = metrics.ascent * 0.93f
val scaledActorWidth = actor.width * (scaledActorHeight / actor.height)
val boxHeight = ceil(metrics.height).toInt()
val boxWidth = ceil(scaledActorWidth).toInt()
// Nudge down by the border size if it's a Portrait having one, so the "core" sits on the baseline
val border = (actor as? Portrait)?.borderSize ?: 0f
// Scale to desired font dimensions - modifying the actor this way is OK as the decisions are
// the same each repetition, and size in the Group case or aspect ratio otherwise is preserved
if (actor is Group) {
// We can't just actor.setSize - a Group won't scale its children that way
actor.isTransform = true
val scale = scaledActorWidth / actor.width
actor.setScale(scale, -scale)
// Now the Actor is scaled, we need to position it at the baseline, Y from top of the box
// The +1f is empirical because the result still looked off.
actor.setPosition(0f, metrics.leading + metrics.ascent + border * scale + 1f)
} else {
// Assume it's an Image obeying Actor size, but needing explicit Y flipping
// place actor from top of bounding box down
// (don't think the Gdx (Y is upwards) way - due to the transformMatrix below)
actor.setPosition(0f, metrics.leading + border)
actor.setSize(scaledActorWidth, scaledActorHeight)
transform.idt().scl(1f, -1f, 1f).trn(0f, boxHeight.toFloat(), 0f)
spriteBatch.transformMatrix = transform
// (copies matrix, not a set-by-reference, ignored when actor isTransform is on)
}
return boxWidth to boxHeight
}
const val turn = '⏳' // U+23F3 'hourglass'
const val strength = '†' // U+2020 'dagger'
const val rangedStrength = '‡' // U+2021 'double dagger'

View File

@ -2,6 +2,10 @@ package com.unciv.ui.components.extensions
import com.badlogic.gdx.Input
import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.graphics.Pixmap
import com.badlogic.gdx.graphics.TextureData
import com.badlogic.gdx.graphics.glutils.FileTextureData
import com.badlogic.gdx.graphics.glutils.PixmapTextureData
import com.badlogic.gdx.math.Rectangle
import com.badlogic.gdx.math.Vector2
import com.badlogic.gdx.scenes.scene2d.Actor
@ -402,3 +406,15 @@ fun equalizeColumns(vararg tables: Table) {
table.invalidate()
}
}
/** Retrieve a texture Pixmap without reload or ownership transfer, useable for read operations only.
*
* (FileTextureData.consumePixmap forces a reload of the entire file - inefficient if we only want to look at pixel values) */
fun TextureData.getReadonlyPixmap(): Pixmap {
if (!isPrepared) prepare()
if (this is PixmapTextureData) return consumePixmap()
if (this !is FileTextureData) throw TypeCastException("getReadonlyPixmap only works on file or pixmap based textures")
val field = FileTextureData::class.java.getDeclaredField("pixmap")
field.isAccessible = true
return field.get(this) as Pixmap
}

View File

@ -3,10 +3,7 @@ package com.unciv.ui.screens.civilopediascreen
import com.badlogic.gdx.Gdx
import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.graphics.Pixmap
import com.badlogic.gdx.graphics.TextureData
import com.badlogic.gdx.graphics.g2d.TextureRegion
import com.badlogic.gdx.graphics.glutils.FileTextureData
import com.badlogic.gdx.graphics.glutils.PixmapTextureData
import com.badlogic.gdx.scenes.scene2d.Actor
import com.badlogic.gdx.scenes.scene2d.ui.Image
import com.badlogic.gdx.scenes.scene2d.ui.Table
@ -18,6 +15,7 @@ import com.unciv.models.metadata.BaseRuleset
import com.unciv.models.ruleset.Ruleset
import com.unciv.models.ruleset.RulesetCache
import com.unciv.models.ruleset.unique.Unique
import com.unciv.ui.components.extensions.getReadonlyPixmap
import com.unciv.ui.components.widgets.ColorMarkupLabel
import com.unciv.ui.components.extensions.toLabel
import com.unciv.ui.images.ImageGetter
@ -393,18 +391,6 @@ class FormattedLine (
getPixel(x, it) and 255 == 0
}
}
/** Retrieve a texture Pixmap without reload or ownership transfer, useable for read operations only.
*
* (FileTextureData.consumePixmap forces a reload of the entire file - inefficient if we only want to look at pixel values) */
private fun TextureData.getReadonlyPixmap(): Pixmap {
if (!isPrepared) prepare()
if (this is PixmapTextureData) return consumePixmap()
if (this !is FileTextureData) throw TypeCastException("getReadonlyPixmap only works on file or pixmap based textures")
val field = FileTextureData::class.java.getDeclaredField("pixmap")
field.isAccessible = true
return field.get(this) as Pixmap
}
// endregion
// region Integer Rectangle class

View File

@ -2,11 +2,13 @@ package com.unciv.ui.screens.worldscreen.topbar
import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.scenes.scene2d.Actor
import com.badlogic.gdx.scenes.scene2d.Group
import com.badlogic.gdx.scenes.scene2d.ui.Cell
import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.badlogic.gdx.utils.Align
import com.unciv.logic.civilization.Civilization
import com.unciv.models.translations.tr
import com.unciv.ui.components.Fonts
import com.unciv.ui.components.extensions.darken
import com.unciv.ui.components.extensions.setFontSize
import com.unciv.ui.components.extensions.toLabel
@ -190,20 +192,31 @@ class WorldScreenTopBar(internal val worldScreen: WorldScreen) : Table() {
private class SelectedCivilizationTable(worldScreen: WorldScreen) : Table(BaseScreen.skin) {
private var selectedCiv = ""
// Instead of allowing tr() to insert the nation icon - we don't want it scaled with fontSizeMultiplier
private var selectedCivIcon = Group()
private val selectedCivIconCell: Cell<Group>
private val selectedCivLabel = "".toLabel()
private val menuButton = ImageGetter.getImage("OtherIcons/MenuIcon")
init {
// vertically align the Nation name by ascender height without descender:
// Normal vertical centering uses the entire font height, but that looks off here because there's
// few descenders in the typical Nation name. So we calculate an estimate of the descender height
// in world coordinates (25 is the Label font size set below), then, since the cells themselves
// have no default padding, we remove that much padding from the top of this entire Table, and
// give the Label that much top padding in return. Approximated since we're ignoring 'leading'.
val descenderHeight = Fonts.fontImplementation.getMetrics().run { descent / height } * 25f
left()
defaults().pad(10f)
pad(10f)
padTop((10f - descenderHeight).coerceAtLeast(0f))
menuButton.color = Color.WHITE
menuButton.onActivation(binding = KeyboardBinding.Menu) {
WorldScreenMenuPopup(worldScreen).open(force = true)
}
selectedCivLabel.setFontSize(25)
selectedCivLabel.onClick {
val onNationClick = {
val civilopediaScreen = CivilopediaScreen(
worldScreen.selectedCiv.gameInfo.ruleset,
CivilopediaCategories.Nation,
@ -212,8 +225,13 @@ class WorldScreenTopBar(internal val worldScreen: WorldScreen) : Table() {
worldScreen.game.pushScreen(civilopediaScreen)
}
add(menuButton).size(50f).padRight(0f)
add(selectedCivLabel)
selectedCivLabel.setFontSize(25)
selectedCivLabel.onClick(onNationClick)
selectedCivIcon.onClick(onNationClick)
add(menuButton).size(50f)
selectedCivIconCell = add(selectedCivIcon).padLeft(10f)
add(selectedCivLabel).padTop(descenderHeight)
pack()
}
@ -222,7 +240,9 @@ class WorldScreenTopBar(internal val worldScreen: WorldScreen) : Table() {
if (this.selectedCiv == newCiv) return
this.selectedCiv = newCiv
selectedCivLabel.setText(newCiv.tr()) // Will include nation icon
selectedCivIcon = ImageGetter.getNationPortrait(worldScreen.selectedCiv.nation, 25f)
selectedCivIconCell.setActor(selectedCivIcon)
selectedCivLabel.setText(newCiv.tr(hideIcons = true))
invalidate()
pack()
}

View File

@ -4,6 +4,7 @@ import com.badlogic.gdx.Gdx
import com.badlogic.gdx.graphics.Pixmap
import com.unciv.ui.components.FontFamilyData
import com.unciv.ui.components.FontImplementation
import com.unciv.ui.components.FontMetricsCommon
import com.unciv.ui.components.Fonts
import java.awt.Color
import java.awt.Font
@ -62,17 +63,20 @@ class DesktopFont : FontImplementation {
override fun getCharPixmap(char: Char): Pixmap {
var width = metric.charWidth(char)
var height = metric.ascent + metric.descent
var height = metric.height
if (width == 0) {
// This happens e.g. for the Tab character
height = font.size
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 = Color.WHITE
g.drawString(char.toString(), 0, metric.ascent)
g.drawString(char.toString(), 0, metric.leading + 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) {
@ -92,4 +96,16 @@ class DesktopFont : FontImplementation {
.map { FontFamilyData(it.family, it.getFamily(Locale.ROOT)) }
.distinctBy { it.invariantName }
}
// Note: AWT uses the FontDesignMetrics implementation in our case, which has more precise
// float fields but rounds to integers to satisfy the interface.
// Additionally, the rounding is weird: x.049 rounds down, x.051 rounds up.
// There is no way around the privacy crap: FontUtilities.getFont2D(metric.font).getStrike(metric.font, metric.fontRenderContext).getFontMetrics() would work if that last method wasn't private too...
// Reflection is out too, since java.desktop refuses to open sun.font - we must die with rounding errors!
override fun getMetrics() = FontMetricsCommon(
ascent = metric.ascent.toFloat(),
descent = metric.descent.toFloat(),
height = metric.height.toFloat(),
leading = metric.leading.toFloat()
)
}

View File

@ -16,6 +16,7 @@ import com.unciv.dev.FasterUIDevelopment.DevElement
import com.unciv.logic.files.UncivFiles
import com.unciv.ui.components.FontFamilyData
import com.unciv.ui.components.FontImplementation
import com.unciv.ui.components.FontMetricsCommon
import com.unciv.ui.components.Fonts
import com.unciv.ui.components.extensions.center
import com.unciv.ui.components.extensions.toLabel
@ -150,23 +151,23 @@ class FontDesktop : FontImplementation {
// Empty
}
override fun getFontSize(): Int {
return Fonts.ORIGINAL_FONT_SIZE.toInt()
}
override fun getFontSize() = Fonts.ORIGINAL_FONT_SIZE.toInt()
override fun getCharPixmap(char: Char): Pixmap {
var width = metric.charWidth(char)
var height = metric.ascent + metric.descent
var height = metric.height
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)
g.drawString(char.toString(), 0, metric.leading + 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) {
@ -179,7 +180,12 @@ class FontDesktop : FontImplementation {
return pixmap
}
override fun getSystemFonts(): Sequence<FontFamilyData> {
return sequenceOf(FontFamilyData(Fonts.DEFAULT_FONT_FAMILY))
}
override fun getSystemFonts() = sequenceOf(FontFamilyData(Fonts.DEFAULT_FONT_FAMILY))
override fun getMetrics() = FontMetricsCommon(
ascent = metric.ascent.toFloat(),
descent = metric.descent.toFloat(),
height = metric.height.toFloat(),
leading = metric.leading.toFloat()
)
}