mirror of
https://github.com/odin-lang/Odin.git
synced 2026-06-14 14:23:43 +00:00
1190 lines
27 KiB
Odin
1190 lines
27 KiB
Odin
// An Odin-native source port of [[ Fontstash ; https://github.com/memononen/fontstash ]].
|
|
#+feature using-stmt
|
|
package fontstash
|
|
|
|
import "base:runtime"
|
|
|
|
import "core:log"
|
|
import "core:mem"
|
|
import "core:math"
|
|
import "core:strings"
|
|
import "core:slice"
|
|
|
|
import stbtt "vendor:stb/truetype"
|
|
|
|
// This is a port from Fontstash into odin - specialized for nanovg
|
|
|
|
// Notable features of Fontstash:
|
|
// Contains a *single* channel texture atlas for multiple fonts
|
|
// Manages a lookup table for frequent glyphs
|
|
// Allows blurred font glyphs
|
|
// Atlas can resize
|
|
|
|
// Changes from the original:
|
|
// stb truetype only
|
|
// no scratch allocation -> parts use odins dynamic arrays
|
|
// leaves GPU vertex creation & texture management up to the user
|
|
// texture atlas expands by default
|
|
|
|
INVALID :: -1
|
|
MAX_STATES :: 20
|
|
HASH_LUT_SIZE :: 256
|
|
INIT_GLYPHS :: 256
|
|
INIT_ATLAS_NODES :: 256
|
|
MAX_FALLBACKS :: 20
|
|
Glyph_Index :: i32 // in case you want to change the handle for glyph indices
|
|
|
|
AlignHorizontal :: enum {
|
|
LEFT,
|
|
CENTER,
|
|
RIGHT,
|
|
}
|
|
|
|
AlignVertical :: enum {
|
|
TOP,
|
|
MIDDLE,
|
|
BOTTOM,
|
|
BASELINE,
|
|
}
|
|
|
|
Font :: struct {
|
|
name: string, // allocated
|
|
|
|
info: stbtt.fontinfo,
|
|
loadedData: []byte,
|
|
freeLoadedData: bool, // in case you dont want loadedData to be removed
|
|
|
|
ascender: f32,
|
|
descender: f32,
|
|
lineHeight: f32,
|
|
|
|
glyphs: [dynamic]Glyph,
|
|
lut: [HASH_LUT_SIZE]int,
|
|
|
|
fallbacks: [MAX_FALLBACKS]int,
|
|
nfallbacks: int,
|
|
}
|
|
|
|
Glyph :: struct {
|
|
codepoint: rune,
|
|
index: Glyph_Index,
|
|
next: int,
|
|
isize: i16,
|
|
blurSize: i16,
|
|
x0, y0, x1, y1: i16,
|
|
xoff, yoff: i16,
|
|
xadvance: i16,
|
|
}
|
|
|
|
AtlasNode :: struct {
|
|
x, y, width: i16,
|
|
}
|
|
|
|
Vertex :: struct #packed {
|
|
x, y: f32,
|
|
u, v: f32,
|
|
color: [4]u8,
|
|
}
|
|
|
|
QuadLocation :: enum {
|
|
TOPLEFT,
|
|
BOTTOMLEFT,
|
|
}
|
|
|
|
FontContext :: struct {
|
|
fonts: [dynamic]Font, // allocated using context.allocator
|
|
|
|
// always assuming user wants to resize
|
|
nodes: [dynamic]AtlasNode,
|
|
|
|
// actual pixels
|
|
textureData: []byte, // allocated using context.allocator
|
|
width, height: int,
|
|
// 1 / texture_atlas_width, 1 / texture_atlas_height
|
|
itw, ith: f32,
|
|
|
|
// state
|
|
states: []State,
|
|
state_count: int, // used states
|
|
|
|
location: QuadLocation,
|
|
|
|
// dirty rectangle of the texture region that was updated
|
|
dirtyRect: [4]f32,
|
|
|
|
// callbacks with userData passed
|
|
userData: rawptr, // by default set to the context
|
|
|
|
// called when a texture is expanded and needs handling
|
|
callbackResize: proc(data: rawptr, w, h: int),
|
|
// called in state_end to update the texture region that changed
|
|
callbackUpdate: proc(data: rawptr, dirtyRect: [4]f32, textureData: rawptr),
|
|
}
|
|
|
|
Init :: proc(ctx: ^FontContext, w, h: int, loc: QuadLocation) {
|
|
ctx.userData = ctx
|
|
ctx.location = loc
|
|
ctx.fonts = make([dynamic]Font, 0, 8)
|
|
|
|
ctx.itw, ctx.ith = 1.0 / f32(w), 1.0 / f32(h)
|
|
|
|
ctx.textureData = make([]byte, w * h)
|
|
|
|
ctx.width = w
|
|
ctx.height = h
|
|
ctx.nodes = make([dynamic]AtlasNode, 0, INIT_ATLAS_NODES)
|
|
__dirtyRectReset(ctx)
|
|
|
|
ctx.states = make([]State, MAX_STATES)
|
|
|
|
// NOTE NECESSARY
|
|
append(&ctx.nodes, AtlasNode{
|
|
width = i16(w),
|
|
})
|
|
|
|
__AtlasAddWhiteRect(ctx, 2, 2)
|
|
|
|
PushState(ctx)
|
|
ClearState(ctx)
|
|
}
|
|
|
|
Destroy :: proc(ctx: ^FontContext) {
|
|
for font in ctx.fonts {
|
|
if font.freeLoadedData {
|
|
delete(font.loadedData)
|
|
}
|
|
|
|
delete(font.name)
|
|
delete(font.glyphs)
|
|
}
|
|
|
|
delete(ctx.states)
|
|
delete(ctx.textureData)
|
|
delete(ctx.fonts)
|
|
delete(ctx.nodes)
|
|
}
|
|
|
|
Reset :: proc(ctx: ^FontContext) {
|
|
__atlasReset(ctx, ctx.width, ctx.height)
|
|
__dirtyRectReset(ctx)
|
|
slice.zero(ctx.textureData)
|
|
|
|
for &font in ctx.fonts {
|
|
__lutReset(&font)
|
|
}
|
|
|
|
__AtlasAddWhiteRect(ctx, 2, 2)
|
|
PushState(ctx)
|
|
ClearState(ctx)
|
|
}
|
|
|
|
__atlasInsertNode :: proc(ctx: ^FontContext, idx: int, x, y, w: int) {
|
|
inject_at(&ctx.nodes, idx, AtlasNode{
|
|
x = i16(x),
|
|
y = i16(y),
|
|
width = i16(w),
|
|
})
|
|
}
|
|
|
|
__atlasRemoveNode :: proc(ctx: ^FontContext, idx: int) {
|
|
if len(ctx.nodes) == 0 {
|
|
return
|
|
}
|
|
|
|
ordered_remove(&ctx.nodes, idx)
|
|
}
|
|
|
|
__atlasExpand :: proc(ctx: ^FontContext, w, h: int) {
|
|
if w > ctx.width {
|
|
__atlasInsertNode(ctx, len(ctx.nodes), ctx.width, 0, w - ctx.width)
|
|
}
|
|
|
|
ctx.width, ctx.height = w, h
|
|
}
|
|
|
|
__atlasReset :: proc(ctx: ^FontContext, w, h: int) {
|
|
ctx.width, ctx.height = w, h
|
|
clear(&ctx.nodes)
|
|
|
|
// init root node
|
|
append(&ctx.nodes, AtlasNode{
|
|
width = i16(w),
|
|
})
|
|
}
|
|
|
|
__AtlasAddSkylineLevel :: proc(using ctx: ^FontContext, idx: int, x, y, w, h: int) {
|
|
// insert new node
|
|
__atlasInsertNode(ctx, idx, x, y + h, w)
|
|
|
|
// Delete skyline segments that fall under the shadow of the new segment.
|
|
for i := idx + 1; i < len(nodes); i += 1 {
|
|
if nodes[i].x >= nodes[i-1].x + nodes[i-1].width {
|
|
break
|
|
}
|
|
shrink := nodes[i-1].x + nodes[i-1].width - nodes[i].x
|
|
nodes[i].x += i16(shrink)
|
|
nodes[i].width -= i16(shrink)
|
|
|
|
if nodes[i].width > 0 {
|
|
break
|
|
}
|
|
__atlasRemoveNode(ctx, i)
|
|
i -= 1
|
|
}
|
|
|
|
// Merge same height skyline segments that are next to each other.
|
|
for i := 0; i < len(nodes) - 1; /**/ {
|
|
if nodes[i].y == nodes[i+1].y {
|
|
nodes[i].width += nodes[i+1].width
|
|
__atlasRemoveNode(ctx, i+1)
|
|
} else {
|
|
i += 1
|
|
}
|
|
}
|
|
}
|
|
|
|
__AtlasRectFits :: proc(using ctx: ^FontContext, i, w, h: int) -> int {
|
|
// Checks if there is enough space at the location of skyline span 'i',
|
|
// and return the max height of all skyline spans under that at that location,
|
|
// (think tetris block being dropped at that position). Or -1 if no space found.
|
|
|
|
i := i
|
|
x, y := int(nodes[i].x), int(nodes[i].y)
|
|
|
|
if x + w > width {
|
|
return -1
|
|
}
|
|
|
|
space_left := w
|
|
for space_left > 0 {
|
|
if i == len(nodes) {
|
|
return -1
|
|
}
|
|
|
|
y = max(y, int(nodes[i].y))
|
|
if y + h > height {
|
|
return -1
|
|
}
|
|
|
|
space_left -= int(nodes[i].width)
|
|
i += 1
|
|
}
|
|
|
|
return y
|
|
}
|
|
|
|
__AtlasAddRect :: proc(using ctx: ^FontContext, rw, rh: int) -> (rx, ry: int, ok: bool) {
|
|
bestw, besth := width, height
|
|
besti, bestx, besty := -1, -1, -1
|
|
|
|
// Bottom left fit heuristic.
|
|
for i in 0..<len(nodes) {
|
|
y := __AtlasRectFits(ctx, i, rw, rh)
|
|
|
|
if y != -1 {
|
|
if y + rh < besth || (y + rh == besth && int(nodes[i].width) < bestw) {
|
|
besti = i
|
|
bestw = int(nodes[i].width)
|
|
besth = y + rh
|
|
bestx = int(nodes[i].x)
|
|
besty = y
|
|
}
|
|
}
|
|
}
|
|
|
|
if besti == -1 {
|
|
return
|
|
}
|
|
|
|
// Perform the actual packing.
|
|
__AtlasAddSkylineLevel(ctx, besti, bestx, besty, rw, rh)
|
|
return bestx, besty, true
|
|
}
|
|
|
|
__AtlasAddWhiteRect :: proc(ctx: ^FontContext, w, h: int) -> bool {
|
|
gx, gy := __AtlasAddRect(ctx, w, h) or_return
|
|
|
|
// Rasterize
|
|
dst := ctx.textureData[gx + gy * ctx.width:]
|
|
for _ in 0..<h {
|
|
for x in 0..<w {
|
|
dst[x] = 0xff
|
|
}
|
|
|
|
dst = dst[ctx.width:]
|
|
}
|
|
|
|
ctx.dirtyRect[0] = f32(min(int(ctx.dirtyRect[0]), gx))
|
|
ctx.dirtyRect[1] = f32(min(int(ctx.dirtyRect[1]), gy))
|
|
ctx.dirtyRect[2] = f32(max(int(ctx.dirtyRect[2]), gx + w))
|
|
ctx.dirtyRect[3] = f32(max(int(ctx.dirtyRect[3]), gy + h))
|
|
|
|
return true
|
|
}
|
|
|
|
// push a font to the font stack
|
|
// optionally init with ascii characters at a wanted size
|
|
//
|
|
// 'fontIndex' controls which font you want to load within a multi-font format such
|
|
// as TTC. Leave it as zero if you are loading a single-font format such as TTF.
|
|
AddFontMem :: proc(
|
|
ctx: ^FontContext,
|
|
name: string,
|
|
data: []u8,
|
|
freeLoadedData: bool,
|
|
fontIndex: int = 0,
|
|
) -> int {
|
|
append(&ctx.fonts, Font{})
|
|
res := &ctx.fonts[len(ctx.fonts) - 1]
|
|
res.loadedData = data
|
|
res.freeLoadedData = freeLoadedData
|
|
res.name = strings.clone(name)
|
|
|
|
num_fonts := stbtt.GetNumberOfFonts(raw_data(data))
|
|
font_index_clamped := num_fonts > 0 ? clamp(i32(fontIndex), 0, num_fonts-1) : 0
|
|
font_offset := stbtt.GetFontOffsetForIndex(raw_data(data), font_index_clamped)
|
|
stbtt.InitFont(&res.info, raw_data(data), font_offset)
|
|
ascent, descent, line_gap: i32
|
|
|
|
stbtt.GetFontVMetrics(&res.info, &ascent, &descent, &line_gap)
|
|
fh := f32(ascent - descent)
|
|
res.ascender = f32(ascent) / fh
|
|
res.descender = f32(descent) / fh
|
|
res.lineHeight = (fh + f32(line_gap)) / fh
|
|
res.glyphs = make([dynamic]Glyph, 0, INIT_GLYPHS)
|
|
|
|
__lutReset(res)
|
|
return len(ctx.fonts) - 1
|
|
}
|
|
|
|
AddFont :: proc { AddFontPath, AddFontMem }
|
|
|
|
AddFallbackFont :: proc(ctx: ^FontContext, base, fallback: int) -> bool {
|
|
base_font := __getFont(ctx, base)
|
|
|
|
if base_font.nfallbacks < MAX_FALLBACKS {
|
|
base_font.fallbacks[base_font.nfallbacks] = fallback
|
|
base_font.nfallbacks += 1
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
ResetFallbackFont :: proc(ctx: ^FontContext, base: int) {
|
|
base_font := __getFont(ctx, base)
|
|
base_font.nfallbacks = 0
|
|
clear(&base_font.glyphs)
|
|
__lutReset(base_font)
|
|
}
|
|
|
|
// find font by name
|
|
GetFontByName :: proc(ctx: ^FontContext, name: string) -> int {
|
|
for font, i in ctx.fonts {
|
|
if font.name == name {
|
|
return i
|
|
}
|
|
}
|
|
|
|
return INVALID
|
|
}
|
|
|
|
__lutReset :: proc(font: ^Font) {
|
|
// set lookup table
|
|
slice.fill(font.lut[:], -1)
|
|
}
|
|
|
|
__hashint :: proc(a: u32) -> u32 {
|
|
a := a
|
|
a += ~(a << 15)
|
|
a ~= (a >> 10)
|
|
a += (a << 3)
|
|
a ~= (a >> 6)
|
|
a += (a << 11)
|
|
a ~= (a >> 16)
|
|
return a
|
|
}
|
|
|
|
__renderGlyphBitmap :: proc(
|
|
font: ^Font,
|
|
output: []u8,
|
|
outWidth: i32,
|
|
outHeight: i32,
|
|
outStride: i32,
|
|
scaleX: f32,
|
|
scaleY: f32,
|
|
glyphIndex: Glyph_Index,
|
|
) {
|
|
stbtt.MakeGlyphBitmap(&font.info, raw_data(output), outWidth, outHeight, outStride, scaleX, scaleY, glyphIndex)
|
|
}
|
|
|
|
__buildGlyphBitmap :: proc(
|
|
font: ^Font,
|
|
glyphIndex: Glyph_Index,
|
|
pixelSize: f32,
|
|
scale: f32,
|
|
) -> (advance, lsb, x0, y0, x1, y1: i32) {
|
|
stbtt.GetGlyphHMetrics(&font.info, glyphIndex, &advance, &lsb)
|
|
stbtt.GetGlyphBitmapBox(&font.info, glyphIndex, scale, scale, &x0, &y0, &x1, &y1)
|
|
return
|
|
}
|
|
|
|
// get glyph and push to atlas if not exists
|
|
__getGlyph :: proc(
|
|
ctx: ^FontContext,
|
|
font: ^Font,
|
|
codepoint: rune,
|
|
isize: i16,
|
|
blur: i16 = 0,
|
|
) -> (res: ^Glyph, ok: bool) #no_bounds_check {
|
|
if isize < 2 {
|
|
return
|
|
}
|
|
|
|
// find code point and size
|
|
h := __hashint(u32(codepoint)) & (HASH_LUT_SIZE - 1)
|
|
for i := font.lut[h]; i != -1; /**/ {
|
|
glyph := &font.glyphs[i]
|
|
|
|
if glyph.codepoint == codepoint &&
|
|
glyph.isize == isize &&
|
|
glyph.blurSize == blur {
|
|
res = glyph
|
|
ok = true
|
|
return
|
|
}
|
|
|
|
i = glyph.next
|
|
}
|
|
|
|
// could not find glyph, create it.
|
|
render_font := font // font used to render
|
|
glyph_index := __getGlyph_index(font, codepoint)
|
|
if glyph_index == 0 {
|
|
// lookout for possible fallbacks
|
|
for i in 0..<font.nfallbacks {
|
|
fallback_font := __getFont(ctx, font.fallbacks[i])
|
|
fallback_index := __getGlyph_index(fallback_font, codepoint)
|
|
|
|
if fallback_index != 0 {
|
|
glyph_index = fallback_index
|
|
render_font = fallback_font
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
pixel_size := f32(isize) / 10
|
|
blurSize := min(blur, 20)
|
|
padding := i16(blurSize + 2) // 2 minimum padding
|
|
scale := __getPixelHeightScale(render_font, pixel_size)
|
|
advance, _, x0, y0, x1, y1 := __buildGlyphBitmap(render_font, glyph_index, pixel_size, scale)
|
|
gw := (x1 - x0) + i32(padding) * 2
|
|
gh := (y1 - y0) + i32(padding) * 2
|
|
|
|
// Find free spot for the rect in the atlas
|
|
gx, gy, rect_ok := __AtlasAddRect(ctx, int(gw), int(gh))
|
|
if !rect_ok {
|
|
// try again with expanded
|
|
ExpandAtlas(ctx, ctx.width * 2, ctx.height * 2)
|
|
gx, gy = __AtlasAddRect(ctx, int(gw), int(gh)) or_return
|
|
}
|
|
|
|
// Init glyph.
|
|
append(&font.glyphs, Glyph{
|
|
codepoint = codepoint,
|
|
isize = isize,
|
|
blurSize = blurSize,
|
|
index = glyph_index,
|
|
x0 = i16(gx),
|
|
y0 = i16(gy),
|
|
x1 = i16(i32(gx) + gw),
|
|
y1 = i16(i32(gy) + gh),
|
|
xadvance = i16(scale * f32(advance) * 10),
|
|
xoff = i16(x0 - i32(padding)),
|
|
yoff = i16(y0 - i32(padding)),
|
|
|
|
// insert char to hash lookup.
|
|
next = font.lut[h],
|
|
})
|
|
font.lut[h] = len(font.glyphs) - 1
|
|
res = &font.glyphs[len(font.glyphs) - 1]
|
|
|
|
// rasterize
|
|
dst := ctx.textureData[int(res.x0 + padding) + int(res.y0 + padding) * ctx.width:]
|
|
__renderGlyphBitmap(
|
|
render_font,
|
|
dst,
|
|
gw - i32(padding) * 2,
|
|
gh - i32(padding) * 2,
|
|
i32(ctx.width),
|
|
scale,
|
|
scale,
|
|
glyph_index,
|
|
)
|
|
|
|
// make sure there is one pixel empty border.
|
|
dst = ctx.textureData[int(res.x0) + int(res.y0) * ctx.width:]
|
|
// y direction
|
|
for y in 0..<int(gh) {
|
|
dst[y * ctx.width] = 0
|
|
dst[int(gw - 1) + y * ctx.width] = 0
|
|
}
|
|
// x direction
|
|
for x in 0..<int(gw) {
|
|
dst[x] = 0
|
|
dst[x + int(gh - 1) * ctx.width] = 0
|
|
}
|
|
|
|
if blurSize > 0 {
|
|
__blur(dst, int(gw), int(gh), ctx.width, blurSize)
|
|
}
|
|
|
|
ctx.dirtyRect[0] = f32(min(int(ctx.dirtyRect[0]), int(res.x0)))
|
|
ctx.dirtyRect[1] = f32(min(int(ctx.dirtyRect[1]), int(res.y0)))
|
|
ctx.dirtyRect[2] = f32(max(int(ctx.dirtyRect[2]), int(res.x1)))
|
|
ctx.dirtyRect[3] = f32(max(int(ctx.dirtyRect[3]), int(res.y1)))
|
|
|
|
ok = true
|
|
return
|
|
}
|
|
|
|
/////////////////////////////////
|
|
// blur
|
|
/////////////////////////////////
|
|
|
|
// Based on Exponential blur, Jani Huhtanen, 2006
|
|
|
|
BLUR_APREC :: 16
|
|
BLUR_ZPREC :: 7
|
|
|
|
__blurCols :: proc(dst: []u8, w, h, dstStride, alpha: int) {
|
|
dst := dst
|
|
|
|
for _ in 0..<h {
|
|
z := 0 // force zero border
|
|
|
|
for x in 1..<w {
|
|
z += (alpha * ((int(dst[x]) << BLUR_ZPREC) - z)) >> BLUR_APREC
|
|
dst[x] = u8(z >> BLUR_ZPREC)
|
|
}
|
|
|
|
dst[w - 1] = 0 // force zero border
|
|
z = 0
|
|
|
|
for x := w - 2; x >= 0; x -= 1 {
|
|
z += (alpha * ((int(dst[x]) << BLUR_ZPREC) - z)) >> BLUR_APREC
|
|
dst[x] = u8(z >> BLUR_ZPREC)
|
|
}
|
|
|
|
dst[0] = 0 // force zero border
|
|
dst = dst[dstStride:] // advance slice
|
|
}
|
|
}
|
|
|
|
__blurRows :: proc(dst: []u8, w, h, dstStride, alpha: int) {
|
|
dst := dst
|
|
|
|
for _ in 0..<w {
|
|
z := 0 // force zero border
|
|
for y := dstStride; y < h * dstStride; y += dstStride {
|
|
z += (alpha * ((int(dst[y]) << BLUR_ZPREC) - z)) >> BLUR_APREC
|
|
dst[y] = u8(z >> BLUR_ZPREC)
|
|
}
|
|
|
|
dst[(h - 1) * dstStride] = 0 // force zero border
|
|
z = 0
|
|
|
|
for y := (h - 2) * dstStride; y >= 0; y -= dstStride {
|
|
z += (alpha * ((int(dst[y]) << BLUR_ZPREC) - z)) >> BLUR_APREC
|
|
dst[y] = u8(z >> BLUR_ZPREC)
|
|
}
|
|
|
|
dst[0] = 0 // force zero border
|
|
dst = dst[1:] // advance
|
|
}
|
|
}
|
|
|
|
__blur :: proc(dst: []u8, w, h, dstStride: int, blurSize: i16) {
|
|
assert(blurSize > 0)
|
|
|
|
// Calculate the alpha such that 90% of the kernel is within the radius. (Kernel extends to infinity)
|
|
sigma := f32(blurSize) * 0.57735 // 1 / sqrt(3)
|
|
alpha := int((1 << BLUR_APREC) * (1 - math.exp(-2.3 / (sigma + 1))))
|
|
__blurRows(dst, w, h, dstStride, alpha)
|
|
__blurCols(dst, w, h, dstStride, alpha)
|
|
__blurRows(dst, w, h, dstStride, alpha)
|
|
__blurCols(dst, w, h, dstStride, alpha)
|
|
}
|
|
|
|
/////////////////////////////////
|
|
// Texture expansion
|
|
/////////////////////////////////
|
|
|
|
ExpandAtlas :: proc(ctx: ^FontContext, width, height: int, allocator := context.allocator) -> bool {
|
|
w := max(ctx.width, width)
|
|
h := max(ctx.height, height)
|
|
|
|
if w == ctx.width && h == ctx.height {
|
|
return true
|
|
}
|
|
|
|
if ctx.callbackResize != nil {
|
|
ctx.callbackResize(ctx.userData, w, h)
|
|
}
|
|
|
|
data := make([]byte, w * h, allocator)
|
|
|
|
for i in 0..<ctx.height {
|
|
dst := &data[i * w]
|
|
src := &ctx.textureData[i * ctx.width]
|
|
mem.copy(dst, src, ctx.width)
|
|
|
|
if w > ctx.width {
|
|
mem.set(&data[i * w + ctx.width], 0, w - ctx.width)
|
|
}
|
|
}
|
|
|
|
if h > ctx.height {
|
|
mem.set(&data[ctx.height * w], 0, (h - ctx.height) * w)
|
|
}
|
|
|
|
delete(ctx.textureData)
|
|
ctx.textureData = data
|
|
|
|
// increase atlas size
|
|
__atlasExpand(ctx, w, h)
|
|
|
|
// add existing data as dirty
|
|
maxy := i16(0)
|
|
for node in ctx.nodes {
|
|
maxy = max(maxy, node.y)
|
|
}
|
|
ctx.dirtyRect[0] = 0
|
|
ctx.dirtyRect[1] = 0
|
|
ctx.dirtyRect[2] = f32(ctx.width)
|
|
ctx.dirtyRect[3] = f32(maxy)
|
|
|
|
ctx.width = w
|
|
ctx.height = h
|
|
ctx.itw = 1.0 / f32(w)
|
|
ctx.ith = 1.0 / f32(h)
|
|
|
|
return true
|
|
}
|
|
|
|
ResetAtlas :: proc(ctx: ^FontContext, width, height: int, allocator := context.allocator) -> bool {
|
|
if width == ctx.width && height == ctx.height {
|
|
// just clear
|
|
slice.zero(ctx.textureData)
|
|
} else {
|
|
// realloc
|
|
delete(ctx.textureData, allocator)
|
|
ctx.textureData = make([]byte, width * height, allocator)
|
|
}
|
|
|
|
ctx.dirtyRect[0] = f32(width)
|
|
ctx.dirtyRect[1] = f32(height)
|
|
ctx.dirtyRect[2] = 0
|
|
ctx.dirtyRect[3] = 0
|
|
|
|
// reset fonts
|
|
for &font in ctx.fonts {
|
|
clear(&font.glyphs)
|
|
__lutReset(&font)
|
|
}
|
|
|
|
ctx.width = width
|
|
ctx.height = height
|
|
ctx.itw = 1.0 / f32(width)
|
|
ctx.ith = 1.0 / f32(height)
|
|
|
|
_ = __AtlasAddWhiteRect(ctx, 2, 2)
|
|
return true
|
|
}
|
|
|
|
__getGlyph_index :: proc(font: ^Font, codepoint: rune) -> Glyph_Index {
|
|
return stbtt.FindGlyphIndex(&font.info, codepoint)
|
|
}
|
|
|
|
__getPixelHeightScale :: proc(font: ^Font, pixel_height: f32) -> f32 {
|
|
return stbtt.ScaleForPixelHeight(&font.info, pixel_height)
|
|
}
|
|
|
|
__getGlyphKernAdvance :: proc(font: ^Font, glyph1, glyph2: Glyph_Index) -> i32 {
|
|
return stbtt.GetGlyphKernAdvance(&font.info, glyph1, glyph2)
|
|
}
|
|
|
|
// get a font with bounds checking
|
|
__getFont :: proc(ctx: ^FontContext, index: int, loc := #caller_location) -> ^Font #no_bounds_check {
|
|
runtime.bounds_check_error_loc(loc, index, len(ctx.fonts))
|
|
return &ctx.fonts[index]
|
|
}
|
|
|
|
// only useful for single glyphs where you quickly want the width
|
|
CodepointWidth :: proc(
|
|
font: ^Font,
|
|
codepoint: rune,
|
|
scale: f32,
|
|
) -> f32 {
|
|
glyph_index := __getGlyph_index(font, codepoint)
|
|
xadvance, lsb: i32
|
|
stbtt.GetGlyphHMetrics(&font.info, glyph_index, &xadvance, &lsb)
|
|
return f32(xadvance) * scale
|
|
}
|
|
|
|
// get top and bottom line boundary
|
|
LineBounds :: proc(ctx: ^FontContext, y: f32) -> (miny, maxy: f32) {
|
|
state := __getState(ctx)
|
|
font := __getFont(ctx, state.font)
|
|
isize := i16(state.size * 10.0)
|
|
y := y
|
|
y += __getVerticalAlign(ctx, font, state.av, isize)
|
|
|
|
if ctx.location == .TOPLEFT {
|
|
miny = y - font.ascender * f32(isize) / 10
|
|
maxy = miny + font.lineHeight * f32(isize / 10)
|
|
} else if ctx.location == .BOTTOMLEFT {
|
|
miny = y + font.ascender * f32(isize) / 10
|
|
maxy = miny - font.lineHeight * f32(isize / 10)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// reset dirty rect
|
|
__dirtyRectReset :: proc(using ctx: ^FontContext) {
|
|
dirtyRect[0] = f32(width)
|
|
dirtyRect[1] = f32(height)
|
|
dirtyRect[2] = 0
|
|
dirtyRect[3] = 0
|
|
}
|
|
|
|
// true when the dirty rectangle is valid and needs a texture update on the gpu
|
|
ValidateTexture :: proc(using ctx: ^FontContext, dirty: ^[4]f32) -> bool {
|
|
if dirtyRect[0] < dirtyRect[2] && dirtyRect[1] < dirtyRect[3] {
|
|
dirty[0] = dirtyRect[0]
|
|
dirty[1] = dirtyRect[1]
|
|
dirty[2] = dirtyRect[2]
|
|
dirty[3] = dirtyRect[3]
|
|
__dirtyRectReset(ctx)
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// get alignment based on font
|
|
__getVerticalAlign :: proc(
|
|
ctx: ^FontContext,
|
|
font: ^Font,
|
|
av: AlignVertical,
|
|
pixelSize: i16,
|
|
) -> (res: f32) {
|
|
switch ctx.location {
|
|
case .TOPLEFT:
|
|
switch av {
|
|
case .TOP: res = font.ascender * f32(pixelSize) / 10
|
|
case .MIDDLE: res = (font.ascender + font.descender) / 2 * f32(pixelSize) / 10
|
|
case .BASELINE: res = 0
|
|
case .BOTTOM: res = font.descender * f32(pixelSize) / 10
|
|
}
|
|
|
|
case .BOTTOMLEFT:
|
|
switch av {
|
|
case .TOP: res = -font.ascender * f32(pixelSize) / 10
|
|
case .MIDDLE: res = -(font.ascender + font.descender) / 2 * f32(pixelSize) / 10
|
|
case .BASELINE: res = 0
|
|
case .BOTTOM: res = -font.descender * f32(pixelSize) / 10
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
@(private)
|
|
UTF8_ACCEPT :: 0
|
|
|
|
@(private)
|
|
UTF8_REJECT :: 1
|
|
|
|
@(private)
|
|
utf8d := [400]u8{
|
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 00..1f
|
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 20..3f
|
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 40..5f
|
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 60..7f
|
|
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, // 80..9f
|
|
7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, // a0..bf
|
|
8,8,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, // c0..df
|
|
0xa,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x4,0x3,0x3, // e0..ef
|
|
0xb,0x6,0x6,0x6,0x5,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8, // f0..ff
|
|
0x0,0x1,0x2,0x3,0x5,0x8,0x7,0x1,0x1,0x1,0x4,0x6,0x1,0x1,0x1,0x1, // s0..s0
|
|
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,0,1,0,1,1,1,1,1,1, // s1..s2
|
|
1,2,1,1,1,1,1,2,1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1, // s3..s4
|
|
1,2,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,3,1,3,1,1,1,1,1,1, // s5..s6
|
|
1,3,1,1,1,1,1,3,1,3,1,1,1,1,1,1,1,3,1,1,1,1,1,1,1,1,1,1,1,1,1,1, // s7..s8
|
|
}
|
|
|
|
// decode codepoints from a state
|
|
@(private)
|
|
__decutf8 :: #force_inline proc(state: ^rune, codep: ^rune, b: byte) -> bool {
|
|
b := rune(b)
|
|
type := utf8d[b]
|
|
codep^ = (state^ != UTF8_ACCEPT) ? ((b & 0x3f) | (codep^ << 6)) : ((0xff >> type) & (b))
|
|
state^ = rune(utf8d[256 + state^ * 16 + rune(type)])
|
|
return state^ == UTF8_ACCEPT
|
|
}
|
|
|
|
// state used to share font options
|
|
State :: struct {
|
|
font: int,
|
|
size: f32,
|
|
color: [4]u8,
|
|
spacing: f32,
|
|
blur: f32,
|
|
|
|
ah: AlignHorizontal,
|
|
av: AlignVertical,
|
|
}
|
|
|
|
// quad that should be used to draw from the texture atlas
|
|
Quad :: struct {
|
|
x0, y0, s0, t0: f32,
|
|
x1, y1, s1, t1: f32,
|
|
}
|
|
|
|
// text iteration with custom settings
|
|
TextIter :: struct {
|
|
x, y, nextx, nexty, scale, spacing: f32,
|
|
isize, iblur: i16,
|
|
|
|
font: ^Font,
|
|
previousGlyphIndex: Glyph_Index,
|
|
|
|
// unicode iteration
|
|
utf8state: rune, // utf8
|
|
codepoint: rune,
|
|
text: string,
|
|
codepointCount: int,
|
|
|
|
// byte indices
|
|
str: int,
|
|
next: int,
|
|
end: int,
|
|
}
|
|
|
|
// push a state, copies the current one over to the next one
|
|
PushState :: proc(using ctx: ^FontContext, loc := #caller_location) #no_bounds_check {
|
|
runtime.bounds_check_error_loc(loc, state_count, MAX_STATES)
|
|
|
|
if state_count > 0 {
|
|
states[state_count] = states[state_count - 1]
|
|
}
|
|
|
|
state_count += 1
|
|
}
|
|
|
|
// pop a state
|
|
PopState :: proc(using ctx: ^FontContext) {
|
|
if state_count <= 1 {
|
|
log.error("FONTSTASH: state underflow! to many pops were called")
|
|
} else {
|
|
state_count -= 1
|
|
}
|
|
}
|
|
|
|
// clear current state
|
|
ClearState :: proc(ctx: ^FontContext) {
|
|
state := __getState(ctx)
|
|
state.size = 12
|
|
state.color = 255
|
|
state.blur = 0
|
|
state.spacing = 0
|
|
state.font = 0
|
|
state.ah = .LEFT
|
|
state.av = .BASELINE
|
|
}
|
|
|
|
__getState :: #force_inline proc(ctx: ^FontContext) -> ^State #no_bounds_check {
|
|
return &ctx.states[ctx.state_count - 1]
|
|
}
|
|
|
|
SetSize :: proc(ctx: ^FontContext, size: f32) {
|
|
__getState(ctx).size = size
|
|
}
|
|
|
|
SetColor :: proc(ctx: ^FontContext, color: [4]u8) {
|
|
__getState(ctx).color = color
|
|
}
|
|
|
|
SetSpacing :: proc(ctx: ^FontContext, spacing: f32) {
|
|
__getState(ctx).spacing = spacing
|
|
}
|
|
|
|
SetBlur :: proc(ctx: ^FontContext, blur: f32) {
|
|
__getState(ctx).blur = blur
|
|
}
|
|
|
|
SetFont :: proc(ctx: ^FontContext, font: int) {
|
|
__getState(ctx).font = font
|
|
}
|
|
|
|
SetAH :: SetAlignHorizontal
|
|
SetAV :: SetAlignVertical
|
|
|
|
SetAlignHorizontal :: proc(ctx: ^FontContext, ah: AlignHorizontal) {
|
|
__getState(ctx).ah = ah
|
|
}
|
|
|
|
SetAlignVertical :: proc(ctx: ^FontContext, av: AlignVertical) {
|
|
__getState(ctx).av = av
|
|
}
|
|
|
|
__getQuad :: proc(
|
|
ctx: ^FontContext,
|
|
font: ^Font,
|
|
|
|
previousGlyphIndex: i32,
|
|
glyph: ^Glyph,
|
|
|
|
scale: f32,
|
|
spacing: f32,
|
|
|
|
x, y: ^f32,
|
|
quad: ^Quad,
|
|
) {
|
|
if previousGlyphIndex != -1 {
|
|
adv := f32(__getGlyphKernAdvance(font, previousGlyphIndex, glyph.index)) * scale
|
|
x^ += f32(int(adv + spacing + 0.5))
|
|
}
|
|
|
|
// fill props right
|
|
rx, ry, x0, y0, x1, y1, xoff, yoff: f32
|
|
xoff = f32(glyph.xoff + 1)
|
|
yoff = f32(glyph.yoff + 1)
|
|
x0 = f32(glyph.x0 + 1)
|
|
y0 = f32(glyph.y0 + 1)
|
|
x1 = f32(glyph.x1 - 1)
|
|
y1 = f32(glyph.y1 - 1)
|
|
|
|
switch ctx.location {
|
|
case .TOPLEFT:
|
|
rx = math.floor(x^ + xoff)
|
|
ry = math.floor(y^ + yoff)
|
|
|
|
quad.x0 = rx
|
|
quad.y0 = ry
|
|
quad.x1 = rx + x1 - x0
|
|
quad.y1 = ry + y1 - y0
|
|
|
|
quad.s0 = x0 * ctx.itw
|
|
quad.t0 = y0 * ctx.ith
|
|
quad.s1 = x1 * ctx.itw
|
|
quad.t1 = y1 * ctx.ith
|
|
|
|
case .BOTTOMLEFT:
|
|
rx = math.floor(x^ + xoff)
|
|
ry = math.floor(y^ - yoff)
|
|
|
|
quad.x0 = rx
|
|
quad.y0 = ry
|
|
quad.x1 = rx + x1 - x0
|
|
quad.y1 = ry - y1 + y0
|
|
|
|
quad.s0 = x0 * ctx.itw
|
|
quad.t0 = y0 * ctx.ith
|
|
quad.s1 = x1 * ctx.itw
|
|
quad.t1 = y1 * ctx.ith
|
|
}
|
|
|
|
x^ += f32(int(f32(glyph.xadvance) / 10 + 0.5))
|
|
}
|
|
|
|
// init text iter struct with settings
|
|
TextIterInit :: proc(
|
|
ctx: ^FontContext,
|
|
x: f32,
|
|
y: f32,
|
|
text: string,
|
|
) -> (res: TextIter) {
|
|
|
|
x, y := x, y
|
|
|
|
state := __getState(ctx)
|
|
res.font = __getFont(ctx, state.font)
|
|
res.isize = i16(f32(state.size) * 10)
|
|
res.iblur = i16(state.blur)
|
|
res.scale = __getPixelHeightScale(res.font, f32(res.isize) / 10)
|
|
|
|
// align horizontally
|
|
switch state.ah {
|
|
case .LEFT:
|
|
/**/
|
|
case .CENTER:
|
|
width := TextBounds(ctx, text, x, y, nil)
|
|
x = math.round(x - width * 0.5)
|
|
case .RIGHT:
|
|
width := TextBounds(ctx, text, x, y, nil)
|
|
x -= width
|
|
}
|
|
|
|
// align vertically
|
|
y = math.round(y + __getVerticalAlign(ctx, res.font, state.av, res.isize))
|
|
|
|
// set positions
|
|
res.x, res.nextx = x, x
|
|
res.y, res.nexty = y, y
|
|
res.previousGlyphIndex = -1
|
|
res.spacing = state.spacing
|
|
res.text = text
|
|
|
|
res.str = 0
|
|
res.next = 0
|
|
res.end = len(text)
|
|
|
|
return
|
|
}
|
|
|
|
// step through each codepoint
|
|
TextIterNext :: proc(
|
|
ctx: ^FontContext,
|
|
iter: ^TextIter,
|
|
quad: ^Quad,
|
|
) -> (ok: bool) {
|
|
str := iter.next
|
|
iter.str = iter.next
|
|
|
|
for str < iter.end {
|
|
defer str += 1
|
|
|
|
if __decutf8(&iter.utf8state, &iter.codepoint, iter.text[str]) {
|
|
iter.x = iter.nextx
|
|
iter.y = iter.nexty
|
|
iter.codepointCount += 1
|
|
if glyph, glyph_ok := __getGlyph(ctx, iter.font, iter.codepoint, iter.isize, iter.iblur); glyph_ok {
|
|
__getQuad(ctx, iter.font, iter.previousGlyphIndex, glyph, iter.scale, iter.spacing, &iter.nextx, &iter.nexty, quad)
|
|
iter.previousGlyphIndex = glyph.index
|
|
} else {
|
|
iter.previousGlyphIndex = -1
|
|
}
|
|
ok = true
|
|
break
|
|
}
|
|
}
|
|
|
|
iter.next = str
|
|
return
|
|
}
|
|
|
|
// width of a text line, optionally the full rect
|
|
TextBounds :: proc(
|
|
ctx: ^FontContext,
|
|
text: string,
|
|
x: f32 = 0,
|
|
y: f32 = 0,
|
|
bounds: ^[4]f32 = nil,
|
|
) -> f32 {
|
|
state := __getState(ctx)
|
|
isize := i16(state.size * 10)
|
|
iblur := i16(state.blur)
|
|
font := __getFont(ctx, state.font)
|
|
|
|
// bunch of state
|
|
x, y := x, y
|
|
minx, maxx := x, x
|
|
miny, maxy := y, y
|
|
start_x := x
|
|
|
|
// iterate
|
|
scale := __getPixelHeightScale(font, f32(isize) / 10)
|
|
previousGlyphIndex: Glyph_Index = -1
|
|
quad: Quad
|
|
utf8state: rune
|
|
codepoint: rune
|
|
for byte_offset in 0..<len(text) {
|
|
if __decutf8(&utf8state, &codepoint, text[byte_offset]) {
|
|
if glyph, ok := __getGlyph(ctx, font, codepoint, isize, iblur); ok {
|
|
__getQuad(ctx, font, previousGlyphIndex, glyph, scale, state.spacing, &x, &y, &quad)
|
|
|
|
if quad.x0 < minx {
|
|
minx = quad.x0
|
|
}
|
|
if quad.x1 > maxx {
|
|
maxx = quad.x1
|
|
}
|
|
|
|
if ctx.location == .TOPLEFT {
|
|
if quad.y0 < miny {
|
|
miny = quad.y0
|
|
}
|
|
if quad.y1 > maxy {
|
|
maxy = quad.y1
|
|
}
|
|
} else if ctx.location == .BOTTOMLEFT {
|
|
if quad.y1 < miny {
|
|
miny = quad.y1
|
|
}
|
|
if quad.y0 > maxy {
|
|
maxy = quad.y0
|
|
}
|
|
}
|
|
|
|
previousGlyphIndex = glyph.index
|
|
} else {
|
|
previousGlyphIndex = -1
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
// horizontal alignment
|
|
advance := x - start_x
|
|
switch state.ah {
|
|
case .LEFT:
|
|
/**/
|
|
case .CENTER:
|
|
minx -= advance * 0.5
|
|
maxx -= advance * 0.5
|
|
case .RIGHT:
|
|
minx -= advance
|
|
maxx -= advance
|
|
}
|
|
|
|
if bounds != nil {
|
|
bounds^ = { minx, miny, maxx, maxy }
|
|
}
|
|
|
|
return advance
|
|
}
|
|
|
|
VerticalMetrics :: proc(
|
|
ctx: ^FontContext,
|
|
) -> (ascender, descender, lineHeight: f32) {
|
|
state := __getState(ctx)
|
|
isize := i16(state.size * 10.0)
|
|
font := __getFont(ctx, state.font)
|
|
ascender = font.ascender * f32(isize / 10)
|
|
descender = font.descender * f32(isize / 10)
|
|
lineHeight = font.lineHeight * f32(isize / 10)
|
|
return
|
|
}
|
|
|
|
// reset to single state
|
|
BeginState :: proc(ctx: ^FontContext) {
|
|
ctx.state_count = 0
|
|
PushState(ctx)
|
|
ClearState(ctx)
|
|
}
|
|
|
|
// checks for texture updates after potential __getGlyph calls
|
|
EndState :: proc(using ctx: ^FontContext) {
|
|
// check for texture update
|
|
if dirtyRect[0] < dirtyRect[2] && dirtyRect[1] < dirtyRect[3] {
|
|
if callbackUpdate != nil {
|
|
callbackUpdate(userData, dirtyRect, raw_data(textureData))
|
|
}
|
|
__dirtyRectReset(ctx)
|
|
}
|
|
}
|