text alignment

This commit is contained in:
Bixilon 2023-06-15 16:08:10 +02:00
parent f455f2a20b
commit 81ff9c9f7a
No known key found for this signature in database
GPG Key ID: 5CAD791931B09AC4
6 changed files with 193 additions and 19 deletions

View File

@ -11,6 +11,7 @@ import de.bixilon.minosoft.gui.rendering.font.renderer.element.TextRenderInfo
import de.bixilon.minosoft.gui.rendering.font.renderer.element.TextRenderProperties
import de.bixilon.minosoft.gui.rendering.font.types.dummy.DummyFontType
import de.bixilon.minosoft.gui.rendering.font.types.font.EmptyFont
import de.bixilon.minosoft.gui.rendering.gui.elements.HorizontalAlignments
import de.bixilon.minosoft.gui.rendering.gui.mesh.GUIVertexConsumer
import de.bixilon.minosoft.gui.rendering.util.vec.vec2.Vec2Util.MAX
import org.testng.Assert.assertEquals
@ -22,7 +23,11 @@ class ChatComponentRendererTest {
private fun render(text: ChatComponent, fontManager: FontManager = this.fontManager, properties: TextRenderProperties = TextRenderProperties(shadow = false), maxSize: Vec2 = Vec2.MAX, consumer: GUIVertexConsumer? = null): TextRenderInfo {
val info = TextRenderInfo(maxSize)
ChatComponentRenderer.render(TextOffset(Vec2(10, 10)), fontManager, properties, info, consumer, null, text)
ChatComponentRenderer.render(TextOffset(Vec2(10, 10)), fontManager, properties, info, null, null, text)
if (consumer != null) {
info.rewind()
ChatComponentRenderer.render(TextOffset(Vec2(10, 10)), fontManager, properties, info, consumer, null, text)
}
return info
}
@ -286,5 +291,76 @@ class ChatComponentRendererTest {
)
}
// TODO: shadow, underline, strikethrough, using with consumer, formatting (just basic, that is code point renderer's job)
fun `single char rendering`() {
val consumer = DummyComponentConsumer()
render(TextComponent("b"), fontManager = FontManager(consumer.Font()), consumer = consumer)
consumer.assert(
DummyComponentConsumer.RendererdCodePoint(Vec2(10, 10)),
)
}
fun `multiple char rendering`() {
val consumer = DummyComponentConsumer()
render(TextComponent("bc"), fontManager = FontManager(consumer.Font()), consumer = consumer)
consumer.assert(
DummyComponentConsumer.RendererdCodePoint(Vec2(10, 10)),
DummyComponentConsumer.RendererdCodePoint(Vec2(11.5, 10)),
)
}
fun `newline rendering`() {
val consumer = DummyComponentConsumer()
render(TextComponent("bc\nde"), fontManager = FontManager(consumer.Font()), consumer = consumer)
consumer.assert(
DummyComponentConsumer.RendererdCodePoint(Vec2(10, 10)),
DummyComponentConsumer.RendererdCodePoint(Vec2(11.5, 10)),
DummyComponentConsumer.RendererdCodePoint(Vec2(10.0, 21)),
DummyComponentConsumer.RendererdCodePoint(Vec2(12.5, 21)),
)
}
fun `left alignment`() { // default
val consumer = DummyComponentConsumer()
render(TextComponent("bc\nde"), fontManager = FontManager(consumer.Font()), consumer = consumer, properties = TextRenderProperties(alignment = HorizontalAlignments.LEFT, shadow = false))
consumer.assert(
DummyComponentConsumer.RendererdCodePoint(Vec2(10, 10)),
DummyComponentConsumer.RendererdCodePoint(Vec2(11.5, 10)),
DummyComponentConsumer.RendererdCodePoint(Vec2(10.0, 21)),
DummyComponentConsumer.RendererdCodePoint(Vec2(12.5, 21)),
)
}
fun `center alignment`() {
val consumer = DummyComponentConsumer()
render(TextComponent("bc\nde"), fontManager = FontManager(consumer.Font()), consumer = consumer, properties = TextRenderProperties(alignment = HorizontalAlignments.CENTER, shadow = false))
consumer.assert(
DummyComponentConsumer.RendererdCodePoint(Vec2(11, 10)),
DummyComponentConsumer.RendererdCodePoint(Vec2(12.5, 10)),
DummyComponentConsumer.RendererdCodePoint(Vec2(10.0, 21)),
DummyComponentConsumer.RendererdCodePoint(Vec2(12.5, 21)),
)
}
fun `right alignment`() {
val consumer = DummyComponentConsumer()
render(TextComponent("bc\nde"), fontManager = FontManager(consumer.Font()), consumer = consumer, properties = TextRenderProperties(alignment = HorizontalAlignments.RIGHT, shadow = false))
consumer.assert(
DummyComponentConsumer.RendererdCodePoint(Vec2(12, 10)),
DummyComponentConsumer.RendererdCodePoint(Vec2(13.5, 10)),
DummyComponentConsumer.RendererdCodePoint(Vec2(10.0, 21)),
DummyComponentConsumer.RendererdCodePoint(Vec2(12.5, 21)),
)
}
// TODO: shadow, underline, strikethrough, formatting (just basic, that is code point renderer's job)
}

