summaryrefslogtreecommitdiff
path: root/src/gui/font.c
blob: 5ea595f9bbfbce016018009503186554d4ca3cf3 (plain) (blame)
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
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
// 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 <freetype/ftbitmap.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;
}

static int raster(UIFont *f, float px, int32_t cp, int bold, uint8_t *out,
                  int32_t stride, int32_t cell_w, int32_t cell_h,
                  int32_t ascent, int centered) {
  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 (bold) {
    // Synthetic bold thickens the hinted outline before it is rendered, so
    // the wider stems are rasterized as one shape and keep hinted edges; the
    // advance is untouched, as the cell grid requires. Strength is Ghostty's
    // heuristic: 1/32 of the line height in 26.6, rounded up (one pixel at a
    // 32 px line). A bitmap strike has no outline: widen it by one whole pixel,
    // the classic overstrike, which keeps its pixels sharp.
    FT_GlyphSlot slot = f->face->glyph;
    if (slot->format == FT_GLYPH_FORMAT_OUTLINE) {
      const FT_Pos height = f->face->size->metrics.height;
      FT_Outline_Embolden(&slot->outline, (height + 31) / 32);
    } else if (slot->format == FT_GLYPH_FORMAT_BITMAP &&
               FT_GlyphSlot_Own_Bitmap(slot) == 0) {
      FT_Bitmap_Embolden(f->library, &slot->bitmap, 64, 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 = centered ? (cell_w - (int)bitmap->width) / 2
                          : glyph->bitmap_left + (cell_w - advance) / 2;
  const int gy = centered ? (cell_h - (int)bitmap->rows) / 2
                          : 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;
}

int ui_font_raster(UIFont *f, float px, int32_t cp, int bold, uint8_t *out,
                   int32_t stride, int32_t cell_w, int32_t cell_h,
                   int32_t ascent) {
  return raster(f, px, cp, bold, out, stride, cell_w, cell_h, ascent, 0);
}

int ui_font_raster_centered(UIFont *f, float px, int32_t cp, int bold,
                            uint8_t *out, int32_t stride, int32_t width,
                            int32_t height) {
  return raster(f, px, cp, bold, out, stride, width, height, 0, 1);
}