mirror of
https://github.com/yairm210/Unciv.git
synced 2025-07-05 07:49:17 +07:00
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:
@ -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
|
||||
)
|
||||
}
|
||||
|
@ -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'
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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()
|
||||
)
|
||||
}
|
||||
|
@ -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()
|
||||
)
|
||||
}
|
||||
|
Reference in New Issue
Block a user