Exit Full View

Games Cupboard / gamescupboard-client / src / commonMain / kotlin / uk / co / nickthecoder / gamescupboard / client / PrintContainer.kt

package uk.co.nickthecoder.gamescupboard.client

import com.soywiz.korge.annotations.KorgeExperimental
import com.soywiz.korge.input.cursor
import com.soywiz.korge.input.onClick
import com.soywiz.korge.ui.UIScrollable
import com.soywiz.korge.view.Text
import com.soywiz.korge.view.position
import com.soywiz.korgw.GameWindow
import com.soywiz.korim.color.Colors
import com.soywiz.korim.color.RGBA
import com.soywiz.korim.font.DefaultTtfFont
import com.soywiz.korim.font.Font
import com.soywiz.korio.lang.substr
import com.soywiz.korio.resources.Resourceable

/**
 * A [UIScrollable] that can be printed to (i.e. text can be added to it using [print] and [println]).
 */
@OptIn(KorgeExperimental::class)
class PrintContainer(
    private val myWidth: Double,
    myHeight: Double,
    val paddingLeft: Double = 0.0,
    val paddingRight: Double = paddingLeft,
    val paddingTop: Double = paddingRight,
    val font: Resourceable<out Font> = DefaultTtfFont,
    val textSize: Double = Text.DEFAULT_TEXT_SIZE,
    val defaultColor: RGBA = textColor

) : UIScrollable(myWidth, myHeight) {

    /**
     * Needed for [println].
     */
    private var textHeight = createText("X").height

    /**
     * The location where the next text will be printed.
     * At the end of a line, [px] is set to 0.0, and [py] is incremented by [textHeight].
     */
    private var px = paddingLeft
    private var py = paddingTop

    /**
     * The text color
     */
    private var color = defaultColor

    fun print(str: String, color: RGBA = defaultColor, clickAction: (() -> Unit)? = null) {
        //normalPrintln("print $str @ $px,$py")
        this.color = color
        tryToFit(str, "", clickAction)
        scrollTopRatio = 1.0
        scrollLeftRatio = 0.0
    }

    fun println() {
        px = paddingLeft
        py += textHeight
        scrollTopRatio = 1.0
        scrollLeftRatio = 0.0
    }

    fun println(str: String, color: RGBA = defaultColor) {
        //normalPrintln("println $str @ $px, $py")
        this.color = color
        if (str.isNotBlank()) {
            tryToFit(str, "")
            scrollTopRatio = 1.0
            scrollLeftRatio = 0.0
        }
        px = paddingLeft
        py += textHeight
    }

    private fun createText(str: String) = Text(str, font = font, textSize = textSize, color = color)

    /**
     * The first time this is called, [remainder] will be blank, and [firstPart] is the text to be printed.
     * If [firstPart] cannot be fit without being split, then this will be called recursively,
     * with parts of [firstPart] removed from the end, and added to the front of [remainder].
     */
    private fun tryToFit(firstPart: String, remainder: String, clickAction: (() -> Unit)? = null) {
        with(container) {
            val text = createText(firstPart)
            if (clickAction != null) {
                text.onClick { clickAction() }
                this.cursor = GameWindow.Cursor.HAND
            }

            if (text.width + px > myWidth - paddingRight) {
                // We need to break the text over multiple lines.
                val space = firstPart.lastIndexOf(" ")
                if (space < 0) {
                    // There is no space
                    if (px == paddingLeft) {
                        // Oh well, there will be a horizontal scroll bar :-(
                        text.position(px, py)
                        addChild(text)
                        px = paddingLeft
                        py += text.height
                        // We HAVE added `firstPart` (badly)
                        //normalPrintln("Line break (badly)")
                    } else {
                        // We couldn't fit this word on the current line, so try again on a new line.
                        px = paddingLeft
                        py += text.height
                        //normalPrintln("Line break long word")
                        tryToFit(firstPart.trimStart() + " " + remainder.trimStart(), "", clickAction)
                        return
                    }
                } else {
                    // There IS a space, so chop off the last word, and add it to remainder,
                    // Try again with the shorter first part and longer remainder.
                    val newRemainder = (firstPart.substring(space) + " " + remainder.trimStart())
                    tryToFit(firstPart.substr(0, space), newRemainder, clickAction)
                    return
                }
            } else {
                // The whole of first part fits onto the current line.
                text.position(px, py)
                px += text.width
                if (text.text.isNotBlank()) {
                    addChild(text)
                }
                // We HAVE added `firstPart` (without overlapping the edge)
            }

            if (remainder.isNotBlank()) {
                // If we get here, then `firstPart` must have been added.
                // So now we do the same for `remainder` on the next line.
                px = paddingLeft
                py += text.height
                //normalPrintln("Line break for the remainder")
                tryToFit(remainder.trimStart(), "", clickAction)
            }
        }
    }
}

private fun normalPrintln(str: String) = println(str)