summary refs log tree commit diff
path: root/src/SystemFonts.c
diff options
context:
space:
mode:
authorWlodekM <[email protected]>2024-06-16 10:35:45 +0300
committerWlodekM <[email protected]>2024-06-16 10:35:45 +0300
commitabef6da56913f1c55528103e60a50451a39628b1 (patch)
treeb3c8092471ecbb73e568cd0d336efa0e7871ee8d /src/SystemFonts.c
initial commit
Diffstat (limited to 'src/SystemFonts.c')
-rw-r--r--src/SystemFonts.c1111
1 files changed, 1111 insertions, 0 deletions
diff --git a/src/SystemFonts.c b/src/SystemFonts.c
new file mode 100644
index 0000000..8f86246
--- /dev/null
+++ b/src/SystemFonts.c
@@ -0,0 +1,1111 @@
+#include "SystemFonts.h"
+#include "Drawer2D.h"
+#include "String.h"
+#include "Funcs.h"
+#include "Platform.h"
+#include "ExtMath.h"
+#include "Logger.h"
+#include "Game.h"
+#include "Event.h"
+#include "Stream.h"
+#include "Utils.h"
+#include "Errors.h"
+#include "Window.h"
+#include "Options.h"
+
+static char defaultBuffer[STRING_SIZE];
+static cc_string font_default = String_FromArray(defaultBuffer);
+
+void SysFont_SetDefault(const cc_string* fontName) {
+	String_Copy(&font_default, fontName);
+	Event_RaiseVoid(&ChatEvents.FontChanged);
+}
+
+static void OnInit(void) {
+	Options_Get(OPT_FONT_NAME, &font_default, "");
+	if (Game_ClassicMode) font_default.length = 0;
+}
+
+struct IGameComponent SystemFonts_Component = {
+	OnInit /* Init  */
+};
+
+
+/*########################################################################################################################*
+*------------------------------------------------------Fallback font------------------------------------------------------*
+*#########################################################################################################################*/
+#define FallbackFont_ValidChar(c) ((c) > 32 && (c) < 127)
+#define FallbackFont_ToIndex(c)   ((c) - 33) /* First valid char is ! */
+#define SPACE_WIDTH 2
+#define CELL_SIZE 8
+
+#define FallbackFont_GetRows(c) (FallbackFont_ValidChar(c) ? font_bitmap[FallbackFont_ToIndex(c)] : missing_cell)
+#define FallbackFont_GetScale(size) ((size) >> 3)
+
+static const cc_uint8 missing_cell[CELL_SIZE] = { 
+	0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF 
+};
+
+/* 8x8 font bitmap, represented with 1 bit for each pixel */
+/* Source: Goodly's texture pack for ClassiCube */
+static const cc_uint8 font_bitmap[][CELL_SIZE] = {
+	  { 0x01,0x01,0x01,0x01,0x01,0x00,0x01,0x00 }, /* ! */
+	  { 0x05,0x05,0x05,0x00,0x00,0x00,0x00,0x00 }, /* " */
+	  { 0x0A,0x0A,0x1F,0x0A,0x1F,0x0A,0x0A,0x00 }, /* # */
+	  { 0x04,0x1F,0x01,0x1F,0x10,0x1F,0x04,0x00 }, /* $ */
+	  { 0x00,0x21,0x11,0x08,0x04,0x22,0x21,0x00 }, /* % */
+	  { 0x0C,0x12,0x0C,0x2E,0x19,0x11,0x2E,0x00 }, /* & */
+	  { 0x01,0x01,0x01,0x00,0x00,0x00,0x00,0x00 }, /* ' */
+	  { 0x04,0x02,0x01,0x01,0x01,0x02,0x04,0x00 }, /* ( */
+	  { 0x01,0x02,0x04,0x04,0x04,0x02,0x01,0x00 }, /* ) */
+	  { 0x00,0x02,0x07,0x02,0x05,0x00,0x00,0x00 }, /* * */
+	  { 0x00,0x04,0x04,0x1F,0x04,0x04,0x00,0x00 }, /* + */
+	  { 0x00,0x00,0x00,0x00,0x00,0x02,0x02,0x01 }, /* , */
+	  { 0x00,0x00,0x00,0x1F,0x00,0x00,0x00,0x00 }, /* - */
+	  { 0x00,0x00,0x00,0x00,0x00,0x01,0x01,0x00 }, /* . */
+	  { 0x08,0x08,0x04,0x04,0x02,0x02,0x01,0x00 }, /* / */
+	  { 0x06,0x09,0x0D,0x0B,0x09,0x09,0x06,0x00 }, /* 0 */
+	  { 0x02,0x03,0x02,0x02,0x02,0x02,0x07,0x00 }, /* 1 */
+	  { 0x06,0x09,0x08,0x04,0x02,0x09,0x0F,0x00 }, /* 2 */
+	  { 0x06,0x09,0x08,0x06,0x08,0x09,0x06,0x00 }, /* 3 */
+	  { 0x05,0x05,0x05,0x0F,0x04,0x04,0x04,0x00 }, /* 4 */
+	  { 0x0F,0x01,0x07,0x08,0x08,0x09,0x06,0x00 }, /* 5 */
+	  { 0x06,0x09,0x01,0x07,0x09,0x09,0x06,0x00 }, /* 6 */
+	  { 0x0F,0x08,0x08,0x04,0x04,0x02,0x02,0x00 }, /* 7 */
+	  { 0x06,0x09,0x09,0x06,0x09,0x09,0x06,0x00 }, /* 8 */
+	  { 0x06,0x09,0x09,0x0E,0x08,0x09,0x06,0x00 }, /* 9 */
+	  { 0x00,0x01,0x01,0x00,0x00,0x01,0x01,0x00 }, /* : */
+	  { 0x00,0x02,0x02,0x00,0x00,0x02,0x02,0x01 }, /* ; */
+	  { 0x00,0x04,0x02,0x01,0x02,0x04,0x00,0x00 }, /* < */
+	  { 0x00,0x00,0x1F,0x00,0x00,0x1F,0x00,0x00 }, /* = */
+	  { 0x00,0x01,0x02,0x04,0x02,0x01,0x00,0x00 }, /* > */
+	  { 0x07,0x09,0x08,0x04,0x02,0x00,0x02,0x00 }, /* ? */
+	  { 0x0E,0x11,0x1D,0x1D,0x1D,0x01,0x0E,0x00 }, /* @ */
+	  { 0x06,0x09,0x09,0x0F,0x09,0x09,0x09,0x00 }, /* A */
+	  { 0x07,0x09,0x09,0x07,0x09,0x09,0x07,0x00 }, /* B */
+	  { 0x06,0x09,0x01,0x01,0x01,0x09,0x06,0x00 }, /* C */
+	  { 0x07,0x09,0x09,0x09,0x09,0x09,0x07,0x00 }, /* D */
+	  { 0x0F,0x01,0x01,0x07,0x01,0x01,0x0F,0x00 }, /* E */
+	  { 0x0F,0x01,0x01,0x07,0x01,0x01,0x01,0x00 }, /* F */
+	  { 0x06,0x09,0x01,0x0D,0x09,0x09,0x06,0x00 }, /* G */
+	  { 0x09,0x09,0x09,0x0F,0x09,0x09,0x09,0x00 }, /* H */
+	  { 0x07,0x02,0x02,0x02,0x02,0x02,0x07,0x00 }, /* I */
+	  { 0x08,0x08,0x08,0x08,0x08,0x09,0x07,0x00 }, /* J */
+	  { 0x09,0x09,0x05,0x03,0x05,0x09,0x09,0x00 }, /* K */
+	  { 0x01,0x01,0x01,0x01,0x01,0x01,0x0F,0x00 }, /* L */
+	  { 0x11,0x1B,0x15,0x11,0x11,0x11,0x11,0x00 }, /* M */
+	  { 0x09,0x0B,0x0D,0x09,0x09,0x09,0x09,0x00 }, /* N */
+	  { 0x06,0x09,0x09,0x09,0x09,0x09,0x06,0x00 }, /* O */
+	  { 0x07,0x09,0x09,0x07,0x01,0x01,0x01,0x00 }, /* P */
+	  { 0x06,0x09,0x09,0x09,0x09,0x05,0x0E,0x00 }, /* Q */
+	  { 0x07,0x09,0x09,0x07,0x09,0x09,0x09,0x00 }, /* R */
+	  { 0x06,0x09,0x01,0x06,0x08,0x09,0x06,0x00 }, /* S */
+	  { 0x07,0x02,0x02,0x02,0x02,0x02,0x02,0x00 }, /* T */
+	  { 0x09,0x09,0x09,0x09,0x09,0x09,0x06,0x00 }, /* U */
+	  { 0x11,0x11,0x11,0x11,0x11,0x0A,0x04,0x00 }, /* V */
+	  { 0x11,0x11,0x11,0x11,0x15,0x1B,0x11,0x00 }, /* W */
+	  { 0x11,0x11,0x0A,0x04,0x0A,0x11,0x11,0x00 }, /* X */
+	  { 0x11,0x11,0x0A,0x04,0x04,0x04,0x04,0x00 }, /* Y */
+	  { 0x0F,0x08,0x04,0x02,0x01,0x01,0x0F,0x00 }, /* Z */
+	  { 0x07,0x01,0x01,0x01,0x01,0x01,0x07,0x00 }, /* [ */
+	  { 0x01,0x01,0x02,0x02,0x04,0x04,0x08,0x00 }, /* \ */
+	  { 0x07,0x04,0x04,0x04,0x04,0x04,0x07,0x00 }, /* ] */
+	  { 0x04,0x0A,0x11,0x00,0x00,0x00,0x00,0x00 }, /* ^ */
+	  { 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x1F }, /* _ */
+	  { 0x01,0x01,0x02,0x00,0x00,0x00,0x00,0x00 }, /* ` */
+	  { 0x00,0x00,0x0E,0x09,0x09,0x0D,0x0B,0x00 }, /* a */
+	  { 0x01,0x01,0x07,0x09,0x09,0x09,0x07,0x00 }, /* b */
+	  { 0x00,0x00,0x06,0x09,0x01,0x09,0x06,0x00 }, /* c */
+	  { 0x08,0x08,0x0E,0x09,0x09,0x09,0x0E,0x00 }, /* d */
+	  { 0x00,0x00,0x06,0x09,0x0F,0x01,0x0E,0x00 }, /* e */
+	  { 0x06,0x01,0x07,0x01,0x01,0x01,0x01,0x00 }, /* f */
+	  { 0x00,0x00,0x0E,0x09,0x09,0x0E,0x08,0x07 }, /* g */
+	  { 0x01,0x01,0x07,0x09,0x09,0x09,0x09,0x00 }, /* h */
+	  { 0x01,0x00,0x01,0x01,0x01,0x01,0x01,0x00 }, /* i */
+	  { 0x08,0x00,0x08,0x08,0x08,0x08,0x09,0x06 }, /* j */
+	  { 0x01,0x01,0x09,0x05,0x03,0x05,0x09,0x00 }, /* k */
+	  { 0x01,0x01,0x01,0x01,0x01,0x01,0x02,0x00 }, /* l */
+	  { 0x00,0x00,0x0B,0x15,0x15,0x11,0x11,0x00 }, /* m */
+	  { 0x00,0x00,0x07,0x09,0x09,0x09,0x09,0x00 }, /* n */
+	  { 0x00,0x00,0x06,0x09,0x09,0x09,0x06,0x00 }, /* o */
+	  { 0x00,0x00,0x07,0x09,0x09,0x07,0x01,0x01 }, /* p */
+	  { 0x00,0x00,0x0E,0x09,0x09,0x0E,0x08,0x08 }, /* q */
+	  { 0x00,0x00,0x05,0x03,0x01,0x01,0x01,0x00 }, /* r */
+	  { 0x00,0x00,0x0E,0x01,0x06,0x08,0x07,0x00 }, /* s */
+	  { 0x02,0x02,0x07,0x02,0x02,0x02,0x02,0x00 }, /* t */
+	  { 0x00,0x00,0x09,0x09,0x09,0x09,0x0E,0x00 }, /* u */
+	  { 0x00,0x00,0x09,0x09,0x09,0x05,0x03,0x00 }, /* v */
+	  { 0x00,0x00,0x11,0x11,0x15,0x15,0x1A,0x00 }, /* w */
+	  { 0x00,0x00,0x05,0x05,0x02,0x05,0x05,0x00 }, /* x */
+	  { 0x00,0x00,0x09,0x09,0x09,0x0E,0x08,0x07 }, /* y */
+	  { 0x00,0x00,0x0F,0x08,0x04,0x02,0x0F,0x00 }, /* z */
+	  { 0x04,0x02,0x02,0x01,0x02,0x02,0x04,0x00 }, /* { */
+	  { 0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01 }, /* | */
+	  { 0x01,0x02,0x02,0x04,0x02,0x02,0x01,0x00 }, /* } */
+	  { 0x00,0x00,0x26,0x19,0x00,0x00,0x00,0x00 }, /* ~ */
+};
+
+static int Fallback_CellWidth(const cc_uint8* rows) {
+	int y, width, widest = 0;
+
+	for (y = 0; y < CELL_SIZE; y++) 
+	{
+		widest = max(widest, rows[y]);
+	}
+	width = Math_ilog2(widest) + 1;
+
+	return width + 1; /* add 1 for padding */
+}
+
+int FallbackFont_TextWidth(const struct DrawTextArgs* args) { 
+	cc_string left = args->text, part;
+	int size  = args->font->size;
+	int scale = FallbackFont_GetScale(size);
+	char colorCode = 'f';
+	int i, width = 0;
+
+	while (Drawer2D_UNSAFE_NextPart(&left, &part, &colorCode))
+	{
+		for (i = 0; i < part.length; i++) 
+		{
+			cc_uint8 c = part.buffer[i];
+			if (c == ' ') {
+				width += SPACE_WIDTH * scale;
+			} else {
+				width += Fallback_CellWidth(FallbackFont_GetRows(c)) * scale;
+			}
+		}
+	}
+
+	width = max(1, width);
+	if (args->useShadow) width += 1 * scale;
+	return width;
+}
+
+static void Fallback_DrawCell(struct Bitmap* bmp, int x, int y, 
+					int scale, const cc_uint8* rows, BitmapCol color) {
+	int dst_width  = CELL_SIZE * scale;
+	int dst_height = CELL_SIZE * scale;
+	int xx, srcX, dstX;
+	int yy, srcY, dstY;
+	BitmapCol* dst_row;
+	cc_uint8 src_row;
+
+	for (yy = 0; yy < dst_height; yy++)
+	{
+		srcY = yy / scale;
+		dstY = y + yy;
+		if (dstY < 0 || dstY >= bmp->height) continue;
+
+		dst_row = Bitmap_GetRow(bmp, dstY);
+		src_row = rows[srcY];
+
+		for (xx = 0; xx < dst_width; xx++)
+		{
+			srcX = xx / scale;
+			dstX = x + xx;
+			if (dstX < 0 || dstX >= bmp->width) continue;
+
+			if (src_row & (1 << srcX)) {
+				dst_row[dstX] = color;
+			}
+		}
+	}
+}
+
+void FallbackFont_DrawText(struct DrawTextArgs* args, struct Bitmap* bmp, int x, int y, cc_bool shadow) {
+	cc_string left = args->text, part;
+	int size  = args->font->size;
+	int scale = FallbackFont_GetScale(size);
+	char colorCode = 'f';
+	const cc_uint8* rows;
+	BitmapCol color;
+	int i;
+
+	if (shadow) { 
+		x += 1 * scale; 
+		y += 1 * scale;
+	}
+
+	/* adjust coords to make drawn text match GDI fonts */
+	y += (args->font->height - size) / 2;
+
+	while (Drawer2D_UNSAFE_NextPart(&left, &part, &colorCode))
+	{
+		color = Drawer2D_GetColor(colorCode);
+		if (shadow) color = GetShadowColor(color);
+	
+		for (i = 0; i < part.length; i++) 
+		{
+			cc_uint8 c = part.buffer[i];
+			if (c == ' ') { x += SPACE_WIDTH * scale; continue; }
+
+			rows = FallbackFont_GetRows(c);
+
+			Fallback_DrawCell(bmp, x, y, scale, rows, color);
+			x += Fallback_CellWidth(rows) * scale;
+		}
+	}
+}
+
+
+/*########################################################################################################################*
+*--------------------------------------------------------Freetype---------------------------------------------------------*
+*#########################################################################################################################*/
+#if defined CC_BUILD_FREETYPE
+#include "freetype/ft2build.h"
+#include "freetype/freetype.h"
+#include "freetype/ftmodapi.h"
+#include "freetype/ftglyph.h"
+
+int cc_strncmp(const char* strA, const char* strB, size_t maxCount) {
+	const unsigned char* a = (const unsigned char*)strA;
+	const unsigned char* b = (const unsigned char*)strB;
+	int i;
+
+	for (i = 0; a[i] && i < maxCount; i++)
+	{
+		if (a[i] != b[i]) return a[i] - b[i];
+	}
+	return 0;
+}
+
+int cc_strcmp(const char* strA, const char* strB) {
+	const unsigned char* a = (const unsigned char*)strA;
+	const unsigned char* b = (const unsigned char*)strB;
+	int i;
+
+	for (i = 0; a[i]; i++)
+	{
+		if (a[i] != b[i]) return a[i] - b[i];
+	}
+	return 0;
+}
+
+size_t cc_strlen(const char* a) {
+	int i = 0;
+	while (*a++) i++;
+	return i;
+}
+
+char* cc_strstr(const char* str, const char* substr) {
+	if (!substr[0]) return (char*)str;
+
+	for (; *str; str++) 
+	{
+		/* Definitely not a match */
+		if (*str != substr[0]) continue;
+
+		/* It's a possible match */
+		if (cc_strcmp(str, substr) == 0) return (char*)str;
+	}
+	return NULL;
+}
+
+int cc_memcmp(const void* ptrA, const void* ptrB, size_t num) {
+	const unsigned char* a = (const unsigned char*)ptrA;
+	const unsigned char* b = (const unsigned char*)ptrB;
+
+	for (; num > 0; num--, a++, b++)
+	{
+		if (*a != *b) return *a - *b;
+	}
+	return 0;
+}
+
+void* cc_memchr(const void* ptr, int ch, size_t num) {
+	const char* a = (const char*)ptr;
+
+	for (; num > 0; num--, a++)
+	{
+		if (*a == ch) return (void*)a;
+	}
+	return NULL;
+}
+
+static void swap(void* v1, void* v2, int size) {
+	char buffer[1024]; 
+
+	Mem_Copy(buffer, v1, size); 
+	Mem_Copy(v1,     v2, size); 
+	Mem_Copy(v2, buffer, size); 
+}
+
+// https://www.geeksforgeeks.org/generic-implementation-of-quicksort-algorithm-in-c/
+static void _qsort(void* v, int size, int left, int right,
+					int (*comp)(const void*, const void*)) {
+	void* ptrL;
+	void* ptrM;
+	void* ptrI;
+	void* ptrE; 
+	int i, last, mid;
+
+	if (left >= right) return;
+	mid = (left + right) / 2; 
+
+	ptrL = (char*)v + (left * size); 
+	ptrM = (char*)v + (mid  * size); 
+	swap(ptrL, ptrM, size); 
+	last = left; 
+
+	for (i = left + 1; i <= right; i++) 
+	{
+		ptrI = (char*)v + (i * size); 
+		if ((*comp)(ptrL, ptrI) > 0) { 
+			last++;
+			ptrE = (char*)v + (last * size); 
+			swap(ptrI, ptrE, size); 
+		} 
+	}
+
+	ptrE = (char*)v + (last * size); 
+	swap(ptrL, ptrE, size); 
+	_qsort(v, size, left, last - 1,  comp); 
+	_qsort(v, size, last + 1, right, comp); 
+}
+
+void cc_qsort(void* v, size_t count, size_t size,
+					int (*comp)(const void*, const void*)) {
+	if (!count) return;
+	_qsort(v, 0, count - 1, size, comp);
+}
+
+
+
+static FT_Library ft_lib;
+static struct FT_MemoryRec_ ft_mem;
+static struct StringsBuffer font_list;
+static cc_bool fonts_changed;
+/* Finds the path and face number of the given system font, with closest matching style */
+static cc_string Font_Lookup(const cc_string* fontName, int flags);
+
+struct SysFont {
+	FT_Face face;
+	struct Stream src, file;
+	FT_StreamRec stream;
+	cc_uint8 buffer[8192]; /* small buffer to minimise disk I/O */
+	cc_uint16 widths[256]; /* cached width of each character glyph */
+	FT_BitmapGlyph glyphs[256];	       /* cached glyphs */
+	FT_BitmapGlyph shadow_glyphs[256]; /* cached glyphs (for back layer shadow) */
+#ifdef CC_BUILD_DARWIN
+	char filename[FILENAME_SIZE + 1];
+#endif
+};
+
+static unsigned long SysFont_Read(FT_Stream s, unsigned long offset, unsigned char* buffer, unsigned long count) {
+	struct SysFont* font;
+	cc_result res;
+	if (!count && offset > s->size) return 1;
+
+	font = (struct SysFont*)s->descriptor.pointer;
+	if (s->pos != offset) font->src.Seek(&font->src, offset);
+
+	res = Stream_Read(&font->src, buffer, count);
+	return res ? 0 : count;
+}
+
+static void SysFont_Done(struct SysFont* font) {
+	int i;
+
+	/* Close the actual underlying file */
+	struct Stream* source = &font->file;
+	if (!source->meta.file) return;
+	source->Close(source);
+
+	for (i = 0; i < 256; i++) 
+	{
+		if (!font->glyphs[i]) continue;
+		FT_Done_Glyph((FT_Glyph)font->glyphs[i]);
+	}
+	for (i = 0; i < 256; i++) 
+	{
+		if (!font->shadow_glyphs[i]) continue;
+		FT_Done_Glyph((FT_Glyph)font->shadow_glyphs[i]);
+	}
+}
+
+static void SysFont_Close(FT_Stream stream) {
+	struct SysFont* font = (struct SysFont*)stream->descriptor.pointer;
+	SysFont_Done(font);
+}
+
+static cc_result SysFont_Init(const cc_string* path, struct SysFont* font, FT_Open_Args* args) {
+	cc_file file;
+	cc_uint32 size;
+	cc_result res;
+#ifdef CC_BUILD_DARWIN
+	cc_string filename;
+#endif
+
+	if ((res = File_Open(&file, path))) return res;
+	if ((res = File_Length(file, &size))) { File_Close(file); return res; }
+
+	font->stream.base = NULL;
+	font->stream.size = size;
+	font->stream.pos  = 0;
+
+	font->stream.descriptor.pointer = font;
+	font->stream.read   = SysFont_Read;
+	font->stream.close  = SysFont_Close;
+
+	font->stream.memory = &ft_mem;
+	font->stream.cursor = NULL;
+	font->stream.limit  = NULL;
+
+	args->flags    = FT_OPEN_STREAM;
+	args->pathname = NULL;
+	args->stream   = &font->stream;
+
+	Stream_FromFile(&font->file, file);
+	Stream_ReadonlyBuffered(&font->src, &font->file, font->buffer, sizeof(font->buffer));
+
+	/* For OSX font suitcase files */
+#ifdef CC_BUILD_DARWIN
+	String_InitArray_NT(filename, font->filename);
+	String_Copy(&filename, path);
+	font->filename[filename.length] = '\0';
+	args->pathname = font->filename;
+#endif
+	Mem_Set(font->widths,        0xFF, sizeof(font->widths));
+	Mem_Set(font->glyphs,        0x00, sizeof(font->glyphs));
+	Mem_Set(font->shadow_glyphs, 0x00, sizeof(font->shadow_glyphs));
+	return 0;
+}
+
+static void* FT_AllocWrapper(FT_Memory memory, long size) { return Mem_TryAlloc(size, 1); }
+static void FT_FreeWrapper(FT_Memory memory, void* block) { Mem_Free(block); }
+static void* FT_ReallocWrapper(FT_Memory memory, long cur_size, long new_size, void* block) {
+	return Mem_TryRealloc(block, new_size, 1);
+}
+
+
+#define FONT_CACHE_FILE "fontscache.txt"
+static cc_string font_candidates[] = {
+	String_FromConst(""),			     /* replaced with font_default */
+	String_FromConst("Arial"),		     /* preferred font on all platforms */
+	String_FromConst("Liberation Sans"), /* Nice looking fallbacks for linux */
+	String_FromConst("Nimbus Sans"),
+	String_FromConst("Bitstream Charter"),
+	String_FromConst("Cantarell"),
+	String_FromConst("DejaVu Sans Book"), 
+	String_FromConst("Century Schoolbook L Roman"), /* commonly available on linux */
+	String_FromConst("Liberation Serif"), /* for SerenityOS */
+	String_FromConst("Slate For OnePlus"), /* Android 10, some devices */
+	String_FromConst("Roboto"), /* Android (broken on some Android 10 devices) */
+	String_FromConst("Geneva"), /* for ancient macOS versions */
+	String_FromConst("Droid Sans"), /* for old Android versions */
+	String_FromConst("Google Sans") /* Droid Sans is now known as Google Sans on some Android devices (e.g. a Pixel 6) */
+};
+
+static void InitFreeTypeLibrary(void) {
+	FT_Error err;
+	if (ft_lib) return;
+
+	ft_mem.alloc   = FT_AllocWrapper;
+	ft_mem.free    = FT_FreeWrapper;
+	ft_mem.realloc = FT_ReallocWrapper;
+
+	err = FT_New_Library(&ft_mem, &ft_lib);
+	if (err) Logger_Abort2(err, "Failed to init freetype");
+	FT_Add_Default_Modules(ft_lib);
+}
+
+static cc_bool loadedPlatformFonts;
+/* Updates fonts list cache with platform's list of fonts */
+/* This should be avoided due to overhead potential */
+static void SysFonts_LoadPlatform(void) {
+	if (loadedPlatformFonts) return;
+	loadedPlatformFonts = true;
+
+	/* TODO this basically gets called all the time on non-Window platforms */
+	/* Maybe we can cache the default system font to avoid this extra work? */
+	if (font_list.count == 0)
+		Window_ShowDialog("One time load", "Initialising font cache, this can take several seconds.");
+
+	InitFreeTypeLibrary();
+	Platform_LoadSysFonts();
+
+	if (fonts_changed) SysFonts_SaveCache();
+}
+
+static cc_bool loadedCachedFonts;
+static void SysFonts_LoadCached(void) {
+	if (loadedCachedFonts) return;
+	loadedCachedFonts = true;
+
+	EntryList_UNSAFE_Load(&font_list, FONT_CACHE_FILE);
+}
+
+void SysFonts_SaveCache(void) {
+	EntryList_Save(&font_list, FONT_CACHE_FILE);
+}
+
+
+/* Some language-specific fonts don't support English letters */
+/* and show entirely as '[]' - better off ignoring such fonts */
+static cc_bool SysFonts_SkipFont(FT_Face face) {
+	if (!face->charmap) return false;
+
+	return FT_Get_Char_Index(face, 'a') == 0 && FT_Get_Char_Index(face, 'z') == 0
+		&& FT_Get_Char_Index(face, 'A') == 0 && FT_Get_Char_Index(face, 'Z') == 0;
+}
+
+static void SysFonts_Add(const cc_string* path, FT_Face face, int index, char type, const char* defStyle) {
+	cc_string key;   char keyBuffer[STRING_SIZE];
+	cc_string value; char valueBuffer[FILENAME_SIZE];
+	cc_string style = String_Empty;
+
+	if (!face->family_name || !(face->face_flags & FT_FACE_FLAG_SCALABLE)) return;
+	/* don't want 'Arial Regular' or 'Arial Bold' */
+	if (face->style_name) {
+		style = String_FromReadonly(face->style_name);
+		if (String_CaselessEqualsConst(&style, defStyle)) style.length = 0;
+	}
+	if (SysFonts_SkipFont(face)) type = 'X';
+
+	String_InitArray(key, keyBuffer);
+	if (style.length) {
+		String_Format3(&key, "%c %c %r", face->family_name, face->style_name, &type);
+	} else {
+		String_Format2(&key, "%c %r", face->family_name, &type);
+	}
+
+	String_InitArray(value, valueBuffer);
+	String_Format2(&value, "%s,%i", path, &index);
+
+	Platform_Log2("Face: %s = %s", &key, &value);
+	EntryList_Set(&font_list, &key, &value, '=');
+	fonts_changed = true;
+}
+
+static int SysFonts_DoRegister(const cc_string* path, int faceIndex, SysFont_RegisterCallback callback) {
+	struct SysFont font;
+	FT_Open_Args args;
+	FT_Error err;
+	int flags, count;
+
+	if (SysFont_Init(path, &font, &args)) return 0;
+	err = FT_New_Face(ft_lib, &args, faceIndex, &font.face);
+	if (err) { SysFont_Done(&font); return 0; }
+
+	flags = font.face->style_flags;
+	count = font.face->num_faces;
+
+	if (flags == (FT_STYLE_FLAG_BOLD | FT_STYLE_FLAG_ITALIC)) {
+		SysFonts_Add(path, font.face, faceIndex, 'Z', "Bold Italic");
+	} else if (flags == FT_STYLE_FLAG_BOLD) {
+		SysFonts_Add(path, font.face, faceIndex, 'B', "Bold");
+	} else if (flags == FT_STYLE_FLAG_ITALIC) {
+		SysFonts_Add(path, font.face, faceIndex, 'I', "Italic");
+	} else if (flags == 0) {
+		SysFonts_Add(path, font.face, faceIndex, 'R', "Regular");
+	}
+
+	if (callback) callback(path);
+	FT_Done_Face(font.face);
+	return count;
+}
+
+cc_result SysFonts_Register(const cc_string* path, SysFont_RegisterCallback callback) {
+	cc_string entry, name, value;
+	cc_string fontPath, index;
+	int i, count;
+
+	/* if font is already known, skip it */
+	for (i = 0; i < font_list.count; i++) {
+		StringsBuffer_UNSAFE_GetRaw(&font_list, i, &entry);
+		String_UNSAFE_Separate(&entry, '=', &name, &value);
+
+		String_UNSAFE_Separate(&value, ',', &fontPath, &index);
+		if (String_CaselessEquals(path, &fontPath)) return 0;
+	}
+
+	count = SysFonts_DoRegister(path, 0, callback);
+	/* there may be more than one font in a font file */
+	for (i = 1; i < count; i++) {
+		SysFonts_DoRegister(path, i, callback);
+	}
+	return 0;
+}
+
+
+static cc_string Font_LookupOf(const cc_string* fontName, const char type) {
+	cc_string name; char nameBuffer[STRING_SIZE + 2];
+	String_InitArray(name, nameBuffer);
+
+	String_Format2(&name, "%s %r", fontName, &type);
+	return EntryList_UNSAFE_Get(&font_list, &name, '=');
+}
+
+static cc_string Font_DoLookup(const cc_string* fontName, int flags) {
+	cc_string path = String_Empty;
+
+	if (flags & FONT_FLAGS_BOLD) path = Font_LookupOf(fontName, 'B');
+	return path.length ? path : Font_LookupOf(fontName, 'R');
+}
+
+static cc_string Font_Lookup(const cc_string* fontName, int flags) {
+	cc_string path;
+	
+	SysFonts_LoadCached();
+	path = Font_DoLookup(fontName, flags);
+	if (path.length) return path;
+
+	SysFonts_LoadPlatform();
+	return Font_DoLookup(fontName, flags);
+}
+
+
+const cc_string* SysFonts_UNSAFE_GetDefault(void) {
+	cc_string* font, path;
+	int i;
+	font_candidates[0] = font_default;
+
+	for (i = 0; i < Array_Elems(font_candidates); i++) {
+		font = &font_candidates[i];
+		if (!font->length) continue;
+
+		path = Font_Lookup(font, FONT_FLAGS_NONE);
+		if (path.length) return font;
+	}
+	return &String_Empty;
+}
+
+void SysFonts_GetNames(struct StringsBuffer* buffer) {
+	cc_string entry, name, path;
+	int i;
+	SysFonts_LoadCached();
+	SysFonts_LoadPlatform();
+
+	for (i = 0; i < font_list.count; i++) {
+		StringsBuffer_UNSAFE_GetRaw(&font_list, i, &entry);
+		String_UNSAFE_Separate(&entry, '=', &name, &path);
+
+		/* only want Regular fonts here */
+		if (name.length < 2 || name.buffer[name.length - 1] != 'R') continue;
+		name.length -= 2;
+		StringsBuffer_Add(buffer, &name);
+	}
+	StringsBuffer_Sort(buffer);
+}
+
+#define TEXT_CEIL(x) (((x) + 63) >> 6)
+cc_result SysFont_Make(struct FontDesc* desc, const cc_string* fontName, int size, int flags) {
+	struct SysFont* font;
+	cc_string value, path, index;
+	int faceIndex, dpiX, dpiY;
+	FT_Open_Args args;
+	FT_Error err;
+
+	desc->size   = size;
+	desc->flags  = flags;
+	desc->handle = NULL;
+
+	value = Font_Lookup(fontName, flags);
+	if (!value.length) return ERR_INVALID_ARGUMENT;
+	String_UNSAFE_Separate(&value, ',', &path, &index);
+	Convert_ParseInt(&index, &faceIndex);
+
+	font = (struct SysFont*)Mem_TryAlloc(1, sizeof(struct SysFont));
+	if (!font) return ERR_OUT_OF_MEMORY;
+
+	InitFreeTypeLibrary();
+	if ((err = SysFont_Init(&path, font, &args))) { Mem_Free(font); return err; }
+	desc->handle = font;
+
+	/* TODO: Use 72 instead of 96 dpi for mobile devices */
+	dpiX = (int)(DisplayInfo.ScaleX * 96);
+	dpiY = (int)(DisplayInfo.ScaleY * 96);
+
+	if ((err = FT_New_Face(ft_lib, &args, faceIndex, &font->face)))     return err;
+	if ((err = FT_Set_Char_Size(font->face, size * 64, 0, dpiX, dpiY))) return err;
+
+	/* height of any text when drawn with the given system font */
+	desc->height = TEXT_CEIL(font->face->size->metrics.height);
+	return 0;
+}
+
+void SysFont_MakeDefault(struct FontDesc* desc, int size, int flags) {
+	cc_string* font;
+	cc_result res;
+	int i;
+	font_candidates[0] = font_default;
+
+	for (i = 0; i < Array_Elems(font_candidates); i++) {
+		font = &font_candidates[i];
+		if (!font->length) continue;
+		res  = SysFont_Make(desc, &font_candidates[i], size, flags);
+
+		/* Cached system fonts list may be outdated - force update it */
+		if (res == ReturnCode_FileNotFound && !loadedPlatformFonts) {
+			StringsBuffer_Clear(&font_list);
+			SysFonts_LoadPlatform();
+			res = SysFont_Make(desc, &font_candidates[i], size, flags);
+		}
+
+		if (res == ERR_INVALID_ARGUMENT) {
+			/* Fon't doesn't exist in list, skip over it */
+		} else if (res) {
+			Font_Free(desc);
+			Logger_SysWarn2(res, "creating font", font);
+		} else {
+			if (i) String_Copy(&font_candidates[0], font);
+			return;
+		}
+	}
+
+	Window_ShowDialog("Failed to init default font", "Falling back to built-in font");
+	Font_MakeBitmapped(desc, size, flags);
+}
+
+void SysFont_Free(struct FontDesc* desc) {
+	struct SysFont* font = (struct SysFont*)desc->handle;
+	FT_Done_Face(font->face);
+	Mem_Free(font);
+}
+
+int SysFont_TextWidth(struct DrawTextArgs* args) {
+	struct SysFont* font = (struct SysFont*)args->font->handle;
+	FT_Face face   = font->face;
+	cc_string text = args->text;
+	int i, width = 0, charWidth;
+	FT_Error res;
+	cc_unichar uc;
+
+	for (i = 0; i < text.length; i++) {
+		char c = text.buffer[i];
+		if (c == '&' && Drawer2D_ValidColorCodeAt(&text, i + 1)) {
+			i++; continue; /* skip over the color code */
+		}
+
+		charWidth = font->widths[(cc_uint8)c];
+		/* need to calculate glyph width */
+		if (charWidth == UInt16_MaxValue) {
+			uc  = Convert_CP437ToUnicode(c);
+			res = FT_Load_Char(face, uc, 0);
+
+			if (res) {
+				Platform_Log2("Error %e measuring width of %r", &res, &c);
+				charWidth = 0;
+			} else {
+				charWidth = face->glyph->advance.x;		
+			}
+
+			font->widths[(cc_uint8)c] = charWidth;
+		}
+		width += charWidth;
+	}
+	if (!width) return 0;
+
+	width = TEXT_CEIL(width);
+	if (args->useShadow) width += 2;
+	return width;
+}
+
+static void DrawGrayscaleGlyph(FT_Bitmap* img, struct Bitmap* bmp, int x, int y, BitmapCol col) {
+	cc_uint8* src;
+	BitmapCol* dst;
+	cc_uint8 I, invI; /* intensity */
+	int xx, yy;
+
+	for (yy = 0; yy < img->rows; yy++) {
+		if ((unsigned)(y + yy) >= (unsigned)bmp->height) continue;
+		src = img->buffer + (yy * img->pitch);
+		dst = Bitmap_GetRow(bmp, y + yy) + x;
+
+		for (xx = 0; xx < img->width; xx++, src++, dst++) {
+			if ((unsigned)(x + xx) >= (unsigned)bmp->width) continue;
+			I = *src; invI = UInt8_MaxValue - I;
+
+			/* TODO: Support transparent text */
+			/* dst->A = ((col.A * intensity) >> 8) + ((dst->A * invIntensity) >> 8);*/
+			/* TODO: Not shift when multiplying */
+			*dst = BitmapCol_Make(
+				((BitmapCol_R(col) * I) >> 8) + ((BitmapCol_R(*dst) * invI) >> 8),
+				((BitmapCol_G(col) * I) >> 8) + ((BitmapCol_G(*dst) * invI) >> 8),
+				((BitmapCol_B(col) * I) >> 8) + ((BitmapCol_B(*dst) * invI) >> 8),
+				                           I  + ((BitmapCol_A(*dst) * invI) >> 8)
+			);
+		}
+	}
+}
+
+static void DrawBlackWhiteGlyph(FT_Bitmap* img, struct Bitmap* bmp, int x, int y, BitmapCol col) {
+	cc_uint8* src;
+	BitmapCol* dst;
+	cc_uint8 intensity;
+	int xx, yy;
+
+	for (yy = 0; yy < img->rows; yy++) {
+		if ((unsigned)(y + yy) >= (unsigned)bmp->height) continue;
+		src = img->buffer + (yy * img->pitch);
+		dst = Bitmap_GetRow(bmp, y + yy) + x;
+
+		for (xx = 0; xx < img->width; xx++, dst++) {
+			if ((unsigned)(x + xx) >= (unsigned)bmp->width) continue;
+			intensity = src[xx >> 3];
+
+			/* TODO: transparent text (don't set A to 255) */
+			if (intensity & (1 << (7 - (xx & 7)))) {
+				*dst = col | BitmapColor_A_Bits(255);
+			}
+		}
+	}
+}
+
+static FT_Vector shadow_delta = { 83, -83 };
+void SysFont_DrawText(struct DrawTextArgs* args, struct Bitmap* bmp, int x, int y, cc_bool shadow) {
+	struct SysFont* font  = (struct SysFont*)args->font->handle;
+	FT_BitmapGlyph* glyphs = font->glyphs;
+
+	FT_Face face   = font->face;
+	cc_string text = args->text;
+	int descender, height, begX = x;
+	BitmapCol color;
+	
+	/* glyph state */
+	FT_BitmapGlyph glyph;
+	FT_Bitmap* img;
+	int i, offset;
+	FT_Error res;
+	cc_unichar uc;
+
+	if (shadow) {
+		glyphs = font->shadow_glyphs;
+		FT_Set_Transform(face, NULL, &shadow_delta);
+	}
+
+	height    = args->font->height;
+	descender = TEXT_CEIL(face->size->metrics.descender);
+
+	color = Drawer2D.Colors['f'];
+	if (shadow) color = GetShadowColor(color);
+
+	for (i = 0; i < text.length; i++) {
+		char c = text.buffer[i];
+		if (c == '&' && Drawer2D_ValidColorCodeAt(&text, i + 1)) {
+			color = Drawer2D_GetColor(text.buffer[i + 1]);
+
+			if (shadow) color = GetShadowColor(color);
+			i++; continue; /* skip over the color code */
+		}
+
+		glyph = glyphs[(cc_uint8)c];
+		if (!glyph) {
+			uc  = Convert_CP437ToUnicode(c);
+			res = FT_Load_Char(face, uc, FT_LOAD_RENDER);
+
+			if (res) {
+				Platform_Log2("Error %e drawing %r", &res, &text.buffer[i]);
+				continue;
+			}
+
+			/* due to FT_LOAD_RENDER, glyph is always a bitmap one */
+			FT_Get_Glyph(face->glyph, (FT_Glyph*)&glyph); /* TODO: Check error */
+			glyphs[(cc_uint8)c] = glyph;
+		}
+
+		offset = (height + descender) - glyph->top;
+		x += glyph->left; y += offset;
+
+		img = &glyph->bitmap;
+		if (img->num_grays == 2) {
+			DrawBlackWhiteGlyph(img, bmp, x, y, color);
+		} else {
+			DrawGrayscaleGlyph(img, bmp, x, y, color);
+		}
+
+		x += TEXT_CEIL(glyph->root.advance.x >> 10);
+		x -= glyph->left; y -= offset;
+	}
+
+	if (args->font->flags & FONT_FLAGS_UNDERLINE) {
+		int ul_pos   = FT_MulFix(face->underline_position,  face->size->metrics.y_scale);
+		int ul_thick = FT_MulFix(face->underline_thickness, face->size->metrics.y_scale);
+
+		int ulHeight = TEXT_CEIL(ul_thick);
+		int ulY	     = height + TEXT_CEIL(ul_pos);
+		Drawer2D_Fill(bmp, begX, ulY + y, x - begX, ulHeight, color);
+	}
+
+	if (shadow) FT_Set_Transform(face, NULL, NULL);
+}
+#elif defined CC_BUILD_WEB
+static cc_string font_arial = String_FromConst("Arial");
+
+const cc_string* SysFonts_UNSAFE_GetDefault(void) {
+	/* Fallback to Arial as default font */
+	/* TODO use serif instead?? */
+	return font_default.length ? &font_default : &font_arial;
+}
+
+void SysFonts_GetNames(struct StringsBuffer* buffer) {
+	static const char* font_names[] = { 
+		"Arial", "Arial Black", "Courier New", "Comic Sans MS", "Georgia", "Garamond", 
+		"Helvetica", "Impact", "Tahoma", "Times New Roman", "Trebuchet MS", "Verdana",
+		"cursive", "fantasy", "monospace", "sans-serif", "serif", "system-ui"
+	};
+	int i;
+
+	for (i = 0; i < Array_Elems(font_names); i++) {
+		cc_string str = String_FromReadonly(font_names[i]);
+		StringsBuffer_Add(buffer, &str);
+	}
+}
+
+cc_result SysFont_Make(struct FontDesc* desc, const cc_string* fontName, int size, int flags) {
+	desc->size   = size;
+	desc->flags  = flags;
+	desc->height = Drawer2D_AdjHeight(size);
+
+	desc->handle = Mem_TryAlloc(fontName->length + 1, 1);
+	if (!desc->handle) return ERR_OUT_OF_MEMORY;
+	
+	String_CopyToRaw(desc->handle, fontName->length + 1, fontName);
+	return 0;
+}
+
+void SysFont_MakeDefault(struct FontDesc* desc, int size, int flags) {
+	SysFont_Make(desc, SysFonts_UNSAFE_GetDefault(), size, flags);
+}
+
+void SysFont_Free(struct FontDesc* desc) {
+	Mem_Free(desc->handle);
+}
+
+void SysFonts_SaveCache(void) { }
+cc_result SysFonts_Register(const cc_string* path, SysFont_RegisterCallback callback) {
+	return ERR_NOT_SUPPORTED;
+}
+
+extern void   interop_SetFont(const char* font, int size, int flags);
+extern double interop_TextWidth(const char* text, const int len);
+extern double interop_TextDraw(const char* text, const int len, struct Bitmap* bmp, int x, int y, cc_bool shadow, const char* hex);
+
+int SysFont_TextWidth(struct DrawTextArgs* args) {
+	struct FontDesc* font = args->font;
+	cc_string left = args->text, part;
+	double width   = 0;
+	char colorCode;
+
+	interop_SetFont(font->handle, font->size, font->flags);
+	while (Drawer2D_UNSAFE_NextPart(&left, &part, &colorCode))
+	{
+		char buffer[NATIVE_STR_LEN];
+		int len = String_EncodeUtf8(buffer, &part);
+		width += interop_TextWidth(buffer, len);
+	}
+	return Math_Ceil(width);
+}
+
+void SysFont_DrawText(struct DrawTextArgs* args, struct Bitmap* bmp, int x, int y, cc_bool shadow) {
+	struct FontDesc* font = args->font;
+	cc_string left  = args->text, part;
+	BitmapCol color;
+	char colorCode = 'f';
+	double xOffset = 0;
+	char hexBuffer[7];
+	cc_string hex;
+
+	/* adjust y position to more closely match FreeType drawn text */
+	y += (args->font->height - args->font->size) / 2;
+	interop_SetFont(font->handle, font->size, font->flags);
+
+	while (Drawer2D_UNSAFE_NextPart(&left, &part, &colorCode))
+	{
+		char buffer[NATIVE_STR_LEN];
+		int len = String_EncodeUtf8(buffer, &part);
+
+		color = Drawer2D_GetColor(colorCode);
+		if (shadow) color = GetShadowColor(color);
+
+		String_InitArray(hex, hexBuffer);
+		String_Append(&hex, '#');
+		String_AppendHex(&hex, BitmapCol_R(color));
+		String_AppendHex(&hex, BitmapCol_G(color));
+		String_AppendHex(&hex, BitmapCol_B(color));
+
+		/* TODO pass as double directly instead of (int) ?*/
+		xOffset += interop_TextDraw(buffer, len, bmp, x + (int)xOffset, y, shadow, hexBuffer);
+	}
+}
+#elif defined CC_BUILD_IOS
+/* implemented in interop_ios.m */
+extern void interop_GetFontNames(struct StringsBuffer* buffer);
+extern cc_result interop_SysFontMake(struct FontDesc* desc, const cc_string* fontName, int size, int flags);
+extern void interop_SysMakeDefault(struct FontDesc* desc, int size, int flags);
+extern void interop_SysFontFree(void* handle);
+extern int interop_SysTextWidth(struct DrawTextArgs* args);
+extern void interop_SysTextDraw(struct DrawTextArgs* args, struct Bitmap* bmp, int x, int y, cc_bool shadow);
+
+void SysFonts_SaveCache(void) { }
+cc_result SysFonts_Register(const cc_string* path, SysFont_RegisterCallback callback) {
+	return ERR_NOT_SUPPORTED;
+}
+
+const cc_string* SysFonts_UNSAFE_GetDefault(void) {
+	return &String_Empty;
+}
+
+void SysFonts_GetNames(struct StringsBuffer* buffer) {
+	interop_GetFontNames(buffer);
+}
+
+cc_result SysFont_Make(struct FontDesc* desc, const cc_string* fontName, int size, int flags) {
+	return interop_SysFontMake(desc, fontName, size, flags);
+}
+
+void SysFont_MakeDefault(struct FontDesc* desc, int size, int flags) {
+	interop_SysMakeDefault(desc, size, flags);
+}
+
+void SysFont_Free(struct FontDesc* desc) {
+	interop_SysFontFree(desc->handle);
+}
+
+int SysFont_TextWidth(struct DrawTextArgs* args) {
+	return interop_SysTextWidth(args);
+}
+
+void SysFont_DrawText(struct DrawTextArgs* args, struct Bitmap* bmp, int x, int y, cc_bool shadow) {
+	interop_SysTextDraw(args, bmp, x, y, shadow);
+}
+#else
+void SysFonts_SaveCache(void) { }
+cc_result SysFonts_Register(const cc_string* path, SysFont_RegisterCallback callback) {
+	return ERR_NOT_SUPPORTED;
+}
+
+const cc_string* SysFonts_UNSAFE_GetDefault(void) { return &String_Empty; }
+
+void SysFonts_GetNames(struct StringsBuffer* buffer) { }
+
+cc_result SysFont_Make(struct FontDesc* desc, const cc_string* fontName, int size, int flags) {
+	size *= DisplayInfo.ScaleY;
+	/* Round upwards to nearest 8 */
+	size = (size + 7) & ~0x07;
+
+	desc->size   = size;
+	desc->flags  = flags;
+	desc->height = Drawer2D_AdjHeight(size);
+
+	desc->handle = (void*)1;
+	return 0;
+}
+
+void SysFont_MakeDefault(struct FontDesc* desc, int size, int flags) {
+	SysFont_Make(desc, NULL, size, flags);
+}
+
+void SysFont_Free(struct FontDesc* desc) {
+	desc->handle = NULL;
+}
+
+int SysFont_TextWidth(struct DrawTextArgs* args) {
+	return FallbackFont_TextWidth(args);
+}
+
+void SysFont_DrawText(struct DrawTextArgs* args, struct Bitmap* bmp, int x, int y, cc_bool shadow) {
+	FallbackFont_DrawText(args, bmp, x, y, shadow);
+}
+#endif