[orx-text-writer] Add orx-text-writer

This commit is contained in:
Edwin Jakobs
2024-03-15 20:38:17 +01:00
parent 058e833330
commit 05b00d2878
19 changed files with 310 additions and 27 deletions

View File

@@ -10,7 +10,7 @@ dependencies {
demoImplementation(project(":orx-shader-phrases"))
demoImplementation(project(":orx-camera"))
demoImplementation(project(":orx-shapes"))
demoImplementation(project(":orx-svg"))
demoImplementation(libs.slf4j.simple)
demoImplementation(libs.openrndr.ffmpeg)
demoImplementation(libs.openrndr.svg)
}

View File

@@ -15,6 +15,7 @@ tasks.test {
dependencies {
implementation(project(":orx-expression-evaluator"))
implementation(project(":orx-text-writer"))
implementation(libs.openrndr.application)
implementation(libs.openrndr.math)
implementation(libs.kotlin.coroutines)

View File

@@ -3,9 +3,10 @@ package org.openrndr.panel.elements
import org.openrndr.color.ColorRGBa
import org.openrndr.draw.Drawer
import org.openrndr.draw.FontImageMap
import org.openrndr.draw.Writer
import org.openrndr.draw.isolated
import org.openrndr.events.Event
import org.openrndr.extra.textwriter.TextWriter
import org.openrndr.panel.style.*
import org.openrndr.shape.Rectangle
@@ -53,7 +54,7 @@ class Button : Element(ElementType("button")) {
val fontSize = (style.fontSize as? LinearDimension.PX)?.value ?: 14.0
val fontMap = FontImageMap.fromUrl(fontUrl, fontSize)
val writer = Writer(null)
val writer = TextWriter(null)
writer.box = Rectangle(0.0,
0.0,
@@ -84,7 +85,7 @@ class Button : Element(ElementType("button")) {
(root() as? Body)?.controlManager?.fontManager?.let {
val font = it.font(computedStyle)
val writer = Writer(drawer)
val writer = TextWriter(drawer)
drawer.fontMap = (font)
val textWidth = writer.textWidth(label)
val textHeight = font.ascenderLength

View File

@@ -4,8 +4,9 @@ import kotlinx.coroutines.yield
import org.openrndr.color.ColorRGBa
import org.openrndr.draw.Drawer
import org.openrndr.draw.LineCap
import org.openrndr.draw.Writer
import org.openrndr.events.Event
import org.openrndr.extra.textwriter.TextWriter
import org.openrndr.launch
import org.openrndr.panel.style.*
@@ -62,7 +63,7 @@ class ColorpickerButton : Element(ElementType("colorpicker-button")), Disposable
(root() as? Body)?.controlManager?.fontManager?.let {
val font = it.font(computedStyle)
val writer = Writer(drawer)
val writer = TextWriter(drawer)
drawer.fontMap = (font)
val text = "$label"

View File

@@ -5,12 +5,12 @@ import org.openrndr.draw.Drawer
import org.openrndr.draw.FontImageMap
import org.openrndr.panel.style.*
import org.openrndr.shape.Rectangle
import org.openrndr.extra.textwriter.TextWriter
import kotlinx.coroutines.yield
import org.openrndr.KEY_ARROW_DOWN
import org.openrndr.KEY_ARROW_UP
import org.openrndr.KEY_ENTER
import org.openrndr.draw.Writer
import org.openrndr.events.Event
import org.openrndr.launch
import kotlin.math.max
@@ -80,7 +80,7 @@ class DropdownButton : Element(ElementType("dropdown-button")), DisposableElemen
val fontUrl = (root() as? Body)?.controlManager?.fontManager?.resolve(style.fontFamily) ?: "broken"
val fontSize = (style.fontSize as? LinearDimension.PX)?.value ?: 16.0
val fontMap = FontImageMap.fromUrl(fontUrl, fontSize)
val writer = Writer(null)
val writer = TextWriter(null)
writer.box = Rectangle(0.0,
0.0,
@@ -116,7 +116,7 @@ class DropdownButton : Element(ElementType("dropdown-button")), DisposableElemen
(root() as? Body)?.controlManager?.fontManager?.let {
val font = it.font(computedStyle)
val writer = Writer(drawer)
val writer = TextWriter(drawer)
drawer.fontMap = (font)
val text = (value?.label) ?: "<choose>"

View File

@@ -1,9 +1,11 @@
package org.openrndr.panel.elements
import org.openrndr.color.ColorRGBa
import org.openrndr.draw.Cursor
import org.openrndr.draw.Drawer
import org.openrndr.draw.Writer
import org.openrndr.extra.textwriter.Cursor
import org.openrndr.extra.textwriter.TextWriter
import org.openrndr.math.Vector2
import org.openrndr.panel.style.*
@@ -53,7 +55,7 @@ class EnvelopeButton : Element(ElementType("envelope-button")) {
(root() as? Body)?.controlManager?.fontManager?.let {
val font = it.font(computedStyle)
val writer = Writer(drawer)
val writer = TextWriter(drawer)
drawer.fontMap = (font)
drawer.fill = (ColorRGBa.BLACK)
writer.cursor = Cursor(0.0,layout.screenHeight - 4.0)

View File

@@ -5,6 +5,8 @@ import org.openrndr.KeyModifier
import org.openrndr.color.ColorRGBa
import org.openrndr.draw.*
import org.openrndr.events.Event
import org.openrndr.extra.textwriter.Cursor
import org.openrndr.extra.textwriter.TextWriter
import org.openrndr.math.Vector2
import org.openrndr.math.map
import org.openrndr.panel.style.effectiveColor
@@ -196,7 +198,7 @@ open class SequenceEditorBase(type: String = "sequence-editor-base") : Element(E
drawer.fill = computedStyle.effectiveColor
(root() as? Body)?.controlManager?.fontManager?.let {
val font = it.font(computedStyle)
val writer = Writer(drawer)
val writer = TextWriter(drawer)
drawer.fontMap = (font)
drawer.fill = computedStyle.effectiveColor
writer.cursor = Cursor(0.0, 4.0)

View File

@@ -3,11 +3,13 @@ package org.openrndr.panel.elements
import io.github.oshai.kotlinlogging.KotlinLogging
import kotlinx.coroutines.*
import org.openrndr.*
import org.openrndr.draw.Cursor
import org.openrndr.draw.Drawer
import org.openrndr.draw.LineCap
import org.openrndr.draw.Writer
import org.openrndr.events.Event
import org.openrndr.extra.textwriter.Cursor
import org.openrndr.extra.textwriter.TextWriter
import org.openrndr.math.Vector2
import org.openrndr.panel.style.Color
import org.openrndr.panel.style.color
@@ -267,7 +269,7 @@ class Slider : Element(ElementType("slider")), DisposableElement {
(root() as? Body)?.controlManager?.fontManager?.let {
val font = it.font(computedStyle)
val writer = Writer(drawer)
val writer = TextWriter(drawer)
drawer.fontMap = (font)
drawer.fill = computedStyle.effectiveColor
writer.cursor = Cursor(0.0, 8.0)

View File

@@ -4,7 +4,8 @@ import kotlinx.coroutines.yield
import org.openrndr.color.ColorRGBa
import org.openrndr.draw.Drawer
import org.openrndr.draw.FontImageMap
import org.openrndr.draw.Writer
import org.openrndr.extra.textwriter.TextWriter
import org.openrndr.launch
import org.openrndr.math.Vector2
import org.openrndr.panel.style.*
@@ -20,7 +21,7 @@ class TextNode(var text: String) : Element(ElementType("text")) {
drawer.fill = (fill)
}
val fontMap = (root() as Body).controlManager.fontManager.font(computedStyle)
val writer = Writer(drawer)
val writer = TextWriter(drawer)
drawer.fontMap = (fontMap)
writer.box = Rectangle(Vector2(layout.screenX * 0.0, layout.screenY * 0.0), layout.screenWidth, layout.screenHeight)
@@ -35,7 +36,7 @@ class TextNode(var text: String) : Element(ElementType("text")) {
val fontSize = (style.fontSize as? LinearDimension.PX)?.value?: 14.0
val fontMap = FontImageMap.fromUrl(fontUrl, fontSize)
val writer = Writer(null)
val writer = TextWriter(null)
writer.box = Rectangle(layout.screenX,
layout.screenY,

View File

@@ -7,9 +7,9 @@ import org.openrndr.draw.Drawer
import org.openrndr.draw.LineCap
import org.openrndr.panel.style.*
import org.openrndr.KeyModifier
import org.openrndr.draw.Cursor
import org.openrndr.draw.writer
import org.openrndr.events.Event
import org.openrndr.extra.textwriter.Cursor
import org.openrndr.extra.textwriter.writer
import org.openrndr.launch
import org.openrndr.shape.Rectangle
import kotlin.reflect.KMutableProperty0

View File

@@ -7,8 +7,9 @@ import org.openrndr.draw.LineCap
import org.openrndr.panel.style.*
import org.openrndr.shape.Rectangle
import org.openrndr.draw.Writer
import org.openrndr.events.Event
import org.openrndr.extra.textwriter.TextWriter
import org.openrndr.launch
import kotlin.reflect.KMutableProperty0
@@ -35,7 +36,7 @@ class Toggle : Element(ElementType("toggle")), DisposableElement {
val fontSize = (style.fontSize as? LinearDimension.PX)?.value ?: 14.0
val fontMap = FontImageMap.fromUrl(fontUrl, fontSize)
val writer = Writer(null)
val writer = TextWriter(null)
writer.box = Rectangle(0.0,
0.0,

View File

@@ -4,8 +4,9 @@ import kotlinx.coroutines.yield
import org.openrndr.*
import org.openrndr.color.ColorRGBa
import org.openrndr.draw.Drawer
import org.openrndr.draw.Writer
import org.openrndr.events.Event
import org.openrndr.extra.textwriter.TextWriter
import org.openrndr.math.Vector2
import org.openrndr.math.clamp
import org.openrndr.math.map
@@ -213,7 +214,7 @@ class XYPad : Element(ElementType("xy-pad")) {
val valueLabel = "${String.format("%.0${precision}f", value.x)}, ${String.format("%.0${precision}f", value.y)}"
(root() as? Body)?.controlManager?.fontManager?.let {
val writer = Writer(drawer)
val writer = TextWriter(drawer)
drawer.fontMap = it.font(computedStyle)
val textWidth = writer.textWidth(valueLabel)

View File

@@ -4,7 +4,8 @@ import org.openrndr.color.ColorRGBa
import org.openrndr.draw.Drawer
import org.openrndr.draw.FontImageMap
import org.openrndr.draw.isolated
import org.openrndr.draw.writer
import org.openrndr.extra.textwriter.writer
import org.openrndr.math.Vector2
import org.openrndr.panel.elements.Body
import org.openrndr.panel.elements.Element

View File

@@ -0,0 +1,6 @@
# orx-text-writer
Writing texts with layouts
Code in `orx-text-writer` was previously part of `openrndr-draw`.

View File

@@ -0,0 +1,22 @@
plugins {
org.openrndr.extra.convention.`kotlin-multiplatform`
alias(libs.plugins.kotest.multiplatform)
}
kotlin {
sourceSets {
val commonMain by getting {
dependencies {
implementation(libs.openrndr.shape)
implementation(libs.openrndr.draw)
implementation(libs.openrndr.application)
}
}
val jvmDemo by getting {
dependencies {
implementation(project(":orx-text-writer"))
}
}
}
}

View File

@@ -0,0 +1,16 @@
package org.openrndr.extra.textwriter
import org.openrndr.draw.Drawer
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
import kotlin.jvm.JvmName
@OptIn(ExperimentalContracts::class)
@JvmName("drawerWriter")
fun <T> Drawer.writer(f: TextWriter.() -> T): T {
contract {
callsInPlace(f, InvocationKind.EXACTLY_ONCE)
}
return writer(this, f)
}

View File

@@ -0,0 +1,14 @@
package org.openrndr.extra.textwriter
import org.openrndr.Program
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
@OptIn(ExperimentalContracts::class)
fun <T> Program.writer(f: TextWriter.() -> T): T {
contract {
callsInPlace(f, InvocationKind.EXACTLY_ONCE)
}
return writer(drawer, f)
}

View File

@@ -0,0 +1,211 @@
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
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.size)
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)
}
}
fun <T> writer(drawer: Drawer, f: TextWriter.() -> T): T {
val textWriter = TextWriter(drawer)
return textWriter.f()
}

View File

@@ -91,6 +91,7 @@ include(
"orx-depth-camera",
"orx-jvm:orx-depth-camera-calibrator",
"orx-view-box",
"orx-text-writer",
"orx-turtle"
)
)