FREETYPEEEEEE

This commit is contained in:
2026-03-05 02:31:31 -05:00
parent fb358e3c4b
commit 6c43a29f9f
293 changed files with 153022 additions and 262 deletions

View File

@@ -1910,7 +1910,8 @@ static void do_frame(AppState *app) {
}
app->ui_scale = Clamp(0.5f, app->ui_scale, 3.0f);
g_ui_scale = app->ui_scale;
renderer_set_font_scale(app->renderer, app->ui_scale);
F32 dpi_scale = platform_get_dpi_scale(app->window);
renderer_set_font_scale(app->renderer, app->ui_scale * dpi_scale);
// Handle theme change
if (app->settings_theme_sel != app->settings_theme_prev) {

View File

@@ -97,6 +97,10 @@ enum PlatformCursor {
void platform_set_cursor(PlatformCursor cursor);
// DPI scale factor for the window's current monitor.
// Returns 1.0 at 96 DPI (100%), 1.5 at 144 DPI (150%), etc.
F32 platform_get_dpi_scale(PlatformWindow *window);
// Clipboard operations (null-terminated UTF-8 strings).
// platform_clipboard_set copies text to the system clipboard.
// platform_clipboard_get returns a pointer to a static buffer (valid until next call), or nullptr.

View File

@@ -411,6 +411,11 @@ PlatformInput platform_get_input(PlatformWindow *window) {
return result;
}
F32 platform_get_dpi_scale(PlatformWindow *window) {
(void)window;
return 1.0f; // macOS handles Retina via backing scale factor, not DPI
}
void platform_set_cursor(PlatformCursor cursor) {
switch (cursor) {
case PLATFORM_CURSOR_SIZE_WE: [[NSCursor resizeLeftRightCursor] set]; break;

View File

@@ -68,6 +68,14 @@ static LRESULT CALLBACK win32_wndproc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM
return TRUE;
}
break;
case WM_DPICHANGED:
if (g_current_window) {
RECT *suggested = (RECT *)lparam;
SetWindowPos(hwnd, nullptr, suggested->left, suggested->top,
suggested->right - suggested->left, suggested->bottom - suggested->top,
SWP_NOZORDER | SWP_NOACTIVATE);
}
return 0;
case WM_CLOSE:
if (g_current_window)
g_current_window->should_close = true;
@@ -84,6 +92,8 @@ static LRESULT CALLBACK win32_wndproc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM
}
PlatformWindow *platform_create_window(PlatformWindowDesc *desc) {
SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
WNDCLASSEXW wc = {};
wc.cbSize = sizeof(wc);
wc.style = CS_CLASSDC;
@@ -93,13 +103,14 @@ PlatformWindow *platform_create_window(PlatformWindowDesc *desc) {
wc.lpszClassName = L"autosample_wc";
RegisterClassExW(&wc);
UINT dpi = GetDpiForSystem();
int screen_w = GetSystemMetrics(SM_CXSCREEN);
int screen_h = GetSystemMetrics(SM_CYSCREEN);
int x = (screen_w - desc->width) / 2;
int y = (screen_h - desc->height) / 2;
RECT rect = { 0, 0, (LONG)desc->width, (LONG)desc->height };
AdjustWindowRect(&rect, WS_OVERLAPPEDWINDOW, FALSE);
AdjustWindowRectExForDpi(&rect, WS_OVERLAPPEDWINDOW, FALSE, 0, dpi);
int wchar_count = MultiByteToWideChar(CP_UTF8, 0, desc->title, -1, nullptr, 0);
wchar_t *wtitle = (wchar_t *)_malloca(wchar_count * sizeof(wchar_t));
@@ -226,6 +237,11 @@ PlatformInput platform_get_input(PlatformWindow *window) {
return result;
}
F32 platform_get_dpi_scale(PlatformWindow *window) {
if (!window || !window->hwnd) return 1.0f;
return (F32)GetDpiForWindow(window->hwnd) / 96.0f;
}
void platform_set_cursor(PlatformCursor cursor) {
switch (cursor) {
case PLATFORM_CURSOR_SIZE_WE: g_current_cursor = LoadCursor(nullptr, IDC_SIZEWE); break;

View File

@@ -12,6 +12,15 @@
#endif
#include <windows.h>
// FreeType headers — temporarily undefine `internal` macro (base_core.h: #define internal static)
// because FreeType uses `internal` as a struct field name.
#undef internal
#include <ft2build.h>
#include FT_FREETYPE_H
#include FT_BITMAP_H
#define internal static
#include "generated/font_inter.h"
#ifdef _DEBUG
#define DX12_ENABLE_DEBUG_LAYER
#endif
@@ -214,10 +223,9 @@ struct Renderer {
ID3D12Resource *icon_texture;
ID3D12DescriptorHeap *icon_srv_heap;
// GDI text measurement
HDC measure_dc;
HFONT measure_font;
F32 measure_font_size;
// FreeType
FT_Library ft_lib;
FT_Face ft_face;
// Clear color
F32 clear_r = 0.12f;
@@ -400,126 +408,66 @@ static FrameContext *wait_for_next_frame(Renderer *r) {
}
////////////////////////////////
// Font atlas
// Font atlas (FreeType)
static void init_text_measurement(Renderer *r) {
r->measure_dc = CreateCompatibleDC(nullptr);
r->measure_font_size = 0;
r->measure_font = nullptr;
}
static void ensure_measure_font(Renderer *r, F32 font_size) {
if (r->measure_font && r->measure_font_size == font_size) return;
if (r->measure_font) DeleteObject(r->measure_font);
r->measure_font = CreateFontW(
-(S32)(font_size + 0.5f), 0, 0, 0,
FW_NORMAL, FALSE, FALSE, FALSE,
DEFAULT_CHARSET, OUT_TT_PRECIS, CLIP_DEFAULT_PRECIS,
CLEARTYPE_QUALITY, DEFAULT_PITCH | FF_DONTCARE,
L"Segoe UI"
);
r->measure_font_size = font_size;
SelectObject(r->measure_dc, r->measure_font);
static void init_freetype(Renderer *r) {
FT_Init_FreeType(&r->ft_lib);
FT_New_Memory_Face(r->ft_lib, font_inter_data, font_inter_size, 0, &r->ft_face);
}
static B32 create_font_atlas(Renderer *r, F32 font_size) {
const S32 SS = 2; // supersample factor
F32 render_size = font_size * SS;
S32 render_w = FONT_ATLAS_W * SS;
S32 render_h = FONT_ATLAS_H * SS;
S32 pixel_size = (S32)(font_size + 0.5f);
r->font_atlas_size = font_size;
// Create a GDI bitmap to render glyphs at supersampled resolution
HDC dc = CreateCompatibleDC(nullptr);
HFONT font = CreateFontW(
-(S32)(render_size + 0.5f), 0, 0, 0,
FW_NORMAL, FALSE, FALSE, FALSE,
DEFAULT_CHARSET, OUT_TT_PRECIS, CLIP_DEFAULT_PRECIS,
ANTIALIASED_QUALITY, DEFAULT_PITCH | FF_DONTCARE,
L"Segoe UI"
);
SelectObject(dc, font);
FT_Set_Pixel_Sizes(r->ft_face, 0, pixel_size);
r->font_line_height = (F32)(r->ft_face->size->metrics.height >> 6);
// Get line height (at supersample scale, divide back to get 1x)
TEXTMETRICW tm;
GetTextMetricsW(dc, &tm);
r->font_line_height = (F32)tm.tmHeight / SS;
U8 *atlas_data = (U8 *)calloc(1, FONT_ATLAS_W * FONT_ATLAS_H);
// Create DIB section at supersampled resolution
BITMAPINFO bmi = {};
bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
bmi.bmiHeader.biWidth = render_w;
bmi.bmiHeader.biHeight = -render_h; // top-down
bmi.bmiHeader.biPlanes = 1;
bmi.bmiHeader.biBitCount = 32;
bmi.bmiHeader.biCompression = BI_RGB;
void *dib_bits = nullptr;
HBITMAP dib = CreateDIBSection(dc, &bmi, DIB_RGB_COLORS, &dib_bits, nullptr, 0);
SelectObject(dc, dib);
SelectObject(dc, font);
// Clear to black
memset(dib_bits, 0, render_w * render_h * 4);
SetTextColor(dc, RGB(255, 255, 255));
SetBkMode(dc, TRANSPARENT);
// Render each glyph at supersampled resolution
S32 pen_x = SS, pen_y = SS;
S32 pen_x = 1, pen_y = 1;
S32 row_height = 0;
F32 ascender = (F32)(r->ft_face->size->metrics.ascender >> 6);
for (S32 i = 0; i < GLYPH_COUNT; i++) {
char ch = (char)(GLYPH_FIRST + i);
SIZE ch_size = {};
GetTextExtentPoint32A(dc, &ch, 1, &ch_size);
if (FT_Load_Char(r->ft_face, ch, FT_LOAD_RENDER)) continue;
S32 gw = ch_size.cx + 2 * SS; // padding scaled by SS
S32 gh = ch_size.cy + 2 * SS;
FT_GlyphSlot g = r->ft_face->glyph;
S32 bw = (S32)g->bitmap.width;
S32 bh = (S32)g->bitmap.rows;
S32 pad = 2;
S32 cell_w = bw + pad;
S32 cell_h = (S32)r->font_line_height + pad;
if (pen_x + gw >= render_w) {
pen_x = SS;
pen_y += row_height + SS;
if (pen_x + cell_w >= FONT_ATLAS_W) {
pen_x = 1;
pen_y += row_height + 1;
row_height = 0;
}
if (pen_y + cell_h >= FONT_ATLAS_H) break;
if (pen_y + gh >= render_h) break; // out of space
TextOutA(dc, pen_x + SS, pen_y + SS, &ch, 1);
// UVs are fractional — pen_x/render_w == (pen_x/SS)/FONT_ATLAS_W
r->glyphs[i].u0 = (F32)pen_x / (F32)render_w;
r->glyphs[i].v0 = (F32)pen_y / (F32)render_h;
r->glyphs[i].u1 = (F32)(pen_x + gw) / (F32)render_w;
r->glyphs[i].v1 = (F32)(pen_y + gh) / (F32)render_h;
r->glyphs[i].w = (F32)gw / SS; // store at 1x scale
r->glyphs[i].h = (F32)gh / SS;
r->glyphs[i].x_advance = (F32)ch_size.cx / SS;
if (gh > row_height) row_height = gh;
pen_x += gw + SS;
}
GdiFlush();
// Box-filter downsample from supersampled resolution to atlas resolution
U8 *atlas_data = (U8 *)malloc(FONT_ATLAS_W * FONT_ATLAS_H);
U8 *src = (U8 *)dib_bits;
for (S32 y = 0; y < FONT_ATLAS_H; y++) {
for (S32 x = 0; x < FONT_ATLAS_W; x++) {
S32 sum = 0;
for (S32 sy = 0; sy < SS; sy++) {
for (S32 sx = 0; sx < SS; sx++) {
S32 src_idx = ((y * SS + sy) * render_w + (x * SS + sx)) * 4;
sum += src[src_idx + 2]; // R channel from BGRA
}
// Copy glyph bitmap into atlas at baseline-relative position
S32 y_off = (S32)ascender - g->bitmap_top;
for (S32 y = 0; y < bh; y++) {
S32 dst_y = pen_y + y_off + y;
if (dst_y < 0 || dst_y >= FONT_ATLAS_H) continue;
for (S32 x = 0; x < bw; x++) {
S32 dst_x = pen_x + g->bitmap_left + x;
if (dst_x < 0 || dst_x >= FONT_ATLAS_W) continue;
atlas_data[dst_y * FONT_ATLAS_W + dst_x] = g->bitmap.buffer[y * g->bitmap.pitch + x];
}
F32 a = (F32)sum / (F32)(SS * SS * 255);
a = powf(a, 0.55f);
atlas_data[y * FONT_ATLAS_W + x] = (U8)(a * 255.0f + 0.5f);
}
r->glyphs[i].u0 = (F32)pen_x / (F32)FONT_ATLAS_W;
r->glyphs[i].v0 = (F32)pen_y / (F32)FONT_ATLAS_H;
r->glyphs[i].u1 = (F32)(pen_x + cell_w) / (F32)FONT_ATLAS_W;
r->glyphs[i].v1 = (F32)(pen_y + cell_h) / (F32)FONT_ATLAS_H;
r->glyphs[i].w = (F32)cell_w;
r->glyphs[i].h = (F32)cell_h;
r->glyphs[i].x_advance = (F32)(g->advance.x >> 6);
if (cell_h > row_height) row_height = cell_h;
pen_x += cell_w + 1;
}
// Create D3D12 texture
@@ -540,9 +488,6 @@ static B32 create_font_atlas(Renderer *r, F32 font_size) {
&tex_desc, D3D12_RESOURCE_STATE_COPY_DEST, nullptr,
IID_PPV_ARGS(&r->font_texture)) != S_OK) {
free(atlas_data);
DeleteObject(dib);
DeleteObject(font);
DeleteDC(dc);
return false;
}
@@ -611,9 +556,6 @@ static B32 create_font_atlas(Renderer *r, F32 font_size) {
upload_buf->Release();
free(atlas_data);
DeleteObject(dib);
DeleteObject(font);
DeleteDC(dc);
// Create SRV
D3D12_SHADER_RESOURCE_VIEW_DESC srv_desc = {};
@@ -821,11 +763,15 @@ Vec2F32 renderer_measure_text(const char *text, S32 length, F32 font_size, void
Renderer *r = (Renderer *)user_data;
if (!r || length == 0) return v2f32(0, font_size);
ensure_measure_font(r, font_size);
FT_Set_Pixel_Sizes(r->ft_face, 0, (FT_UInt)(font_size + 0.5f));
SIZE sz = {};
GetTextExtentPoint32A(r->measure_dc, text, length, &sz);
return v2f32((F32)sz.cx, (F32)sz.cy);
F32 width = 0;
for (S32 i = 0; i < length; i++) {
if (FT_Load_Char(r->ft_face, (FT_ULong)(unsigned char)text[i], FT_LOAD_DEFAULT)) continue;
width += (F32)(r->ft_face->glyph->advance.x >> 6);
}
F32 height = (F32)(r->ft_face->size->metrics.height >> 6);
return v2f32(width, height);
}
////////////////////////////////
@@ -1107,8 +1053,8 @@ Renderer *renderer_create(RendererDesc *desc) {
if (!create_ui_pipeline(r)) goto fail;
if (!create_ui_buffers(r)) goto fail;
init_text_measurement(r);
if (!create_font_atlas(r, 15.0f)) goto fail;
init_freetype(r);
if (!create_font_atlas(r, 22.0f)) goto fail;
return r;
@@ -1132,8 +1078,8 @@ void renderer_destroy(Renderer *r) {
if (r->pipeline_state) r->pipeline_state->Release();
if (r->root_signature) r->root_signature->Release();
if (r->measure_font) DeleteObject(r->measure_font);
if (r->measure_dc) DeleteDC(r->measure_dc);
if (r->ft_face) FT_Done_Face(r->ft_face);
if (r->ft_lib) FT_Done_FreeType(r->ft_lib);
cleanup_render_targets(r);
@@ -1504,7 +1450,9 @@ void renderer_set_clear_color(Renderer *r, F32 cr, F32 cg, F32 cb) {
}
void renderer_set_font_scale(Renderer *r, F32 scale) {
F32 target_size = 15.0f * scale;
// Build atlas at the largest font size used in the UI (22px clock display)
// so all smaller sizes scale down (crisper) rather than up (blurry).
F32 target_size = 22.0f * scale;
if (fabsf(target_size - r->font_atlas_size) < 0.1f) return;
wait_for_pending(r);
if (r->font_texture) { r->font_texture->Release(); r->font_texture = nullptr; }

View File

@@ -5,10 +5,17 @@
#import <Metal/Metal.h>
#import <QuartzCore/CAMetalLayer.h>
#import <Cocoa/Cocoa.h>
#import <CoreText/CoreText.h>
#import <CoreGraphics/CoreGraphics.h>
#include <math.h>
// FreeType headers — temporarily undefine `internal` macro (base_core.h: #define internal static)
// because FreeType uses `internal` as a struct field name.
#undef internal
#include <ft2build.h>
#include FT_FREETYPE_H
#include FT_BITMAP_H
#define internal static
#include "generated/font_inter.h"
#define NUM_BACK_BUFFERS 2
#define MAX_VERTICES (64 * 1024)
#define MAX_INDICES (MAX_VERTICES * 3)
@@ -173,9 +180,9 @@ struct Renderer {
// Icon atlas
id<MTLTexture> icon_texture;
// Text measurement (Core Text)
CTFontRef measure_font;
F32 measure_font_size;
// FreeType
FT_Library ft_lib;
FT_Face ft_face;
// Current drawable (acquired in begin_frame)
id<CAMetalDrawable> current_drawable;
@@ -185,121 +192,67 @@ struct Renderer {
};
////////////////////////////////
// Font atlas (Core Text + CoreGraphics)
// Font atlas (FreeType)
static void init_freetype(Renderer *r) {
FT_Init_FreeType(&r->ft_lib);
FT_New_Memory_Face(r->ft_lib, font_inter_data, font_inter_size, 0, &r->ft_face);
}
static B32 create_font_atlas(Renderer *r, F32 font_size) {
const S32 SS = 2;
F32 render_size = font_size * SS;
S32 render_w = FONT_ATLAS_W * SS;
S32 render_h = FONT_ATLAS_H * SS;
S32 pixel_size = (S32)(font_size + 0.5f);
r->font_atlas_size = font_size;
// Create Core Text font (system font = SF Pro on macOS)
CTFontRef font = CTFontCreateUIFontForLanguage(kCTFontUIFontSystem, render_size, nullptr);
if (!font) return false;
FT_Set_Pixel_Sizes(r->ft_face, 0, pixel_size);
r->font_line_height = (F32)(r->ft_face->size->metrics.height >> 6);
// Get line height
F32 ascent = (F32)CTFontGetAscent(font);
F32 descent = (F32)CTFontGetDescent(font);
F32 leading = (F32)CTFontGetLeading(font);
r->font_line_height = (ascent + descent + leading) / SS;
U8 *atlas_data = (U8 *)calloc(1, FONT_ATLAS_W * FONT_ATLAS_H);
// Create bitmap context at supersampled resolution
CGColorSpaceRef color_space = CGColorSpaceCreateDeviceGray();
CGContextRef ctx = CGBitmapContextCreate(nullptr, render_w, render_h, 8, render_w,
color_space, kCGImageAlphaNone);
CGColorSpaceRelease(color_space);
if (!ctx) { CFRelease(font); return false; }
// Clear to black
CGContextSetGrayFillColor(ctx, 0.0, 1.0);
CGContextFillRect(ctx, CGRectMake(0, 0, render_w, render_h));
// White text
CGContextSetGrayFillColor(ctx, 1.0, 1.0);
// Flip CG context so (0,0) is top-left, matching our pen_y convention
CGContextTranslateCTM(ctx, 0, render_h);
CGContextScaleCTM(ctx, 1.0, -1.0);
// In the flipped context, Core Text draws upside down unless we undo the
// text matrix flip. Set the text matrix to flip Y back for glyph rendering.
CGContextSetTextMatrix(ctx, CGAffineTransformMake(1, 0, 0, -1, 0, 0));
// Render each glyph
S32 pen_x = SS, pen_y = SS;
S32 pen_x = 1, pen_y = 1;
S32 row_height = 0;
NSDictionary *attrs = @{
(id)kCTFontAttributeName: (__bridge id)font,
(id)kCTForegroundColorFromContextAttributeName: @YES
};
F32 ascender = (F32)(r->ft_face->size->metrics.ascender >> 6);
for (S32 i = 0; i < GLYPH_COUNT; i++) {
char ch = (char)(GLYPH_FIRST + i);
NSString *str = [[NSString alloc] initWithBytes:&ch length:1 encoding:NSASCIIStringEncoding];
NSAttributedString *astr = [[NSAttributedString alloc] initWithString:str attributes:attrs];
CTLineRef line = CTLineCreateWithAttributedString((__bridge CFAttributedStringRef)astr);
if (FT_Load_Char(r->ft_face, ch, FT_LOAD_RENDER)) continue;
CGRect bounds = CTLineGetBoundsWithOptions(line, 0);
S32 gw = (S32)ceilf((F32)bounds.size.width) + 2 * SS;
S32 gh = (S32)ceilf((F32)(ascent + descent)) + 2 * SS;
FT_GlyphSlot g = r->ft_face->glyph;
S32 bw = (S32)g->bitmap.width;
S32 bh = (S32)g->bitmap.rows;
S32 pad = 2;
S32 cell_w = bw + pad;
S32 cell_h = (S32)r->font_line_height + pad;
if (pen_x + gw >= render_w) {
pen_x = SS;
pen_y += row_height + SS;
if (pen_x + cell_w >= FONT_ATLAS_W) {
pen_x = 1;
pen_y += row_height + 1;
row_height = 0;
}
if (pen_y + cell_h >= FONT_ATLAS_H) break;
if (pen_y + gh >= render_h) { CFRelease(line); [astr release]; [str release]; break; }
// Context is flipped to top-left origin. Position baseline:
// pen_y + SS is the top of the glyph cell, baseline is ascent below that.
F32 draw_x = (F32)(pen_x + SS) - (F32)bounds.origin.x;
F32 draw_y = (F32)(pen_y + SS) + ascent;
CGContextSetTextPosition(ctx, draw_x, draw_y);
CTLineDraw(line, ctx);
// UVs (same fractional math as DX12)
r->glyphs[i].u0 = (F32)pen_x / (F32)render_w;
r->glyphs[i].v0 = (F32)pen_y / (F32)render_h;
r->glyphs[i].u1 = (F32)(pen_x + gw) / (F32)render_w;
r->glyphs[i].v1 = (F32)(pen_y + gh) / (F32)render_h;
r->glyphs[i].w = (F32)gw / SS;
r->glyphs[i].h = (F32)gh / SS;
r->glyphs[i].x_advance = (F32)bounds.size.width / SS;
if (gh > row_height) row_height = gh;
pen_x += gw + SS;
CFRelease(line);
[astr release];
[str release];
}
// Box-filter downsample (context is flipped, so bitmap is already top-down)
U8 *src = (U8 *)CGBitmapContextGetData(ctx);
U8 *atlas_data = (U8 *)malloc(FONT_ATLAS_W * FONT_ATLAS_H);
for (S32 y = 0; y < FONT_ATLAS_H; y++) {
for (S32 x = 0; x < FONT_ATLAS_W; x++) {
S32 sum = 0;
for (S32 sy = 0; sy < SS; sy++) {
for (S32 sx = 0; sx < SS; sx++) {
S32 src_idx = (y * SS + sy) * render_w + (x * SS + sx);
sum += src[src_idx];
}
// Copy glyph bitmap into atlas at baseline-relative position
S32 y_off = (S32)ascender - g->bitmap_top;
for (S32 y = 0; y < bh; y++) {
S32 dst_y = pen_y + y_off + y;
if (dst_y < 0 || dst_y >= FONT_ATLAS_H) continue;
for (S32 x = 0; x < bw; x++) {
S32 dst_x = pen_x + g->bitmap_left + x;
if (dst_x < 0 || dst_x >= FONT_ATLAS_W) continue;
atlas_data[dst_y * FONT_ATLAS_W + dst_x] = g->bitmap.buffer[y * g->bitmap.pitch + x];
}
F32 a = (F32)sum / (F32)(SS * SS * 255);
a = powf(a, 0.55f);
atlas_data[y * FONT_ATLAS_W + x] = (U8)(a * 255.0f + 0.5f);
}
}
CGContextRelease(ctx);
CFRelease(font);
r->glyphs[i].u0 = (F32)pen_x / (F32)FONT_ATLAS_W;
r->glyphs[i].v0 = (F32)pen_y / (F32)FONT_ATLAS_H;
r->glyphs[i].u1 = (F32)(pen_x + cell_w) / (F32)FONT_ATLAS_W;
r->glyphs[i].v1 = (F32)(pen_y + cell_h) / (F32)FONT_ATLAS_H;
r->glyphs[i].w = (F32)cell_w;
r->glyphs[i].h = (F32)cell_h;
r->glyphs[i].x_advance = (F32)(g->advance.x >> 6);
if (cell_h > row_height) row_height = cell_h;
pen_x += cell_w + 1;
}
// Create Metal texture
MTLTextureDescriptor *tex_desc = [[MTLTextureDescriptor alloc] init];
@@ -328,38 +281,21 @@ static B32 create_font_atlas(Renderer *r, F32 font_size) {
}
////////////////////////////////
// Text measurement
static void ensure_measure_font(Renderer *r, F32 font_size) {
if (r->measure_font && r->measure_font_size == font_size) return;
if (r->measure_font) CFRelease(r->measure_font);
r->measure_font = CTFontCreateUIFontForLanguage(kCTFontUIFontSystem, font_size, nullptr);
r->measure_font_size = font_size;
}
// Text measurement (FreeType)
Vec2F32 renderer_measure_text(const char *text, S32 length, F32 font_size, void *user_data) {
Renderer *r = (Renderer *)user_data;
if (!r || length == 0) return v2f32(0, font_size);
ensure_measure_font(r, font_size);
FT_Set_Pixel_Sizes(r->ft_face, 0, (FT_UInt)(font_size + 0.5f));
Vec2F32 result = v2f32(0, font_size);
@autoreleasepool {
NSString *str = [[NSString alloc] initWithBytes:text length:length encoding:NSUTF8StringEncoding];
if (!str) return result;
NSDictionary *attrs = @{ (id)kCTFontAttributeName: (__bridge id)r->measure_font };
NSAttributedString *astr = [[NSAttributedString alloc] initWithString:str attributes:attrs];
CTLineRef line = CTLineCreateWithAttributedString((__bridge CFAttributedStringRef)astr);
CGRect bounds = CTLineGetBoundsWithOptions(line, 0);
CFRelease(line);
[astr release];
[str release];
result = v2f32((F32)bounds.size.width, (F32)bounds.size.height);
F32 width = 0;
for (S32 i = 0; i < length; i++) {
if (FT_Load_Char(r->ft_face, (FT_ULong)(unsigned char)text[i], FT_LOAD_DEFAULT)) continue;
width += (F32)(r->ft_face->glyph->advance.x >> 6);
}
return result;
F32 height = (F32)(r->ft_face->size->metrics.height >> 6);
return v2f32(width, height);
}
////////////////////////////////
@@ -689,16 +625,13 @@ Renderer *renderer_create(RendererDesc *desc) {
options:MTLResourceStorageModeShared];
}
// Font atlas
if (!create_font_atlas(r, 15.0f)) {
// FreeType + font atlas
init_freetype(r);
if (!create_font_atlas(r, 22.0f)) {
delete r;
return nullptr;
}
// Init text measurement
r->measure_font = nullptr;
r->measure_font_size = 0;
// Default clear color (dark theme bg_dark)
r->clear_r = 0.12f;
r->clear_g = 0.12f;
@@ -709,7 +642,8 @@ Renderer *renderer_create(RendererDesc *desc) {
void renderer_destroy(Renderer *r) {
if (!r) return;
if (r->measure_font) CFRelease(r->measure_font);
if (r->ft_face) FT_Done_Face(r->ft_face);
if (r->ft_lib) FT_Done_FreeType(r->ft_lib);
delete r;
}
@@ -981,7 +915,9 @@ void renderer_set_clear_color(Renderer *r, F32 cr, F32 cg, F32 cb) {
}
void renderer_set_font_scale(Renderer *r, F32 scale) {
F32 target_size = 15.0f * scale;
// Build atlas at the largest font size used in the UI (22px clock display)
// so all smaller sizes scale down (crisper) rather than up (blurry).
F32 target_size = 22.0f * scale;
if (fabsf(target_size - r->font_atlas_size) < 0.1f) return;
[r->font_texture release];
[r->font_sampler release];