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