view semicongine/text.nim @ 1430:db3af8a0b86b

add: lerp
author sam <sam@basx.dev>
date Sun, 19 Jan 2025 21:31:38 +0700
parents ab01c577d91c
children
line wrap: on
line source

import std/algorithm
import std/os
import std/sequtils
import std/strformat
import std/strutils
import std/tables
import std/unicode

import ./core
import ./rendering
import ./images
import ./rendering/renderer
import ./rendering/memory

proc initTextBuffer*[MaxGlyphs: static int](
    font: Font[MaxGlyphs],
    bufferSize: int,
    renderdata: var RenderData,
    baseScale = 1'f32,
): TextBuffer[MaxGlyphs] =
  result.cursor = 0
  result.font = font
  result.baseScale = baseScale
  result.position.data.setLen(bufferSize)
  result.scale.data.setLen(bufferSize)
  result.color.data.setLen(bufferSize)
  result.glyphIndex.data.setLen(bufferSize)
  assignBuffers(renderdata, result)

iterator splitLines(text: seq[Rune]): seq[Rune] =
  var current = newSeq[Rune]()
  for c in text:
    if c == Rune('\n'):
      yield current
      current = newSeq[Rune]()
    else:
      current.add c
  yield current

proc width*(font: Font, text: seq[Rune]): float32 =
  for i in 0 ..< text.len:
    if not (i == text.len - 1 and text[i].isWhiteSpace):
      if text[i] in font.advance:
        result += font.advance[text[i]]
      else:
        result += font.advance[font.fallbackCharacter]
    if i < text.len - 1:
      result += font.kerning.getOrDefault((text[i], text[i + 1]), 0)
  return result

proc width*(font: Font, text: string): float32 =
  width(font, text.toRunes)

proc textDimension*(font: Font, text: seq[Rune]): Vec2f =
  let nLines = text.countIt(it == Rune('\n')).float32
  let h = (nLines * font.lineAdvance + font.lineHeight)
  let w = max(splitLines(text).toSeq.mapIt(width(font, it)))
  return vec2(w, h)

proc textDimension*(font: Font, text: string): Vec2f =
  textDimension(font, text.toRunes())

proc textDimension*(textBuffer: TextBuffer, text: string | seq[Rune]): Vec2f =
  textDimension(textBuffer.font, text) * textBuffer.baseScale

proc updateGlyphData*(textbuffer: var TextBuffer, textHandle: TextHandle) =
  assert textHandle.generation == textbuffer.generation

  let
    textI = textHandle.index
    text = textbuffer.texts[textI].text
    position = textbuffer.texts[textI].position
    anchor = textbuffer.texts[textI].anchor

    globalScale = textbuffer.texts[textI].scale * textbuffer.baseScale
    box = textbuffer.textDimension(text) * textbuffer.texts[textI].scale
    xH = textbuffer.font.xHeight * globalScale
    aratio = getAspectRatio()
    origin = vec3(
      position.x - (anchor.x * 0.5 + 0.5) * box.x / aratio,
      position.y + (anchor.y * -0.5 + 0.5) * box.y - xH * 0.5 -
        textbuffer.font.lineHeight * globalScale * 0.5,
      position.z,
    )
    lineWidths = splitLines(text).toSeq.mapIt(width(textbuffer.font, it) * globalScale)
    maxWidth = box.x

  template leftBearing(r: Rune): untyped =
    textbuffer.font.leftBearing.getOrDefault(r, 0) * globalScale

  var
    cursorPos = origin
    lineI = 0

  case textbuffer.texts[textI].alignment
  of Left:
    cursorPos.x = origin.x
  of Center:
    cursorPos.x = origin.x + ((maxWidth - lineWidths[lineI]) / aratio * 0.5)
  of Right:
    cursorPos.x = origin.x + (maxWidth - lineWidths[lineI]) / aratio

  # add left bearing for first character at line start
  if text.len > 0:
    cursorPos.x = cursorPos.x - leftBearing(text[0])

  var bufferOffset = textbuffer.texts[textI].bufferOffset
  let bufferEnd =
    textbuffer.texts[textI].bufferOffset + textbuffer.texts[textI].capacity
  var i = 0
  while i < textbuffer.texts[textI].capacity and bufferOffset < bufferEnd:
    # for i in 0 ..< textbuffer.texts[textI].capacity:
    if i < text.len:
      if text[i] == Rune('\n'):
        inc lineI
        case textbuffer.texts[textI].alignment
        of Left:
          cursorPos.x = origin.x
        of Center:
          cursorPos.x = origin.x + ((maxWidth - lineWidths[lineI]) / aratio * 0.5)
        of Right:
          cursorPos.x = origin.x + (maxWidth - lineWidths[lineI]) / aratio

        # add left bearing for first character at line start
        if text.len > i + 1:
          cursorPos.x = cursorPos.x - leftBearing(text[i + 1])

        cursorPos.y = cursorPos.y - textbuffer.font.lineAdvance * globalScale
      else:
        if not text[i].isWhitespace():
          textbuffer.position[bufferOffset] = cursorPos
          textbuffer.scale[bufferOffset] = globalScale
          textbuffer.color[bufferOffset] = textbuffer.texts[textI].color
          if text[i] in textbuffer.font.descriptorGlyphIndex:
            textbuffer.glyphIndex[bufferOffset] =
              textbuffer.font.descriptorGlyphIndex[text[i]]
          else:
            textbuffer.glyphIndex[bufferOffset] =
              textbuffer.font.descriptorGlyphIndex[textbuffer.font.fallbackCharacter]
          # only use up buffer space when we actually draw a glyph i.e. whitespace is not using buffer space
          inc bufferOffset

        if text[i] in textbuffer.font.advance:
          cursorPos.x =
            cursorPos.x + textbuffer.font.advance[text[i]] * globalScale / aratio
        else:
          cursorPos.x =
            cursorPos.x +
            textbuffer.font.advance[textbuffer.font.fallbackCharacter] * globalScale /
            aratio

        if i < text.len - 1:
          cursorPos.x =
            cursorPos.x +
            textbuffer.font.kerning.getOrDefault((text[i], text[i + 1]), 0) * globalScale /
            aratio
    if i >= text.len or text[i].isWhiteSpace():
      textbuffer.position[bufferOffset] = vec3()
      textbuffer.scale[bufferOffset] = 0
      textbuffer.color[bufferOffset] = vec4()
      textbuffer.glyphIndex[bufferOffset] = 0
      inc bufferOffset
    inc i

