AtlatestRepositorysigil-graphics

sigil-graphics / tree / src / cfont.c

1/*
2 * font.c - Font loading and text rendering for Sigil Graphics
3 *
4 * Provides TrueType font loading via stb_truetype and text rendering.
5 * Fonts are rasterized to an atlas texture at a specified size.
6 */
7
8#include "sigil-internal.h"
9
10#include "sokol_gfx.h"
11#include "sokol_gp.h"
12#include "stb_truetype.h"
14#include <stdio.h>
15#include <stdlib.h>
16#include <string.h>
18/* Font type tag */
19static Value font_type_tag = SIGIL_UNDEFINED;
21/* ASCII printable range */
22#define FIRST_CHAR 32 /* space */
23#define LAST_CHAR 126 /* tilde */
24#define NUM_CHARS (LAST_CHAR - FIRST_CHAR + 1)
26/* Font structure */
27typedef struct {
28 sg_image atlas;
29 sg_view atlas_view;
30 sg_sampler sampler;
31 int atlas_width;
32 int atlas_height;
33 float font_size;
34 float ascent;
35 float descent;
36 float line_gap;
37 stbtt_bakedchar char_data[NUM_CHARS];
38} StudioFont;
40/* Initialize font type tag */
41static void ensure_font_type(SigilVM *vm)
43 if (sigil_is_undefined(font_type_tag)) {
44 font_type_tag = sigil_intern_symbol(vm, "sigil-graphics-font", 19);
45 }
48/* Get font from Value */
49static StudioFont *get_font(SigilVM *vm, Value v)
51 if (!sigil_is_foreign(v)) return NULL;
52 ensure_font_type(vm);
53 if (sigil_foreign_type(v) != font_type_tag) return NULL;
54 return (StudioFont *)sigil_foreign_data(v);
57/* Font destructor */
58static void font_destructor(void *data)
60 StudioFont *font = (StudioFont *)data;
61 if (font) {
62 if (font->atlas_view.id != SG_INVALID_ID) {
63 sg_destroy_view(font->atlas_view);
64 }
65 if (font->atlas.id != SG_INVALID_ID) {
66 sg_destroy_image(font->atlas);
67 }
68 if (font->sampler.id != SG_INVALID_ID) {
69 sg_destroy_sampler(font->sampler);
70 }
71 free(font);
72 }
75/* Helper to read entire file */
76static unsigned char *read_file(const char *path, size_t *size_out)
78 FILE *f = fopen(path, "rb");
79 if (!f) return NULL;
81 fseek(f, 0, SEEK_END);
82 size_t size = (size_t)ftell(f);
83 fseek(f, 0, SEEK_SET);
85 unsigned char *data = malloc(size);
86 if (!data) {
87 fclose(f);
88 return NULL;
89 }
91 if (fread(data, 1, size, f) != size) {
92 free(data);
93 fclose(f);
94 return NULL;
95 }
97 fclose(f);
98 if (size_out) *size_out = size;
99 return data;
102/* Helper to extract float from fixnum or flonum */
103static float value_to_float(Value v)
105 if (sigil_is_fixnum(v)) {
106 return (float)sigil_as_fixnum(v);
107 } else if (sigil_is_flonum(v)) {
108 return (float)sigil_as_flonum(v);
109 }
110 return 0.0f;
113/*
114 * (load-font path size) -> <font> or #f
115 *
116 * Load a TrueType font and rasterize it at the given pixel size.
117 */
118static Value native_load_font(SigilVM *vm, int argc, Value *args)
120 if (argc < 2) {
121 sigil__vm_error(vm, SIGIL_ERR_ARITY, "load-font: requires path and size");
122 return SIGIL_UNDEFINED;
123 }
125 if (!sigil_is_string(args[0])) {
126 sigil__vm_set_error(vm, SIGIL_ERR_TYPE, "load-font: expected string path");
127 return SIGIL_FALSE;
128 }
130 SigilString *path_str = (SigilString *)sigil_as_ptr(args[0]);
131 const char *path = path_str->data;
132 float font_size = value_to_float(args[1]);
134 if (font_size < 1.0f) font_size = 16.0f;
136 /* Read font file */
137 size_t font_data_size;
138 unsigned char *font_data = read_file(path, &font_data_size);
139 if (!font_data) {
140 return SIGIL_FALSE;
141 }
143 /* Calculate atlas size based on font size */
144 int atlas_width = 512;
145 int atlas_height = 512;
146 if (font_size > 32) {
147 atlas_width = 1024;
148 atlas_height = 1024;
149 }
151 /* Allocate atlas bitmap */
152 unsigned char *atlas_bitmap = calloc(1, (size_t)(atlas_width * atlas_height));
153 if (!atlas_bitmap) {
154 free(font_data);
155 sigil__vm_set_error(vm, SIGIL_ERR_MEMORY, "load-font: out of memory");
156 return SIGIL_FALSE;
157 }
159 /* Create font structure */
160 StudioFont *font = calloc(1, sizeof(StudioFont));
161 if (!font) {
162 free(atlas_bitmap);
163 free(font_data);
164 sigil__vm_set_error(vm, SIGIL_ERR_MEMORY, "load-font: out of memory");
165 return SIGIL_FALSE;
166 }
168 /* Bake font to atlas */
169 int result = stbtt_BakeFontBitmap(font_data, 0, font_size,
170 atlas_bitmap, atlas_width, atlas_height,
171 FIRST_CHAR, NUM_CHARS, font->char_data);
172 if (result <= 0) {
173 free(font);
174 free(atlas_bitmap);
175 free(font_data);
176 sigil__vm_set_error(vm, SIGIL_ERR_RUNTIME, "load-font: failed to bake font");
177 return SIGIL_FALSE;
178 }
180 /* Get font metrics */
181 stbtt_fontinfo info;
182 if (stbtt_InitFont(&info, font_data, 0)) {
183 float scale = stbtt_ScaleForPixelHeight(&info, font_size);
184 int ascent, descent, line_gap;
185 stbtt_GetFontVMetrics(&info, &ascent, &descent, &line_gap);
186 font->ascent = (float)ascent * scale;
187 font->descent = (float)descent * scale;
188 font->line_gap = (float)line_gap * scale;
189 }
191 free(font_data);
193 /* Convert 8-bit bitmap to RGBA for GPU */
194 unsigned char *rgba = malloc((size_t)(atlas_width * atlas_height * 4));
195 if (!rgba) {
196 free(font);
197 free(atlas_bitmap);
198 sigil__vm_set_error(vm, SIGIL_ERR_MEMORY, "load-font: out of memory");
199 return SIGIL_FALSE;
200 }
202 for (int i = 0; i < atlas_width * atlas_height; i++) {
203 rgba[i * 4 + 0] = 255; /* R */
204 rgba[i * 4 + 1] = 255; /* G */
205 rgba[i * 4 + 2] = 255; /* B */
206 rgba[i * 4 + 3] = atlas_bitmap[i]; /* A from grayscale */
207 }
208 free(atlas_bitmap);
210 /* Create GPU texture */
211 sg_image_desc img_desc = {
212 .width = atlas_width,
213 .height = atlas_height,
214 .pixel_format = SG_PIXELFORMAT_RGBA8,
215 .data.mip_levels[0] = {
216 .ptr = rgba,
217 .size = (size_t)(atlas_width * atlas_height * 4)
218 }
219 };
220 font->atlas = sg_make_image(&img_desc);
221 free(rgba);
223 if (font->atlas.id == SG_INVALID_ID) {
224 free(font);
225 sigil__vm_set_error(vm, SIGIL_ERR_RUNTIME, "load-font: failed to create atlas texture");
226 return SIGIL_FALSE;
227 }
229 /* Create view for sokol_gp */
230 font->atlas_view = sgp_make_texture_view_from_image(font->atlas, "font-atlas");
231 if (font->atlas_view.id == SG_INVALID_ID) {
232 sg_destroy_image(font->atlas);
233 free(font);
234 sigil__vm_set_error(vm, SIGIL_ERR_RUNTIME, "load-font: failed to create texture view");
235 return SIGIL_FALSE;
236 }
238 /* Create sampler (linear filtering for smooth text) */
239 sg_sampler_desc smp_desc = {
240 .min_filter = SG_FILTER_LINEAR,
241 .mag_filter = SG_FILTER_LINEAR,
242 .wrap_u = SG_WRAP_CLAMP_TO_EDGE,
243 .wrap_v = SG_WRAP_CLAMP_TO_EDGE
244 };
245 font->sampler = sg_make_sampler(&smp_desc);
247 font->atlas_width = atlas_width;
248 font->atlas_height = atlas_height;
249 font->font_size = font_size;
251 ensure_font_type(vm);
252 return sigil_make_foreign(vm, font_type_tag, font, font_destructor,
253 sizeof(StudioFont));
256/*
257 * (font? obj) -> boolean
258 */
259static Value native_font_p(SigilVM *vm, int argc, Value *args)
261 (void)argc;
262 return get_font(vm, args[0]) ? SIGIL_TRUE : SIGIL_FALSE;
265/*
266 * (font-size font) -> number
267 */
268static Value native_font_size(SigilVM *vm, int argc, Value *args)
270 (void)argc;
271 StudioFont *font = get_font(vm, args[0]);
272 if (!font) {
273 sigil__vm_set_error(vm, SIGIL_ERR_TYPE, "font-size: expected font");
274 return SIGIL_UNDEFINED;
275 }
276 return sigil_flonum(font->font_size);
279/*
280 * (font-line-height font) -> number
281 *
282 * Returns the recommended line height (ascent - descent + line_gap).
283 */
284static Value native_font_line_height(SigilVM *vm, int argc, Value *args)
286 (void)argc;
287 StudioFont *font = get_font(vm, args[0]);
288 if (!font) {
289 sigil__vm_set_error(vm, SIGIL_ERR_TYPE, "font-line-height: expected font");
290 return SIGIL_UNDEFINED;
291 }
292 float line_height = font->ascent - font->descent + font->line_gap;
293 return sigil_flonum(line_height);
296/*
297 * (draw-text-char font char x y) -> number (advance width)
298 *
299 * Draw a single character at position. Returns the advance width for spacing.
300 * This is useful for custom text effects like wavy text.
301 */
302static Value native_draw_text_char(SigilVM *vm, int argc, Value *args)
304 if (argc < 4) {
305 sigil__vm_error(vm, SIGIL_ERR_ARITY, "draw-text-char: requires font, char, x, y");
306 return SIGIL_UNDEFINED;
307 }
309 StudioFont *font = get_font(vm, args[0]);
310 if (!font) {
311 sigil__vm_set_error(vm, SIGIL_ERR_TYPE, "draw-text-char: expected font");
312 return SIGIL_UNDEFINED;
313 }
315 /* Get character - accept either a char or single-char string */
316 int c;
317 if (sigil_is_char(args[1])) {
318 c = (unsigned char)sigil_as_char(args[1]);
319 } else if (sigil_is_string(args[1])) {
320 SigilString *s = (SigilString *)sigil_as_ptr(args[1]);
321 if (s->byte_length == 0) return sigil_flonum(0.0);
322 c = (unsigned char)s->data[0];
323 } else {
324 sigil__vm_set_error(vm, SIGIL_ERR_TYPE, "draw-text-char: expected char or string");
325 return SIGIL_UNDEFINED;
326 }
328 float x = value_to_float(args[2]);
329 float y = value_to_float(args[3]);
331 /* Handle characters outside our range */
332 if (c < FIRST_CHAR || c > LAST_CHAR) {
333 return sigil_flonum(0.0);
334 }
336 stbtt_bakedchar *bc = &font->char_data[c - FIRST_CHAR];
338 /* Bind font atlas */
339 sgp_set_view(0, font->atlas_view);
340 sgp_set_sampler(0, font->sampler);
341 sgp_set_blend_mode(SGP_BLENDMODE_BLEND);
343 /* Destination rectangle */
344 float dx = x + bc->xoff;
345 float dy = y + bc->yoff;
346 float dw = bc->x1 - bc->x0;
347 float dh = bc->y1 - bc->y0;
349 /* Source rectangle in atlas */
350 float sx = (float)bc->x0;
351 float sy = (float)bc->y0;
352 float sw = (float)(bc->x1 - bc->x0);
353 float sh = (float)(bc->y1 - bc->y0);
355 sgp_rect dest = {dx, dy, dw, dh};
356 sgp_rect src = {sx, sy, sw, sh};
357 sgp_draw_textured_rect(0, dest, src);
359 /* Reset state */
360 sgp_reset_view(0);
361 sgp_reset_sampler(0);
363 return sigil_flonum(bc->xadvance);
366/*
367 * (draw-text font text x y) -> void
368 *
369 * Draw text at the given position. x,y is the baseline start position.
370 */
371static Value native_draw_text(SigilVM *vm, int argc, Value *args)
373 if (argc < 4) {
374 sigil__vm_error(vm, SIGIL_ERR_ARITY, "draw-text: requires font, text, x, y");
375 return SIGIL_UNDEFINED;
376 }
378 StudioFont *font = get_font(vm, args[0]);
379 if (!font) {
380 sigil__vm_set_error(vm, SIGIL_ERR_TYPE, "draw-text: expected font");
381 return SIGIL_UNDEFINED;
382 }
384 if (!sigil_is_string(args[1])) {
385 sigil__vm_set_error(vm, SIGIL_ERR_TYPE, "draw-text: expected string");
386 return SIGIL_UNDEFINED;
387 }
389 SigilString *text_str = (SigilString *)sigil_as_ptr(args[1]);
390 const char *text = text_str->data;
391 float x = value_to_float(args[2]);
392 float y = value_to_float(args[3]);
394 /* Bind font atlas */
395 sgp_set_view(0, font->atlas_view);
396 sgp_set_sampler(0, font->sampler);
398 /* Enable blending for text */
399 sgp_set_blend_mode(SGP_BLENDMODE_BLEND);
401 /* Draw each character */
402 float cursor_x = x;
403 for (const char *p = text; *p; p++) {
404 int c = (unsigned char)*p;
406 /* Skip characters outside our range */
407 if (c < FIRST_CHAR || c > LAST_CHAR) {
408 if (c == '\n') {
409 cursor_x = x;
410 y += font->ascent - font->descent + font->line_gap;
411 }
412 continue;
413 }
415 stbtt_bakedchar *bc = &font->char_data[c - FIRST_CHAR];
417 /* Destination rectangle */
418 float dx = cursor_x + bc->xoff;
419 float dy = y + bc->yoff;
420 float dw = bc->x1 - bc->x0;
421 float dh = bc->y1 - bc->y0;
423 /* Source rectangle in atlas (in pixels) */
424 float sx = (float)bc->x0;
425 float sy = (float)bc->y0;
426 float sw = (float)(bc->x1 - bc->x0);
427 float sh = (float)(bc->y1 - bc->y0);
429 sgp_rect dest = {dx, dy, dw, dh};
430 sgp_rect src = {sx, sy, sw, sh};
431 sgp_draw_textured_rect(0, dest, src);
433 cursor_x += bc->xadvance;
434 }
436 /* Reset state */
437 sgp_reset_view(0);
438 sgp_reset_sampler(0);
440 return SIGIL_NIL;
443/*
444 * (text-width font text) -> number
445 *
446 * Calculate the width of text in pixels.
447 */
448static Value native_text_width(SigilVM *vm, int argc, Value *args)
450 if (argc < 2) {
451 sigil__vm_error(vm, SIGIL_ERR_ARITY, "text-width: requires font and text");
452 return SIGIL_UNDEFINED;
453 }
455 StudioFont *font = get_font(vm, args[0]);
456 if (!font) {
457 sigil__vm_set_error(vm, SIGIL_ERR_TYPE, "text-width: expected font");
458 return SIGIL_UNDEFINED;
459 }
461 if (!sigil_is_string(args[1])) {
462 sigil__vm_set_error(vm, SIGIL_ERR_TYPE, "text-width: expected string");
463 return SIGIL_UNDEFINED;
464 }
466 SigilString *text_str = (SigilString *)sigil_as_ptr(args[1]);
467 const char *text = text_str->data;
469 float width = 0.0f;
470 for (const char *p = text; *p; p++) {
471 int c = (unsigned char)*p;
472 if (c >= FIRST_CHAR && c <= LAST_CHAR) {
473 width += font->char_data[c - FIRST_CHAR].xadvance;
474 }
475 }
477 return sigil_flonum(width);
480/*
481 * Module initialization
482 */
483void sigil__init_sigil_graphics_font_module(SigilVM *vm)
485 SigilModule *module = sigil_begin_module(vm, "(sigil graphics font)");
486 if (!module) return;
488 sigil_module_register_native(vm, "load-font", native_load_font,
489 SIGIL_ARITY_EXACT(2), "Load TrueType font at size");
490 sigil_module_register_native(vm, "font?", native_font_p,
491 SIGIL_ARITY_EXACT(1), "Check if object is a font");
492 sigil_module_register_native(vm, "font-size", native_font_size,
493 SIGIL_ARITY_EXACT(1), "Get font pixel size");
494 sigil_module_register_native(vm, "font-line-height", native_font_line_height,
495 SIGIL_ARITY_EXACT(1), "Get recommended line height");
496 sigil_module_register_native(vm, "draw-text", native_draw_text,
497 SIGIL_ARITY_EXACT(4), "Draw text at position");
498 sigil_module_register_native(vm, "draw-text-char", native_draw_text_char,
499 SIGIL_ARITY_EXACT(4), "Draw single char, return advance");
500 sigil_module_register_native(vm, "text-width", native_text_width,
501 SIGIL_ARITY_EXACT(2), "Calculate text width");
503 sigil_module_export(vm, "load-font");
504 sigil_module_export(vm, "font?");
505 sigil_module_export(vm, "font-size");
506 sigil_module_export(vm, "font-line-height");
507 sigil_module_export(vm, "draw-text");
508 sigil_module_export(vm, "draw-text-char");
509 sigil_module_export(vm, "text-width");
511 sigil_end_module(vm);