Add support for TrueType fonts

* Keeps original font system in place
* Uses the same API as original font system:
   - You can render only one line at a time
   - You can only use one font and color for one gr_text* call
* Caches all rendered text, with a string cache limited to 400
  entries, then it trucates to 250, which results in memory
  usage hovering around 5-10MB

Change-Id: I36107b9dcd8d57bae4486fce8b8f64e49ef3d906
Signed-off-by: Vojtech Bocek <vbocek@gmail.com>
diff --git a/minuitwrp/Android.mk b/minuitwrp/Android.mk
index ba81f27..bc4e054 100644
--- a/minuitwrp/Android.mk
+++ b/minuitwrp/Android.mk
@@ -107,8 +107,16 @@
   LOCAL_CFLAGS += -DWHITELIST_INPUT=$(TW_WHITELIST_INPUT)
 endif
 
-LOCAL_SHARED_LIBRARIES += libz libc libcutils libjpeg
-LOCAL_STATIC_LIBRARIES += libpng libpixelflinger_static
+ifeq ($(TW_DISABLE_TTF), true)
+    LOCAL_CFLAGS += -DTW_DISABLE_TTF
+else
+    LOCAL_SHARED_LIBRARIES += libft2
+    LOCAL_C_INCLUDES += external/freetype/include
+    LOCAL_SRC_FILES += truetype.c
+endif
+
+LOCAL_SHARED_LIBRARIES += libz libc libcutils libjpeg libpng
+LOCAL_STATIC_LIBRARIES += libpixelflinger_static
 LOCAL_MODULE_TAGS := eng
 LOCAL_MODULE := libminuitwrp
 
