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)