TextComponent: Default formatting, hud rendering improvements

This commit is contained in:
Bixilon 2021-09-10 20:52:33 +02:00
parent dcb8a05cb5
commit cc166127f8
No known key found for this signature in database
GPG Key ID: 5CAD791931B09AC4
13 changed files with 148 additions and 113 deletions

View File

@ -42,7 +42,7 @@ class BaseComponent : ChatComponent {
constructor(parent: TextComponent? = null, legacy: String = "", restrictedMode: Boolean = false) {
val currentText = StringBuilder()
var currentColor = parent?.color
val currentFormatting: MutableSet<ChatFormattingCode> = parent?.formatting?.toMutableSet() ?: mutableSetOf()
var currentFormatting: MutableSet<ChatFormattingCode> = parent?.formatting?.toMutableSet() ?: TextComponent.DEFAULT_FORMATTING.toMutableSet()
val iterator = StringCharacterIterator(legacy)
@ -95,7 +95,7 @@ class BaseComponent : ChatComponent {
}
}
push(null)
currentFormatting.clear()
currentFormatting = TextComponent.DEFAULT_FORMATTING.toMutableSet()
currentColor = null
currentText.clear()
}
@ -146,7 +146,7 @@ class BaseComponent : ChatComponent {
val color = json["color"]?.nullCast<String>()?.toColor() ?: parent?.color
val formatting = parent?.formatting?.toMutableSet() ?: mutableSetOf()
val formatting = parent?.formatting?.toMutableSet() ?: TextComponent.DEFAULT_FORMATTING.toMutableSet()
formatting.addOrRemove(PreChatFormattingCodes.BOLD, json["bold"]?.toBoolean())
formatting.addOrRemove(PreChatFormattingCodes.ITALIC, json["italic"]?.toBoolean())

View File

@ -32,7 +32,7 @@ import javafx.util.Duration
open class TextComponent(
message: Any? = "",
override var color: RGBColor? = null,
override val formatting: MutableSet<ChatFormattingCode> = mutableSetOf(),
override val formatting: MutableSet<ChatFormattingCode> = DEFAULT_FORMATTING.toMutableSet(),
var clickEvent: ClickEvent? = null,
var hoverEvent: HoverEvent? = null,
) : ChatComponent, TextStyle {
@ -191,4 +191,8 @@ open class TextComponent(
}
return nodes
}
companion object {
val DEFAULT_FORMATTING: Set<ChatFormattingCode> = setOf(PreChatFormattingCodes.SHADOWED)
}
}

View File

