FREETYPEEEEEE
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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];
|
||||
|
||||
Reference in New Issue
Block a user