View File

@ -0,0 +1,75 @@
/*
* Minosoft
* Copyright (C) 2020-2023 Moritz Zwerger
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This software is not affiliated with Mojang AB, the original developer of Minecraft.
*/
package de.bixilon.minosoft.gui.rendering.font.renderer.component
import de.bixilon.kotlinglm.vec2.Vec2
import de.bixilon.kutil.exception.Broken
import de.bixilon.minosoft.data.text.formatting.color.RGBColor
import de.bixilon.minosoft.gui.rendering.font.renderer.code.CodePointRenderer
import de.bixilon.minosoft.gui.rendering.font.types.FontType
import de.bixilon.minosoft.gui.rendering.gui.mesh.GUIMeshCache
import de.bixilon.minosoft.gui.rendering.gui.mesh.GUIVertexConsumer
import de.bixilon.minosoft.gui.rendering.gui.mesh.GUIVertexOptions
import de.bixilon.minosoft.gui.rendering.system.base.texture.ShaderIdentifiable
import org.testng.Assert.assertEquals
class DummyComponentConsumer : GUIVertexConsumer {
val chars: MutableList<RendererdCodePoint> = mutableListOf()
override val order: Array<Pair<Int, Int>> get() = Broken()
override fun addVertex(position: Vec2, texture: ShaderIdentifiable?, uv: Vec2, tint: RGBColor, options: GUIVertexOptions?) = Broken()
override fun addCache(cache: GUIMeshCache) = Broken()
override fun ensureSize(size: Int) = Unit
data class RendererdCodePoint(val start: Vec2)
inner class ConsumerCodePointRenderer(val width: Float) : CodePointRenderer {
override fun calculateWidth(scale: Float, shadow: Boolean): Float {
return width * scale
}
override fun render(position: Vec2, color: RGBColor, shadow: Boolean, bold: Boolean, italic: Boolean, scale: Float, consumer: GUIVertexConsumer, options: GUIVertexOptions?) {
chars += RendererdCodePoint(Vec2(position))
}
}
inner class Font : FontType {
private val chars: Array<ConsumerCodePointRenderer?> = arrayOfNulls(26) // a-z
// a:0 b:0.5 c:1.0 d:1.5 e:2.0 f:2.5 g:3.0 h:3.5
init {
build()
}
fun build() {
for (i in 0 until chars.size) {
chars[i] = ConsumerCodePointRenderer(width = i / 2.0f)
}
}
override fun get(codePoint: Int): CodePointRenderer? {
if (codePoint in 'a'.code..'z'.code) {
return chars[codePoint - 'a'.code]
}
return null
}
}
fun assert(vararg chars: RendererdCodePoint) {
assertEquals(this.chars, chars.toList())
}
}

