diff semiconginev2/text/textbox.nim @ 1234:841e12f33c47

add: text & font rendering, not tested yet
author sam <sam@basx.dev>
date Sat, 20 Jul 2024 00:03:57 +0700
parents
children 176383220123
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/semiconginev2/text/textbox.nim	Sat Jul 20 00:03:57 2024 +0700
@@ -0,0 +1,222 @@
+type
+  Textbox* = object
+    font*: Font
+    maxLen*: int                # maximum amount of characters that will be rendered
+    maxWidth: float32 = 0       # if set, will cause automatic word breaks at maxWidth
+                                # properties:
+    text: seq[Rune]
+    horizontalAlignment: HorizontalAlignment = Center
+    verticalAlignment: VerticalAlignment = Center
+    # management/internal:
+    dirtyGeometry: bool         # is true if any of the attributes changed
+    dirtyShaderdata: bool       # is true if any of the attributes changed
+    processedText: seq[Rune]    # used to store processed (word-wrapper) text to preserve original
+    lastRenderedText: seq[Rune] # stores the last rendered text, to prevent unnecessary updates
+
+    # rendering data
+    position: GPUArray[Vec3f, VertexBuffer]
+    uv: GPUArray[Vec2f, VertexBuffer]
+    indices: GPUArray[uint16, IndexBuffer]
+    shaderdata: DescriptorSet[TextboxDescriptorSet]
+
+func `$`*(text: Textbox): string =
+  "\"" & $text.text[0 ..< min(text.text.len, 16)] & "\""
+
+proc RefreshShaderdata(text: Textbox) =
+  if not text.dirtyShaderdata:
+    return
+  text.shaderdata.data.textbox.UpdateGPUBuffer()
+
+proc RefreshGeometry(text: var Textbox) =
+  if not text.dirtyGeometry and text.processedText == text.lastRenderedText:
+    return
+
+  # pre-calculate text-width
+  var width = 0'f32
+  var lineWidths: seq[float32]
+  for i in 0 ..< text.processedText.len:
+    if text.processedText[i] == NEWLINE:
+      lineWidths.add width
+      width = 0'f32
+    else:
+      if not (i == text.processedText.len - 1 and text.processedText[i].isWhiteSpace):
+        width += text.font.glyphs[text.processedText[i]].advance
+      if i < text.processedText.len - 1:
+        width += text.font.kerning[(text.processedText[i], text.processedText[i + 1])]
+  lineWidths.add width
+  var height = float32(lineWidths.len - 1) * text.font.lineAdvance + text.font.capHeight
+  if lineWidths[^1] == 0 and lineWidths.len > 1:
+    height -= 1
+
+  let anchorY = (case text.verticalAlignment
+    of Top: 0'f32
+    of Center: height / 2
+    of Bottom: height) - text.font.capHeight
+
+  var
+    offsetX = 0'f32
+    offsetY = 0'f32
+    lineIndex = 0
+    anchorX = case text.horizontalAlignment
+      of Left: 0'f32
+      of Center: lineWidths[lineIndex] / 2
+      of Right: lineWidths[lineIndex]
+  for i in 0 ..< text.maxLen:
+    let vertexOffset = i * 4
+    if i < text.processedText.len:
+      if text.processedText[i] == Rune('\n'):
+        offsetX = 0
+        offsetY += text.font.lineAdvance
+        text.position.data[vertexOffset + 0] = NewVec3f()
+        text.position.data[vertexOffset + 1] = NewVec3f()
+        text.position.data[vertexOffset + 2] = NewVec3f()
+        text.position.data[vertexOffset + 3] = NewVec3f()
+        inc lineIndex
+        anchorX = case text.horizontalAlignment
+          of Left: 0'f32
+          of Center: lineWidths[lineIndex] / 2
+          of Right: lineWidths[lineIndex]
+      else:
+        let
+          glyph = text.font.glyphs[text.processedText[i]]
+          left = offsetX + glyph.leftOffset
+          right = offsetX + glyph.leftOffset + glyph.dimension.x
+          top = offsetY + glyph.topOffset
+          bottom = offsetY + glyph.topOffset + glyph.dimension.y
+
+        text.position.data[vertexOffset + 0] = NewVec3f(left - anchorX, bottom - anchorY)
+        text.position.data[vertexOffset + 1] = NewVec3f(left - anchorX, top - anchorY)
+        text.position.data[vertexOffset + 2] = NewVec3f(right - anchorX, top - anchorY)
+        text.position.data[vertexOffset + 3] = NewVec3f(right - anchorX, bottom - anchorY)
+
+        text.uv.data[vertexOffset + 0] = glyph.uvs[0]
+        text.uv.data[vertexOffset + 1] = glyph.uvs[1]
+        text.uv.data[vertexOffset + 2] = glyph.uvs[2]
+        text.uv.data[vertexOffset + 3] = glyph.uvs[3]
+
+        offsetX += glyph.advance
+        if i < text.processedText.len - 1:
+          offsetX += text.font.kerning[(text.processedText[i], text.processedText[i + 1])]
+    else:
+      text.position.data[vertexOffset + 0] = NewVec3f()
+      text.position.data[vertexOffset + 1] = NewVec3f()
+      text.position.data[vertexOffset + 2] = NewVec3f()
+      text.position.data[vertexOffset + 3] = NewVec3f()
+  text.lastRenderedText = text.processedText
+  text.dirtyGeometry = false
+
+proc Refresh*(textbox: var Textbox) =
+  textbox.RefreshShaderdata()
+  textbox.RefreshGeometry()
+
+func text*(text: Textbox): seq[Rune] =
+  text.text
+
+proc `text=`*(text: var Textbox, newText: seq[Rune]) =
+  text.text = newText[0 ..< min(newText.len, text.maxLen)]
+
+  text.processedText = text.text
+  if text.maxWidth > 0:
+    text.processedText = WordWrapped(
+      text.processedText,
+      text.font[],
+      text.maxWidth / text.shaderdata.data.textbox.data.scale,
+    )
+
+proc `text=`*(text: var Textbox, newText: string) =
+  `text=`(text, newText.toRunes)
+
+proc Color*(text: Textbox): Vec4f =
+  text.shaderdata.data.textbox.data.color
+
+proc `Color=`*(text: var Textbox, value: Vec4f) =
+  if text.shaderdata.data.textbox.data.color != value:
+    text.dirtyShaderdata = true
+    text.shaderdata.data.textbox.data.color = value
+
+proc Scale*(text: Textbox): float32 =
+  text.shaderdata.data.textbox.data.scale
+
+proc `Scale=`*(text: var Textbox, value: float32) =
+  if text.shaderdata.data.textbox.data.scale != value:
+    text.dirtyShaderdata = true
+    text.shaderdata.data.textbox.data.scale = value
+
+proc Position*(text: Textbox): Vec3f =
+  text.shaderdata.data.textbox.data.position
+
+proc `Position=`*(text: var Textbox, value: Vec3f) =
+  if text.shaderdata.data.textbox.data.position != value:
+    text.dirtyShaderdata = true
+    text.shaderdata.data.textbox.data.position = value
+
+proc horizontalAlignment*(text: Textbox): HorizontalAlignment =
+  text.horizontalAlignment
+proc `horizontalAlignment=`*(text: var Textbox, value: HorizontalAlignment) =
+  if value != text.horizontalAlignment:
+    text.horizontalAlignment = value
+    text.dirtyGeometry = true
+
+proc verticalAlignment*(text: Textbox): VerticalAlignment =
+  text.verticalAlignment
+proc `verticalAlignment=`*(text: var Textbox, value: VerticalAlignment) =
+  if value != text.verticalAlignment:
+    text.verticalAlignment = value
+    text.dirtyGeometry = true
+
+proc Draw(text: Textbox, commandbuffer: VkCommandBuffer, pipeline: Pipeline, currentFiF: int) =
+  WithBind(commandbuffer, (textbox.shaderdata, ), pipeline, currentFiF):
+    Render(commandbuffer = commandbuffer, pipeline = pipeline, mesh = text)
+
+proc InitTextbox*(
+  renderdata: var RenderData,
+  descriptorSetLayout: VkDescriptorSetLayout,
+  font: Font,
+  text = "".toRunes,
+  scale: float32 = 1,
+  position: Vec3f = NewVec3f(),
+  color: Vec4f = NewVec4f(0, 0, 0, 1),
+  maxLen: int = text.len,
+  verticalAlignment: VerticalAlignment = Center,
+  horizontalAlignment: HorizontalAlignment = Center,
+  maxWidth = 0'f32
+): Textbox =
+
+  result = Textbox(
+    maxLen: maxLen,
+    font: font,
+    dirtyGeometry: true,
+    horizontalAlignment: horizontalAlignment,
+    verticalAlignment: verticalAlignment,
+    maxWidth: maxWidth,
+    position: asGPUArray(newSeq[Vec3f](int(maxLen * 4)), VertexBuffer),
+    uv: asGPUArray(newSeq[Vec2f](int(maxLen * 4)), VertexBuffer),
+    indices: asGPUArray(newSeq[uint16](int(maxLen * 6)), IndexBuffer),
+    shaderdata: asDescriptorSet(
+      TextboxDescriptorSet(
+        textbox: asGPUValue(TextboxData(
+          scale: scale,
+          position: position,
+          color: color,
+    ), UniformBufferMapped),
+    fontAtlas: font.fontAtlas
+  )
+    )
+  )
+
+  for i in 0 ..< maxLen:
+    let vertexIndex = i.uint16 * 4'u16
+    result.indices.data[i * 6 + 0] = vertexIndex + 0
+    result.indices.data[i * 6 + 1] = vertexIndex + 1
+    result.indices.data[i * 6 + 2] = vertexIndex + 2
+    result.indices.data[i * 6 + 3] = vertexIndex + 2
+    result.indices.data[i * 6 + 4] = vertexIndex + 3
+    result.indices.data[i * 6 + 5] = vertexIndex + 0
+
+  `text=`(result, text)
+
+  AssignBuffers(renderdata, result)
+  UploadImages(renderdata, result.shaderdata)
+  InitDescriptorSet(renderdata, descriptorSetLayout, result.shaderdata)
+
+  result.Refresh()