From 81ff9c9f7a29f22e3dca2d194598b2b1aea24bd5 Mon Sep 17 00:00:00 2001 From: Bixilon Date: Thu, 15 Jun 2023 16:08:10 +0200 Subject: [PATCH] text alignment --- .../component/ChatComponentRendererTest.kt | 80 ++++++++++++++++++- .../component/DummyComponentConsumer.kt | 75 +++++++++++++++++ .../font/renderer/code/CodePointRenderer.kt | 29 ++++--- .../component/TextComponentRenderer.kt | 6 +- .../font/renderer/element/TextOffset.kt | 18 ++++- .../font/renderer/element/TextRenderInfo.kt | 4 +- 6 files changed, 193 insertions(+), 19 deletions(-) create mode 100644 src/integration-test/kotlin/de/bixilon/minosoft/gui/rendering/font/renderer/component/DummyComponentConsumer.kt diff --git a/src/integration-test/kotlin/de/bixilon/minosoft/gui/rendering/font/renderer/component/ChatComponentRendererTest.kt b/src/integration-test/kotlin/de/bixilon/minosoft/gui/rendering/font/renderer/component/ChatComponentRendererTest.kt index 28182dbee..da97a3fa4 100644 --- a/src/integration-test/kotlin/de/bixilon/minosoft/gui/rendering/font/renderer/component/ChatComponentRendererTest.kt +++ b/src/integration-test/kotlin/de/bixilon/minosoft/gui/rendering/font/renderer/component/ChatComponentRendererTest.kt @@ -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) } diff --git a/src/integration-test/kotlin/de/bixilon/minosoft/gui/rendering/font/renderer/component/DummyComponentConsumer.kt b/src/integration-test/kotlin/de/bixilon/minosoft/gui/rendering/font/renderer/component/DummyComponentConsumer.kt new file mode 100644 index 000000000..0970ee3cc --- /dev/null +++ b/src/integration-test/kotlin/de/bixilon/minosoft/gui/rendering/font/renderer/component/DummyComponentConsumer.kt @@ -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 . + * + * 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 = mutableListOf() + + override val order: Array> 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 = 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()) + } +} diff --git a/src/main/java/de/bixilon/minosoft/gui/rendering/font/renderer/code/CodePointRenderer.kt b/src/main/java/de/bixilon/minosoft/gui/rendering/font/renderer/code/CodePointRenderer.kt index a237ed33c..bb91357ee 100644 --- a/src/main/java/de/bixilon/minosoft/gui/rendering/font/renderer/code/CodePointRenderer.kt +++ b/src/main/java/de/bixilon/minosoft/gui/rendering/font/renderer/code/CodePointRenderer.kt @@ -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 diff --git a/src/main/java/de/bixilon/minosoft/gui/rendering/font/renderer/component/TextComponentRenderer.kt b/src/main/java/de/bixilon/minosoft/gui/rendering/font/renderer/component/TextComponentRenderer.kt index d31a42127..e1a400192 100644 --- a/src/main/java/de/bixilon/minosoft/gui/rendering/font/renderer/component/TextComponentRenderer.kt +++ b/src/main/java/de/bixilon/minosoft/gui/rendering/font/renderer/component/TextComponentRenderer.kt @@ -41,9 +41,9 @@ object TextComponentRenderer : ChatComponentRenderer { 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 { 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) } diff --git a/src/main/java/de/bixilon/minosoft/gui/rendering/font/renderer/element/TextOffset.kt b/src/main/java/de/bixilon/minosoft/gui/rendering/font/renderer/element/TextOffset.kt index 753476644..d1d0d5f71 100644 --- a/src/main/java/de/bixilon/minosoft/gui/rendering/font/renderer/element/TextOffset.kt +++ b/src/main/java/de/bixilon/minosoft/gui/rendering/font/renderer/element/TextOffset.kt @@ -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 diff --git a/src/main/java/de/bixilon/minosoft/gui/rendering/font/renderer/element/TextRenderInfo.kt b/src/main/java/de/bixilon/minosoft/gui/rendering/font/renderer/element/TextRenderInfo.kt index 2c75eaad2..c83d8aab5 100644 --- a/src/main/java/de/bixilon/minosoft/gui/rendering/font/renderer/element/TextRenderInfo.kt +++ b/src/main/java/de/bixilon/minosoft/gui/rendering/font/renderer/element/TextRenderInfo.kt @@ -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 }