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