View File

@ -23,6 +23,7 @@ import de.bixilon.minosoft.gui.rendering.font.renderer.element.TextOffset
import de.bixilon.minosoft.gui.rendering.font.renderer.element.TextRenderInfo
import de.bixilon.minosoft.gui.rendering.font.renderer.element.TextRenderProperties
import de.bixilon.minosoft.gui.rendering.font.renderer.properties.FormattingProperties.SHADOW_OFFSET
import de.bixilon.minosoft.gui.rendering.gui.elements.HorizontalAlignments.Companion.getOffset
import de.bixilon.minosoft.gui.rendering.gui.mesh.GUIVertexConsumer
import de.bixilon.minosoft.gui.rendering.gui.mesh.GUIVertexOptions
import de.bixilon.minosoft.gui.rendering.util.vec.vec2.Vec2Util.EMPTY
@ -39,8 +40,12 @@ interface CodePointRenderer {
return calculateWidth(scale, shadow)
}
private fun getVerticalSpacing(offset: TextOffset, properties: TextRenderProperties): Float {
if (offset.offset.x == offset.initial.x) return 0.0f
private fun getVerticalSpacing(offset: TextOffset, properties: TextRenderProperties, info: TextRenderInfo, align: Boolean): Float {
var lineStart = offset.initial.x
if (align) {
lineStart += properties.alignment.getOffset(info.lines[info.lineIndex].width, info.size.x)
}
if (offset.offset.x == lineStart) return 0.0f
// not at line start
var spacing = properties.charSpacing.vertical
if (properties.shadow) {
@ -52,15 +57,18 @@ interface CodePointRenderer {
fun render(offset: TextOffset, color: RGBColor, properties: TextRenderProperties, info: TextRenderInfo, formatting: TextFormatting, codePoint: Int, consumer: GUIVertexConsumer?, options: GUIVertexOptions?): CodePointAddResult {
val codePointWidth = calculateWidth(properties.scale, properties.shadow)
var width = codePointWidth + getVerticalSpacing(offset, properties)
val width = calculateWidth(properties.scale, properties.shadow)
var spacing = getVerticalSpacing(offset, properties, info, consumer != null)
val height = offset.getNextLineHeight(properties)
val canAdd = offset.canAdd(properties, info, width, height)
val canAdd = offset.canAdd(properties, info, width + spacing, height, consumer != null)
when (canAdd) {
CodePointAddResult.FINE -> Unit
CodePointAddResult.FINE -> {
offset.offset.x += spacing
}
CodePointAddResult.NEW_LINE -> {
width = codePointWidth // new line, remove vertical spacing
spacing = 0.0f
info.size.y += height
}
@ -69,11 +77,14 @@ interface CodePointRenderer {
if (consumer != null) {
if (info.lineIndex == 0 && offset.offset.x == offset.initial.x) {
// switched to consumer mode but offset was not updated yet
offset.align(properties.alignment, info.lines.first().width, info.size)
}
render(offset.offset, color, properties.shadow, FormattingCodes.BOLD in formatting, FormattingCodes.ITALIC in formatting, properties.scale, consumer, options)
} else {
info.update(offset, properties, width) // info should only be updated when we determinate text properties, we know all that already when actually rendering it
info.update(offset, properties, width, spacing) // info should only be updated when we determinate text properties, we know all that already when actually rendering it }
}
offset.offset.x += width
return canAdd

View File

@ -41,9 +41,9 @@ object TextComponentRenderer : ChatComponentRenderer<TextComponent> {
return properties.forcedColor ?: text.color ?: properties.fallbackColor
}
private fun renderNewline(properties: TextRenderProperties, offset: TextOffset, info: TextRenderInfo, updateSize: Boolean): Boolean {
private fun renderNewline(properties: TextRenderProperties, offset: TextOffset, info: TextRenderInfo, updateSize: Boolean, align: Boolean): Boolean {
val height = offset.getNextLineHeight(properties)
if (!offset.addLine(info, properties.lineHeight, height)) {
if (!offset.addLine(properties, info, properties.lineHeight, height, align)) {
info.cutOff = true
return true
}
@ -80,7 +80,7 @@ object TextComponentRenderer : ChatComponentRenderer<TextComponent> {
val codePoint = stream.nextInt()
if (codePoint == '\n'.code) {
val lineIndex = info.lineIndex
filled = renderNewline(properties, offset, info, consumer == null)
filled = renderNewline(properties, offset, info, consumer == null, consumer != null)
if (line.isNotEmpty()) {
info.lines[lineIndex].push(text, line)
}

View File

@ -15,6 +15,8 @@ package de.bixilon.minosoft.gui.rendering.font.renderer.element
import de.bixilon.kotlinglm.vec2.Vec2
import de.bixilon.minosoft.gui.rendering.font.renderer.CodePointAddResult
import de.bixilon.minosoft.gui.rendering.gui.elements.HorizontalAlignments
import de.bixilon.minosoft.gui.rendering.gui.elements.HorizontalAlignments.Companion.getOffset
import de.bixilon.minosoft.gui.rendering.util.vec.vec2.Vec2Util.EMPTY
class TextOffset(
@ -22,6 +24,13 @@ class TextOffset(
) {
var offset = Vec2(initial)
fun align(alignment: HorizontalAlignments, width: Float, size: Vec2) {
this.offset.x = initial.x
this.offset.x += alignment.getOffset(width, size.x)
}
private fun fits(offset: Float, initial: Float, max: Float, value: Float): Boolean {
val size = offset - initial
val remaining = max - size
@ -53,11 +62,14 @@ class TextOffset(
return height
}
fun addLine(info: TextRenderInfo, offset: Float, height: Float): Boolean {
fun addLine(properties: TextRenderProperties, info: TextRenderInfo, offset: Float, height: Float, align: Boolean): Boolean {
if (!fitsY(info, offset, height)) return false
this.offset.y += height
this.offset.x = initial.x
if (align) {
align(properties.alignment, info.lines[info.lineIndex].width, info.size)
}
info.lines += LineRenderInfo()
info.lineIndex++
@ -65,13 +77,13 @@ class TextOffset(
}
fun canAdd(properties: TextRenderProperties, info: TextRenderInfo, width: Float, height: Float): CodePointAddResult {
fun canAdd(properties: TextRenderProperties, info: TextRenderInfo, width: Float, height: Float, align: Boolean): CodePointAddResult {
if (!canEverFit(info, width)) {
info.cutOff = true
return CodePointAddResult.BREAK
}
if (fitsInLine(properties, info, width)) return CodePointAddResult.FINE
if (addLine(info, 0.0f, height) && fitsInLine(properties, info, width)) return CodePointAddResult.NEW_LINE
if (addLine(properties, info, 0.0f, height, align) && fitsInLine(properties, info, width)) return CodePointAddResult.NEW_LINE
info.cutOff = true
return CodePointAddResult.BREAK

View File

@ -26,7 +26,7 @@ class TextRenderInfo(
var cutOff = false
fun update(offset: TextOffset, properties: TextRenderProperties, width: Float): LineRenderInfo {
fun update(offset: TextOffset, properties: TextRenderProperties, width: Float, spacing: Float): LineRenderInfo {
size.x = maxOf(offset.offset.x - offset.initial.x + width, size.x)
val line: LineRenderInfo
@ -39,7 +39,7 @@ class TextRenderInfo(
line = lines[lineIndex]
}
line.width += width
line.width += width + spacing
return line
}