diff --git a/minuitwrp/graphics.c b/minuitwrp/graphics.c
index 79b1e9a..9926904 100644
--- a/minuitwrp/graphics.c
+++ b/minuitwrp/graphics.c
@@ -58,6 +58,7 @@
 // #define PRINT_SCREENINFO 1 // Enables printing of screen info to log
 
 typedef struct {
+    int type;
     GGLSurface texture;
     unsigned offset[97];
     unsigned cheight;
@@ -392,6 +393,11 @@
 
     if (!fnt)   fnt = gr_font;
 
+#ifndef TW_DISABLE_TTF
+    if(fnt->type == FONT_TYPE_TTF)
+        return gr_ttf_measureEx(s, font);
+#endif
+
     while ((off = *s++))
     {
         off -= 32;
@@ -410,6 +416,11 @@
 
     if (!fnt)   fnt = gr_font;
 
+#ifndef TW_DISABLE_TTF
+    if(fnt->type == FONT_TYPE_TTF)
+        return gr_ttf_maxExW(s, font, max_width);
+#endif
+
     while ((off = *s++))
     {
         off -= 32;
@@ -425,21 +436,6 @@
     return total;
 }
 
-unsigned character_width(const char *s, void* pFont)
-{
-	GRFont *font = (GRFont*) pFont;
-	unsigned off;
-
-	/* Handle default font */
-    if (!font)  font = gr_font;
-
-	off = *s - 32;
-	if (off == 0)
-		return 0;
-
-	return font->offset[off+1] - font->offset[off];
-}
-
 int gr_textEx(int x, int y, const char *s, void* pFont)
 {
     GGLContext *gl = gr_context;
@@ -450,6 +446,11 @@
     /* Handle default font */
     if (!font)  font = gr_font;
 
+#ifndef TW_DISABLE_TTF
+    if(font->type == FONT_TYPE_TTF)
+        return gr_ttf_textExWH(gl, x, y, s, pFont, -1, -1);
+#endif
+
     gl->bindTexture(gl, &font->texture);
     gl->texEnvi(gl, GGL_TEXTURE_ENV, GGL_TEXTURE_ENV_MODE, GGL_REPLACE);
     gl->texGeni(gl, GGL_S, GGL_TEXTURE_GEN_MODE, GGL_ONE_TO_ONE);
@@ -480,6 +481,11 @@
     /* Handle default font */
     if (!font)  font = gr_font;
 
+#ifndef TW_DISABLE_TTF
+    if(font->type == FONT_TYPE_TTF)
+        return gr_ttf_textExWH(gl, x, y, s, pFont, max_width, -1);
+#endif
+
     gl->bindTexture(gl, &font->texture);
     gl->texEnvi(gl, GGL_TEXTURE_ENV, GGL_TEXTURE_ENV_MODE, GGL_REPLACE);
     gl->texGeni(gl, GGL_S, GGL_TEXTURE_GEN_MODE, GGL_ONE_TO_ONE);
@@ -518,6 +524,11 @@
     /* Handle default font */
     if (!font)  font = gr_font;
 
+#ifndef TW_DISABLE_TTF
+    if(font->type == FONT_TYPE_TTF)
+        return gr_ttf_textExWH(gl, x, y, s, pFont, max_width, max_height);
+#endif
+
     gl->bindTexture(gl, &font->texture);
     gl->texEnvi(gl, GGL_TEXTURE_ENV, GGL_TEXTURE_ENV_MODE, GGL_REPLACE);
     gl->texGeni(gl, GGL_S, GGL_TEXTURE_GEN_MODE, GGL_ONE_TO_ONE);
@@ -549,34 +560,6 @@
     return x;
 }
 
-int twgr_text(int x, int y, const char *s)
-{
-    GGLContext *gl = gr_context;
-    GRFont *font = gr_font;
-    unsigned off;
-    unsigned cwidth = 0;
-
-    y -= font->ascent;
-
-    gl->bindTexture(gl, &font->texture);
-    gl->texEnvi(gl, GGL_TEXTURE_ENV, GGL_TEXTURE_ENV_MODE, GGL_REPLACE);
-    gl->texGeni(gl, GGL_S, GGL_TEXTURE_GEN_MODE, GGL_ONE_TO_ONE);
-    gl->texGeni(gl, GGL_T, GGL_TEXTURE_GEN_MODE, GGL_ONE_TO_ONE);
-    gl->enable(gl, GGL_TEXTURE_2D);
-
-    while((off = *s++)) {
-        off -= 32;
-        if (off < 96) {
-            cwidth = font->offset[off+1] - font->offset[off];
-            gl->texCoord2i(gl, (off * cwidth) - x, 0 - y);
-            gl->recti(gl, x, y, x + cwidth, y + font->cheight);
-        }
-        x += cwidth;
-    }
-
-    return x;
-}
-
 void gr_fill(int x, int y, int w, int h)
 {
     GGLContext *gl = gr_context;
@@ -682,33 +665,32 @@
     ftex->stride = width;
     ftex->data = (void*) bits;
     ftex->format = GGL_PIXEL_FORMAT_A_8;
+    font->type = FONT_TYPE_TWRP;
     font->cheight = height;
     font->ascent = height - 2;
     return (void*) font;
 }
 
-int gr_getFontDetails(void* font, unsigned* cheight, unsigned* maxwidth)
+void gr_freeFont(void *font)
+{
+    GRFont *f = font;
+    free(f->texture.data);
+    free(f);
+}
+
+int gr_getMaxFontHeight(void *font)
 {
     GRFont *fnt = (GRFont*) font;
 
     if (!fnt)   fnt = gr_font;
     if (!fnt)   return -1;
 
-    if (cheight)    *cheight = fnt->cheight;
-    if (maxwidth)
-    {
-        int pos;
-        *maxwidth = 0;
-        for (pos = 0; pos < 96; pos++)
-        {
-            unsigned int width = fnt->offset[pos+1] - fnt->offset[pos];
-            if (width > *maxwidth)
-            {
-                *maxwidth = width;
-            }
-        }
-    }
-    return 0;
+#ifndef TW_DISABLE_TTF
+    if(fnt->type == FONT_TYPE_TTF)
+        return gr_ttf_getMaxFontHeight(font);
+#endif
+
+    return fnt->cheight;
 }
 
 static void gr_init_font(void)
@@ -746,6 +728,7 @@
     ftex->stride = width;
     ftex->data = (void*) bits;
     ftex->format = GGL_PIXEL_FORMAT_A_8;
+    gr_font->type = FONT_TYPE_TWRP;
     gr_font->cheight = height;
     gr_font->ascent = height - 2;
     return;
diff --git a/minuitwrp/minui.h b/minuitwrp/minui.h
index f04f518..cb9f8a3 100644
--- a/minuitwrp/minui.h
+++ b/minuitwrp/minui.h
@@ -20,6 +20,12 @@
 typedef void* gr_surface;
 typedef unsigned short gr_pixel;
 
+#define FONT_TYPE_TWRP 0
+
+#ifndef TW_DISABLE_TTF
+#define FONT_TYPE_TTF  1
+#endif
+
 int gr_init(void);
 void gr_exit(void);
 
@@ -35,16 +41,25 @@
 int gr_textEx(int x, int y, const char *s, void* font);
 int gr_textExW(int x, int y, const char *s, void* font, int max_width);
 int gr_textExWH(int x, int y, const char *s, void* pFont, int max_width, int max_height);
-int twgr_text(int x, int y, const char *s);
 static inline int gr_text(int x, int y, const char *s)     { return gr_textEx(x, y, s, NULL); }
 int gr_measureEx(const char *s, void* font);
 static inline int gr_measure(const char *s)                { return gr_measureEx(s, NULL); }
 int gr_maxExW(const char *s, void* font, int max_width);
 
-int gr_getFontDetails(void* font, unsigned* cheight, unsigned* maxwidth);
-static inline void gr_font_size(int *x, int *y)            { gr_getFontDetails(NULL, (unsigned*) y, (unsigned*) x); }
+int gr_getMaxFontHeight(void *font);
 
 void* gr_loadFont(const char* fontName);
+void gr_freeFont(void *font);
+
+#ifndef TW_DISABLE_TTF
+void *gr_ttf_loadFont(const char *filename, int size, int dpi);
+void gr_ttf_freeFont(void *font);
+int gr_ttf_textExWH(void *context, int x, int y, const char *s, void *pFont, int max_width, int max_height);
+int gr_ttf_measureEx(const char *s, void *font);
+int gr_ttf_maxExW(const char *s, void *font, int max_width);
+int gr_ttf_getMaxFontHeight(void *font);
+void gr_ttf_dump_stats(void);
+#endif
 
 void gr_blit(gr_surface source, int sx, int sy, int w, int h, int dx, int dy);
 unsigned int gr_get_width(gr_surface surface);
diff --git a/minuitwrp/truetype.c b/minuitwrp/truetype.c
new file mode 100644
index 0000000..8e0df42
--- /dev/null
+++ b/minuitwrp/truetype.c
@@ -0,0 +1,731 @@
+#include <stdbool.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#include <errno.h>
+#include <stdio.h>
+
+#include "minui.h"
+
+#include <cutils/hashmap.h>
+#include <ft2build.h>
+#include FT_FREETYPE_H
+#include FT_GLYPH_H
+
+#include <pixelflinger/pixelflinger.h>
+#include <pthread.h>
+
+#define STRING_CACHE_MAX_ENTRIES 400
+#define STRING_CACHE_TRUNCATE_ENTRIES 150
+
+typedef struct
+{
+    int size;
+    int dpi;
+    char *path;
+} TrueTypeFontKey;
+
+typedef struct
+{
+    int type;
+    int refcount;
+    int size;
+    int dpi;
+    int max_height;
+    int base;
+    FT_Face face;
+    Hashmap *glyph_cache;
+    Hashmap *string_cache;
+    struct StringCacheEntry *string_cache_head;
+    struct StringCacheEntry *string_cache_tail;
+    pthread_mutex_t mutex;
+    TrueTypeFontKey *key;
+} TrueTypeFont;
+
+typedef struct
+{
+    FT_BBox bbox;
+    FT_BitmapGlyph glyph;
+} TrueTypeCacheEntry;
+
+typedef struct
+{
+    char *text;
+    int max_width;
+} StringCacheKey;
+
+struct StringCacheEntry
+{
+    GGLSurface surface;
+    int rendered_len;
+    StringCacheKey *key;
+    struct StringCacheEntry *prev;
+    struct StringCacheEntry *next;
+};
+
+typedef struct StringCacheEntry StringCacheEntry;
+
+typedef struct 
+{
+    FT_Library ft_library;
+    Hashmap *fonts;
+    pthread_mutex_t mutex;
+} FontData;
+
+static FontData font_data = {
+    .ft_library = NULL,
+    .fonts = NULL,
+    .mutex = PTHREAD_MUTEX_INITIALIZER,
+};
+
+#define MIN(X,Y) ((X) < (Y) ? (X) : (Y))
+#define MAX(X,Y) ((X) > (Y) ? (X) : (Y))
+
+// 32bit FNV-1a hash algorithm
+// http://isthe.com/chongo/tech/comp/fnv/#FNV-1a
+static const uint32_t FNV_prime = 16777619U;
+static const uint32_t offset_basis = 2166136261U;
+
+static uint32_t fnv_hash(void *data, uint32_t len)
+{
+    uint8_t *d8 = data;
+    uint32_t *d32 = data;
+    uint32_t i, max;
+    uint32_t hash = offset_basis;
+
+    max = len/4;
+
+    // 32 bit data
+    for(i = 0; i < max; ++i)
+    {
+        hash ^= *d32++;
+        hash *= FNV_prime;
+    }
+
+    // last bits
+    for(i *= 4; i < len; ++i)
+    {
+        hash ^= (uint32_t) d8[i];
+        hash *= FNV_prime;
+    }
+    return hash;
+}
+
+static inline uint32_t fnv_hash_add(uint32_t cur_hash, uint32_t word)
+{
+    cur_hash ^= word;
+    cur_hash *= FNV_prime;
+    return cur_hash;
+}
+
+static bool gr_ttf_string_cache_equals(void *keyA, void *keyB)
+{
+    StringCacheKey *a = keyA;
+    StringCacheKey *b = keyB;
+    return a->max_width == b->max_width && strcmp(a->text, b->text) == 0;
+}
+
+static int gr_ttf_string_cache_hash(void *key)
+{
+    StringCacheKey *k = key;
+    return fnv_hash(k->text, strlen(k->text));
+}
+
+static bool gr_ttf_font_cache_equals(void *keyA, void *keyB)
+{
+    TrueTypeFontKey *a = keyA;
+    TrueTypeFontKey *b = keyB;
+    return (a->size == b->size) && (a->dpi == b->dpi) && !strcmp(a->path, b->path);
+}
+
+static int gr_ttf_font_cache_hash(void *key)
+{
+    TrueTypeFontKey *k = key;
+
+    uint32_t hash = fnv_hash(k->path, strlen(k->path));
+    hash = fnv_hash_add(hash, k->size);
+    hash = fnv_hash_add(hash, k->dpi);
+    return hash;
+}
+
+void *gr_ttf_loadFont(const char *filename, int size, int dpi)
+{
+    int error;
+    TrueTypeFont *res = NULL;
+
+    pthread_mutex_lock(&font_data.mutex);
+
+    if(font_data.fonts)
+    {
+        TrueTypeFontKey k = {
+            .size = size,
+            .dpi = dpi,
+            .path = (char*)filename
+        };
+
+        res = hashmapGet(font_data.fonts, &k);
+        if(res)
+        {
+            ++res->refcount;
+            goto exit;
+        }
+    }
+
+    if(!font_data.ft_library)
+    {
+        error = FT_Init_FreeType(&font_data.ft_library);
+        if(error)
+        {
+            fprintf(stderr, "Failed to init libfreetype! %d\n", error);
+            goto exit;
+        }
+    }
+
+    FT_Face face;
+    error = FT_New_Face(font_data.ft_library, filename, 0, &face);
+    if(error)
+    {
+        fprintf(stderr, "Failed to load truetype face %s: %d\n", filename, error);
+        goto exit;
+    }
+
+    error = FT_Set_Char_Size(face, 0, size*16, dpi, dpi);
+    if(error)
+    {
+         fprintf(stderr, "Failed to set truetype face size to %d, dpi %d: %d\n", size, dpi, error);
+         FT_Done_Face(face);
+         goto exit;
+    }
+
+    res = malloc(sizeof(TrueTypeFont));
+    memset(res, 0, sizeof(TrueTypeFont));
+    res->type = FONT_TYPE_TTF;
+    res->size = size;
+    res->dpi = dpi;
+    res->face = face;
+    res->max_height = -1;
+    res->base = -1;
+    res->refcount = 1;
+    res->glyph_cache = hashmapCreate(32, hashmapIntHash, hashmapIntEquals);
+    res->string_cache = hashmapCreate(128, gr_ttf_string_cache_hash, gr_ttf_string_cache_equals);
+    pthread_mutex_init(&res->mutex, 0);
+
+    if(!font_data.fonts)
+        font_data.fonts = hashmapCreate(4, gr_ttf_font_cache_hash, gr_ttf_font_cache_equals);
+
+    TrueTypeFontKey *key = malloc(sizeof(TrueTypeFontKey));
+    memset(key, 0, sizeof(TrueTypeFontKey));
+    key->path = strdup(filename);
+    key->size = size;
+    key->dpi = dpi;
+
+    res->key = key;
+
+    hashmapPut(font_data.fonts, key, res);
+
+exit:
+    pthread_mutex_unlock(&font_data.mutex);
+    return res;
+}
+
+static bool gr_ttf_freeFontCache(void *key, void *value, void *context)
+{
+    TrueTypeCacheEntry *e = value;
+    FT_Done_Glyph((FT_Glyph)e->glyph);
+    free(e);
+    free(key);
+    return true;
+}
+
+static bool gr_ttf_freeStringCache(void *key, void *value, void *context)
+{
+    StringCacheKey *k = key;
+    free(k->text);
+    free(k);
+
+    StringCacheEntry *e = value;
+    free(e->surface.data);
+    free(e);
+    return true;
+}
+
+void gr_ttf_freeFont(void *font)
+{
+    pthread_mutex_lock(&font_data.mutex);
+
+    TrueTypeFont *d = font;
+
+    if(--d->refcount == 0)
+    {
+        hashmapRemove(font_data.fonts, d->key);
+
+        if(hashmapSize(font_data.fonts) == 0)
+        {
+            hashmapFree(font_data.fonts);
+            font_data.fonts = NULL;
+        }
+
+        free(d->key->path);
+        free(d->key);
+
+        FT_Done_Face(d->face);
+        hashmapForEach(d->string_cache, gr_ttf_freeStringCache, NULL);
+        hashmapFree(d->string_cache);
+        hashmapForEach(d->glyph_cache, gr_ttf_freeFontCache, NULL);
+        hashmapFree(d->glyph_cache);
+        pthread_mutex_destroy(&d->mutex);
+        free(d);
+    }
+
+    pthread_mutex_unlock(&font_data.mutex);
+}
+
+static TrueTypeCacheEntry *gr_ttf_glyph_cache_peek(TrueTypeFont *font, int char_index)
+{
+    return hashmapGet(font->glyph_cache, &char_index);
+}
+
+static TrueTypeCacheEntry *gr_ttf_glyph_cache_get(TrueTypeFont *font, int char_index)
+{
+    TrueTypeCacheEntry *res = hashmapGet(font->glyph_cache, &char_index);
+    if(!res)
+    {
+        int error = FT_Load_Glyph(font->face, char_index, FT_LOAD_RENDER);
+        if(error)
+        {
+            fprintf(stderr, "Failed to load glyph idx %d: %d\n", char_index, error);
+            return NULL;
+        }
+
+        FT_BitmapGlyph glyph;
+        error = FT_Get_Glyph(font->face->glyph, (FT_Glyph*)&glyph);
+        if(error)
+        {
+            fprintf(stderr, "Failed to copy glyph %d: %d\n", char_index, error);
+            return NULL;
+        }
+
+        res = malloc(sizeof(TrueTypeCacheEntry));
+        memset(res, 0, sizeof(TrueTypeCacheEntry));
+        res->glyph = glyph;
+        FT_Glyph_Get_CBox((FT_Glyph)glyph, FT_GLYPH_BBOX_PIXELS, &res->bbox);
+
+        int *key = malloc(sizeof(int));
+        *key = char_index;
+
+        hashmapPut(font->glyph_cache, key, res);
+    }
+
+    return res;
+}
+
+static int gr_ttf_copy_glyph_to_surface(GGLSurface *dest, FT_BitmapGlyph glyph, int offX, int offY, int base)
+{
+    int y;
+    uint8_t *src_itr = glyph->bitmap.buffer;
+    uint8_t *dest_itr = dest->data;
+
+    if(glyph->bitmap.pixel_mode != FT_PIXEL_MODE_GRAY)
+    {
+        fprintf(stderr, "Unsupported pixel mode in FT_BitmapGlyph %d\n", glyph->bitmap.pixel_mode);
+        return -1;
+    }
+
+    dest_itr += (offY + base - glyph->top)*dest->stride + (offX + glyph->left);
+
+    for(y = 0; y < glyph->bitmap.rows; ++y)
+    {
+        memcpy(dest_itr, src_itr, glyph->bitmap.width);
+        src_itr += glyph->bitmap.pitch;
+        dest_itr += dest->stride;
+    }
+    return 0;
+}
+
+static int gr_ttf_render_text(TrueTypeFont *font, GGLSurface *surface, const char *text, int max_width)
+{
+    TrueTypeFont *f = font;
+    TrueTypeCacheEntry *ent;
+    int max_len = 0, total_w = 0;
+    char c;
+    int i, x, diff, char_idx, prev_idx = 0;
+    int height, base;
+    FT_Vector delta;
+    uint8_t *data = NULL;
+    const char *text_itr = text;
+
+    while((c = *text_itr++))
+    {
+        char_idx = FT_Get_Char_Index(f->face, c);
+        ent = gr_ttf_glyph_cache_get(f, char_idx);
+        if(ent)
+        {
+            diff = ent->glyph->root.advance.x >> 16;
+
+            if(FT_HAS_KERNING(f->face) && prev_idx && char_idx)
+            {
+                FT_Get_Kerning(f->face, prev_idx, char_idx, FT_KERNING_DEFAULT, &delta);
+                diff += delta.x >> 6;
+            }
+
+            if(max_width != -1 && total_w + diff > max_width)
+                break;
+
+            total_w += diff;
+        }
+        prev_idx = char_idx;
+        ++max_len;
+    }
+
+    if(font->max_height == -1)
+        gr_ttf_getMaxFontHeight(font);
+
+    if(font->max_height == -1)
+        return -1;
+
+    height = font->max_height;
+
+    data = malloc(total_w*height);
+    memset(data, 0, total_w*height);
+    x = 0;
+    prev_idx = 0;
+
+    surface->version = sizeof(*surface);
+    surface->width = total_w;
+    surface->height = height;
+    surface->stride = total_w;
+    surface->data = (void*)data;
+    surface->format = GGL_PIXEL_FORMAT_A_8;
+
+    for(i = 0; i < max_len; ++i)
+    {
+        char_idx = FT_Get_Char_Index(f->face, text[i]);
+        if(FT_HAS_KERNING(f->face) && prev_idx && char_idx)
+        {
+            FT_Get_Kerning(f->face, prev_idx, char_idx, FT_KERNING_DEFAULT, &delta);
+            x += delta.x >> 6;
+        }
+
+        ent = gr_ttf_glyph_cache_get(f, char_idx);
+        if(ent)
+        {
+            gr_ttf_copy_glyph_to_surface(surface, ent->glyph, x, 0, font->base);
+            x += ent->glyph->root.advance.x >> 16;
+        }
+
+        prev_idx = char_idx;
+    }
+
+    return max_len;
+}
+
+static StringCacheEntry *gr_ttf_string_cache_peek(TrueTypeFont *font, const char *text, int max_width)
+{
+    StringCacheEntry *res;
+    StringCacheKey k = {
+        .text = (char*)text,
+        .max_width = max_width
+    };
+
+    return hashmapGet(font->string_cache, &k);
+}
+
+static StringCacheEntry *gr_ttf_string_cache_get(TrueTypeFont *font, const char *text, int max_width)
+{
+    StringCacheEntry *res;
+    StringCacheKey k = {
+        .text = (char*)text,
+        .max_width = max_width
+    };
+
+    res = hashmapGet(font->string_cache, &k);
+    if(!res)
+    {
+        res = malloc(sizeof(StringCacheEntry));
+        memset(res, 0, sizeof(StringCacheEntry));
+        res->rendered_len = gr_ttf_render_text(font, &res->surface, text, max_width);
+        if(res->rendered_len < 0)
+        {
+            free(res);
+            return NULL;
+        }
+
+        StringCacheKey *new_key = malloc(sizeof(StringCacheKey));
+        memset(new_key, 0, sizeof(StringCacheKey));
+        new_key->max_width = max_width;
+        new_key->text = strdup(text);
+
+        res->key = new_key;
+
+        if(font->string_cache_tail)
+        {
+            res->prev = font->string_cache_tail;
+            res->prev->next = res;
+        }
+        else
+            font->string_cache_head = res;
+        font->string_cache_tail = res;
+
+        hashmapPut(font->string_cache, new_key, res);
+    }
+    else if(res->next)
+    {
+        // move this entry to the tail of the linked list
+        // if it isn't already there
+        if(res->prev)
+            res->prev->next = res->next;
+
+        res->next->prev = res->prev;
+
+        if(!res->prev)
+            font->string_cache_head = res->next;
+
+        res->next = NULL;
+        res->prev = font->string_cache_tail;
+        res->prev->next = res;
+        font->string_cache_tail = res;
+
+        // truncate old entries
+        if(hashmapSize(font->string_cache) >= STRING_CACHE_MAX_ENTRIES)
+        {
+            printf("Truncating string cache entries.\n");
+            int i;
+            StringCacheEntry *ent;
+            for(i = 0; i < STRING_CACHE_TRUNCATE_ENTRIES; ++i)
+            {
+                ent = font->string_cache_head;
+                font->string_cache_head = ent->next;
+                font->string_cache_head->prev = NULL;
+
+                hashmapRemove(font->string_cache, ent->key);
+
+                gr_ttf_freeStringCache(ent->key, ent, NULL);
+            }
+        }
+    }
+    return res;
+}
+
+int gr_ttf_measureEx(const char *s, void *font)
+{
+    TrueTypeFont *f = font;
+    int res = -1;
+
+    pthread_mutex_lock(&f->mutex);
+    StringCacheEntry *e = gr_ttf_string_cache_get(font, s, -1);
+    if(e)
+        res = e->surface.width;
+    pthread_mutex_unlock(&f->mutex);
+
+    return res;
+}
+
+int gr_ttf_maxExW(const char *s, void *font, int max_width)
+{
+    TrueTypeFont *f = font;
+    TrueTypeCacheEntry *ent;
+    int max_len = 0, total_w = 0;
+    char c;
+    int char_idx, prev_idx = 0;
+    FT_Vector delta;
+    StringCacheEntry *e;
+
+    pthread_mutex_lock(&f->mutex);
+
+    e = gr_ttf_string_cache_peek(font, s, max_width);
+    if(e)
+    {
+        max_len = e->rendered_len;
+        pthread_mutex_unlock(&f->mutex);
+        return max_len;
+    }
+
+    for(; (c = *s++); ++max_len)
+    {
+        char_idx = FT_Get_Char_Index(f->face, c);
+        if(FT_HAS_KERNING(f->face) && prev_idx && char_idx)
+        {
+            FT_Get_Kerning(f->face, prev_idx, char_idx, FT_KERNING_DEFAULT, &delta);
+            total_w += delta.x >> 6;
+        }
+        prev_idx = char_idx;
+
+        if(total_w > max_width)
+            break;
+
+        ent = gr_ttf_glyph_cache_get(f, char_idx);
+        if(!ent)
+            continue;
+
+        total_w += ent->glyph->root.advance.x >> 16;
+    }
+    pthread_mutex_unlock(&f->mutex);
+    return max_len > 0 ? max_len - 1 : 0;
+}
+
+int gr_ttf_textExWH(void *context, int x, int y, const char *s, void *pFont, int max_width, int max_height)
+{
+    GGLContext *gl = context;
+    TrueTypeFont *font = pFont;
+
+    // not actualy max width, but max_width + x
+    if(max_width != -1)
+    {
+        max_width -= x;
+        if(max_width <= 0)
+            return 0;
+    }
+
+    pthread_mutex_lock(&font->mutex);
+
+    StringCacheEntry *e = gr_ttf_string_cache_get(font, s, max_width);
+    if(!e)
+    {
+        pthread_mutex_unlock(&font->mutex);
+        return -1;
+    }
+
+    int y_bottom = y + e->surface.height;
+    int res = e->rendered_len;
+
+    if(max_height != -1 && max_height < y_bottom)
+    {
+        y_bottom = max_height;
+        if(y_bottom <= y)
+        {
+            pthread_mutex_unlock(&font->mutex);
+            return 0;
+        }
+    }
+
+    gl->bindTexture(gl, &e->surface);
+    gl->texEnvi(gl, GGL_TEXTURE_ENV, GGL_TEXTURE_ENV_MODE, GGL_REPLACE);
+    gl->texGeni(gl, GGL_S, GGL_TEXTURE_GEN_MODE, GGL_ONE_TO_ONE);
+    gl->texGeni(gl, GGL_T, GGL_TEXTURE_GEN_MODE, GGL_ONE_TO_ONE);
+    gl->enable(gl, GGL_TEXTURE_2D);
+
+    gl->texCoord2i(gl, -x, -y);
+    gl->recti(gl, x, y, x + e->surface.width, y_bottom);
+
+    pthread_mutex_unlock(&font->mutex);
+    return res;
+}
+
+int gr_ttf_getMaxFontHeight(void *font)
+{
+    int res;
+    TrueTypeFont *f = font;
+
+    pthread_mutex_lock(&f->mutex);
+
+    if(f->max_height == -1)
+    {
+        char c;
+        int char_idx;
+        int error;
+        FT_Glyph glyph;
+        FT_BBox bbox;
+        FT_BBox bbox_glyph;
+        TrueTypeCacheEntry *ent;
+
+        bbox.yMin = bbox_glyph.yMin = LONG_MAX;
+        bbox.yMax = bbox_glyph.yMax = LONG_MIN;
+
+        for(c = '!'; c <= '~'; ++c)
+        {
+            char_idx = FT_Get_Char_Index(f->face, c);
+            ent = gr_ttf_glyph_cache_peek(f, char_idx);
+            if(ent)
+            {
+                bbox.yMin = MIN(bbox.yMin, ent->bbox.yMin);
+                bbox.yMax = MAX(bbox.yMax, ent->bbox.yMax);
+            }
+            else
+            {
+                error = FT_Load_Glyph(f->face, char_idx, 0);
+                if(error)
+                    continue;
+
+                error = FT_Get_Glyph(f->face->glyph, &glyph);
+                if(error)
+                    continue;
+
+                FT_Glyph_Get_CBox(glyph, FT_GLYPH_BBOX_PIXELS, &bbox_glyph);
+                bbox.yMin = MIN(bbox.yMin, bbox_glyph.yMin);
+                bbox.yMax = MAX(bbox.yMax, bbox_glyph.yMax);
+
+                FT_Done_Glyph(glyph);
+            }
+        }
+
+        if(bbox.yMin > bbox.yMax)
+            bbox.yMin = bbox.yMax = 0;
+
+        f->max_height = bbox.yMax - bbox.yMin;
+        f->base = bbox.yMax;
+
+        // FIXME: twrp fonts have some padding on top, I'll add it here
+        // Should be fixed in the themes
+        f->max_height += f->size / 4;
+        f->base += f->size / 4;
+    }
+
+    res = f->max_height;
+
+    pthread_mutex_unlock(&f->mutex);
+    return res;
+}
+
+static bool gr_ttf_dump_stats_count_string_cache(void *key, void *value, void *context)
+{
+    int *string_cache_size = context;
+    StringCacheEntry *e = value;
+    *string_cache_size += e->surface.height*e->surface.width + sizeof(StringCacheEntry);
+    return true;
+}
+
+static bool gr_ttf_dump_stats_font(void *key, void *value, void *context)
+{
+    TrueTypeFontKey *k = key;
+    TrueTypeFont *f = value;
+    int *total_string_cache_size = context;
+    int string_cache_size = 0;
+
+    pthread_mutex_lock(&f->mutex);
+
+    hashmapForEach(f->string_cache, gr_ttf_dump_stats_count_string_cache, &string_cache_size);
+
+    printf("  Font %s (size %d, dpi %d):\n"
+            "    refcount: %d\n"
+            "    max_height: %d\n"
+            "    base: %d\n"
+            "    glyph_cache: %d entries\n"
+            "    string_cache: %d entries (%.2f kB)\n",
+            k->path, k->size, k->dpi,
+            f->refcount, f->max_height, f->base,
+            hashmapSize(f->glyph_cache),
+            hashmapSize(f->string_cache), ((double)string_cache_size)/1024);
+
+    pthread_mutex_unlock(&f->mutex);
+
+    *total_string_cache_size += string_cache_size;
+    return true;
+}
+
+void gr_ttf_dump_stats(void)
+{
+    pthread_mutex_lock(&font_data.mutex);
+
+    printf("TrueType fonts system stats: ");
+    if(!font_data.fonts)
+        printf("no truetype fonts loaded.\n");
+    else
+    {
+        int total_string_cache_size = 0;
+        printf("%d fonts loaded.\n", hashmapSize(font_data.fonts));
+        hashmapForEach(font_data.fonts, gr_ttf_dump_stats_font, &total_string_cache_size);
+        printf("  Total string cache size: %.2f kB\n", ((double)total_string_cache_size)/1024);
+    }
+
+    pthread_mutex_unlock(&font_data.mutex);
+}