All Downloads are FREE. Search and download functionalities are using the official Maven repository.

commonMain.ru.casperix.multiplatform.text.impl.CacheBasedTextRenderer.kt Maven / Gradle / Ivy

There is a newer version: 1.1.1
Show newest version
package ru.casperix.multiplatform.text.impl

import ru.casperix.math.axis_aligned.float32.Box2f
import ru.casperix.math.color.Colors
import ru.casperix.math.vector.float32.Vector2f
import ru.casperix.math.vector.toQuad
import ru.casperix.multiplatform.font.FontMetrics
import ru.casperix.multiplatform.font.FontReference
import ru.casperix.multiplatform.text.StringMetrics
import ru.casperix.multiplatform.text.TextGraphicProcessor
import ru.casperix.multiplatform.text.TextRenderConfig
import ru.casperix.multiplatform.text.TextRendererApi
import ru.casperix.renderer.material.SimpleMaterial
import ru.casperix.renderer.misc.AlignMode
import ru.casperix.renderer.vector.GeometryBuilder
import ru.casperix.renderer.vector.VectorGraphic
import ru.casperix.renderer.vector.VectorShape
import kotlin.math.max


class CacheBasedTextRenderer(val processor: TextGraphicProcessor) : TextRendererApi {
    data class GraphicKey(val items: List, val availableArea: Vector2f)

    private val lineListCache = mutableMapOf()
    private val textGraphicCache = mutableMapOf()
    private val metricsGraphicCache = mutableMapOf()


    data class Cursor(
        val xOffset: Float,
        val yOffset: Float,
        val maxAscent: Float,
        val maxDescent: Float,
        val maxLeading: Float
    )

    data class TextPartInfo(
        val parent: TextView,
        val part: TextPart,
        val xOffset: Float,
        val fontMetrics: FontMetrics,
        val stringMetrics: StringMetrics
    )

    override fun getFontMetrics(font: FontReference): FontMetrics {
        return processor.getFontMetrics(font)
    }

    override fun getTextGraphic(scheme: TextScheme): VectorGraphic {
        return textGraphicCache.getOrPut(scheme) {
            createBackgroundGraphic(scheme) + processor.create(scheme)
        }
    }

    private fun createBackgroundGraphic(scheme: TextScheme): VectorGraphic {
        return VectorGraphic(scheme.elements.mapNotNull {
            if (it.background != null) {
                VectorShape(SimpleMaterial(it.background), GeometryBuilder.texturedBox(it.textArea, Box2f.ONE))
            } else null
        })
    }

    override fun getTextMetricGraphic(scheme: TextScheme): VectorGraphic {
        return metricsGraphicCache.getOrPut(scheme) {
            createMetrics(scheme)
        }
    }

    override fun getTextScheme(
        blocks: List,
        availableArea: Vector2f,
        alignMode: AlignMode,
    ): TextScheme {
        val key = GraphicKey(blocks, availableArea)
        return lineListCache.getOrPut(key) {
            createTextScheme(blocks, availableArea, alignMode)
        }
    }

    private fun createTextScheme(
        viewList: List,
        availableArea: Vector2f,
        alignMode: AlignMode,
    ): TextScheme {
        var cursor = Cursor(0f, 0f, 0f, 0f, 0f)
        val lineBuffer = mutableListOf()
        val lineList = mutableListOf()


        fun callDrawLine() {
            var leftToRightDirection = true
            lineBuffer.forEach {
                if (!processor.isLeftToRight(it.part.text)) {
                    leftToRightDirection = false
                    return@forEach
                }
            }

            lineList += generateLineElements(cursor, lineBuffer, leftToRightDirection)
            lineBuffer.clear()
        }

        viewList.forEach { view ->
            val font = view.font
            val fontMetrics = processor.getFontMetrics(font)
            val partList = TextProcessor.splitByParts(view.text)
            partList.forEach { part ->
                val stringMetrics = processor.getStringMetrics(font, part.text)
                val stringWidth = stringMetrics.size.width

                val needNextLine =
                    (!part.isWhitespace && cursor.xOffset + stringWidth > availableArea.x) || part.isLineEnd

                if (needNextLine) {
                    callDrawLine()

                    val lineHeight = cursor.maxAscent + cursor.maxDescent + cursor.maxLeading
                    cursor = Cursor(
                        0f,
                        cursor.yOffset + lineHeight,
                        0f,
                        0f,
                        0f
                    )
                }

                lineBuffer += TextPartInfo(view, part, cursor.xOffset, fontMetrics, stringMetrics)

                cursor = cursor.copy(
                    xOffset = cursor.xOffset + stringWidth,
                    maxAscent = max(cursor.maxAscent, fontMetrics.ascent),
                    maxDescent = max(cursor.maxDescent, fontMetrics.descent),
                    maxLeading = max(cursor.maxLeading, fontMetrics.leading),
                )
            }
        }

        callDrawLine()

        val scheme = TextScheme(lineList)
        return applyAlign(scheme, availableArea, alignMode)
    }

    private fun generateLineElements(
        cursor: Cursor,
        buffer: List,
        leftToRightDirection: Boolean,
    ): List {
        if (buffer.isEmpty()) {
            return emptyList()
        }

        val baseline = cursor.yOffset + cursor.maxAscent
        val fullWidth = buffer.maxOf { it.xOffset + it.stringMetrics.size.width }

        return buffer.map { bufferItem ->
            bufferItem.run {
                val leftOffset = if (leftToRightDirection) {
                    xOffset
                } else {
                    fullWidth - xOffset - stringMetrics.size.width
                }

                val leftTop = Vector2f(leftOffset, baseline - fontMetrics.ascent)
                TextSchemeElement(
                    part,
                    Box2f.byDimension(leftTop, bufferItem.stringMetrics.size.toVector2f()),
                    stringMetrics,
                    parent.font,
                    parent.foreground,
                    parent.background,
                )
            }
        }
    }

    private fun createMetrics(scheme: TextScheme): VectorGraphic {
        return VectorGraphic(scheme.elements.flatMap { item ->
            val fontMetrics = processor.getFontMetrics(item.font)
            listOf(
                Pair(Colors.GREEN.setAlpha(0.5f), 0f..fontMetrics.ascent),
                Pair(
                    Colors.RED.setAlpha(0.5f),
                    fontMetrics.ascent..fontMetrics.textHeight
                ),
                Pair(
                    Colors.BLUE.setAlpha(0.5f),
                    fontMetrics.textHeight..fontMetrics.lineHeight
                ),
            ).map { (color, range) ->
                val quad = Box2f.byDimension(
                    item.textArea.min + Vector2f(0f, range.start),
                    Vector2f(item.textArea.dimension.x, range.endInclusive - range.start)
                ).toQuad()

                VectorShape(SimpleMaterial(color), GeometryBuilder.texturedQuad(quad))
            }
        })
    }

    private fun applyAlign(scheme: TextScheme, viewportSize: Vector2f, alignMode: AlignMode): TextScheme {
        if (alignMode == AlignMode.LEFT_TOP) {
            return scheme
        }
        if (!viewportSize.isFinite()) {
            throw Exception("Invalid viewport")
        }

        val dimension = scheme.summaryArea.value.dimension
        val alignOffset = alignMode.getPosition(viewportSize, dimension)
        if (alignOffset == Vector2f.ZERO) {
            return scheme
        }

        return TextScheme(scheme.elements.map {
            val last = it.textArea
            val nextCorner = (last.min + alignOffset).run {
                if (TextRenderConfig.textRoundToPixel) round()
                else this
            }
            it.copy(textArea = Box2f.byDimension(nextCorner, last.dimension))
        })
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy