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