Mercurial > games > semicongine
view semicongine/text.nim @ 417:b032768df631
add: alignment for text boxes
author | Sam <sam@basx.dev> |
---|---|
date | Sun, 28 Jan 2024 00:41:11 +0700 |
parents | 73cca428e27a |
children | 009d93d69170 |
line wrap: on
line source
import std/tables # import std/sequtils import std/unicode import std/strformat import ./core import ./mesh import ./material import ./vulkan/shader const SHADER_ATTRIB_PREFIX = "semicon_text_" var instanceCounter = 0 type HorizontalAlignment = enum Left Center Right VerticalAlignment = enum Top Center Bottom Text* = object maxLen*: int text: seq[Rune] dirty: bool horizontalAlignment*: HorizontalAlignment = Center verticalAlignment*: VerticalAlignment = Center font*: Font mesh*: Mesh color*: Vec4f const NEWLINE = Rune('\n') POSITION_ATTRIB = SHADER_ATTRIB_PREFIX & "position" UV_ATTRIB = SHADER_ATTRIB_PREFIX & "uv" TEXT_MATERIAL_TYPE* = MaterialType( name: "default-text-material-type", vertexAttributes: {TRANSFORM_ATTRIB: Mat4F32, POSITION_ATTRIB: Vec3F32, UV_ATTRIB: Vec2F32}.toTable, attributes: {"fontAtlas": TextureType, "color": Vec4F32}.toTable, ) TEXT_SHADER* = createShaderConfiguration( inputs = [ attr[Mat4](TRANSFORM_ATTRIB, memoryPerformanceHint = PreferFastWrite, perInstance = true), attr[Vec3f](POSITION_ATTRIB, memoryPerformanceHint = PreferFastWrite), attr[Vec2f](UV_ATTRIB, memoryPerformanceHint = PreferFastWrite), ], intermediates = [attr[Vec2f]("uvFrag")], outputs = [attr[Vec4f]("color")], uniforms = [attr[Vec4f]("color")], samplers = [attr[Texture]("fontAtlas")], vertexCode = &"""gl_Position = vec4({POSITION_ATTRIB}, 1.0) * {TRANSFORM_ATTRIB}; uvFrag = {UV_ATTRIB};""", fragmentCode = &"""color = vec4(Uniforms.color.rgb, Uniforms.color.a * texture(fontAtlas, uvFrag).r);""" ) proc updateMesh(textbox: var Text) = # pre-calculate text-width var width = 0'f32 var lineWidths: seq[float32] for i in 0 ..< min(textbox.text.len, textbox.maxLen): if textbox.text[i] == NEWLINE: lineWidths.add width width = 0'f32 else: width += textbox.font.glyphs[textbox.text[i]].advance if i < textbox.text.len - 1: width += textbox.font.kerning[(textbox.text[i], textbox.text[i + 1])] lineWidths.add width let height = float32(lineWidths.len) * textbox.font.lineAdvance let anchorY = (case textbox.verticalAlignment of Top: 0'f32 of Center: height / 2 of Bottom: height) - textbox.font.lineAdvance var offsetX = 0'f32 offsetY = 0'f32 lineIndex = 0 anchorX = case textbox.horizontalAlignment of Left: 0'f32 of Center: lineWidths[lineIndex] / 2 of Right: lineWidths.max for i in 0 ..< textbox.maxLen: let vertexOffset = i * 4 if i < textbox.text.len: if textbox.text[i] == Rune('\n'): offsetX = 0 offsetY += textbox.font.lineAdvance textbox.mesh[POSITION_ATTRIB, vertexOffset + 0] = newVec3f() textbox.mesh[POSITION_ATTRIB, vertexOffset + 1] = newVec3f() textbox.mesh[POSITION_ATTRIB, vertexOffset + 2] = newVec3f() textbox.mesh[POSITION_ATTRIB, vertexOffset + 3] = newVec3f() inc lineIndex anchorX = case textbox.horizontalAlignment of Left: 0'f32 of Center: lineWidths[lineIndex] / 2 of Right: lineWidths.max else: let glyph = textbox.font.glyphs[textbox.text[i]] left = offsetX + glyph.leftOffset right = offsetX + glyph.leftOffset + glyph.dimension.x top = offsetY + glyph.topOffset bottom = offsetY + glyph.topOffset + glyph.dimension.y textbox.mesh[POSITION_ATTRIB, vertexOffset + 0] = newVec3f(left - anchorX, bottom - anchorY) textbox.mesh[POSITION_ATTRIB, vertexOffset + 1] = newVec3f(left - anchorX, top - anchorY) textbox.mesh[POSITION_ATTRIB, vertexOffset + 2] = newVec3f(right - anchorX, top - anchorY) textbox.mesh[POSITION_ATTRIB, vertexOffset + 3] = newVec3f(right - anchorX, bottom - anchorY) textbox.mesh[UV_ATTRIB, vertexOffset + 0] = glyph.uvs[0] textbox.mesh[UV_ATTRIB, vertexOffset + 1] = glyph.uvs[1] textbox.mesh[UV_ATTRIB, vertexOffset + 2] = glyph.uvs[2] textbox.mesh[UV_ATTRIB, vertexOffset + 3] = glyph.uvs[3] offsetX += glyph.advance if i < textbox.text.len - 1: offsetX += textbox.font.kerning[(textbox.text[i], textbox.text[i + 1])] else: textbox.mesh[POSITION_ATTRIB, vertexOffset + 0] = newVec3f() textbox.mesh[POSITION_ATTRIB, vertexOffset + 1] = newVec3f() textbox.mesh[POSITION_ATTRIB, vertexOffset + 2] = newVec3f() textbox.mesh[POSITION_ATTRIB, vertexOffset + 3] = newVec3f() func text*(textbox: Text): seq[Rune] = textbox.text proc `text=`*(textbox: var Text, text: seq[Rune]) = let newText = text[0 ..< min(text.len, textbox.maxLen)] if textbox.text != newText: textbox.text = newText textbox.updateMesh() proc `text=`*(textbox: var Text, text: string) = `text=`(textbox, text.toRunes) proc horizontalAlignment*(textbox: Text): HorizontalAlignment = textbox.horizontalAlignment proc verticalAlignment*(textbox: Text): VerticalAlignment = textbox.verticalAlignment proc `horizontalAlignment=`*(textbox: var Text, value: HorizontalAlignment) = if value != textbox.horizontalAlignment: textbox.horizontalAlignment = value textbox.updateMesh() proc `verticalAlignment=`*(textbox: var Text, value: VerticalAlignment) = if value != textbox.verticalAlignment : textbox.verticalAlignment = value textbox.updateMesh() proc initText*(maxLen: int, font: Font, text = "".toRunes, color = newVec4f(0, 0, 0, 1)): Text = var positions = newSeq[Vec3f](int(maxLen * 4)) indices: seq[array[3, uint16]] uvs = newSeq[Vec2f](int(maxLen * 4)) for i in 0 ..< maxLen: let offset = i * 4 indices.add [ [uint16(offset + 0), uint16(offset + 1), uint16(offset + 2)], [uint16(offset + 2), uint16(offset + 3), uint16(offset + 0)], ] result = Text(maxLen: maxLen, text: text, font: font, dirty: true) result.mesh = newMesh(positions = positions, indices = indices, uvs = uvs, name = &"textbox-{instanceCounter}") inc instanceCounter result.mesh[].renameAttribute("position", POSITION_ATTRIB) result.mesh[].renameAttribute("uv", UV_ATTRIB) result.mesh.material = initMaterialData( theType = TEXT_MATERIAL_TYPE, name = font.name & " text", attributes = {"fontAtlas": initDataList(@[font.fontAtlas]), "color": initDataList(@[color])}, ) result.updateMesh() proc initText*(maxLen: int, font: Font, text = "", color = newVec4f(0, 0, 0, 1)): Text = initText(maxLen = maxLen, font = font, text = text.toRunes, color = color)