@ -22,9 +22,12 @@ import glm_.vec2.Vec2i
object BaseComponentRenderer : ChatComponentRenderer<BaseComponent> {
override fun render(initialOffset: Vec2i, offset: Vec2i, size: Vec2i, z: Int, element: Element, fontAlignment: ElementAlignments, renderWindow: RenderWindow, consumer: GUIVertexConsumer?, renderInfo: TextRenderInfo, text: BaseComponent) {
override fun render(initialOffset: Vec2i, offset: Vec2i, size: Vec2i, z: Int, element: Element, fontAlignment: ElementAlignments, renderWindow: RenderWindow, consumer: GUIVertexConsumer?, renderInfo: TextRenderInfo, text: BaseComponent): Boolean {
for (part in text.parts) {
ChatComponentRenderer.render(initialOffset, offset, size, z, element, fontAlignment, renderWindow, consumer, renderInfo, part)
if (ChatComponentRenderer.render(initialOffset, offset, size, z, element, fontAlignment, renderWindow, consumer, renderInfo, part)) {
return true
}
}
return false
}
}

View File

@ -24,13 +24,16 @@ import glm_.vec2.Vec2i
interface ChatComponentRenderer<T : ChatComponent> {
fun render(initialOffset: Vec2i, offset: Vec2i, size: Vec2i, z: Int, element: Element, fontAlignment: ElementAlignments, renderWindow: RenderWindow, consumer: GUIVertexConsumer?, renderInfo: TextRenderInfo, text: T)
/**
* Returns true if the text exceeded the maximum size
*/
fun render(initialOffset: Vec2i, offset: Vec2i, size: Vec2i, z: Int, element: Element, fontAlignment: ElementAlignments, renderWindow: RenderWindow, consumer: GUIVertexConsumer?, renderInfo: TextRenderInfo, text: T): Boolean
companion object : ChatComponentRenderer<ChatComponent> {
override fun render(initialOffset: Vec2i, offset: Vec2i, size: Vec2i, z: Int, element: Element, fontAlignment: ElementAlignments, renderWindow: RenderWindow, consumer: GUIVertexConsumer?, renderInfo: TextRenderInfo, text: ChatComponent) {
when (text) {
override fun render(initialOffset: Vec2i, offset: Vec2i, size: Vec2i, z: Int, element: Element, fontAlignment: ElementAlignments, renderWindow: RenderWindow, consumer: GUIVertexConsumer?, renderInfo: TextRenderInfo, text: ChatComponent): Boolean {
return when (text) {
is BaseComponent -> BaseComponentRenderer.render(initialOffset, offset, size, z, element, fontAlignment, renderWindow, consumer, renderInfo, text)
is TextComponent -> TextComponentRenderer.render(initialOffset, offset, size, z, element, fontAlignment, renderWindow, consumer, renderInfo, text)
else -> TODO("Don't know how to render ${text::class.java}")

View File

@ -23,131 +23,142 @@ import de.bixilon.minosoft.gui.rendering.gui.elements.ElementAlignments
import de.bixilon.minosoft.gui.rendering.gui.elements.ElementAlignments.Companion.getOffset
import de.bixilon.minosoft.gui.rendering.gui.mesh.GUIVertexConsumer
import de.bixilon.minosoft.util.MMath.ceil
import de.bixilon.minosoft.util.logging.Log
import de.bixilon.minosoft.util.logging.LogLevels
import de.bixilon.minosoft.util.logging.LogMessageType
import glm_.vec2.Vec2i
object TextComponentRenderer : ChatComponentRenderer<TextComponent> {
override fun render(initialOffset: Vec2i, offset: Vec2i, size: Vec2i, z: Int, element: Element, fontAlignment: ElementAlignments, renderWindow: RenderWindow, consumer: GUIVertexConsumer?, renderInfo: TextRenderInfo, text: TextComponent) {
override fun render(initialOffset: Vec2i, offset: Vec2i, size: Vec2i, z: Int, element: Element, fontAlignment: ElementAlignments, renderWindow: RenderWindow, consumer: GUIVertexConsumer?, renderInfo: TextRenderInfo, text: TextComponent): Boolean {
val elementMaxSize = element.maxSize
// ToDo: Only 1 quad for the underline and the strikethrough
var first = true
var currentLineInfo = renderInfo.lines.getOrElse(renderInfo.currentLine) {
val lineInfo = TextLineInfo()
renderInfo.lines += lineInfo
lineInfo
var alignmentXOffset = 0
if (size.x >= elementMaxSize.x || size.y >= elementMaxSize.y) {
// The size is already bigger/equals the maximum size
return true
}
fun pushLine() {
renderInfo.currentLine++
currentLineInfo = TextLineInfo()
renderInfo.lines += currentLineInfo
}
fun updateOffset() {
val renderInfo = renderInfo
offset.x = initialOffset.x
if (consumer != null) {
// set offset of the next line to match the expected alignment
offset.x += fontAlignment.getOffset(element.size.x, renderInfo.lines[renderInfo.currentLine].width)
}
}
/**
* @return If the text can't fit into the layout anymore
*/
fun wrap(): Boolean {
val yAdd = Font.CHAR_HEIGHT + Font.VERTICAL_SPACING
if (size.y + yAdd > elementMaxSize.y) {
fun addY(height: Int): Boolean {
val nextY = offset.y + height
val nextSizeY = nextY - initialOffset.y + Font.CHAR_HEIGHT // add initial height for chars
if (nextSizeY >= elementMaxSize.y) {
return true
}
if (consumer == null) {
pushLine()
} else {
renderInfo.currentLine++
offset.y = nextY
if (nextSizeY > size.y) {
size.y = nextSizeY
}
updateOffset()
offset.y += yAdd
size.y += yAdd
return false
}
/**
* @return If the text can't fit into the layout anymore
*/
fun add(x: Int): Boolean {
if (offset.x - initialOffset.x + x > elementMaxSize.x) {
fun wrap(): Boolean {
if (addY(Font.CHAR_HEIGHT + Font.VERTICAL_SPACING)) {
return true
}
renderInfo.currentLine++
offset.x = initialOffset.x
if (consumer == null) {
// preparing phase
renderInfo.lines += TextLineInfo()
} else {
if (renderInfo.currentLine >= renderInfo.lines.size) {
Log.log(LogMessageType.OTHER, LogLevels.FATAL) { "Crash because of $text (size=$size, maxSize=$elementMaxSize)" }
}
alignmentXOffset = fontAlignment.getOffset(element.size.x, renderInfo.lines[renderInfo.currentLine].width)
}
return false
}
fun addX(width: Int, wrap: Boolean = true): Boolean {
val nextX = offset.x + width
val nextSizeX = nextX + alignmentXOffset - initialOffset.x
if (nextSizeX > elementMaxSize.x) {
if (!wrap) {
return true
}
if (wrap()) {
return true
}
} else {
offset.x += x
if (consumer == null) {
currentLineInfo.width += x
}
return addX(width, false)
}
if (size.x < offset.x - initialOffset.x) {
size.x += x
if (consumer == null) {
renderInfo.lines[renderInfo.currentLine].width += width
}
offset.x = nextX
if (nextSizeX > size.x) {
size.x = nextSizeX
}
return false
}
if (offset.x == initialOffset.x) {
updateOffset()
if (size.y == 0) {
// Add initial height of the letter for the first line
val nextSizeY = Font.CHAR_HEIGHT
if (nextSizeY > elementMaxSize.y) {
return true
}
if (consumer != null) {
alignmentXOffset = fontAlignment.getOffset(element.size.x, renderInfo.lines[renderInfo.currentLine].width)
} else {
renderInfo.lines += TextLineInfo() // add line 0
}
size.y = nextSizeY
}
for (char in text.message.toCharArray()) {
for (charCode in text.message.codePoints().toArray()) {
val char = charCode.toChar()
if (char == '\n') {
if (wrap()) {
return
return true
}
continue
}
// skip wrapped spaces
// skip spaces that are wrapped (because of a line break)
if (offset.y != initialOffset.y && offset.x == initialOffset.x && char == ' ') {
continue
}
val charData = renderWindow.font[char] ?: continue
if (first) {
first = false
val charWidth = charData.calculateWidth(text)
var width = charWidth
// Add initial size
if (size.y == 0) {
size.y = Font.CHAR_HEIGHT + Font.VERTICAL_SPACING
}
} else if (offset.x != initialOffset.x && add(Font.HORIZONTAL_SPACING)) { // ToDo: Only add space when char fits
return
if (offset.x != initialOffset.x) {
// add spacing between letters
width += Font.HORIZONTAL_SPACING
}
val previousY = offset.y
if (addX(width)) {
return true
}
val width = charData.calculateWidth(text)
val letterOffset = Vec2i(offset.x + alignmentXOffset, offset.y)
if (offset.x == initialOffset.x && offset.x - initialOffset.x + width > element.maxSize.x) {
return
// remove width from the offset again
letterOffset.x -= charWidth
if (previousY != offset.y) {
// line was wrapped, we want to begin at the offset without the spacing
// ToDo: Remove Font.HORIZONTAL_SPACING
}
consumer?.let { charData.render(offset, z, text, it) }
if (consumer != null) {
consumer?.let { charData.render(letterOffset, z, text, it) }
if (consumer == null) {
renderInfo.lines[renderInfo.currentLine].chars += char
}
if (add(width)) {
return
}
}
if (text.formatting.contains(PreChatFormattingCodes.ITALIC)) {
val italicOffset = CharData.ITALIC_OFFSET.ceil
offset.x += italicOffset
size.x += italicOffset
addX(italicOffset) // ToDo: Should this be forced?
}
return false
}
}

View File

@ -25,6 +25,10 @@ import glm_.vec4.Vec4i
abstract class Element(val hudRenderer: HUDRenderer) {
val renderWindow = hudRenderer.renderWindow
open var parent: Element? = null
set(value) {
field = value
onParentChange()
}
open var prepared: Boolean = false
/**

View File

@ -35,6 +35,7 @@ import java.lang.Integer.max
class RowLayout(
hudRenderer: HUDRenderer,
override var childAlignment: ElementAlignments = ElementAlignments.LEFT,
spacing: Int = 0,
) : Layout(hudRenderer), ChildAlignable {
private var _prefSize = Vec2i.EMPTY
@ -45,6 +46,12 @@ class RowLayout(
get() = _prefSize
set(value) {}
var spacing: Int = spacing
set(value) {
field = value
apply()
}
fun clear() {
children.clear()
}
@ -55,13 +62,14 @@ class RowLayout(
var childYOffset = margin.top
var maxZ = 0
for (child in children) {
val childZ = child.render(Vec2i(offset.x + margin.left + childAlignment.getOffset(size.x, child.size.x), offset.y + childYOffset), z, consumer)
val childZ = child.render(Vec2i(offset.x + margin.left + childAlignment.getOffset(size.x - margin.horizontal, child.size.x), offset.y + childYOffset), z, consumer)
if (maxZ < childZ) {
maxZ = childZ
}
childYOffset += child.margin.top
childYOffset += child.size.y
childYOffset += child.margin.bottom
childYOffset += spacing
}
return maxZ
@ -125,6 +133,9 @@ class RowLayout(
if (addY(child.margin.bottom)) {
break
}
if (addY(spacing)) { // ToDo: Only add of not the last element
break
}
}
this.size = size

View File

@ -24,6 +24,7 @@ class GridCell(
private val columnConstraint: GridColumnConstraint,
private val rowConstraint: GridRowConstraint,
private val child: Element,
override var parent: Element?,
) : Element(hudRenderer) {
override var prepared: Boolean by child::prepared
override var size: Vec2i by child::size

View File

@ -36,12 +36,7 @@ class GridLayout(hudRenderer: HUDRenderer, val grid: Vec2i) : Layout(hudRenderer
fun add(position: Vec2i, element: Element) {
children[position.x][position.y]?.parent = null
val cell = GridCell(hudRenderer, columnConstraints[position.x], rowConstraints[position.y], element)
cell.parent = this
// Apply new maxSize
element.apply()
element.onParentChange()
val cell = GridCell(hudRenderer, columnConstraints[position.x], rowConstraints[position.y], element, this)
children[position.x][position.y] = cell
@ -110,6 +105,9 @@ class GridLayout(hudRenderer: HUDRenderer, val grid: Vec2i) : Layout(hudRenderer
}
}
size = Vec2i(width.sum(), 0)
// apply the size changes to all children
applyOnlyChildren()
@ -123,8 +121,6 @@ class GridLayout(hudRenderer: HUDRenderer, val grid: Vec2i) : Layout(hudRenderer
columnStart[x] = offset + previousWidth
}
this.columnStart = columnStart
size = Vec2i(width.sum(), 0)
}
override fun apply() {
@ -138,7 +134,7 @@ class GridLayout(hudRenderer: HUDRenderer, val grid: Vec2i) : Layout(hudRenderer
for (x in 0 until grid.x) {
for (y in 0 until grid.y) {
val child = children[x][y] ?: continue
val childZ = child.render(offset + margin.offset + Vec2i(columnStart[x], rowStart[y]) + columnConstraints[x].alignment.getOffset(columnConstraints[x].width, child.size.x), z, consumer)
val childZ = child.render(offset + margin.offset + Vec2i(columnStart[x] + columnConstraints[x].alignment.getOffset(columnConstraints[x].width, child.size.x), rowStart[y]), z, consumer)
if (childZ > maxZ) {
maxZ = childZ
}

View File

@ -30,6 +30,7 @@ open class TextElement(
text: Any,
override var fontAlignment: ElementAlignments = ElementAlignments.LEFT,
) : LabeledElement(hudRenderer) {
private var preparedSize = Vec2i.EMPTY
private var renderInfo = TextRenderInfo()
override var text: Any = text
@ -42,6 +43,9 @@ open class TextElement(
final override var textComponent: ChatComponent = ChatComponent.of("")
protected set(value) {
field = value
val prefSize = Vec2i.EMPTY
ChatComponentRenderer.render(Vec2i.EMPTY, Vec2i.EMPTY, prefSize, 0, InfiniteSizeElement(hudRenderer), fontAlignment, renderWindow, null, TextRenderInfo(), value)
this.prefSize = prefSize
apply()
}
@ -52,20 +56,16 @@ open class TextElement(
}
override fun silentApply() {
val text = textComponent
size = Vec2i.EMPTY
if (text.message.isNotEmpty()) {
if (textComponent.message.isNotEmpty()) {
val size = Vec2i.EMPTY
val renderInfo = TextRenderInfo()
ChatComponentRenderer.render(Vec2i.EMPTY, Vec2i.EMPTY, size, 0, this, fontAlignment, renderWindow, null, renderInfo, text)
val prefSize = Vec2i.EMPTY
ChatComponentRenderer.render(Vec2i.EMPTY, Vec2i.EMPTY, prefSize, 0, InfiniteSizeElement(hudRenderer), fontAlignment, renderWindow, null, TextRenderInfo(), text)
this.prefSize = prefSize
ChatComponentRenderer.render(Vec2i.EMPTY, Vec2i.EMPTY, size, 0, this, fontAlignment, renderWindow, null, renderInfo, textComponent)
// ToDo: Set prefSize
renderInfo.currentLine = 0
this.renderInfo = renderInfo
this.size = size
preparedSize = size
}
}
@ -77,13 +77,13 @@ open class TextElement(
override fun onChildChange(child: Element?) = error("A TextElement can not have a child!")
override fun onParentChange() {
val size = Vec2i(size)
val maxSize = maxSize
val prefSize = prefSize
if (size.x > maxSize.x) {
if (preparedSize.x < prefSize.x || preparedSize.x > maxSize.x) {
return silentApply()
}
if (size.y > maxSize.y) {
if (preparedSize.y < prefSize.y || preparedSize.y > maxSize.y) {
return silentApply()
}
}

View File

@ -17,6 +17,7 @@ import de.bixilon.minosoft.data.text.ChatColors
import de.bixilon.minosoft.data.text.TextComponent
import de.bixilon.minosoft.gui.rendering.RenderWindow
import de.bixilon.minosoft.gui.rendering.block.WorldRenderer
import de.bixilon.minosoft.gui.rendering.font.Font
import de.bixilon.minosoft.gui.rendering.gui.elements.Element
import de.bixilon.minosoft.gui.rendering.gui.elements.ElementAlignments
import de.bixilon.minosoft.gui.rendering.gui.elements.layout.RowLayout
@ -33,6 +34,7 @@ import de.bixilon.minosoft.terminal.RunConfiguration
import de.bixilon.minosoft.util.KUtil.format
import de.bixilon.minosoft.util.MMath.round10
import glm_.vec2.Vec2i
import glm_.vec4.Vec4i
class DebugHUD(val hudRenderer: HUDRenderer) : HUD<GridLayout> {
override val renderWindow: RenderWindow = hudRenderer.renderWindow
@ -55,8 +57,8 @@ class DebugHUD(val hudRenderer: HUDRenderer) : HUD<GridLayout> {
}
private fun initLeft(): Element {
val layout = RowLayout(hudRenderer)
// ToDo: layout.margin = Vec4i(5)
val layout = RowLayout(hudRenderer, spacing = Font.VERTICAL_SPACING)
layout.margin = Vec4i(5)
layout += TextElement(hudRenderer, TextComponent(RunConfiguration.VERSION_STRING, ChatColors.RED))
layout += AutoTextElement(hudRenderer, 1) { "FPS ${renderWindow.renderStats.smoothAvgFPS.round10}" }
renderWindow[WorldRenderer]?.apply {
@ -80,9 +82,9 @@ class DebugHUD(val hudRenderer: HUDRenderer) : HUD<GridLayout> {
}
private fun initRight(): Element {
val layout = RowLayout(hudRenderer, ElementAlignments.RIGHT)
// ToDo: layout.margin = Vec4i(5)
layout += TextElement(hudRenderer, "Java\n${Runtime.version()} ${System.getProperty("sun.arch.data.model")}bit", ElementAlignments.RIGHT) // ToDo: Remove \n
val layout = RowLayout(hudRenderer, ElementAlignments.RIGHT, Font.VERTICAL_SPACING)
layout.margin = Vec4i(5)
layout += TextElement(hudRenderer, "Java ${Runtime.version()} ${System.getProperty("sun.arch.data.model")}bit", ElementAlignments.RIGHT)
layout += LineSpacerElement(hudRenderer)
@ -92,8 +94,8 @@ class DebugHUD(val hudRenderer: HUDRenderer) : HUD<GridLayout> {
})
}
renderWindow.renderSystem.apply {
layout += TextElement(hudRenderer, "GPU $vendorString")
layout += TextElement(hudRenderer, "Version $version")
layout += TextElement(hudRenderer, "GPU $vendorString", ElementAlignments.RIGHT)
layout += TextElement(hudRenderer, "Version $version", ElementAlignments.RIGHT)
}
return layout
}

View File

@ -28,7 +28,7 @@ import glm_.vec4.Vec4
class GUIMesh(
renderWindow: RenderWindow,
val matrix: Mat4,
) : Mesh(renderWindow, HUDMeshStruct), GUIVertexConsumer {
) : Mesh(renderWindow, HUDMeshStruct, initialCacheSize = 40000), GUIVertexConsumer {
override fun addVertex(position: Vec2t<*>, z: Int, texture: AbstractTexture, uv: Vec2, tint: RGBColor) {
val outPosition = matrix * Vec4(position.x.toFloat(), position.y.toFloat(), 1.0f, 1.0f)

View File

@ -63,7 +63,7 @@ interface BaseWindow {
val DEFAULT_WINDOW_SIZE: Vec2i
get() = Vec2i(900, 500)
val DEFAULT_MINIMUM_WINDOW_SIZE: Vec2i
get() = Vec2i(100, 100)
get() = Vec2i(300, 100)
val DEFAULT_MAXIMUM_WINDOW_SIZE: Vec2i
get() = Vec2i(-1, -1)
}