mirror of
https://gitlab.bixilon.de/bixilon/minosoft.git
synced 2025-09-19 12:25:12 -04:00
text alignment
This commit is contained in:
parent
f455f2a20b
commit
81ff9c9f7a
@ -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)
|
||||
}
|
||||
|
@ -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())
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user