218 lines
7.3 KiB
Kotlin
218 lines
7.3 KiB
Kotlin
package org.openrndr.extra.textwriter
|
|
|
|
import org.openrndr.draw.DrawStyle
|
|
import org.openrndr.draw.Drawer
|
|
import org.openrndr.draw.FontImageMap
|
|
import org.openrndr.math.Vector2
|
|
import org.openrndr.shape.Rectangle
|
|
import kotlin.contracts.ExperimentalContracts
|
|
import kotlin.contracts.InvocationKind
|
|
import kotlin.contracts.contract
|
|
|
|
class Cursor(var x: Double = 0.0, var y: Double = 0.0) {
|
|
constructor(cursor: Cursor) : this(cursor.x, cursor.y)
|
|
}
|
|
|
|
@Suppress("unused")
|
|
class TextToken(val token: String, val x: Double, val y: Double, val width: Double, val tracking: Double)
|
|
|
|
class WriteStyle {
|
|
var leading = 0.0
|
|
var tracking = 0.0
|
|
var ellipsis: String? = "…"
|
|
}
|
|
|
|
@Suppress("unused", "UNUSED_PARAMETER")
|
|
class TextWriter(val drawerRef: Drawer?) {
|
|
var cursor = Cursor()
|
|
var box = Rectangle(
|
|
Vector2.ZERO, drawerRef?.width?.toDouble() ?: Double.POSITIVE_INFINITY, drawerRef?.height?.toDouble()
|
|
?: Double.POSITIVE_INFINITY
|
|
)
|
|
set(value) {
|
|
field = value
|
|
cursor.x = value.corner.x
|
|
cursor.y = value.corner.y
|
|
}
|
|
|
|
var style = WriteStyle()
|
|
val styleStack = ArrayDeque<WriteStyle>()
|
|
|
|
var leading
|
|
get() = style.leading
|
|
set(value) {
|
|
style.leading = value
|
|
}
|
|
|
|
var tracking
|
|
get() = style.tracking
|
|
set(value) {
|
|
style.tracking = value
|
|
}
|
|
|
|
var ellipsis
|
|
get() = style.ellipsis
|
|
set(value) {
|
|
style.ellipsis = value
|
|
}
|
|
|
|
var drawStyle: DrawStyle = DrawStyle()
|
|
get() {
|
|
return drawerRef?.drawStyle ?: field
|
|
}
|
|
set(value) {
|
|
field = drawStyle
|
|
}
|
|
|
|
fun newLine() {
|
|
cursor.x = box.corner.x
|
|
cursor.y += (drawStyle.fontMap?.leading ?: 0.0) + style.leading
|
|
}
|
|
|
|
fun gaplessNewLine() {
|
|
cursor.x = box.corner.x
|
|
cursor.y += drawStyle.fontMap?.height ?: 0.0
|
|
}
|
|
|
|
fun move(x: Double, y: Double) {
|
|
cursor.x += x
|
|
cursor.y += y
|
|
}
|
|
|
|
fun textWidth(text: String): Double =
|
|
text.sumOf {
|
|
((drawStyle.fontMap as FontImageMap).glyphMetrics[it]?.advanceWidth ?: 0.0) + style.tracking
|
|
} - (text.count { it == ' ' } + 1) * style.tracking
|
|
|
|
/**
|
|
* Draw text
|
|
* @param text the text to write, may contain newlines
|
|
* @param visible draw the text when set to true, when set to false only type setting is performed
|
|
* @return a list of [TextToken] instances
|
|
*/
|
|
fun text(text: String, visible: Boolean = true): List<TextToken> {
|
|
// Triggers loading the default font (if needed) by accessing .fontMap
|
|
// otherwise makeRenderTokens() is not aware of the default font.
|
|
drawerRef?.fontMap
|
|
|
|
val renderTokens = makeTextTokens(text, false)
|
|
|
|
if (visible) {
|
|
drawTextTokens(renderTokens)
|
|
}
|
|
return renderTokens
|
|
}
|
|
|
|
/**
|
|
* Draw pre-set text tokens.
|
|
* @param tokens a list of [TextToken] instances
|
|
* @since 0.4.3
|
|
*/
|
|
fun drawTextTokens(tokens: List<TextToken>) {
|
|
drawerRef?.let { d ->
|
|
val renderer = d.fontImageMapDrawer
|
|
val queue = renderer.getQueue(tokens.sumOf { it.token.length })
|
|
tokens.forEach {
|
|
renderer.queueText(
|
|
fontMap = d.drawStyle.fontMap!!,
|
|
text = it.token,
|
|
x = it.x,
|
|
y = it.y,
|
|
tracking = style.tracking,
|
|
kerning = drawStyle.kerning,
|
|
textSetting = drawStyle.textSetting,
|
|
queue
|
|
)
|
|
}
|
|
renderer.flush(d.context, d.drawStyle, queue)
|
|
}
|
|
}
|
|
|
|
private fun makeTextTokens(text: String, mustFit: Boolean = false): List<TextToken> {
|
|
drawStyle.fontMap?.let { font ->
|
|
|
|
var fits = true
|
|
font as FontImageMap
|
|
val lines = text.split("((?<=\n)|(?=\n))".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
|
|
val tokens = mutableListOf<String>()
|
|
lines.forEach { line ->
|
|
val lineTokens = line.split(" ")
|
|
tokens.addAll(lineTokens)
|
|
}
|
|
|
|
val localCursor = Cursor(cursor)
|
|
|
|
val spaceWidth = (font.glyphMetrics[' ']?.advanceWidth ?: error("no metrics for space"))
|
|
val verticalSpace = style.leading + font.leading
|
|
|
|
val textTokens = mutableListOf<TextToken>()
|
|
|
|
tokenLoop@ for (i in 0 until tokens.size) {
|
|
val token = tokens[i]
|
|
if (token == "\n") {
|
|
localCursor.x = box.corner.x
|
|
localCursor.y += verticalSpace
|
|
} else {
|
|
val tokenWidth = token.sumOf {
|
|
(font.glyphMetrics[it]?.advanceWidth ?: 0.0)
|
|
} + style.tracking * (token.length - 1).coerceAtLeast(0)
|
|
if (localCursor.x + tokenWidth < box.x + box.width && localCursor.y <= box.y + box.height) run {
|
|
val textToken = TextToken(token, localCursor.x, localCursor.y, tokenWidth, style.tracking)
|
|
emitToken(localCursor, textTokens, textToken)
|
|
} else {
|
|
if (localCursor.y > box.corner.y + box.height) {
|
|
fits = false
|
|
}
|
|
if (localCursor.y + verticalSpace <= box.y + box.height) {
|
|
localCursor.y += verticalSpace
|
|
localCursor.x = box.x
|
|
|
|
emitToken(
|
|
localCursor,
|
|
textTokens,
|
|
TextToken(token, localCursor.x, localCursor.y, tokenWidth, style.tracking)
|
|
)
|
|
} else {
|
|
if (!mustFit && style.ellipsis != null && cursor.y <= box.y + box.height) {
|
|
emitToken(
|
|
localCursor, textTokens, TextToken(
|
|
style.ellipsis
|
|
?: "", localCursor.x, localCursor.y, tokenWidth, style.tracking
|
|
)
|
|
)
|
|
break@tokenLoop
|
|
} else {
|
|
fits = false
|
|
}
|
|
}
|
|
}
|
|
localCursor.x += tokenWidth
|
|
|
|
if (i != tokens.lastIndex) {
|
|
localCursor.x += spaceWidth + tracking
|
|
}
|
|
}
|
|
}
|
|
if (fits || (!fits && !mustFit)) {
|
|
cursor = Cursor(localCursor)
|
|
} else {
|
|
textTokens.clear()
|
|
}
|
|
return textTokens
|
|
}
|
|
return emptyList()
|
|
}
|
|
|
|
private fun emitToken(cursor: Cursor, textTokens: MutableList<TextToken>, textToken: TextToken) {
|
|
textTokens.add(textToken)
|
|
}
|
|
}
|
|
|
|
@OptIn(ExperimentalContracts::class)
|
|
fun <T> writer(drawer: Drawer, f: TextWriter.() -> T): T {
|
|
contract {
|
|
callsInPlace(f, InvocationKind.EXACTLY_ONCE)
|
|
}
|
|
val textWriter = TextWriter(drawer)
|
|
return textWriter.f()
|
|
} |