AtlatestRepositorysigil-graphics
sigil-graphics / tree / src / cfont.c
1
/*2
* font.c - Font loading and text rendering for Sigil Graphics3
*4
* Provides TrueType font loading via stb_truetype and text rendering.5
* Fonts are rasterized to an atlas texture at a specified size.6
*/8
#include "sigil-internal.h"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 */19
static 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 */27
typedef 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 */41
static void ensure_font_type(SigilVM *vm)42
{43
if (sigil_is_undefined(font_type_tag)) {44
font_type_tag = sigil_intern_symbol(vm, "sigil-graphics-font", 19);45
}46
}48
/* Get font from Value */49
static StudioFont *get_font(SigilVM *vm, Value v)50
{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);55
}57
/* Font destructor */58
static void font_destructor(void *data)59
{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
}73
}75
/* Helper to read entire file */76
static unsigned char *read_file(const char *path, size_t *size_out)77
{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;100
}102
/* Helper to extract float from fixnum or flonum */103
static float value_to_float(Value v)104
{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;111
}113
/*114
* (load-font path size) -> <font> or #f115
*116
* Load a TrueType font and rasterize it at the given pixel size.117
*/118
static Value native_load_font(SigilVM *vm, int argc, Value *args)119
{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_EDGE244
};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));254
}256
/*257
* (font? obj) -> boolean258
*/259
static Value native_font_p(SigilVM *vm, int argc, Value *args)260
{261
(void)argc;262
return get_font(vm, args[0]) ? SIGIL_TRUE : SIGIL_FALSE;263
}265
/*266
* (font-size font) -> number267
*/268
static Value native_font_size(SigilVM *vm, int argc, Value *args)269
{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);277
}279
/*280
* (font-line-height font) -> number281
*282
* Returns the recommended line height (ascent - descent + line_gap).283
*/284
static Value native_font_line_height(SigilVM *vm, int argc, Value *args)285
{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);294
}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
*/302
static Value native_draw_text_char(SigilVM *vm, int argc, Value *args)303
{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);364
}366
/*367
* (draw-text font text x y) -> void368
*369
* Draw text at the given position. x,y is the baseline start position.370
*/371
static Value native_draw_text(SigilVM *vm, int argc, Value *args)372
{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;441
}443
/*444
* (text-width font text) -> number445
*446
* Calculate the width of text in pixels.447
*/448
static Value native_text_width(SigilVM *vm, int argc, Value *args)449
{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);478
}480
/*481
* Module initialization482
*/483
void sigil__init_sigil_graphics_font_module(SigilVM *vm)484
{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);512
}