[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-shader-phrases"))
demoImplementation(project(":orx-camera")) demoImplementation(project(":orx-camera"))
demoImplementation(project(":orx-shapes")) demoImplementation(project(":orx-shapes"))
demoImplementation(project(":orx-svg"))
demoImplementation(libs.slf4j.simple) demoImplementation(libs.slf4j.simple)
demoImplementation(libs.openrndr.ffmpeg) demoImplementation(libs.openrndr.ffmpeg)
demoImplementation(libs.openrndr.svg)
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,11 @@
package org.openrndr.panel.elements package org.openrndr.panel.elements
import org.openrndr.color.ColorRGBa import org.openrndr.color.ColorRGBa
import org.openrndr.draw.Cursor
import org.openrndr.draw.Drawer 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.math.Vector2
import org.openrndr.panel.style.* import org.openrndr.panel.style.*
@@ -53,7 +55,7 @@ class EnvelopeButton : Element(ElementType("envelope-button")) {
(root() as? Body)?.controlManager?.fontManager?.let { (root() as? Body)?.controlManager?.fontManager?.let {
val font = it.font(computedStyle) val font = it.font(computedStyle)
val writer = Writer(drawer) val writer = TextWriter(drawer)
drawer.fontMap = (font) drawer.fontMap = (font)
drawer.fill = (ColorRGBa.BLACK) drawer.fill = (ColorRGBa.BLACK)
writer.cursor = Cursor(0.0,layout.screenHeight - 4.0) 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.color.ColorRGBa
import org.openrndr.draw.* import org.openrndr.draw.*
import org.openrndr.events.Event 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.Vector2
import org.openrndr.math.map import org.openrndr.math.map
import org.openrndr.panel.style.effectiveColor import org.openrndr.panel.style.effectiveColor
@@ -196,7 +198,7 @@ open class SequenceEditorBase(type: String = "sequence-editor-base") : Element(E
drawer.fill = computedStyle.effectiveColor drawer.fill = computedStyle.effectiveColor
(root() as? Body)?.controlManager?.fontManager?.let { (root() as? Body)?.controlManager?.fontManager?.let {
val font = it.font(computedStyle) val font = it.font(computedStyle)
val writer = Writer(drawer) val writer = TextWriter(drawer)
drawer.fontMap = (font) drawer.fontMap = (font)
drawer.fill = computedStyle.effectiveColor drawer.fill = computedStyle.effectiveColor
writer.cursor = Cursor(0.0, 4.0) 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 io.github.oshai.kotlinlogging.KotlinLogging
import kotlinx.coroutines.* import kotlinx.coroutines.*
import org.openrndr.* import org.openrndr.*
import org.openrndr.draw.Cursor
import org.openrndr.draw.Drawer import org.openrndr.draw.Drawer
import org.openrndr.draw.LineCap import org.openrndr.draw.LineCap
import org.openrndr.draw.Writer
import org.openrndr.events.Event 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.Vector2
import org.openrndr.panel.style.Color import org.openrndr.panel.style.Color
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 { (root() as? Body)?.controlManager?.fontManager?.let {
val font = it.font(computedStyle) val font = it.font(computedStyle)
val writer = Writer(drawer) val writer = TextWriter(drawer)
drawer.fontMap = (font) drawer.fontMap = (font)
drawer.fill = computedStyle.effectiveColor drawer.fill = computedStyle.effectiveColor
writer.cursor = Cursor(0.0, 8.0) 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.color.ColorRGBa
import org.openrndr.draw.Drawer import org.openrndr.draw.Drawer
import org.openrndr.draw.FontImageMap import org.openrndr.draw.FontImageMap
import org.openrndr.draw.Writer import org.openrndr.extra.textwriter.TextWriter
import org.openrndr.launch import org.openrndr.launch
import org.openrndr.math.Vector2 import org.openrndr.math.Vector2
import org.openrndr.panel.style.* import org.openrndr.panel.style.*
@@ -20,7 +21,7 @@ class TextNode(var text: String) : Element(ElementType("text")) {
drawer.fill = (fill) drawer.fill = (fill)
} }
val fontMap = (root() as Body).controlManager.fontManager.font(computedStyle) val fontMap = (root() as Body).controlManager.fontManager.font(computedStyle)
val writer = Writer(drawer) val writer = TextWriter(drawer)
drawer.fontMap = (fontMap) drawer.fontMap = (fontMap)
writer.box = Rectangle(Vector2(layout.screenX * 0.0, layout.screenY * 0.0), layout.screenWidth, layout.screenHeight) 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 fontSize = (style.fontSize as? LinearDimension.PX)?.value?: 14.0
val fontMap = FontImageMap.fromUrl(fontUrl, fontSize) val fontMap = FontImageMap.fromUrl(fontUrl, fontSize)
val writer = Writer(null) val writer = TextWriter(null)
writer.box = Rectangle(layout.screenX, writer.box = Rectangle(layout.screenX,
layout.screenY, layout.screenY,

View File

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

View File

@@ -7,8 +7,9 @@ import org.openrndr.draw.LineCap
import org.openrndr.panel.style.* import org.openrndr.panel.style.*
import org.openrndr.shape.Rectangle import org.openrndr.shape.Rectangle
import org.openrndr.draw.Writer
import org.openrndr.events.Event import org.openrndr.events.Event
import org.openrndr.extra.textwriter.TextWriter
import org.openrndr.launch import org.openrndr.launch
import kotlin.reflect.KMutableProperty0 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 fontSize = (style.fontSize as? LinearDimension.PX)?.value ?: 14.0
val fontMap = FontImageMap.fromUrl(fontUrl, fontSize) val fontMap = FontImageMap.fromUrl(fontUrl, fontSize)
val writer = Writer(null) val writer = TextWriter(null)
writer.box = Rectangle(0.0, writer.box = Rectangle(0.0,
0.0, 0.0,

View File

@@ -4,8 +4,9 @@ import kotlinx.coroutines.yield
import org.openrndr.* import org.openrndr.*
import org.openrndr.color.ColorRGBa import org.openrndr.color.ColorRGBa
import org.openrndr.draw.Drawer import org.openrndr.draw.Drawer
import org.openrndr.draw.Writer
import org.openrndr.events.Event import org.openrndr.events.Event
import org.openrndr.extra.textwriter.TextWriter
import org.openrndr.math.Vector2 import org.openrndr.math.Vector2
import org.openrndr.math.clamp import org.openrndr.math.clamp
import org.openrndr.math.map 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)}" val valueLabel = "${String.format("%.0${precision}f", value.x)}, ${String.format("%.0${precision}f", value.y)}"
(root() as? Body)?.controlManager?.fontManager?.let { (root() as? Body)?.controlManager?.fontManager?.let {
val writer = Writer(drawer) val writer = TextWriter(drawer)
drawer.fontMap = it.font(computedStyle) drawer.fontMap = it.font(computedStyle)
val textWidth = writer.textWidth(valueLabel) 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.Drawer
import org.openrndr.draw.FontImageMap import org.openrndr.draw.FontImageMap
import org.openrndr.draw.isolated import org.openrndr.draw.isolated
import org.openrndr.draw.writer import org.openrndr.extra.textwriter.writer
import org.openrndr.math.Vector2 import org.openrndr.math.Vector2
import org.openrndr.panel.elements.Body import org.openrndr.panel.elements.Body
import org.openrndr.panel.elements.Element 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-depth-camera",
"orx-jvm:orx-depth-camera-calibrator", "orx-jvm:orx-depth-camera-calibrator",
"orx-view-box", "orx-view-box",
"orx-text-writer",
"orx-turtle" "orx-turtle"
) )
) )