Files
Server_Monitor/code/gui/text_draw.cpp
2023-09-26 19:40:16 +02:00

549 lines
16 KiB
C++

#include "text_draw.h"
#include "../lib/color.h"
#include "../lib/math.h"
#include "../lib/geometry.h"
#include "../lib/text.h"
#include "../lib/ds.h"
#include "../platform.h"
#include "stb_truetype.h"
#include "../debug/logger.h"
#include "../assets.h"
#include "../enginestate.h"
#include <string.h>
#include <stdlib.h>
struct gui_glyph_info
{
utf8_codepoint codepoint;
Rect box; // .position = offset from position for alignment, .size = size of the bitmap to draw
v2u position; // Position of the top left border in the texture
s32 advance;
u32 next; // Anything >= container_capacity is to be considered NULL
u32 previous;
};
/* Glyph caching:
* We store a small number of sizes and a fairly large number of characters for each size.
* Each slot will have the same width and height, so that we can easily replace it without
* re-packing everything. This will also make the code simpler.
* We use 0xFFFFFFFF as a placeholder for empty slots.
*
*/
struct gui_glyph_codepoint_map
{
utf8_codepoint codepoint;
u32 index;
};
struct gui_glyph_texture
{
f32 font_size;
struct gui_glyph_codepoint_map *sorted_indices;
gui_glyph_info *info;
u32 oldest;
u32 newest;
u32 capacity;
v2s glyph_max_size;
r_texture *texture;
};
struct gui_glyph_cache
{
gui_glyph_texture *glyphs;
u32 capacity;
u32 oldest;
u32 max_glyphs_per_texture;
};
static void gui_glyph_cache_init();
static void gui_glyph_cache_deinit();
static gui_glyph_texture gui_glyph_texture_create(f32 font_size, u32 capacity);
static void gui_glyph_texture_destroy(gui_glyph_texture *glyphs);
static gui_glyph_texture *gui_glyph_cache_texture_for_codepoints(f32 font_size, const utf8_codepoint *codepoints, u64 count);
// Globals
static u8 *Font_File;
static stbtt_fontinfo Font_Info;
static gui_glyph_cache Glyph_Cache;
bool gui_text_draw_init()
{
// @Feature: user specified font
// @Cleanup: one-line file read
p_file f;
p_file_init(&f, "assets/fonts/DejaVuSerif-Bold.ttf");
Buffer buf;
buf.size = p_file_size(&f);
buf.data = (u8*)p_alloc(buf.size + 1);
buf.data[buf.size] = '\0';
p_file_read(&f, &buf, buf.size);
p_file_deinit(&f);
Font_File = buf.data;
if(!stbtt_InitFont(&Font_Info, Font_File, 0))
{
LOG(LOG_ERROR, "Cannot load font.");
return false;
}
gui_glyph_cache_init();
return true;
}
void gui_text_draw_deinit()
{
gui_glyph_cache_deinit();
p_free(Font_File);
}
v2 gui_text_draw_size(const char *text, f32 font_size, v2 *cursor_position)
{
// UTF8 conversion
u64 text_length = utf8_codepoint_count(text);
utf8_codepoint *codepoints = (utf8_codepoint*) p_alloc(text_length * sizeof(utf8_codepoint));
{
u64 bytes_read = 0;
text_length = utf8_from_string(text, &bytes_read, codepoints, text_length);
}
// Compute size
v2 result = gui_utf8_text_draw_size(codepoints, text_length, font_size, cursor_position);
p_free(codepoints);
return result;
}
void gui_text_draw(Rect r, const char *text, f32 font_size, v4 color)
{
// UTF8 conversion
u64 text_length = utf8_codepoint_count(text);
utf8_codepoint *codepoints = (utf8_codepoint*) p_alloc(text_length * sizeof(utf8_codepoint));
{
u64 bytes_read = 0;
text_length = utf8_from_string(text, &bytes_read, codepoints, text_length);
}
// Draw
gui_utf8_text_draw(r, codepoints, text_length, font_size, color);
p_free(codepoints);
}
v2 gui_utf8_text_draw_size(const utf8_codepoint *text, u64 length, f32 font_size, v2 *cursor_position)
{
f32 font_scale;
s32 font_ascent;
s32 font_descent;
s32 font_line_gap;
s32 font_baseline;
{
font_scale = stbtt_ScaleForPixelHeight(&Font_Info, font_size);
stbtt_GetFontVMetrics(&Font_Info, &font_ascent, &font_descent, &font_line_gap);
font_baseline = font_ascent;
font_ascent *= font_scale;
font_descent *= font_scale;
font_line_gap *= font_scale;
font_baseline *= font_scale;
}
// Compute size
v2 size = {0, (f32)(font_ascent - font_descent)};
{
v2 cursor = {0, (f32)(font_ascent - font_descent)};
for(u64 i = 0; i < length; i++)
{
s32 advance, lsb;
stbtt_GetCodepointHMetrics(&Font_Info, text[i], &advance, &lsb);
// Special characters
if(text[i] == 10) // '\n'
{
advance = 0;
cursor.x = 0;
cursor.y += font_ascent - font_descent + font_line_gap;
size.y = cursor.y;
}
// Normal characters
cursor.x += floor(advance * font_scale); // Remember to consider kerning
size.x = maximum(size.x, cursor.x);
}
if(cursor_position)
*cursor_position = cursor;
}
return size;
}
void gui_utf8_text_draw(Rect r, const utf8_codepoint *text, u64 length, f32 font_size, v4 color)
{
f32 font_scale;
s32 font_ascent;
s32 font_descent;
s32 font_line_gap;
s32 font_baseline;
{
font_scale = stbtt_ScaleForPixelHeight(&Font_Info, font_size);
stbtt_GetFontVMetrics(&Font_Info, &font_ascent, &font_descent, &font_line_gap);
font_baseline = font_ascent;
font_ascent *= font_scale;
font_descent *= font_scale;
font_line_gap *= font_scale;
font_baseline *= font_scale;
}
// Compute glyphs
gui_glyph_texture *glyphs = gui_glyph_cache_texture_for_codepoints(font_size, text, length);
// Map text to quads
v2 *vertices;
v2 *uvs;
u64 draw_count = 0;
{
vertices = (v2*) p_alloc(6 * length * sizeof(v2)); // 2 triangles, 3 vertices each = 6 vertices
uvs = (v2*) p_alloc(6 * length * sizeof(v2));
v2 position = r.position;
position.y += font_baseline;
for(u64 i = 0; i < length; i++)
{
gui_glyph_codepoint_map *found = (gui_glyph_codepoint_map*) bsearch(&text[i], glyphs->sorted_indices, glyphs->capacity, sizeof(gui_glyph_codepoint_map), u32_cmp);
if(found == NULL)
{
LOG(LOG_ERROR, "Cannot find codepoint 0x%X in glyph list.", text[i]);
continue;
}
u64 glyph_i = glyphs->sorted_indices[found - glyphs->sorted_indices].index;
gui_glyph_info *info = &glyphs->info[glyph_i];
// Special characters
if(text[i] == 10) // '\n'
{
position.x = r.position.x;
position.y += font_ascent - font_descent + font_line_gap;
}
// Normal characters
// Map character to its vertices
{
Rect r;
Rect r_uv;
r.position = position + info->box.position;
r.position.x = floor(r.position.x + 0.5);
r.position.y = floor(r.position.y + 0.5);
r.size = info->box.size;
r_uv.position = V2(info->position) / V2(glyphs->texture->size);
r_uv.size = info->box.size / V2(glyphs->texture->size);
v2 a = r.position + v2{r.w, 0 };
v2 b = r.position + v2{0 , 0 };
v2 c = r.position + v2{0 , r.h};
v2 d = r.position + v2{r.w, r.h};
v2 a_uv = r_uv.position + v2{r_uv.w, 0 };
v2 b_uv = r_uv.position + v2{0 , 0 };
v2 c_uv = r_uv.position + v2{0 , r_uv.h};
v2 d_uv = r_uv.position + v2{r_uv.w, r_uv.h};
vertices[6*draw_count + 0] = a;
vertices[6*draw_count + 1] = b;
vertices[6*draw_count + 2] = c;
vertices[6*draw_count + 3] = a;
vertices[6*draw_count + 4] = c;
vertices[6*draw_count + 5] = d;
uvs[6*draw_count + 0] = a_uv;
uvs[6*draw_count + 1] = b_uv;
uvs[6*draw_count + 2] = c_uv;
uvs[6*draw_count + 3] = a_uv;
uvs[6*draw_count + 4] = c_uv;
uvs[6*draw_count + 5] = d_uv;
}
position.x += info->advance; // Remember to consider kerning
draw_count++;
}
}
// Render quads
r_2d_immediate_mesh(6*draw_count, vertices, color, uvs, glyphs->texture);
p_free(vertices);
p_free(uvs);
}
static void gui_glyph_cache_init()
{
Glyph_Cache.capacity = 4;
Glyph_Cache.oldest = 0;
Glyph_Cache.glyphs = (gui_glyph_texture*)p_alloc(sizeof(gui_glyph_texture) * Glyph_Cache.capacity);
memset(Glyph_Cache.glyphs, 0, sizeof(gui_glyph_texture) * Glyph_Cache.capacity);
Glyph_Cache.max_glyphs_per_texture = 256; // @Correctness: test with small values, to trigger the glyph replacement code
}
static void gui_glyph_cache_deinit()
{
for(u32 i = 0; i < Glyph_Cache.capacity; i++)
gui_glyph_texture_destroy(&Glyph_Cache.glyphs[i]);
p_free(Glyph_Cache.glyphs);
Glyph_Cache.glyphs = NULL;
Glyph_Cache.capacity = 0;
Glyph_Cache.oldest = 0;
}
static gui_glyph_texture gui_glyph_texture_create(f32 font_size, u32 capacity)
{
// Init container for glyphs and info
gui_glyph_texture glyphs;
glyphs.font_size = font_size;
glyphs.sorted_indices = (gui_glyph_codepoint_map*) p_alloc(sizeof(gui_glyph_codepoint_map) * capacity);
glyphs.info = (gui_glyph_info*) p_alloc(sizeof(gui_glyph_info) * capacity);
glyphs.oldest = 0;
glyphs.newest = capacity - 1;
glyphs.capacity = capacity;
// Estimate max glyph size.
// @Correctness: Text draw will fail if a bigger glyph is used.
f32 font_scale = stbtt_ScaleForPixelHeight(&Font_Info, font_size);
utf8_codepoint cp[] = {' ', 'M', 'j', '{', '=', 'w', 'W'};
glyphs.glyph_max_size = v2s{0, 0};
for(s32 i = 0; i < sizeof(cp)/sizeof(utf8_codepoint); i++)
{
v2s top_left, bottom_right;
stbtt_GetCodepointBitmapBox(&Font_Info, cp[i], font_scale, font_scale, &top_left.x, &top_left.y, &bottom_right.x, &bottom_right.y);
v2s size = bottom_right - top_left;
glyphs.glyph_max_size.x = maximum(glyphs.glyph_max_size.x, size.x);
glyphs.glyph_max_size.y = maximum(glyphs.glyph_max_size.y, size.y);
}
LOG(LOG_DEBUG, "Font size %f not in cache. Slot size (%d %d)", font_size, glyphs.glyph_max_size.x, glyphs.glyph_max_size.y);
// Precompile some info data
for(u32 i = 0; i < glyphs.capacity; i++)
{
glyphs.info[i] = gui_glyph_info{
.codepoint = 0xFFFFFFFF,
.box = {0,0,0,0},
.position = v2u{glyphs.glyph_max_size.x * i, 0},
.advance = 0,
.next = i + 1, // Last .next will be >= capacity (== capacity to be precise), so we will consider it to be NULL
.previous = ((i == 0) ? glyphs.capacity : (i - 1))
};
glyphs.sorted_indices[i] = gui_glyph_codepoint_map{0xFFFFFFFF, i};
}
// Initialize texture
v2s texture_size = v2s{glyphs.glyph_max_size.x * glyphs.capacity, glyphs.glyph_max_size.y};
u8 *texture_data = (u8*)p_alloc(sizeof(u8) * texture_size.x * texture_size.y);
memset(texture_data, 0, sizeof(u8) * texture_size.x * texture_size.y);
glyphs.texture = assets_new_textures(&engine.am, 1);
*glyphs.texture = r_texture_create(texture_data, texture_size, R_TEXTURE_ALPHA | R_TEXTURE_NO_MIPMAP);
return glyphs;
}
static void gui_glyph_texture_destroy(gui_glyph_texture *glyphs)
{
if(glyphs->sorted_indices)
p_free(glyphs->sorted_indices);
if(glyphs->info)
p_free(glyphs->info);
if(glyphs->texture)
r_texture_destroy(glyphs->texture);
glyphs->sorted_indices = NULL;
glyphs->info = NULL;
glyphs->texture = NULL;
glyphs->font_size = 0;
glyphs->oldest = 0;
glyphs->newest = 0;
glyphs->capacity = 0;
}
static gui_glyph_texture *gui_glyph_cache_texture_for_codepoints(f32 font_size, const utf8_codepoint *codepoints, u64 count)
{
// Approximate font_size. We don't really want to build different bitmaps for size 12.000000 and 12.000001.
// This will also prevent floating point rounding errors from rebuilding the cache.
font_size = floor(font_size * 10) / 10;
// Find cached texture for this size or build a new one
gui_glyph_texture *glyphs = NULL;
for(u32 i = 0; i < Glyph_Cache.capacity; i++)
{
//LOG(LOG_DEBUG, "Font size: %f - Cached: %f", font_size, Glyph_Cache.glyphs[i].font_size);
if(abs(Glyph_Cache.glyphs[i].font_size - font_size) < 0.01)
{
glyphs = &Glyph_Cache.glyphs[i];
}
}
if(glyphs == NULL)
{
//LOG(LOG_DEBUG, "Size not matched");
glyphs = &Glyph_Cache.glyphs[Glyph_Cache.oldest];
Glyph_Cache.oldest = (Glyph_Cache.oldest + 1) % Glyph_Cache.capacity;
gui_glyph_texture_destroy(glyphs);
*glyphs = gui_glyph_texture_create(font_size, Glyph_Cache.max_glyphs_per_texture);
}
// Build list of unique codepoints (so that we don't render the same codepoint twice)
utf8_codepoint *unique = (utf8_codepoint*) p_alloc(count * sizeof(utf8_codepoint));
memcpy(unique, codepoints, count * sizeof(utf8_codepoint));
u64 unique_count = make_unique(unique, count, sizeof(utf8_codepoint), u32_cmp);
if(unique_count > glyphs->capacity)
LOG(LOG_ERROR, "Unique codepoints count > cache capacity. Some codepoints will not be rendered.");
// Find which codepoints are not already in the cache and need to be rendered
utf8_codepoint to_render[unique_count];
u32 to_render_count = 0;
for(u32 i = 0; i < unique_count; i++)
{
gui_glyph_codepoint_map *found = (gui_glyph_codepoint_map*) bsearch(&unique[i], glyphs->sorted_indices, glyphs->capacity, sizeof(gui_glyph_codepoint_map), u32_cmp);
if(found == NULL)
{
// Not found -> add to the list of glyphs to render
to_render[to_render_count] = unique[i];
to_render_count++;
}
else
{
// Found -> mark it as recent, so that is does not get deleted prematurely
u32 index = glyphs->sorted_indices[found - glyphs->sorted_indices].index;
if(index == glyphs->newest)
{
// Already the newest. Do nothing.
}
else if(index == glyphs->oldest)
{
// We have no previous to fix, only next
u32 next = glyphs->info[index].next;
glyphs->info[next].previous = glyphs->capacity; // Next is the new oldest -> no previous
glyphs->oldest = next;
// Set index as last element
glyphs->info[index].next = glyphs->capacity;
glyphs->info[index].previous = glyphs->newest;
glyphs->info[glyphs->newest].next = index;
glyphs->newest = index;
}
else
{
// We in between the list. We have both previous and next elements to fix.
u32 previous = glyphs->info[index].previous;
u32 next = glyphs->info[index].next;
glyphs->info[previous].next = next;
glyphs->info[next].previous = previous;
// Set index as last element
glyphs->info[index].next = glyphs->capacity;
glyphs->info[index].previous = glyphs->newest;
glyphs->info[glyphs->newest].next = index;
glyphs->newest = index;
}
}
}
p_free(unique);
// Get info for rendering
f32 font_scale;
s32 font_ascent;
s32 font_descent;
s32 font_line_gap;
s32 font_baseline;
{
font_scale = stbtt_ScaleForPixelHeight(&Font_Info, font_size);
stbtt_GetFontVMetrics(&Font_Info, &font_ascent, &font_descent, &font_line_gap);
font_baseline = font_ascent;
font_ascent *= font_scale;
font_descent *= font_scale;
font_line_gap *= font_scale;
font_baseline *= font_scale;
}
// Render glyph in its appropriate place
for(u32 i = 0; i < to_render_count; i++)
{
u32 index = glyphs->oldest;
glyphs->oldest = glyphs->info[index].next;
glyphs->info[glyphs->oldest].previous = glyphs->capacity;
glyphs->info[index].next = glyphs->capacity;
glyphs->info[index].previous = glyphs->newest;
glyphs->info[index].codepoint = to_render[i];
glyphs->info[glyphs->newest].next = index;
glyphs->newest = index;
// Complete gui_glyph_info structure and render
gui_glyph_info *info = &glyphs->info[index];
v2s top_left, bottom_right;
stbtt_GetCodepointBitmapBox(&Font_Info, info->codepoint, font_scale, font_scale, &top_left.x, &top_left.y, &bottom_right.x, &bottom_right.y);
s32 advance, lsb;
stbtt_GetCodepointHMetrics(&Font_Info, info->codepoint, &advance, &lsb);
v2s size = bottom_right - top_left;
// Special codepoints
if(info->codepoint == 10) // '\n'
{
size = {0, 0};
advance = 0;
}
info->box.position = V2(top_left);
info->box.size = V2(size);
//info->position = v2u{glyphs->glyph_max_size.x * index, 0}; // Commented because it's already pre-computed.
info->advance = advance * font_scale;
// @Correctness: needs to be premultiplied alpha
stbtt_MakeCodepointBitmap(&Font_Info, glyphs->texture->data + info->position.x, info->box.size.x, info->box.size.y, glyphs->texture->size.x, font_scale, font_scale, info->codepoint);
r_texture_update(glyphs->texture, glyphs->texture->data + info->position.x, V2S(info->box.size), V2S(info->position), glyphs->texture->size.x);
}
// Build sorted array with indices that point to the element
u32 nonempty_count = glyphs->capacity;
for(u32 i = 0; i < glyphs->capacity; i++)
{
glyphs->sorted_indices[i] = gui_glyph_codepoint_map{glyphs->info[i].codepoint, i};
if(glyphs->info[i].codepoint == 0xFFFFFFFF)
{
// When the cache is mostly empty, this makes the sorting way faster.
nonempty_count = i;
break;
}
}
qsort(glyphs->sorted_indices, nonempty_count, sizeof(gui_glyph_codepoint_map), u32_cmp);
return glyphs;
}