mirror of
https://github.com/curioustorvald/Terrarum.git
synced 2026-03-12 06:41:51 +09:00
519 lines
20 KiB
Kotlin
519 lines
20 KiB
Kotlin
package net.torvald.terrarum.ui
|
|
|
|
import com.badlogic.gdx.Input
|
|
import com.badlogic.gdx.graphics.Camera
|
|
import com.badlogic.gdx.graphics.Color
|
|
import com.badlogic.gdx.graphics.OrthographicCamera
|
|
import com.badlogic.gdx.graphics.Pixmap
|
|
import com.badlogic.gdx.graphics.g2d.SpriteBatch
|
|
import com.badlogic.gdx.graphics.glutils.FrameBuffer
|
|
import com.jme3.math.FastMath
|
|
import net.torvald.terrarum.*
|
|
import net.torvald.terrarum.gamecontroller.IME
|
|
import net.torvald.terrarum.gamecontroller.IngameController
|
|
import net.torvald.terrarum.gamecontroller.TerrarumInputMethod
|
|
import net.torvald.terrarum.utils.Clipboard
|
|
import net.torvald.terrarumsansbitmap.gdx.CodepointSequence
|
|
import net.torvald.terrarumsansbitmap.gdx.TextureRegionPack
|
|
import net.torvald.toJavaString
|
|
import kotlin.streams.toList
|
|
|
|
data class InputLenCap(val count: Int, val unit: CharLenUnit) {
|
|
enum class CharLenUnit {
|
|
UTF8_BYTES, UTF16_CHARS, CODEPOINTS
|
|
}
|
|
|
|
fun exceeds(codepoints: CodepointSequence, extra: List<Int> = CodepointSequence()): Boolean {
|
|
return when (unit) {
|
|
CharLenUnit.CODEPOINTS -> (codepoints.size + extra.size) > count
|
|
CharLenUnit.UTF16_CHARS -> {
|
|
var cnt = 0
|
|
listOf(codepoints,extra).forEach { it.forEach {
|
|
cnt += 1 + (it > 65535).toInt()
|
|
} }
|
|
cnt > count
|
|
}
|
|
CharLenUnit.UTF8_BYTES -> {
|
|
var cnt = 0
|
|
listOf(codepoints,extra).forEach { it.forEach {
|
|
cnt += if (it > 65535) 4 else if (it > 2047) 3 else if (it > 127) 2 else 1
|
|
} }
|
|
cnt > count
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Protip: if there are multiple TextLineInputs on a same UI, draw bottom one first, otherwise the IME's
|
|
* candidate window will be hidden by the bottom UIItem if they overlaps.
|
|
*
|
|
* @param width width of the text input where the text gets drawn, not the entire item
|
|
* @param height height of the text input where the text gets drawn, not the entire item
|
|
*
|
|
* Created by minjaesong on 2021-10-20.
|
|
*/
|
|
class UIItemTextLineInput(
|
|
parentUI: UICanvas,
|
|
initialX: Int, initialY: Int,
|
|
override val width: Int,
|
|
var placeholder: () -> String = { "" },
|
|
val maxLen: InputLenCap = InputLenCap(1000, InputLenCap.CharLenUnit.CODEPOINTS),
|
|
val enablePasteButton: Boolean = true,
|
|
val enableIMEButton: Boolean = true
|
|
) : UIItem(parentUI, initialX, initialY) {
|
|
|
|
init {
|
|
CommonResourcePool.addToLoadingList("inventory_category") {
|
|
TextureRegionPack("assets/graphics/gui/inventory/category.tga", 20, 20)
|
|
}
|
|
CommonResourcePool.loadAll()
|
|
}
|
|
|
|
private val labels = CommonResourcePool.getAsTextureRegionPack("inventory_category")
|
|
|
|
override val height = 24
|
|
|
|
private val buttonsShown = enableIMEButton.toInt() + enablePasteButton.toInt()
|
|
|
|
companion object {
|
|
val TEXTINPUT_COL_TEXT = Color.WHITE
|
|
val TEXTINPUT_COL_TEXT_NOMORE = Color(0xFF8888FF.toInt())
|
|
val TEXTINPUT_COL_TEXT_DISABLED = Toolkit.Theme.COL_DISABLED
|
|
val TEXTINPUT_COL_BACKGROUND = Toolkit.Theme.COL_CELL_FILL
|
|
const val CURSOR_BLINK_TIME = 1f / 3f
|
|
|
|
private const val UI_TEXT_MARGIN = 2
|
|
private const val WIDTH_ONEBUTTON = 24
|
|
}
|
|
|
|
private val fbo = FrameBuffer(
|
|
Pixmap.Format.RGBA8888,
|
|
width - 2 * UI_TEXT_MARGIN - buttonsShown * (WIDTH_ONEBUTTON + 3),
|
|
height - 2 * UI_TEXT_MARGIN,
|
|
true
|
|
)
|
|
|
|
var isActive: Boolean = true
|
|
|
|
var cursorX = 0
|
|
var cursorDrawScroll = 0
|
|
var cursorDrawX = 0 // pixelwise point
|
|
var cursorBlinkCounter = 0f
|
|
var cursorOn = true
|
|
|
|
private val textbuf = CodepointSequence()
|
|
|
|
private var fboUpdateLatch = true
|
|
|
|
private var currentPlaceholderText = ArrayList<Int>(placeholder().toCodePoints()) // the placeholder text may change every time you call it
|
|
|
|
|
|
private val btn1PosX = posX + width - 2*WIDTH_ONEBUTTON - 3
|
|
private val btn2PosX = posX + width - WIDTH_ONEBUTTON
|
|
|
|
private val mouseUpOnTextArea: Boolean
|
|
get() = relativeMouseX in 0 until fbo.width + 2* UI_TEXT_MARGIN && relativeMouseY in 0 until height
|
|
private val mouseUpOnButton1
|
|
get() = buttonsShown > 1 && relativeMouseX in btn1PosX - posX until btn1PosX - posX + WIDTH_ONEBUTTON && relativeMouseY in 0 until height
|
|
private val mouseUpOnButton2
|
|
get() = buttonsShown > 0 && relativeMouseX in btn2PosX - posX until btn2PosX - posX + WIDTH_ONEBUTTON && relativeMouseY in 0 until height
|
|
|
|
private var imeOn = false
|
|
private var candidates: List<CodepointSequence> = listOf()
|
|
|
|
private val candidatesBackCol = TEXTINPUT_COL_BACKGROUND.cpy().mul(1f,1f,1f,1.5f)
|
|
private val candidateNumberStrWidth = App.fontGame.getWidth("8. ")
|
|
|
|
private fun getIME(): TerrarumInputMethod? {
|
|
if (!imeOn) return null
|
|
|
|
val selectedIME = App.getConfigString("inputmethod")
|
|
|
|
if (selectedIME == "none") return null
|
|
try {
|
|
return IME.getHighLayerByName(selectedIME)
|
|
}
|
|
catch (e: NullPointerException) {
|
|
return null
|
|
}
|
|
}
|
|
|
|
private fun forceLitCursor() {
|
|
cursorBlinkCounter = 0f
|
|
cursorOn = true
|
|
}
|
|
|
|
private fun tryCursorBack() {
|
|
if (cursorDrawX > fbo.width) {
|
|
val d = cursorDrawX - fbo.width
|
|
cursorDrawScroll = d
|
|
}
|
|
}
|
|
private fun tryCursorForward() {
|
|
if (cursorDrawX - cursorDrawScroll < 0) {
|
|
cursorDrawScroll -= cursorDrawX
|
|
}
|
|
}
|
|
|
|
override fun update(delta: Float) {
|
|
super.update(delta)
|
|
val mouseDown = Terrarum.mouseDown
|
|
val oldActive = isActive
|
|
|
|
if (mouseDown) {
|
|
isActive = mouseUp
|
|
}
|
|
|
|
if (App.getConfigString("inputmethod") == "none") imeOn = false
|
|
|
|
// process keypresses
|
|
if (isActive) {
|
|
IngameController.withKeyboardEvent { (_, char, headkey, repeatCount, keycodes) ->
|
|
fboUpdateLatch = true
|
|
forceLitCursor()
|
|
val ime = getIME()
|
|
|
|
if (keycodes.contains(App.getConfigInt("control_key_toggleime")) && repeatCount == 1) {
|
|
toggleIME()
|
|
}
|
|
else if (keycodes.contains(Input.Keys.V) && (keycodes.contains(Input.Keys.CONTROL_LEFT) || keycodes.contains(Input.Keys.CONTROL_RIGHT))) {
|
|
endComposing()
|
|
paste(Clipboard.fetch().substringBefore('\n').substringBefore('\t').toCodePoints())
|
|
}
|
|
else if (keycodes.contains(Input.Keys.C) && (keycodes.contains(Input.Keys.CONTROL_LEFT) || keycodes.contains(Input.Keys.CONTROL_RIGHT))) {
|
|
endComposing()
|
|
copyToClipboard()
|
|
}
|
|
else if (keycodes.contains(Input.Keys.BACKSPACE)) {
|
|
if (ime != null && ime.composing()) {
|
|
candidates = ime.backspace().map { CodepointSequence(it.toCodePoints()) }
|
|
}
|
|
else if (cursorX <= 0) {
|
|
cursorX = 0
|
|
cursorDrawX = 0
|
|
cursorDrawScroll = 0
|
|
}
|
|
else {
|
|
endComposing()
|
|
if (cursorX > 0) {
|
|
while (true) {
|
|
cursorX -= 1
|
|
val oldCode = textbuf.removeAt(cursorX)
|
|
// continue deleting hangul pieces because of the font...
|
|
if (cursorX == 0 || (oldCode !in 0x115F..0x11FF && oldCode !in 0xD7B0..0xD7FF)) break
|
|
}
|
|
|
|
cursorDrawX = App.fontGame.getWidth(CodepointSequence(textbuf.subList(0, cursorX)))
|
|
tryCursorForward()
|
|
}
|
|
}
|
|
}
|
|
else if (keycodes.contains(Input.Keys.LEFT)) {
|
|
endComposing()
|
|
|
|
if (cursorX > 0) {
|
|
cursorX -= 1
|
|
cursorDrawX = App.fontGame.getWidth(CodepointSequence(textbuf.subList(0, cursorX)))
|
|
tryCursorForward()
|
|
if (cursorX <= 0) {
|
|
cursorX = 0
|
|
cursorDrawX = 0
|
|
cursorDrawScroll = 0
|
|
}
|
|
}
|
|
}
|
|
else if (keycodes.contains(Input.Keys.RIGHT)) {
|
|
endComposing()
|
|
|
|
if (cursorX < textbuf.size) {
|
|
cursorX += 1
|
|
cursorDrawX = App.fontGame.getWidth(CodepointSequence(textbuf.subList(0, cursorX)))
|
|
tryCursorBack()
|
|
}
|
|
}
|
|
// accept:
|
|
// - literal "<"
|
|
// - keysymbol that does not start with "<" (not always has length of 1 because UTF-16)
|
|
else if (char != null && char.length > 0 && char[0].code >= 32 && (char == "<" || !char.startsWith("<"))) {
|
|
val shiftin = keycodes.contains(Input.Keys.SHIFT_LEFT) || keycodes.contains(Input.Keys.SHIFT_RIGHT)
|
|
val altgrin = keycodes.contains(Input.Keys.ALT_RIGHT)
|
|
|
|
val codepoints = if (ime != null) {
|
|
val newStatus = ime.acceptChar(headkey, shiftin, altgrin, char)
|
|
candidates = newStatus.first.map { CodepointSequence(it.toCodePoints()) }
|
|
|
|
newStatus.second.toCodePoints()
|
|
}
|
|
else char.toCodePoints()
|
|
|
|
// println("textinput codepoints: ${codepoints.map { it.toString(16) }.joinToString()}")
|
|
|
|
if (!maxLen.exceeds(textbuf, codepoints)) {
|
|
textbuf.addAll(cursorX, codepoints)
|
|
|
|
cursorX += codepoints.size
|
|
cursorDrawX = App.fontGame.getWidth(CodepointSequence(textbuf.subList(0, cursorX)))
|
|
|
|
tryCursorBack()
|
|
}
|
|
}
|
|
else if (keycodes.contains(Input.Keys.ENTER) || keycodes.contains(Input.Keys.NUMPAD_ENTER)) {
|
|
endComposing()
|
|
}
|
|
|
|
// don't put innards of tryCursorBack/Forward here -- you absolutely don't want that behaviour
|
|
|
|
}
|
|
|
|
if (textbuf.size == 0) {
|
|
currentPlaceholderText = CodepointSequence(placeholder().toCodePoints())
|
|
}
|
|
|
|
cursorBlinkCounter += delta
|
|
|
|
while (cursorBlinkCounter >= CURSOR_BLINK_TIME) {
|
|
cursorBlinkCounter -= CURSOR_BLINK_TIME
|
|
cursorOn = !cursorOn
|
|
}
|
|
}
|
|
else if (oldActive) { // just became deactivated
|
|
endComposing()
|
|
}
|
|
|
|
if (mouseDown && !mouseLatched && (enablePasteButton && enableIMEButton && mouseUpOnButton1 || enableIMEButton && !enablePasteButton && mouseUpOnButton2)) {
|
|
toggleIME()
|
|
mouseLatched = true
|
|
}
|
|
else if (mouseDown && !mouseLatched && (enablePasteButton && enableIMEButton && mouseUpOnButton2 || enablePasteButton && !enableIMEButton && mouseUpOnButton2)) {
|
|
endComposing()
|
|
paste(Clipboard.fetch().substringBefore('\n').substringBefore('\t').toCodePoints())
|
|
mouseLatched = true
|
|
}
|
|
|
|
if (!mouseDown) mouseLatched = false
|
|
}
|
|
|
|
private fun String.toCodePoints() = this.codePoints().toList().filter { it > 0 }
|
|
|
|
private fun endComposing() {
|
|
getIME()?.let {
|
|
val s = it.endCompose()
|
|
paste(s.toCodePoints())
|
|
}
|
|
fboUpdateLatch = true
|
|
candidates = listOf()
|
|
// resetIME() // not needed; IME will reset itself
|
|
}
|
|
|
|
private fun toggleIME() {
|
|
endComposing()
|
|
|
|
if (App.getConfigString("inputmethod") == "none") {
|
|
imeOn = false
|
|
return
|
|
}
|
|
|
|
imeOn = !imeOn
|
|
}
|
|
|
|
private fun resetIME() {
|
|
getIME()?.reset?.invoke()
|
|
candidates = listOf()
|
|
}
|
|
|
|
private fun paste(codepoints: List<Int>) {
|
|
val actuallyInserted = arrayListOf(0)
|
|
|
|
for (c in codepoints) {
|
|
if (maxLen.exceeds(textbuf, actuallyInserted)) break
|
|
actuallyInserted.add(c)
|
|
}
|
|
|
|
actuallyInserted.removeAt(0)
|
|
|
|
textbuf.addAll(cursorX, actuallyInserted)
|
|
|
|
cursorX += actuallyInserted.size
|
|
cursorDrawX = App.fontGame.getWidth(CodepointSequence(textbuf.subList(0, cursorX)))
|
|
|
|
tryCursorBack()
|
|
|
|
fboUpdateLatch = true
|
|
}
|
|
|
|
private fun copyToClipboard() {
|
|
Clipboard.copy(textbufToString())
|
|
}
|
|
|
|
private fun textbufToString(): String {
|
|
return textbuf.toJavaString()
|
|
}
|
|
|
|
override fun render(batch: SpriteBatch, camera: Camera) {
|
|
|
|
batch.end()
|
|
|
|
if (fboUpdateLatch) {
|
|
fboUpdateLatch = false
|
|
fbo.inAction(camera as OrthographicCamera, batch) { batch.inUse {
|
|
gdxClearAndSetBlend(0f, 0f, 0f, 0f)
|
|
|
|
it.color = Color.WHITE
|
|
App.fontGameFBO.draw(it, if (textbuf.isEmpty()) currentPlaceholderText else textbuf, -1f*cursorDrawScroll, 0f)
|
|
} }
|
|
}
|
|
|
|
batch.begin()
|
|
|
|
val mouseDown = Terrarum.mouseDown
|
|
|
|
// text area cell back
|
|
batch.color = TEXTINPUT_COL_BACKGROUND
|
|
Toolkit.fillArea(batch, posX, posY, fbo.width + 2 * UI_TEXT_MARGIN, height)
|
|
// rightmost button cell back
|
|
if (buttonsShown > 0)
|
|
Toolkit.fillArea(batch, btn2PosX, posY, WIDTH_ONEBUTTON, height)
|
|
if (buttonsShown > 1)
|
|
Toolkit.fillArea(batch, btn1PosX, posY, WIDTH_ONEBUTTON, height)
|
|
|
|
// text area border (base)
|
|
batch.color = Toolkit.Theme.COL_INACTIVE
|
|
Toolkit.drawBoxBorder(batch, posX - 1, posY - 1, width + 2, height + 2)
|
|
if (buttonsShown > 0)
|
|
Toolkit.drawBoxBorder(batch, btn2PosX - 1, posY - 1, WIDTH_ONEBUTTON + 2, height + 2)
|
|
if (buttonsShown > 1)
|
|
Toolkit.drawBoxBorder(batch, btn1PosX - 1, posY - 1, WIDTH_ONEBUTTON + 2, height + 2)
|
|
|
|
// text area border (pop-up for isActive)
|
|
if (isActive) {
|
|
batch.color = Toolkit.Theme.COL_HIGHLIGHT
|
|
Toolkit.drawBoxBorder(batch, posX - 1, posY - 1, width + 2, height + 2)
|
|
}
|
|
|
|
// button border
|
|
if (mouseUpOnButton2) {
|
|
batch.color = if (mouseDown) Toolkit.Theme.COL_HIGHLIGHT else Toolkit.Theme.COL_ACTIVE
|
|
Toolkit.drawBoxBorder(batch, btn2PosX - 1, posY - 1, WIDTH_ONEBUTTON + 2, height + 2)
|
|
}
|
|
else if (mouseUpOnButton1) {
|
|
batch.color = if (mouseDown) Toolkit.Theme.COL_HIGHLIGHT else Toolkit.Theme.COL_ACTIVE
|
|
Toolkit.drawBoxBorder(batch, btn1PosX - 1, posY - 1, WIDTH_ONEBUTTON + 2, height + 2)
|
|
}
|
|
else if (mouseUpOnTextArea && !isActive) {
|
|
batch.color = Toolkit.Theme.COL_ACTIVE
|
|
Toolkit.drawBoxBorder(batch, posX - 1, posY - 1, fbo.width + 2 * UI_TEXT_MARGIN+ 2, height + 2)
|
|
}
|
|
|
|
|
|
// draw text
|
|
batch.color = if (textbuf.isEmpty()) TEXTINPUT_COL_TEXT_DISABLED else TEXTINPUT_COL_TEXT
|
|
batch.draw(fbo.colorBufferTexture, posX + 2f, posY + 2f, fbo.width.toFloat(), fbo.height.toFloat())
|
|
|
|
// draw text cursor
|
|
val cursorXOnScreen = posX - cursorDrawScroll + cursorDrawX + 2
|
|
if (isActive && cursorOn) {
|
|
val baseCol = if (maxLen.exceeds(textbuf, listOf(32))) TEXTINPUT_COL_TEXT_NOMORE else TEXTINPUT_COL_TEXT
|
|
|
|
batch.color = baseCol.cpy().mul(0.5f,0.5f,0.5f,1f)
|
|
Toolkit.fillArea(batch, cursorXOnScreen, posY, 2, 24)
|
|
|
|
batch.color = baseCol
|
|
Toolkit.fillArea(batch, cursorXOnScreen, posY, 1, 23)
|
|
}
|
|
|
|
// draw icon
|
|
if (enablePasteButton && enableIMEButton) {
|
|
// IME button
|
|
batch.color = if (mouseUpOnButton1 && mouseDown || imeOn) Toolkit.Theme.COL_HIGHLIGHT else if (mouseUpOnButton1) Toolkit.Theme.COL_ACTIVE else Toolkit.Theme.COL_INACTIVE
|
|
batch.draw(labels.get(7,2), btn1PosX + 2f, posY + 2f)
|
|
// paste button
|
|
batch.color = if (mouseUpOnButton2 && mouseDown) Toolkit.Theme.COL_HIGHLIGHT else if (mouseUpOnButton2) Toolkit.Theme.COL_ACTIVE else Toolkit.Theme.COL_INACTIVE
|
|
batch.draw(labels.get(8,2), btn2PosX + 2f, posY + 2f)
|
|
}
|
|
else if (!enableIMEButton && enablePasteButton) {
|
|
// paste button
|
|
batch.color = if (mouseUpOnButton2 && mouseDown) Toolkit.Theme.COL_HIGHLIGHT else if (mouseUpOnButton2) Toolkit.Theme.COL_ACTIVE else Toolkit.Theme.COL_INACTIVE
|
|
batch.draw(labels.get(8,2), btn2PosX + 2f, posY + 2f)
|
|
}
|
|
else if (!enablePasteButton && enableIMEButton) {
|
|
// IME button
|
|
batch.color = if (mouseUpOnButton1 && mouseDown || imeOn) Toolkit.Theme.COL_HIGHLIGHT else if (mouseUpOnButton1) Toolkit.Theme.COL_ACTIVE else Toolkit.Theme.COL_INACTIVE
|
|
batch.draw(labels.get(7,2), btn2PosX + 2f, posY + 2f)
|
|
}
|
|
|
|
|
|
// draw candidates view
|
|
if (candidates.isNotEmpty()) {
|
|
val textWidths = candidates.map { App.fontGame.getWidth(CodepointSequence(it)) }
|
|
val candidatesMax = getIME()!!.maxCandidates()
|
|
val candidatesCount = minOf(candidatesMax, candidates.size)
|
|
val halfcount = FastMath.ceil(candidatesCount / 2f)
|
|
val candidateWinH = App.fontGame.lineHeight.toInt() * halfcount
|
|
val candidatePosX = cursorXOnScreen + 4
|
|
val candidatePosY = posY + 2
|
|
|
|
// candidate view text
|
|
if (candidatesMax > 1) {
|
|
val longestCandidateW = textWidths.maxOrNull()!! + candidateNumberStrWidth
|
|
val candidateWinW = if (candidatesCount == 1) longestCandidateW else 2*longestCandidateW + 3
|
|
|
|
// candidate view background
|
|
batch.color = candidatesBackCol
|
|
Toolkit.fillArea(batch, candidatePosX, candidatePosY, candidateWinW + 4, candidateWinH)
|
|
// candidate view border
|
|
batch.color = Toolkit.Theme.COL_ACTIVE
|
|
Toolkit.drawBoxBorder(batch, candidatePosX - 1, candidatePosY - 1, candidateWinW + 6, candidateWinH + 2)
|
|
|
|
// candidate texts
|
|
for (i in 0 until candidatesCount) {
|
|
val candidateNum = listOf(i+48,46,32)
|
|
App.fontGame.draw(batch, CodepointSequence(candidateNum + candidates[i]),
|
|
candidatePosX + (i / halfcount) * (longestCandidateW + 3) + 2,
|
|
candidatePosY + (i % halfcount) * 20
|
|
)
|
|
}
|
|
|
|
// candidate view splitter
|
|
if (candidatesCount > 1) {
|
|
batch.color = batch.color.cpy().mul(0.65f,0.65f,0.65f,1f)
|
|
Toolkit.fillArea(batch, candidatePosX + longestCandidateW + 2, candidatePosY, 1, candidateWinH)
|
|
}
|
|
}
|
|
else {
|
|
val candidateWinW = textWidths.maxOrNull()!!.coerceAtLeast(6)
|
|
|
|
// candidate view background
|
|
batch.color = candidatesBackCol
|
|
Toolkit.fillArea(batch, candidatePosX, candidatePosY, candidateWinW, candidateWinH)
|
|
// candidate view border
|
|
batch.color = Toolkit.Theme.COL_ACTIVE
|
|
Toolkit.drawBoxBorder(batch, candidatePosX - 1, candidatePosY - 1, candidateWinW + 2, candidateWinH + 2)
|
|
|
|
val previewTextWidth = textWidths[0]
|
|
App.fontGame.draw(batch, candidates[0], candidatePosX + (candidateWinW - previewTextWidth) / 2, candidatePosY)
|
|
}
|
|
}
|
|
|
|
batch.color = Color.WHITE
|
|
super.render(batch, camera)
|
|
}
|
|
|
|
fun getText() = textbufToString()
|
|
fun getTextOrPlaceholder(): String = if (textbuf.isEmpty()) currentPlaceholderText.toJavaString() else getText()
|
|
|
|
override fun dispose() {
|
|
fbo.dispose()
|
|
}
|
|
|
|
/*private fun CodepointSequence.toJavaString(): String {
|
|
val sb = StringBuilder()
|
|
this.forEach {
|
|
sb.append(Character.toChars(it))
|
|
}
|
|
return sb.toString()
|
|
}*/
|
|
|
|
}
|
|
|