#include "font.h" #include "util.h" #define STB_TRUETYPE_IMPLEMENTATION #include "stb_truetype.h" #include #include #include const char *font_choose_path(const char *explicit_font_path) { static char candidate[4096]; if (explicit_font_path && explicit_font_path[0]) { return explicit_font_path; } const char *env_font = getenv("ECEX_FONT"); if (env_font && env_font[0]) { return env_font; } const char *guix_env = getenv("GUIX_ENVIRONMENT"); if (guix_env && guix_env[0]) { snprintf(candidate, sizeof(candidate), "%s/share/fonts/truetype/DejaVuSansMono.ttf", guix_env); if (ecex_file_exists(candidate)) return candidate; snprintf(candidate, sizeof(candidate), "%s/share/fonts/truetype/dejavu/DejaVuSansMono.ttf", guix_env); if (ecex_file_exists(candidate)) return candidate; snprintf(candidate, sizeof(candidate), "%s/share/fonts/dejavu/DejaVuSansMono.ttf", guix_env); if (ecex_file_exists(candidate)) return candidate; } const char *home = getenv("HOME"); if (home && home[0]) { snprintf(candidate, sizeof(candidate), "%s/.guix-profile/share/fonts/truetype/DejaVuSansMono.ttf", home); if (ecex_file_exists(candidate)) return candidate; snprintf(candidate, sizeof(candidate), "%s/.guix-profile/share/fonts/truetype/dejavu/DejaVuSansMono.ttf", home); if (ecex_file_exists(candidate)) return candidate; snprintf(candidate, sizeof(candidate), "%s/.guix-profile/share/fonts/dejavu/DejaVuSansMono.ttf", home); if (ecex_file_exists(candidate)) return candidate; } if (ecex_file_exists("/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf")) { return "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf"; } if (ecex_file_exists("/usr/share/fonts/TTF/DejaVuSansMono.ttf")) { return "/usr/share/fonts/TTF/DejaVuSansMono.ttf"; } return NULL; } int font_load(font_t *font, const char *path, float size_px) { if (!font || !path) return -1; memset(font, 0, sizeof(*font)); size_t ttf_size = 0; char *ttf_data = ecex_read_entire_file(path, &ttf_size); if (!ttf_data) { fprintf(stderr, "failed to read font: %s\n", path); return -1; } unsigned char *bitmap = calloc(1, ECEX_FONT_BITMAP_W * ECEX_FONT_BITMAP_H); if (!bitmap) { free(ttf_data); return -1; } stbtt_fontinfo info; int ascent = 0; int descent = 0; int line_gap = 0; if (stbtt_InitFont(&info, (const unsigned char *)ttf_data, 0)) { float scale = stbtt_ScaleForPixelHeight(&info, size_px); stbtt_GetFontVMetrics(&info, &ascent, &descent, &line_gap); font->ascent_px = (float)ascent * scale; font->descent_px = (float)(-descent) * scale; font->line_gap_px = (float)line_gap * scale; } if (font->ascent_px <= 0.0f) { font->ascent_px = size_px * 0.80f; } if (font->descent_px <= 0.0f) { font->descent_px = size_px * 0.20f; } if (font->line_gap_px < 0.0f) { font->line_gap_px = 0.0f; } int bake_result = stbtt_BakeFontBitmap( (const unsigned char *)ttf_data, 0, size_px, bitmap, ECEX_FONT_BITMAP_W, ECEX_FONT_BITMAP_H, ECEX_FONT_FIRST_CHAR, ECEX_FONT_CHAR_COUNT, font->chars ); free(ttf_data); if (bake_result <= 0) { fprintf(stderr, "failed to bake font atlas from: %s\n", path); free(bitmap); return -1; } glGenTextures(1, &font->texture); glBindTexture(GL_TEXTURE_2D, font->texture); glTexImage2D(GL_TEXTURE_2D, 0, GL_ALPHA, ECEX_FONT_BITMAP_W, ECEX_FONT_BITMAP_H, 0, GL_ALPHA, GL_UNSIGNED_BYTE, bitmap); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); free(bitmap); font->size_px = size_px; font->line_height = font->ascent_px + font->descent_px + font->line_gap_px; if (font->line_height < size_px * 1.15f) { font->line_height = size_px * 1.15f; } fprintf(stderr, "ecex: loaded font: %s\n", path); return 0; } void font_free(font_t *font) { if (!font) return; if (font->texture) { glDeleteTextures(1, &font->texture); font->texture = 0; } } void draw_text(font_t *font, float x, float y, const char *text) { if (!font || !font->texture || !text) return; glEnable(GL_TEXTURE_2D); glBindTexture(GL_TEXTURE_2D, font->texture); glBegin(GL_QUADS); float start_x = x; for (const char *p = text; *p; p++) { unsigned char c = (unsigned char)*p; if (c == '\n') { x = start_x; y += font->line_height; continue; } if (c == '\t') { x += font->size_px * 4.0f; continue; } if (c < ECEX_FONT_FIRST_CHAR || c >= ECEX_FONT_FIRST_CHAR + ECEX_FONT_CHAR_COUNT) { c = '?'; } stbtt_aligned_quad q; stbtt_GetBakedQuad(font->chars, ECEX_FONT_BITMAP_W, ECEX_FONT_BITMAP_H, c - ECEX_FONT_FIRST_CHAR, &x, &y, &q, 1); glTexCoord2f(q.s0, q.t0); glVertex2f(q.x0, q.y0); glTexCoord2f(q.s1, q.t0); glVertex2f(q.x1, q.y0); glTexCoord2f(q.s1, q.t1); glVertex2f(q.x1, q.y1); glTexCoord2f(q.s0, q.t1); glVertex2f(q.x0, q.y1); } glEnd(); glDisable(GL_TEXTURE_2D); } float text_width(font_t *font, const char *text) { if (!font || !text) return 0.0f; float x = 0.0f; float y = 0.0f; for (const char *p = text; *p; p++) { unsigned char c = (unsigned char)*p; if (c == '\n') break; if (c == '\t') { x += font->size_px * 4.0f; continue; } if (c < ECEX_FONT_FIRST_CHAR || c >= ECEX_FONT_FIRST_CHAR + ECEX_FONT_CHAR_COUNT) { c = '?'; } stbtt_aligned_quad q; stbtt_GetBakedQuad(font->chars, ECEX_FONT_BITMAP_W, ECEX_FONT_BITMAP_H, c - ECEX_FONT_FIRST_CHAR, &x, &y, &q, 1); } return x; }