summaryrefslogtreecommitdiff
path: root/src/gui/font.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/gui/font.c')
-rw-r--r--src/gui/font.c175
1 files changed, 175 insertions, 0 deletions
diff --git a/src/gui/font.c b/src/gui/font.c
new file mode 100644
index 00000000..e94480b5
--- /dev/null
+++ b/src/gui/font.c
@@ -0,0 +1,175 @@
+// FreeType wrapper for gui.zig. The policy intentionally follows Ghostty's
+// FreeType backend: native hinting enabled, the auto-hinter available as a
+// fallback, and the light target for shape-preserving grayscale text.
+#include "font.h"
+#include <freetype-zig.h>
+
+#include <stdlib.h>
+#include <string.h>
+
+struct UIFont {
+ FT_Library library;
+ FT_Face face;
+ FT_F26Dot6 size;
+};
+
+static FT_F26Dot6 size_26_6(float px) {
+ if (px < 1.0f)
+ px = 1.0f;
+ return (FT_F26Dot6)(px * 64.0f + 0.5f);
+}
+
+static int set_size(UIFont *f, float px) {
+ const FT_F26Dot6 size = size_26_6(px);
+ if (f->size == size)
+ return 1;
+ if (FT_Set_Char_Size(f->face, 0, size, 72, 72) != 0)
+ return 0;
+ f->size = size;
+ return 1;
+}
+
+static int ceil_26_6(FT_Pos value) { return (int)((value + 63) >> 6); }
+
+static FT_Int32 load_flags(void) {
+ // Hinting is on because FT_LOAD_NO_HINTING is absent. Likewise, leaving
+ // FT_LOAD_NO_AUTOHINT absent lets FreeType fall back to its auto-hinter
+ // when the face has no native instructions. This is Ghostty's default.
+ return FT_LOAD_DEFAULT | FT_LOAD_TARGET_LIGHT;
+}
+
+UIFont *ui_font_new(const uint8_t *data, int32_t len) {
+ if (!data || len <= 0)
+ return NULL;
+
+ UIFont *f = (UIFont *)calloc(1, sizeof(UIFont));
+ if (!f)
+ return NULL;
+ if (FT_Init_FreeType(&f->library) != 0) {
+ free(f);
+ return NULL;
+ }
+ if (FT_New_Memory_Face(f->library, data, (FT_Long)len, 0, &f->face) != 0) {
+ FT_Done_FreeType(f->library);
+ free(f);
+ return NULL;
+ }
+ // Some symbol fonts do not expose a Unicode charmap. FreeType already
+ // selects a usable default for those, so failure here is non-fatal.
+ (void)FT_Select_Charmap(f->face, FT_ENCODING_UNICODE);
+ return f;
+}
+
+int ui_font_has_glyph(UIFont *f, int32_t cp) {
+ if (!f || cp < 0)
+ return 0;
+ return FT_Get_Char_Index(f->face, (FT_ULong)(uint32_t)cp) != 0;
+}
+
+void ui_font_free(UIFont *f) {
+ if (!f)
+ return;
+ if (f->face)
+ FT_Done_Face(f->face);
+ if (f->library)
+ FT_Done_FreeType(f->library);
+ free(f);
+}
+
+float ui_font_scale_for_height(UIFont *f, float px) {
+ (void)f;
+ // Kept as a size token so the Zig renderer can continue to distinguish
+ // measurement size from high-DPI raster size.
+ return px;
+}
+
+void ui_font_cell_metrics(UIFont *f, float px, int32_t *cell_w, int32_t *cell_h,
+ int32_t *ascent) {
+ if (!f || !cell_w || !cell_h || !ascent || !set_size(f, px)) {
+ if (cell_w)
+ *cell_w = 1;
+ if (cell_h)
+ *cell_h = 1;
+ if (ascent)
+ *ascent = 1;
+ return;
+ }
+
+ const FT_Size_Metrics metrics = f->face->size->metrics;
+ const int asc = ceil_26_6(metrics.ascender);
+ const int desc = ceil_26_6(-metrics.descender);
+ int height = ceil_26_6(metrics.height);
+ if (height < asc + desc)
+ height = asc + desc;
+
+ int width = ceil_26_6(metrics.max_advance);
+ const FT_UInt m = FT_Get_Char_Index(f->face, 'M');
+ if (FT_Load_Glyph(f->face, m, load_flags()) == 0) {
+ const int advance = ceil_26_6(f->face->glyph->advance.x);
+ if (advance > 0)
+ width = advance;
+ }
+
+ *cell_w = width > 0 ? width : 1;
+ *cell_h = height > 0 ? height : 1;
+ *ascent = asc > 0 ? asc : 1;
+}
+
+int ui_font_raster(UIFont *f, float px, int32_t cp, uint8_t *out,
+ int32_t stride, int32_t cell_w, int32_t cell_h,
+ int32_t ascent) {
+ if (!f || !out || stride < cell_w || cell_w <= 0 || cell_h <= 0)
+ return 0;
+ for (int y = 0; y < cell_h; y++)
+ memset(out + (size_t)y * (size_t)stride, 0, (size_t)cell_w);
+ if (!set_size(f, px))
+ return 0;
+
+ const FT_UInt glyph_index =
+ FT_Get_Char_Index(f->face, (FT_ULong)(uint32_t)cp);
+ if (FT_Load_Glyph(f->face, glyph_index, load_flags()) != 0)
+ return 0;
+ if (FT_Render_Glyph(f->face->glyph, FT_RENDER_MODE_NORMAL) != 0)
+ return 0;
+
+ const FT_GlyphSlot glyph = f->face->glyph;
+ const FT_Bitmap *bitmap = &glyph->bitmap;
+ if (!bitmap->buffer || bitmap->width == 0 || bitmap->rows == 0)
+ return 0;
+
+ const int advance = ceil_26_6(glyph->advance.x);
+ // Fallbacks may be proportional. Center their advance in the primary face's
+ // fixed cell, exactly as Ghostty centers a narrower fallback face.
+ const int gx = glyph->bitmap_left + (cell_w - advance) / 2;
+ const int gy = ascent - glyph->bitmap_top;
+ const int pitch = bitmap->pitch;
+ const int row_bytes = pitch < 0 ? -pitch : pitch;
+
+ for (unsigned int y = 0; y < bitmap->rows; y++) {
+ const int dy = gy + (int)y;
+ if (dy < 0 || dy >= cell_h)
+ continue;
+ const unsigned int source_y = pitch < 0 ? bitmap->rows - 1 - y : y;
+ const uint8_t *src = bitmap->buffer + (size_t)source_y * (size_t)row_bytes;
+
+ for (unsigned int x = 0; x < bitmap->width; x++) {
+ const int dx = gx + (int)x;
+ if (dx < 0 || dx >= cell_w)
+ continue;
+
+ uint8_t coverage;
+ if (bitmap->pixel_mode == FT_PIXEL_MODE_MONO) {
+ coverage = (src[x >> 3] & (0x80u >> (x & 7))) ? 255 : 0;
+ } else if (bitmap->pixel_mode == FT_PIXEL_MODE_GRAY) {
+ coverage = src[x];
+ if (bitmap->num_grays > 1 && bitmap->num_grays != 256)
+ coverage = (uint8_t)((unsigned int)coverage * 255u /
+ (bitmap->num_grays - 1u));
+ } else {
+ continue;
+ }
+ out[(size_t)dy * (size_t)stride + (size_t)dx] = coverage;
+ }
+ }
+ return 1;
+}