1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
|
// 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;
}
const char *ui_font_name(UIFont *f) {
if (!f || !f->face)
return NULL;
const char *postscript = FT_Get_Postscript_Name(f->face);
if (postscript && postscript[0])
return postscript;
if (f->face->family_name && f->face->family_name[0])
return f->face->family_name;
return NULL;
}
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;
}
|