src/text.c
  1#include "alba.h"
  2#include "internal.h"
  3
  4#include "freetype/ftglyph.h"
  5#include "hb.h"
  6#include "hb-ft.h"
  7
  8extern char _binary_Roboto_Regular_ttf_start[];
  9extern char _binary_Roboto_Regular_ttf_end[];
 10
 11FT_Library freetype = NULL;
 12FT_Face roboto = NULL;
 13Atlas atlas;
 14pthread_mutex_t freetype_mutex = PTHREAD_MUTEX_INITIALIZER;
 15
 16void ensure_freetype_initialized() {
 17    // already initialized
 18    if (freetype != NULL) return;
 19
 20    pthread_mutex_lock(&freetype_mutex);
 21    if (FT_Init_FreeType(&freetype)) {
 22        fprintf(stderr, "error: initializeing FreeType failed");
 23        pthread_mutex_unlock(&freetype_mutex);
 24        exit(1);
 25    }
 26
 27    FT_Error error = FT_New_Memory_Face(
 28        freetype,
 29        _binary_Roboto_Regular_ttf_start,
 30        _binary_Roboto_Regular_ttf_end - _binary_Roboto_Regular_ttf_start,
 31        0, // face index
 32        &roboto
 33    );
 34    if (error) {
 35        fprintf(stderr, "error: cound not load built in Roboto font");
 36    }
 37
 38    atlas = create_atlas(256);
 39
 40    pthread_mutex_unlock(&freetype_mutex);
 41}
 42
 43void atlas_add_chars(
 44    Atlas *atlas,
 45    const uint32_t glyph_count, const hb_glyph_info_t *glyph_infos,
 46    const AlbaTextStyle style
 47) {
 48    for (uint32_t i = 0; i < glyph_count; i++) {
 49        atlas_append(atlas, roboto, glyph_infos[i].codepoint, style);
 50    }
 51}
 52
 53uint64_t hash_glyph2(const void *item, const uint64_t seed0, const uint64_t seed1) {
 54    return hashmap_murmur((FT_Long *) item, sizeof(FT_Long), seed0, seed1);
 55}
 56
 57void atlas_generate_text_geometry(
 58    const Atlas *atlas,
 59    const Text *text,
 60    const AlbaWindow *window,
 61    DrawCall *draw_call
 62) {
 63    const AlbaTextStyle style = text->style;
 64    AlbaArray *vertices = &draw_call->new_vertices;
 65    AlbaArray *attributes = &draw_call->new_attributes;
 66    AlbaArray *indices = &draw_call->new_indices;
 67
 68    alba_array_reserve(vertices, vertices->length + text->glyph_count * 4);
 69    alba_array_reserve(attributes, attributes->length + text->glyph_count * 4);
 70    alba_array_reserve(indices, indices->length + text->glyph_count * 6);
 71
 72    AlbaVector current = text->position;
 73    for (uint32_t i = 0; i < text->glyph_count; i++) {
 74        // TODO: mutex?
 75        const uint32_t codepoint = text->glyphs_infos[i].codepoint;
 76        const AlbaGlyph *glyph = hashmap_get(
 77            atlas->glyphs,
 78            &(AlbaGlyph){
 79                .codepoint = codepoint,
 80                .style = style
 81            }
 82        );
 83        if (glyph == NULL) {
 84            fprintf(stderr, "error: unable to find glyph for codepoint '%s'\n", (char *) &codepoint);
 85            exit(1);
 86        }
 87
 88        const hb_glyph_position_t *pos = &text->glyph_positions[i];
 89        const FT_Glyph_Metrics metrics = glyph->metrics;
 90
 91        // All these / 64 are because FreeType reports metrics in 1/64th of pixel
 92        const AlbaVector origin = {
 93            current.x + pos->x_offset / 64 + metrics.horiBearingX / 64 / window->scale.x,
 94            current.y - metrics.horiBearingY / 64 / window->scale.y, // TODO: offset
 95        };
 96        // Divide by scaling to correct the scaling applied by the shader.
 97        // We do not want scaling text geometry to avoid making text blurry.
 98        const float width = metrics.width / 64 / window->scale.x;
 99        const float height = metrics.height / 64 / window->scale.y;
100        // Calculate before adding new vertices
101        const uint32_t vertex_count = vertices->length;
102        alba_array_extend(
103            vertices, 4, (AlbaVector[]){
104                origin,
105                origin.x, origin.y + height,
106                origin.x + width, origin.y + height,
107                origin.x + width, origin.y,
108            });
109
110        alba_array_extend(
111            attributes, 4, (AlbaAttribute[]){
112                {style.color, glyph->start},
113                {style.color, (AlbaVector){glyph->start.x, glyph->end.y}},
114                {style.color, glyph->end},
115                {style.color, (AlbaVector){glyph->end.x, glyph->start.y}},
116            });
117
118        alba_array_extend(
119            indices, 6, (uint32_t[]){
120                // first triangle
121                vertex_count,
122                vertex_count + 1,
123                vertex_count + 2,
124                // second triangle
125                vertex_count,
126                vertex_count + 2,
127                vertex_count + 3
128            });
129
130        current.x += pos->x_advance / 64 / window->scale.x;
131        // TODO: vertical
132    }
133}
134
135void draw_call_generate_text(const AlbaWindow *window, DrawCall *draw_call) {
136    for (uint64_t i = 0; i < draw_call->new_texts.length; i++) {
137        const Text *text = draw_call->new_texts.data + i * draw_call->new_texts.element_size;
138        atlas_generate_text_geometry(&atlas, text, window, draw_call);
139    }
140}
141
142void alba_draw_text(
143    AlbaWindow *window,
144    const AlbaVector pos,
145    const AlbaString string,
146    AlbaTextStyle style
147) {
148    ensure_freetype_initialized();
149
150    if (style.font_size == 0) style.font_size = 16;
151    if (alba_color_is_transparent(&style.color)) style.color = (AlbaColor)WHITE;
152
153    const char *chars = alba_string_to_char(&string);
154    hb_buffer_t *buffer = hb_buffer_create();
155
156    // TODO: remove assumption that string is in a single script
157    const hb_script_t script = HB_SCRIPT_LATIN;
158    hb_buffer_set_direction(buffer, HB_DIRECTION_LTR);
159    hb_buffer_set_script(buffer, script);
160
161    // TODO: remove assumption that string is in a single language
162    hb_buffer_set_language(buffer, hb_language_from_string("en", -1));
163
164    hb_buffer_add_utf8(buffer, chars, string.length, 0, string.length);
165
166    pthread_mutex_lock(&freetype_mutex);
167    // This is necessary now as we will render codepoints in the next call
168    const FT_Error error = FT_Set_Pixel_Sizes(
169        roboto,
170        style.font_size * window->scale.x,
171        style.font_size * window->scale.y
172    );
173    if (error) {
174        fprintf(stderr, "error: cound not set Roboto face size");
175    }
176
177    const hb_feature_t features[] = {
178        {HB_TAG('l', 'i', 'g', 'a'), 1, HB_FEATURE_GLOBAL_START, HB_FEATURE_GLOBAL_END},
179    };
180
181    hb_font_t *hb_font = hb_ft_font_create_referenced(roboto); // TODO: free
182    hb_shape(hb_font, buffer, features, 1);
183
184    Text text = {
185        .string = string,
186        .position = pos,
187        .face = roboto,
188        .style = style,
189    };
190    text.glyphs_infos = hb_buffer_get_glyph_infos(buffer, &text.glyph_count);
191    text.glyph_positions = hb_buffer_get_glyph_positions(buffer, &text.glyph_count);
192
193    atlas_add_chars(&atlas, text.glyph_count, text.glyphs_infos, style);
194    pthread_mutex_unlock(&freetype_mutex);
195
196    DrawCall *draw_call = alba_array_last(&window->draw_calls);
197    if (draw_call->texture_set) {
198        draw_call = window_create_draw_call(window);
199    }
200    alba_array_append(&draw_call->new_texts, &text);
201    draw_call->text_dirty = 1;
202}
203