This repository has been archived on 2025-02-04. You can view files and clone it, but cannot push or open issues or pull requests.
helpless/oogabooga/font.c
2024-08-19 16:19:46 +02:00

515 lines
No EOL
16 KiB
C

// See oogabooga/examples/text_rendering.c for usage
/*
TODO:
- Justify rows in walk_glyphs
*/
// #Memory #Speed
// This is terruble and huge waste of video memory. We should have a constant codepoint range
// with arbitrary atlas sizes instead.
#define FONT_ATLAS_WIDTH 2048
#define FONT_ATLAS_HEIGHT 2048
#define MAX_FONT_HEIGHT 512
typedef struct Gfx_Font Gfx_Font;
typedef struct Gfx_Text_Metrics {
/*
FUNCTIONAL BOX:
x0: left start of text box
x1: right end of text box
y0: The baseline for the bottom line of text
y1: The baseline for the top line of text + latin_ascent
VISUAL BOX:
x0: The minimum X of any pixels
x1: The maximum X of any pixels
y0: The minimum Y of any pixels
y1: The maximum Y of any pixels
Usage:
For a single piece of static text, it might look better to use the visual box for aligning it somewhere.
I might be stupid and that might be useless tho.
Most of the time, you are probably going to want to use the functional box.
For example, to center:
Gfx_Text_Metrics m = measure_text(...);
// This is the point where the center of the text box will be
Vector2 draw_pos = v2(...);
// First justify for the bottom-left to be at the draw point
Vector2 justified = v2_sub(draw_pos, m.functional_pos_min);
// Then move text backwards by functional_size/2 to align its center to the draw point
justified = v2_sub(justified, v2_divf(m.functional_size, 2));
*/
Vector2 functional_pos_min;
Vector2 functional_pos_max;
Vector2 functional_size;
// The visual bounds for the text.
Vector2 visual_pos_min;
Vector2 visual_pos_max;
Vector2 visual_size;
} Gfx_Text_Metrics;
typedef struct Gfx_Font_Metrics {
float latin_ascent;
float latin_descent;
float max_ascent;
float max_descent;
float line_spacing;
float new_line_offset;
} Gfx_Font_Metrics;
typedef struct Gfx_Glyph {
u32 codepoint;
float xoffset, yoffset;
float advance;
float width, height;
Vector4 uv;
} Gfx_Glyph;
typedef struct Gfx_Font_Atlas {
Gfx_Image *image;
u32 first_codepoint;
Gfx_Glyph *glyphs; // first_codepoint + index == the codepoint
} Gfx_Font_Atlas;
typedef struct Gfx_Font_Variation {
Gfx_Font *font;
u32 height;
Gfx_Font_Metrics metrics;
float scale;
u32 codepoint_range_per_atlas;
Hash_Table atlases; // u32 atlas_index, Gfx_Font_Atlas
bool initted;
} Gfx_Font_Variation;
typedef struct Gfx_Font {
stbtt_fontinfo stbtt_handle;
string raw_font_data;
Gfx_Font_Variation variations[MAX_FONT_HEIGHT]; // Variation per font height
Allocator allocator;
} Gfx_Font;
Gfx_Font *load_font_from_disk(string path, Allocator allocator) {
string font_data;
bool read_ok = os_read_entire_file(path, &font_data, allocator);
if (!read_ok) return 0;
third_party_allocator = allocator;
stbtt_fontinfo stbtt_handle;
int result = stbtt_InitFont(&stbtt_handle, font_data.data, stbtt_GetFontOffsetForIndex(font_data.data, 0));
if (result == 0) return 0;
Gfx_Font *font = alloc(allocator, sizeof(Gfx_Font));
memset(font, 0, sizeof(Gfx_Font));
font->stbtt_handle = stbtt_handle;
font->raw_font_data = font_data;
font->allocator = allocator;
third_party_allocator = ZERO(Allocator);
return font;
}
void destroy_font(Gfx_Font *font) {
third_party_allocator = font->allocator;
for (u64 i = 0; i < MAX_FONT_HEIGHT; i++) {
Gfx_Font_Variation *variation = &font->variations[i];
if (!variation->initted) continue;
for (u64 j = 0; j < variation->atlases.count; j++) {
Gfx_Font_Atlas *atlas = (Gfx_Font_Atlas*)hash_table_get_nth_value(&variation->atlases, j);
delete_image(atlas->image);
dealloc(font->allocator, atlas->glyphs);
}
hash_table_destroy(&variation->atlases);
}
dealloc_string(font->allocator, font->raw_font_data);
dealloc(font->allocator, font);
third_party_allocator = ZERO(Allocator);
}
void font_variation_init(Gfx_Font_Variation *variation, Gfx_Font *font, u32 font_height) {
variation->font = font;
variation->height = font_height;
u32 x_range = FONT_ATLAS_WIDTH / font_height;
u32 y_range = FONT_ATLAS_HEIGHT / font_height;
variation->codepoint_range_per_atlas = x_range*y_range;
variation->atlases = make_hash_table(u32, Gfx_Font_Atlas, font->allocator);
variation->scale = stbtt_ScaleForPixelHeight(&font->stbtt_handle, (float)font_height);
// Unscaled !
int ascent, descent, line_gap;
stbtt_GetFontVMetrics(&font->stbtt_handle, &ascent, &descent, &line_gap);
variation->metrics.max_ascent = ((float)ascent*variation->scale);
variation->metrics.max_descent = ((float)descent*variation->scale);
variation->metrics.line_spacing = ((float)line_gap*variation->scale);
variation->metrics.latin_descent = 9999999;
for (u32 c = 'a'; c <= 'z'; c++) {
// This one is bottom-top as opposed to normally in stbtt where it's top-bottom
int x0, y0, x1, y1;
stbtt_GetCodepointBitmapBox(&font->stbtt_handle, (int)c, variation->scale, variation->scale, &x0, &y0, &x1, &y1);
float c_descent = -(float)y1;
if (c_descent < variation->metrics.latin_descent) {
variation->metrics.latin_descent = c_descent;
}
}
for (u32 c = 'A'; c <= 'Z'; c++) {
// This one is bottom-top as opposed to normally in stbtt where it's top-bottom
int x0, y0, x1, y1;
stbtt_GetCodepointBitmapBox(&font->stbtt_handle, (int)c, variation->scale, variation->scale, &x0, &y0, &x1, &y1);
float c_ascent = (float)abs(y0);
if (c_ascent > variation->metrics.latin_ascent)
variation->metrics.latin_ascent = c_ascent;
}
variation->metrics.new_line_offset
= (variation->metrics.latin_ascent-variation->metrics.latin_descent+variation->metrics.line_spacing);
variation->initted = true;
}
void font_atlas_init(Gfx_Font_Atlas *atlas, Gfx_Font_Variation *variation, u32 first_codepoint) {
stbtt_fontinfo stbtt_handle = variation->font->stbtt_handle;
atlas->first_codepoint = first_codepoint;
atlas->image = make_image(FONT_ATLAS_WIDTH, FONT_ATLAS_HEIGHT, 1, 0, variation->font->allocator);
atlas->glyphs = alloc(variation->font->allocator, variation->codepoint_range_per_atlas*sizeof(Gfx_Glyph));
u32 cursor_x = 0;
u32 cursor_y = 0;
third_party_allocator = variation->font->allocator;
// Used for flipping bitmaps
u8 *temp_row = (u8 *)talloc(variation->height);
for (u32 c = first_codepoint; c < first_codepoint + variation->codepoint_range_per_atlas; c++) {
u32 i = c-first_codepoint;
Gfx_Glyph *glyph = &atlas->glyphs[i];
glyph->codepoint = c;
int w, h, x, y;
void *bitmap = stbtt_GetCodepointBitmap(&stbtt_handle, variation->scale, variation->scale, (int)c, &w, &h, &x, &y);
if (cursor_x+w > FONT_ATLAS_WIDTH) {
cursor_x = 0;
cursor_y += variation->height;
}
if (bitmap) {
// #Speed #Loadtimes
for (int row = 0; row < h; ++row) {
gfx_set_image_data(atlas->image, cursor_x, cursor_y + (h - 1 - row), w, 1, bitmap + (row * w));
}
stbtt_FreeBitmap(bitmap, 0);
}
glyph->xoffset = (float)x;
glyph->yoffset = variation->height - (float)y - (float)h - variation->metrics.max_ascent+variation->metrics.max_descent; // Adjusted yoffset for bottom-up rendering
glyph->width = (float)w;
glyph->height = (float)h;
int advance, left_side_bearing;
stbtt_GetCodepointHMetrics(&stbtt_handle, c, &advance, &left_side_bearing);
glyph->advance = (float)advance*variation->scale;
//glyph->xoffset += (float)left_side_bearing*variation->scale;
glyph->uv.x1 = ((float)cursor_x)/(float)FONT_ATLAS_WIDTH;
glyph->uv.y1 = ((float)cursor_y)/(float)FONT_ATLAS_HEIGHT;
glyph->uv.x2 = ((float)cursor_x+glyph->width)/(float)FONT_ATLAS_WIDTH;
glyph->uv.y2 = ((float)cursor_y+glyph->height)/(float)FONT_ATLAS_HEIGHT;
cursor_x += w;
}
third_party_allocator = ZERO(Allocator);
}
void render_atlas_if_not_yet_rendered(Gfx_Font *font, u32 font_height, u32 codepoint) {
assert(font_height <= MAX_FONT_HEIGHT, "Font height too large; maximum of %d is allowed.", MAX_FONT_HEIGHT);
Gfx_Font_Variation *variation = &font->variations[font_height];
if (!variation->initted) {
font_variation_init(variation, font, font_height);
}
u32 atlas_index = codepoint / variation->codepoint_range_per_atlas;
if (!hash_table_contains(&variation->atlases, atlas_index)) {
Gfx_Font_Atlas atlas = ZERO(Gfx_Font_Atlas);
font_atlas_init(&atlas, variation, atlas_index*variation->codepoint_range_per_atlas);
hash_table_add(&variation->atlases, atlas_index, atlas);
}
}
typedef bool(*Walk_Glyphs_Callback_Proc)(Gfx_Glyph glyph, Gfx_Font_Atlas *atlas, float glyph_x, float glyph_y, void *ud);
typedef struct {
Gfx_Font *font;
string text;
u32 raster_height;
Vector2 scale;
bool ignore_control_codes;
void *ud;
} Walk_Glyphs_Spec;
void walk_glyphs(Walk_Glyphs_Spec spec, Walk_Glyphs_Callback_Proc proc) {
Gfx_Font_Variation *variation = &spec.font->variations[spec.raster_height];
float x = 0;
float y = 0;
u32 last_c = 0;
u32 c = next_utf8(&spec.text);
while (c != 0) {
render_atlas_if_not_yet_rendered(spec.font, spec.raster_height, c);
if (c == '\n') {
x = 0;
y -= variation->metrics.new_line_offset*spec.scale.y;
last_c = 0;
}
if (c < 32 && spec.ignore_control_codes) {
c = next_utf8(&spec.text);
continue;
}
u32 atlas_index = c/variation->codepoint_range_per_atlas;
Gfx_Font_Atlas *atlas = (Gfx_Font_Atlas*)hash_table_find(&variation->atlases, atlas_index);
Gfx_Glyph glyph = atlas->glyphs[c-atlas->first_codepoint];
float glyph_x = x+glyph.xoffset*spec.scale.x;
float glyph_y = y+(glyph.yoffset)*spec.scale.y;
bool should_continue = proc(glyph, atlas, glyph_x, glyph_y, spec.ud);
if (!should_continue) break;
// #Incomplete kerning
x += glyph.advance*spec.scale.x;
if (last_c != 0) {
int kerning_unscaled = stbtt_GetCodepointKernAdvance(&spec.font->stbtt_handle, last_c, c);
float kerning_scaled_to_font_height = kerning_unscaled * variation->scale;
x += kerning_scaled_to_font_height*spec.scale.x;
}
last_c = c;
c = next_utf8(&spec.text);
}
}
Gfx_Font_Metrics get_font_metrics(Gfx_Font *font, u32 raster_height) {
Gfx_Font_Variation *variation = &font->variations[raster_height];
if (!variation->initted) {
font_variation_init(variation, font, raster_height);
}
return variation->metrics;
}
Gfx_Font_Metrics get_font_metrics_scaled(Gfx_Font *font, u32 raster_height, Vector2 scale) {
Gfx_Font_Metrics metrics = get_font_metrics(font, raster_height);
metrics.latin_ascent *= scale.y;
metrics.latin_descent *= scale.y;
metrics.max_ascent *= scale.y;
metrics.max_descent *= scale.y;
metrics.line_spacing *= scale.y;
metrics.new_line_offset *= scale.y;
return metrics;
}
typedef struct {
Gfx_Text_Metrics m;
Gfx_Font *font;
u32 raster_height;
Vector2 scale;
} Measure_Text_Walk_Glyphs_Context;
bool measure_text_glyph_callback(Gfx_Glyph glyph, Gfx_Font_Atlas *atlas, float glyph_x, float glyph_y, void *ud) {
Measure_Text_Walk_Glyphs_Context *c = (Measure_Text_Walk_Glyphs_Context*)ud;
Gfx_Font_Metrics m = get_font_metrics_scaled(c->font, c->raster_height, c->scale);
float functional_left = glyph_x-glyph.xoffset*c->scale.x;
float functional_bottom = glyph_y-glyph.yoffset*c->scale.y; // baseline
float functional_right = functional_left + (glyph.width+glyph.xoffset)*c->scale.x;
float functional_top = functional_bottom + (m.latin_ascent+glyph.yoffset)*c->scale.y;
c->m.functional_pos_min.x = min(c->m.functional_pos_min.x, functional_left);
c->m.functional_pos_min.y = min(c->m.functional_pos_min.y, functional_bottom);
c->m.functional_pos_max.x = max(c->m.functional_pos_max.x, functional_right);
c->m.functional_pos_max.y = max(c->m.functional_pos_max.y, functional_top);
float visual_left = glyph_x;
float visual_bottom = glyph_y;
float visual_right = visual_left + glyph.width*c->scale.x;
float visual_top = visual_bottom + glyph.height*c->scale.y;
c->m.visual_pos_min.x = min(c->m.visual_pos_min.x, visual_left);
c->m.visual_pos_min.y = min(c->m.visual_pos_min.y, visual_bottom);
c->m.visual_pos_max.x = max(c->m.visual_pos_max.x, visual_right);
c->m.visual_pos_max.y = max(c->m.visual_pos_max.y, visual_top);
return true;
}
Gfx_Text_Metrics measure_text(Gfx_Font *font, string text, u32 raster_height, Vector2 scale) {
if (text.count <= 0) return ZERO(Gfx_Text_Metrics);
Measure_Text_Walk_Glyphs_Context c = ZERO(Measure_Text_Walk_Glyphs_Context);
c.scale = scale;
c.font = font;
c.raster_height = raster_height;
walk_glyphs((Walk_Glyphs_Spec){font, text, raster_height, scale, true, &c}, measure_text_glyph_callback);
c.m.functional_size = v2_sub(c.m.functional_pos_max, c.m.functional_pos_min);
c.m.visual_size = v2_sub(c.m.visual_pos_max, c.m.visual_pos_min);
return c.m;
}
typedef struct State_For_Glyph_Line_Break_Search {
u64 *line_break_indices;
u64 *glyph_count_per_line;
float32 width;
u64 line_num;
u64 start_index;
u64 index;
u64 last_space_index;
float32 line_start_x;
float32 last_space_x; // -elon
u64 count;
Vector2 scale;
} State_For_Glyph_Line_Break_Search;
bool text_line_wrapping_callback(Gfx_Glyph g, Gfx_Font_Atlas *atlas, float x, float y, void *ud) {
State_For_Glyph_Line_Break_Search *state = (State_For_Glyph_Line_Break_Search*)ud;
bool is_newline = g.codepoint == '\n';
if (g.codepoint < 32 && !is_newline) {
state->index += 1;
state->count += 1;
return true;
}
float32 glyph_right = x + g.width*state->scale.x;
if (state->last_space_index == state->index) state->last_space_x = x;
if ((g.codepoint != 32 && (glyph_right-state->line_start_x) > state->width) || is_newline) {
bool do_break_at_last_space = state->last_space_index > state->start_index && !is_newline;
u64 break_index;
if (do_break_at_last_space) break_index = state->last_space_index;
else break_index = state->index;
growing_array_add((void**)&state->line_break_indices, &state->start_index);
u64 count_from_start_to_space = break_index-state->start_index;
growing_array_add((void**)&state->glyph_count_per_line, &count_from_start_to_space);
u64 count_from_space_to_now = state->index - break_index;
state->count = count_from_space_to_now;
if (break_index == state->index && (is_newline || g.codepoint == ' ')) {
break_index += 1; // Skip \n and space if that's what we break on
}
state->start_index = break_index;
state->line_num += 1;
if (do_break_at_last_space) state->line_start_x = state->last_space_x;
else state->line_start_x = x;
} else {
if (g.codepoint == ' ') {
state->last_space_index = state->index+1;
}
}
state->index += 1;
state->count += 1;
return true;
};
// Returns a Growing_Array of string, allocated with temp allocator
string *split_text_to_lines_with_wrapping(string str, float32 width, Gfx_Font *font, u32 raster_height, Vector2 scale, bool do_trim_lines) {
Gfx_Font_Variation *variation = &font->variations[raster_height];
string text = str;
State_For_Glyph_Line_Break_Search result = ZERO(State_For_Glyph_Line_Break_Search);
result.width = width;
result.scale = scale;
growing_array_init((void**)&result.line_break_indices, sizeof(u64), get_temporary_allocator());
growing_array_init((void**)&result.glyph_count_per_line, sizeof(u64), get_temporary_allocator());
walk_glyphs((Walk_Glyphs_Spec){font, text, raster_height, scale, false, &result}, text_line_wrapping_callback);
string *lines;
growing_array_init((void**)&lines, sizeof(string), get_temporary_allocator());
for (u64 i = 0; i < growing_array_get_valid_count(result.line_break_indices); i += 1) {
u64 utf8_index = result.line_break_indices[i];
u64 byte_index = utf8_index_to_byte_index(text, utf8_index);
u64 utf8_count = result.glyph_count_per_line[i];
u64 byte_count = utf8_index_to_byte_index(text, utf8_index + utf8_count) - byte_index;
string line_str = string_view(text, byte_index, byte_count);
if (do_trim_lines) line_str = string_trim(line_str);
growing_array_add((void**)&lines, &line_str);
}
if (result.count > 0) {
u64 utf8_index = result.start_index;
u64 byte_index = utf8_index_to_byte_index(text, utf8_index);
u64 utf8_count = result.count;
u64 byte_count = utf8_index_to_byte_index(text, utf8_index + utf8_count) - byte_index;
string line_str = string_view(text, byte_index, byte_count);
if (do_trim_lines) line_str = string_trim(line_str);
growing_array_add((void**)&lines, &line_str);
}
return lines;
}