proc updateGlyphData*(textbuffer: var TextBuffer) =
  for i in 0 ..< textbuffer.texts.len:
    textbuffer.updateGlyphData(
      TextHandle(index: uint32(i), generation: textbuffer.generation)
    )

proc reset*(textbuffer: var TextBuffer) =
  inc textbuffer.generation # integer overflow *should* be okay here
  textbuffer.cursor = 0
  textbuffer.texts.setLen(0)

proc refresh*(textbuffer: var TextBuffer, flush = false) =
  textbuffer.updateGlyphData()
  textbuffer.updateAllGPUBuffers(flush = flush)

proc add*(
    textbuffer: var TextBuffer,
    text: seq[Rune],
    position: Vec3f,
    alignment: TextAlignment = Left,
    anchor: Vec2f = vec2(0, 0),
    scale: float32 = 1'f32,
    color: Vec4f = vec4(1, 1, 1, 1),
    capacity: int = 0,
): TextHandle =
  ## This should be called again after aspect ratio of window changes 

  let cap = if capacity == 0: text.len else: capacity
  let l = textbuffer.position.len
  assert textbuffer.cursor + cap <= l,
    &"Text is too big for TextBuffer ({l - textbuffer.cursor} left, but need {cap})"

  result =
    TextHandle(generation: textbuffer.generation, index: textbuffer.texts.len.uint32)

  textbuffer.texts.add Text(
    bufferOffset: textbuffer.cursor,
    text: text,
    position: position,
    alignment: alignment,
    anchor: anchor,
    scale: scale,
    color: color,
    capacity: cap,
  )
  textbuffer.cursor += cap

proc add*(
    textbuffer: var TextBuffer,
    text: string,
    position: Vec3f,
    alignment: TextAlignment = Left,
    anchor: Vec2f = vec2(0, 0),
    scale: float32 = 1'f32,
    color: Vec4f = vec4(1, 1, 1, 1),
    capacity: int = 0,
): TextHandle =
  add(textbuffer, text.toRunes, position, alignment, anchor, scale, color, capacity)

proc text*(textbuffer: var TextBuffer, textHandle: TextHandle, text: seq[Rune]) =
  assert textHandle.generation == textbuffer.generation
  if text.len <= textbuffer.texts[textHandle.index].capacity:
    textbuffer.texts[textHandle.index].text = text
  else:
    textbuffer.texts[textHandle.index].text =
      text[0 ..< textbuffer.texts[textHandle.index].capacity]

proc text*(textbuffer: var TextBuffer, textHandle: TextHandle, text: string) =
  assert textHandle.generation == textbuffer.generation
  text(textbuffer, textHandle, text.toRunes)

proc position*(textbuffer: var TextBuffer, textHandle: TextHandle, position: Vec3f) =
  assert textHandle.generation == textbuffer.generation
  textbuffer.texts[textHandle.index].position = position

proc alignment*(
    textbuffer: var TextBuffer, textHandle: TextHandle, alignment: TextAlignment
) =
  assert textHandle.generation == textbuffer.generation
  textbuffer.texts[textHandle.index].alignment = alignment

proc anchor*(textbuffer: var TextBuffer, textHandle: TextHandle, anchor: Vec2f) =
  assert textHandle.generation == textbuffer.generation
  textbuffer.texts[textHandle.index].anchor = anchor

proc scale*(textbuffer: var TextBuffer, textHandle: TextHandle, scale: float32) =
  assert textHandle.generation == textbuffer.generation
  textbuffer.texts[textHandle.index].scale = scale

proc color*(textbuffer: var TextBuffer, textHandle: TextHandle, color: Vec4f) =
  assert textHandle.generation == textbuffer.generation
  textbuffer.texts[textHandle.index].color = color

type EMPTY = object
const EMPTYOBJECT = EMPTY()

proc renderTextBuffer*(
    commandBuffer: VkCommandBuffer, pipeline: Pipeline, textbuffer: TextBuffer
) =
  renderWithPushConstant(
    commandbuffer,
    pipeline,
    EMPTYOBJECT,
    textbuffer,
    pushConstant = TextRendering(aspectRatio: getAspectRatio()),
    fixedVertexCount = 6,
    fixedInstanceCount = textbuffer.cursor,
  )