Sharp text and support for different styles
Francesco Pasa 10 months ago
Francesco Pasa 10 months ago
examples/text.c M +2 -2
9 AlbaWindow* window = alba_create_window(&options);
10
11 const AlbaString text1 = alba_create_string("7 lm francesco", 0);
12- alba_draw_text(window, (AlbaVector){100, 100}, (AlbaColor)YELLOW, text1);
13+ alba_draw_text(window, (AlbaVector){100, 100}, text1, (AlbaTextStyle){.font_size = 28});
14
15 // alba_draw_text(window, (AlbaVector){100, 150}, (AlbaColor)WHITE, 0, "ß 🍔");
16
17 "DESCRIPTION\n The malloc() function allocates size bytes and returns a pointer to the allocated memory. The memory is not initialized. If size is 0, then malloc() returns either NULL, or a unique pointer value that can later be successfully passed to free(). The free() function frees the memory space pointed to by ptr, which must have been returned by a previous call to malloc(), calloc(), or realloc(). Otherwise, or if free(ptr) has already been called before, undefined behavior occurs. If ptr is NULL, no operation is performed. The calloc() function allocates memory for an array of nmemb elements of size bytes each and returns a pointer to the allocated memory. The memory is set to zero. If nmemb or size is 0, then calloc() returns either NULL, or a unique pointer value that can later be successfully passed to free(). If the multiplication of nmemb and size would result in integer overflow, then calloc() returns an error. By contrast, an integer overflow would not be detected in the following call to malloc(), with the result that an incorrectly sized block of memory would be allocated: malloc(nmemb * size); The realloc() function changes the size of the memory block pointed to by ptr to size bytes. The contents will be unchanged in the range from the start of the region up to the minimum of the old and new sizes. If the new size is larger than the old size, the added memory will not be initialized. If ptr is NULL, then the call is equivalent to malloc(size), for all values of size; if size is equal to zero, and ptr is not NULL, then the call is equivalent to free(ptr). Unless ptr is NULL, it must have been returned by an earlier call to malloc(), calloc(), or realloc(). If the area pointed to was moved, a free(ptr) is done. The reallocarray() function changes the size of the memory block pointed to by ptr to be large enough for an array of nmemb elements, each of which is size bytes. It is equivalent to the call realloc(ptr, nmemb * size); However, unlike that realloc() call, reallocarray() fails safely in the case where the multiplication would overflow. If such an overflow occurs, reallocarray() returns NULL, sets errno to ENOMEM, and leaves the original block of memory unchanged. RETURN VALUE The malloc() and calloc() functions return a pointer to the allocated memory, which is suitably aligned for any built-in type. On error, these functions return NULL. N",
18 0
19 );
20- alba_draw_text(window, (AlbaVector){100, 200}, (AlbaColor)CYAN, text2);
21+ alba_draw_text(window, (AlbaVector){100, 200}, text2, (AlbaTextStyle){0});
22
23 while (!alba_window_should_close(window))
24 {
include/alba.h M +11 -3
43 #define BLUE {0, 0, 1, 1}
44 #define BLACK {0, 0, 0, 1}
45 #define WHITE {1, 1, 1, 1}
46-#define TRANSPARENT {0, 0, 0, 1}
47+#define TRANSPARENT {0, 0, 0, 0}
48 #define YELLOW {1, 1, 0, 1}
49 #define MAGENTA {1, 0, 1, 1}
50 #define CYAN {0, 1, 1, 1}
51
52 void alba_color_print(const AlbaColor* color);
53+int alba_color_is_transparent(const AlbaColor* color);
54
55 typedef struct
56 {
140 void alba_draw_rounded_rect_aa(AlbaWindow* window, float x0, float y0, float x1, float y1, float r, AlbaColor color);
141
142 // Text drawing
143+typedef struct
144+{
145+ AlbaColor color;
146+ uint64_t font_size;
147+} AlbaTextStyle;
148+
149 void alba_draw_text(
150 AlbaWindow* window,
151- AlbaVector pos, AlbaColor color,
152- AlbaString string
153+ AlbaVector pos,
154+ AlbaString string,
155+ AlbaTextStyle style
156 );
157
158 #endif // ALBA_H
src/atlas.c M +15 -14
4
5 uint64_t hash_glyph(const void* item, const uint64_t seed0, const uint64_t seed1)
6 {
7- return hashmap_murmur((FT_Long*)item, sizeof(FT_Long), seed0, seed1);
8+ return hashmap_murmur((void*)item, sizeof(uint32_t) + sizeof(AlbaTextStyle), seed0, seed1);
9 }
10
11 int compare_glyph(const void* va, const void* vb, void* udata)
36 alba_array_reserve(&atlas->glyph_sizes, capacity * 4);
37 }
38
39-void atlas_append(Atlas* atlas, const FT_Face face, const uint32_t character)
40+void atlas_append(Atlas* atlas, const FT_Face face, const uint32_t character, const AlbaTextStyle style)
41 {
42- const FT_Long glyph_index = FT_Load_Char(face, character, FT_LOAD_RENDER);
43-
44- const FT_Bitmap* bitmap = &face->glyph->bitmap;
45 AlbaGlyph glyph = {
46 .character = character,
47- .glyph_index = glyph_index,
48- .metrics = face->glyph->metrics,
49- .bitmap = *bitmap,
50+ .style = style,
51 };
52+ if (hashmap_get(atlas->glyphs, &glyph) == NULL)
53+ {
54+ const FT_Long glyph_index = FT_Load_Char(face, character, FT_LOAD_RENDER);
55
56- // Copy buffer
57- glyph.bitmap.buffer = malloc(bitmap->rows * bitmap->pitch);
58- memcpy(glyph.bitmap.buffer, bitmap->buffer, bitmap->rows * bitmap->pitch);
59+ const FT_Bitmap* bitmap = &face->glyph->bitmap;
60+ glyph.glyph_index = glyph_index;
61+ glyph.metrics = face->glyph->metrics;
62+ glyph.bitmap = *bitmap;
63+
64+ // Copy buffer (freed by hashmap free function above)
65+ glyph.bitmap.buffer = malloc(bitmap->rows * bitmap->pitch);
66+ memcpy(glyph.bitmap.buffer, bitmap->buffer, bitmap->rows * bitmap->pitch);
67
68- if (hashmap_get(atlas->glyphs, &glyph.character) == NULL)
69- {
70 hashmap_set(atlas->glyphs, &glyph);
71 atlas->dirty = 1;
72 }
84 }
85 }
86
87+// TODO: move
88 void write_png(const char* output_file_path, const int width, const int height, const int depth, void* data)
89 {
90 FILE* fp = fopen(output_file_path, "wb");
158 uint32_t image_size = 256;
159 for (; image_size <= 8192; image_size *= 2)
160 {
161- const uint32_t stride = image_size;
162 uint32_t x = 0;
163 uint32_t y = 0;
164 uint32_t row_height = 0;
src/helpers.c M +5
8 printf("AlbaColor{%f, %f, %f, %f}\n", color->r, color->g, color->b, color->a);
9 }
10
11+int alba_color_is_transparent(const AlbaColor* color)
12+{
13+ return color->r == 0 && color->g == 0 && color->b == 0 && color->a == 0;
14+}
15+
16 void alba_vector_print(const AlbaVector* vector)
17 {
18 printf("AlbaVector{%f, %f}\n", vector->x, vector->y);
src/internal.h M +8 -3
15 {
16 AlbaString string;
17 AlbaVector position;
18- AlbaColor color;
19+ // Make part of style?
20 FT_Face face;
21+ AlbaTextStyle style;
22 } Text;
23
24 typedef struct
41 WGPUBindGroup bind_group;
42 } DrawCall;
43
44-void draw_call_generate_text(DrawCall* draw_call);
45+void draw_call_generate_text(const AlbaWindow* window, DrawCall* draw_call);
46 void draw_call_set_texture(DrawCall* draw_call, WGPUTexture texture);
47 void draw_call_update_bind_group(const AlbaWindow* window, DrawCall* draw_call);
48 void draw_call_release(const DrawCall* draw_call);
73 // Atlas
74 typedef struct
75 {
76+ // Key properties
77 uint32_t character;
78+ AlbaTextStyle style;
79+ // Other properties used during atlas creation
80+ // and text rendering
81 FT_Long glyph_index;
82 FT_Glyph_Metrics metrics;
83 FT_Bitmap bitmap;
96
97 Atlas create_atlas(uint32_t capacity);
98 void atlas_reserve(Atlas* atlas, uint32_t capacity);
99-void atlas_append(Atlas* atlas, FT_Face face, uint32_t character);
100+void atlas_append(Atlas* atlas, FT_Face face, uint32_t character, AlbaTextStyle style);
101 void atlas_generate_image(Atlas* atlas);
102 void atlas_release(const Atlas* atlas);
103
src/text.c M +60 -36
37 {
38 fprintf(stderr, "error: cound not load built in Roboto font");
39 }
40- error = FT_Set_Pixel_Sizes(roboto, 0 /* width, same as height */, 18);
41- if (error)
42- {
43- fprintf(stderr, "error: cound not set Roboto face size");
44- }
45
46- atlas = create_atlas(1024);
47+ atlas = create_atlas(256);
48
49 pthread_mutex_unlock(&freetype_mutex);
50 }
51
52-void atlas_add_chars(Atlas* atlas, const AlbaString string)
53+void atlas_add_chars(Atlas* atlas, const AlbaString string, const AlbaTextStyle style)
54 {
55 const char* text = alba_string_to_char(&string);
56 for (uint64_t i = 0; i < string.length; i++)
57 {
58- atlas_append(atlas, roboto, text[i]); // TODO: unicode
59+ atlas_append(atlas, roboto, text[i], style); // TODO: unicode
60 }
61 }
62
59
60 void atlas_generate_text_geometry(
61 const Atlas* atlas,
62- const AlbaVector pos, const AlbaColor color,
63- const AlbaString string,
64- AlbaArray* vertices, AlbaArray* attributes, AlbaArray* indices
65+ const Text* text,
66+ const AlbaWindow* window,
67+ DrawCall* draw_call
68 )
69 {
70+ const AlbaString string = text->string;
71+ const AlbaTextStyle style = text->style;
72+ AlbaArray* vertices = &draw_call->new_vertices;
73+ AlbaArray* attributes = &draw_call->new_attributes;
74+ AlbaArray* indices = &draw_call->new_indices;
75+
76 alba_array_reserve(vertices, vertices->length + string.length * 4);
77 alba_array_reserve(attributes, attributes->length + string.length * 4);
78 alba_array_reserve(indices, indices->length + string.length * 6);
79
80- const char* text = alba_string_to_char(&string);
81- AlbaVector current = pos;
82+ const char* string_char = alba_string_to_char(&string);
83+ AlbaVector current = text->position;
84 for (uint64_t i = 0; i < string.length; i++)
85 {
86 // TODO: mutex?
87- const FT_Long character = text[i]; // TODO: unicode
88- const AlbaGlyph* glyph = hashmap_get(atlas->glyphs, &character);
89+ const FT_Long character = string_char[i]; // TODO: unicode
90+ const AlbaGlyph* glyph = hashmap_get(
91+ atlas->glyphs,
92+ &(AlbaGlyph){
93+ .character = character,
94+ .style = style
95+ }
96+ );
97 if (glyph == NULL)
98 {
99 fprintf(stderr, "error: unable to find glyph for character '%s'\n", (char*)&character);
94 }
95 const FT_Glyph_Metrics metrics = glyph->metrics;
96
97+ // All these / 64 are because FreeType reports metrics in 1/64th of pixel
98 const AlbaVector origin = {
99- current.x + metrics.horiBearingX / 64,
100- current.y - metrics.horiBearingY / 64,
101+ current.x + metrics.horiBearingX / 64 / window->scale.x,
102+ current.y - metrics.horiBearingY / 64 / window->scale.y,
103 };
104+ // Divide by scaling to correct the scaling applied by the shader.
105+ // We do not want scaling text geometry to avoid making text blurry.
106+ const float width = metrics.width / 64 / window->scale.x;
107+ const float height = metrics.height / 64 / window->scale.y;
108+ // Calculate before adding new vertices
109 const uint32_t vertex_count = vertices->length;
110 alba_array_extend(
111 vertices, 4, (AlbaVector[]){
112 origin,
113- origin.x, origin.y + metrics.height / 64,
114- origin.x + metrics.width / 64, origin.y + metrics.height / 64,
115- origin.x + metrics.width / 64, origin.y,
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- {color, glyph->start},
124- {color, (AlbaVector){glyph->start.x, glyph->end.y}},
125- {color, glyph->end},
126- {color, (AlbaVector){glyph->end.x, glyph->start.y}},
127+ {style.color, glyph->start},
128+ {style.color, (AlbaVector){glyph->start.x, glyph->end.y}},
129+ {style.color, glyph->end},
130+ {style.color, (AlbaVector){glyph->end.x, glyph->start.y}},
131 });
132
133 alba_array_extend(
133 vertex_count + 3
134 });
135
136- current.x += metrics.horiAdvance / 64;
137- // current_y += metrics.vertAdvance / 64;
138+ current.x += metrics.horiAdvance / 64 / window->scale.x;
139 }
140 }
141
142-void draw_call_generate_text(DrawCall* draw_call)
143+void draw_call_generate_text(const AlbaWindow* window, DrawCall* draw_call)
144 {
145 for (uint64_t i = 0; i < draw_call->new_texts.length; i++)
146 {
147 const Text* text = draw_call->new_texts.data + i * draw_call->new_texts.element_size;
148- atlas_generate_text_geometry(
149- &atlas,
150- text->position, text->color, text->string,
151- &draw_call->new_vertices, &draw_call->new_attributes, &draw_call->new_indices
152- );
153+ atlas_generate_text_geometry(&atlas, text, window, draw_call);
154 }
155 }
156
157 void alba_draw_text(
158 AlbaWindow* window,
159- const AlbaVector pos, const AlbaColor color,
160- const AlbaString string
161+ const AlbaVector pos,
162+ const AlbaString string,
163+ AlbaTextStyle style
164 )
165 {
166 ensure_freetype_initialized();
167
168+ if (style.font_size == 0) style.font_size = 16;
169+ if (alba_color_is_transparent(&style.color)) style.color = (AlbaColor)WHITE;
170+
171 pthread_mutex_lock(&freetype_mutex);
172- atlas_add_chars(&atlas, string);
173+ const FT_Error error = FT_Set_Pixel_Sizes(
174+ roboto,
175+ style.font_size * window->scale.x,
176+ style.font_size * window->scale.y
177+ );
178+ if (error)
179+ {
180+ fprintf(stderr, "error: cound not set Roboto face size");
181+ }
182+
183+ atlas_add_chars(&atlas, string, style);
184 pthread_mutex_unlock(&freetype_mutex);
185
186 const Text text_obj = {
187 .string = string,
188 .position = pos,
189- .color = color,
190 .face = roboto,
191+ .style = style,
192 };
193
194 DrawCall* draw_call = alba_array_last(&window->draw_calls);
src/window.c M +4 -3
149 if (width == 0 || height == 0) return;
150
151 window->size = (AlbaVector){width, height};
152- window->scale = (AlbaVector){width / (2 * x_scale), height / (2 * y_scale)};
153+ window->scale = (AlbaVector){x_scale, y_scale};
154+ const AlbaVector scaled_size = (AlbaVector){width / (2 * x_scale), height / (2 * y_scale)};
155
156 // TODO: callback when scaling changes (window changes monitor)
157 // Update uniforms
158- wgpuQueueWriteBuffer(window->queue, window->uniforms, 0, &window->scale, sizeof(AlbaVector));
159+ wgpuQueueWriteBuffer(window->queue, window->uniforms, 0, &scaled_size, sizeof(AlbaVector));
160
161 const WGPUTextureFormat format = wgpuSurfaceGetPreferredFormat(window->surface, window->adapter);
162