diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 90885359..7cb29834 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -152,6 +152,8 @@ set(SOURCES # Force cimgui to build as a static library. set(IMGUI_STATIC "yes" CACHE STRING "Build as a static library") +# Support Unicode codepoints outside the basic multilingual plane. +set(IMGUI_WCHAR32 "yes") add_definitions("-DCIMGUI_USE_OPENGL2=1") add_definitions("-DCIMGUI_USE_OPENGL3=1") diff --git a/client/include/app.h b/client/include/app.h index 5c73d9b0..6360c980 100644 --- a/client/include/app.h +++ b/client/include/app.h @@ -131,6 +131,11 @@ void app_freeOverlays(void); */ void app_invalidateOverlay(bool renderTwice); +/** + * Ensure that user-supplied UTF-8 text has glyphs in the UI font atlas. + */ +void app_registerImGuiText(const char * text); + struct OverlayGraph; typedef struct OverlayGraph * GraphHandle; typedef const char * (*GraphFormatFn)(const char * name, diff --git a/client/include/util.h b/client/include/util.h index 1382f4ae..111dd56c 100644 --- a/client/include/util.h +++ b/client/include/util.h @@ -26,6 +26,9 @@ #include "common/types.h" #include "common/util.h" +struct ImFont; +struct ImFontAtlas; + // reads the specified file into a new buffer // the callee must free the buffer bool util_fileGetContents(const char * filename, char ** buffer, size_t * length); @@ -46,5 +49,8 @@ static inline double util_clamp(double x, double min, double max) bool util_initUIFonts(void); void util_freeUIFonts(void); char * util_getUIFont(const char * fontName); +bool util_uiFontAddText(const char * text); +bool util_buildUIFontAtlas( + struct ImFontAtlas * atlas, float size, struct ImFont ** large); #endif diff --git a/client/renderers/EGL/filter_glsl.c b/client/renderers/EGL/filter_glsl.c index cbc9577f..b7605a8e 100644 --- a/client/renderers/EGL/filter_glsl.c +++ b/client/renderers/EGL/filter_glsl.c @@ -1080,6 +1080,7 @@ static bool createFilter(const char * root, const char * relative, char * extension = strrchr((char *)this->base.ops.name, '.'); if (extension && !strcasecmp(extension, ".glsl")) *extension = '\0'; + app_registerImGuiText(this->base.ops.name); parseFile(this); this->enable = enabledInOption(this->base.ops.id); diff --git a/client/renderers/EGL/postprocess.c b/client/renderers/EGL/postprocess.c index 15119bfe..3951f37c 100644 --- a/client/renderers/EGL/postprocess.c +++ b/client/renderers/EGL/postprocess.c @@ -159,6 +159,7 @@ static void loadPresetList(struct EGL_PostProcess * this) goto fail; } stringlist_push(this->presets, name); + app_registerImGuiText(name); if (preset && strcmp(preset, name) == 0) this->activePreset = stringlist_count(this->presets) - 1; diff --git a/client/src/app.c b/client/src/app.c index 3c8881ba..ed6c79ce 100644 --- a/client/src/app.c +++ b/client/src/app.c @@ -569,6 +569,7 @@ void app_handleKeyRelease(int sc) void app_handleKeyboardTyped(const char * typed) { + app_registerImGuiText(typed); ImGuiIO_AddInputCharactersUTF8(g_state.io, typed); } @@ -1151,6 +1152,15 @@ void app_overlayConfigRegisterTab(const char * title, overlayConfig_registerTab(title, callback, udata); } +void app_registerImGuiText(const char * text) +{ + if (!util_uiFontAddText(text)) + return; + + atomic_fetch_add(&g_state.lgrResize, 1); + app_invalidateOverlay(false); +} + void app_invalidateOverlay(bool renderTwice) { if (app_getState() == APP_STATE_SHUTDOWN) diff --git a/client/src/main.c b/client/src/main.c index 8ee78e58..a374cf7f 100644 --- a/client/src/main.c +++ b/client/src/main.c @@ -269,12 +269,8 @@ static int renderThread(void * unused) }; g_state.io->FontGlobalScale = 1.0f / g_state.windowScale; - ImFontAtlas_Clear(g_state.io->Fonts); - ImFontAtlas_AddFontFromFileTTF(g_state.io->Fonts, g_state.fontName, - g_params.uiSize * g_state.windowScale, NULL, g_state.fontRange.Data); - g_state.fontLarge = ImFontAtlas_AddFontFromFileTTF(g_state.io->Fonts, - g_state.fontName, 1.3f * g_params.uiSize * g_state.windowScale, NULL, g_state.fontRange.Data); - if (!ImFontAtlas_Build(g_state.io->Fonts)) + if (!util_buildUIFontAtlas(g_state.io->Fonts, + g_params.uiSize * g_state.windowScale, &g_state.fontLarge)) DEBUG_FATAL("Failed to build font atlas: %s (%s)", g_params.uiFont, g_state.fontName); if (g_state.lgr) @@ -1103,22 +1099,19 @@ static int lg_run(void) g_state.io->BackendFlags |= ImGuiBackendFlags_HasMouseCursors; g_state.windowScale = 1.0; - if (util_initUIFonts()) + if (!util_initUIFonts()) { - g_state.fontName = util_getUIFont(g_params.uiFont); - DEBUG_INFO("Using font: %s", g_state.fontName); + DEBUG_ERROR("Failed to initialize UI fonts"); + return -1; } - ImVector_ImWchar_Init(&g_state.fontRange); - ImFontGlyphRangesBuilder * rangeBuilder = - ImFontGlyphRangesBuilder_ImFontGlyphRangesBuilder(); - ImFontGlyphRangesBuilder_AddRanges(rangeBuilder, (ImWchar[]) { - 0x0020, 0x00FF, // Basic Latin + Latin Supplement - 0x2190, 0x2193, // four directional arrows - 0, - }); - ImFontGlyphRangesBuilder_BuildRanges(rangeBuilder, &g_state.fontRange); - ImFontGlyphRangesBuilder_destroy(rangeBuilder); + g_state.fontName = util_getUIFont(g_params.uiFont); + if (!g_state.fontName) + { + DEBUG_ERROR("Failed to load UI font: %s", g_params.uiFont); + return -1; + } + DEBUG_INFO("Using font: %s", g_state.fontName); // initialize metrics ringbuffers g_state.renderTimings = ringbuffer_new(256, sizeof(float)); @@ -1612,7 +1605,6 @@ static void lg_shutdown(void) ringbuffer_free(&g_state.renderDuration); free(g_state.fontName); - ImVector_ImWchar_UnInit(&g_state.fontRange); igDestroyContext(NULL); free(g_state.imGuiIni); } diff --git a/client/src/main.h b/client/src/main.h index b430d4b1..a9a4d05f 100644 --- a/client/src/main.h +++ b/client/src/main.h @@ -64,7 +64,6 @@ struct AppState struct ll * overlays; char * fontName; ImFont * fontLarge; - ImVector_ImWchar fontRange; bool overlayInput; ImGuiMouseCursor cursorLast; char * imGuiIni; diff --git a/client/src/util.c b/client/src/util.c index 36ac3e85..ac722fcd 100644 --- a/client/src/util.c +++ b/client/src/util.c @@ -30,7 +30,228 @@ #include #include +struct UIFontFace +{ + char * file; + int index; + ImVector_ImWchar ranges; +}; + static FcConfig * FontConfig = NULL; +static FcPattern * UIFontQuery = NULL; +static FcPattern * UIFontPrimary = NULL; +static FcCharSet * UIFontChars = NULL; +static FcCharSet * UIFontRequested = NULL; +static struct UIFontFace * UIFontFaces = NULL; +static unsigned int UIFontFaceCount = 0; +static LG_Lock UIFontLock; +static bool UIFontLockInitialized = false; + +static void uiFontClearFaces(void) +{ + for (unsigned int i = 0; i < UIFontFaceCount; ++i) + { + free(UIFontFaces[i].file); + ImVector_ImWchar_UnInit(&UIFontFaces[i].ranges); + } + + free(UIFontFaces); + UIFontFaces = NULL; + UIFontFaceCount = 0; +} + +static void uiFontAddRanges(const ImWchar * ranges) +{ + for (; ranges[0]; ranges += 2) + for (FcChar32 c = ranges[0]; c <= (FcChar32)ranges[1]; ++c) + FcCharSetAddChar(UIFontChars, c); +} + +static bool uiFontCharsetToRanges( + const FcCharSet * charset, ImVector_ImWchar * ranges) +{ + ImFontGlyphRangesBuilder * builder = + ImFontGlyphRangesBuilder_ImFontGlyphRangesBuilder(); + if (!builder) + return false; + + FcChar32 map[FC_CHARSET_MAP_SIZE]; + FcChar32 next; + for (FcChar32 base = FcCharSetFirstPage(charset, map, &next); + base != FC_CHARSET_DONE; + base = FcCharSetNextPage(charset, map, &next)) + { + for (unsigned int i = 0; i < FC_CHARSET_MAP_SIZE; ++i) + for (unsigned int bit = 0; bit < 32; ++bit) + { + if (!(map[i] & (UINT32_C(1) << bit))) + continue; + + const FcChar32 c = base + i * 32 + bit; + if (c >= 0x20 && c <= IM_UNICODE_CODEPOINT_MAX) + ImFontGlyphRangesBuilder_AddChar(builder, (ImWchar)c); + } + } + + ImVector_ImWchar_Init(ranges); + ImFontGlyphRangesBuilder_BuildRanges(builder, ranges); + ImFontGlyphRangesBuilder_destroy(builder); + return ranges->Size > 1; +} + +static bool uiFontAddFace(const FcPattern * pattern, FcCharSet ** remainingPtr) +{ + FcCharSet * remaining = *remainingPtr; + FcChar8 * file; + FcCharSet * charset; + FcBool outline = FcTrue; + + if (FcPatternGetString(pattern, FC_FILE, 0, &file) != FcResultMatch || + FcPatternGetCharSet(pattern, FC_CHARSET, 0, &charset) != FcResultMatch) + return true; + + if (FcPatternGetBool(pattern, FC_OUTLINE, 0, &outline) == FcResultMatch && + !outline) + return true; + + FcCharSet * covered = FcCharSetIntersect(remaining, charset); + if (!covered) + return false; + + if (FcCharSetCount(covered) == 0) + { + FcCharSetDestroy(covered); + return true; + } + + struct UIFontFace * faces = realloc(UIFontFaces, + sizeof(*UIFontFaces) * (UIFontFaceCount + 1)); + if (!faces) + { + FcCharSetDestroy(covered); + return false; + } + UIFontFaces = faces; + + struct UIFontFace * face = &UIFontFaces[UIFontFaceCount]; + memset(face, 0, sizeof(*face)); + face->file = strdup((const char *)file); + if (!face->file || !uiFontCharsetToRanges(covered, &face->ranges)) + { + free(face->file); + face->file = NULL; + FcCharSetDestroy(covered); + return false; + } + + int index = 0; + if (FcPatternGetInteger(pattern, FC_INDEX, 0, &index) == FcResultMatch) + face->index = index & 0xFFFF; + + ++UIFontFaceCount; + + FcCharSet * next = FcCharSetSubtract(remaining, covered); + FcCharSetDestroy(covered); + if (!next) + return false; + + FcCharSetDestroy(remaining); + *remainingPtr = next; + return true; +} + +static bool uiFontBuildFaces(ImFontAtlas * atlas) +{ + uiFontClearFaces(); + + uiFontAddRanges(ImFontAtlas_GetGlyphRangesDefault(atlas)); + uiFontAddRanges((ImWchar[]) { + 0x2190, 0x2193, // four directional arrows + 0, + }); + + FcCharSet * remaining = FcCharSetCopy(UIFontChars); + if (!remaining) + return false; + + if (!uiFontAddFace(UIFontPrimary, &remaining)) + goto fail; + + if (FcCharSetCount(remaining) > 0) + { + FcPattern * query = FcPatternDuplicate(UIFontQuery); + if (!query) + goto fail; + + FcPatternDel(query, FC_CHARSET); + FcPatternAddCharSet(query, FC_CHARSET, remaining); + + FcResult result; + FcFontSet * fonts = FcFontSort(FontConfig, query, FcTrue, NULL, &result); + FcPatternDestroy(query); + if (!fonts) + goto fail; + + for (int i = 0; i < fonts->nfont && FcCharSetCount(remaining) > 0; ++i) + if (!uiFontAddFace(fonts->fonts[i], &remaining)) + { + FcFontSetSortDestroy(fonts); + goto fail; + } + + FcFontSetSortDestroy(fonts); + } + + if (FcCharSetCount(remaining) > 0) + { + FcCharSet * missing = FcCharSetIntersect(remaining, UIFontRequested); + if (missing) + { + if (FcCharSetCount(missing) > 0) + DEBUG_WARN("%u requested UI glyphs have no usable outline font", + FcCharSetCount(missing)); + FcCharSetDestroy(missing); + } + } + + FcCharSetDestroy(remaining); + return UIFontFaceCount > 0; + +fail: + FcCharSetDestroy(remaining); + uiFontClearFaces(); + return false; +} + +static ImFont * uiFontAddSize(ImFontAtlas * atlas, float size) +{ + ImFont * result = NULL; + + for (unsigned int i = 0; i < UIFontFaceCount; ++i) + { + ImFontConfig * config = ImFontConfig_ImFontConfig(); + if (!config) + return NULL; + + config->MergeMode = result != NULL; + config->FontNo = UIFontFaces[i].index; + + ImFont * font = ImFontAtlas_AddFontFromFileTTF(atlas, + UIFontFaces[i].file, size, config, UIFontFaces[i].ranges.Data); + ImFontConfig_destroy(config); + + if (!font) + { + DEBUG_WARN("Failed to add UI font face: %s", UIFontFaces[i].file); + continue; + } + + if (!result) + result = font; + } + + return result; +} bool util_fileGetContents(const char * filename, char ** buffer, size_t * length) { @@ -238,6 +459,25 @@ bool util_initUIFonts(void) return false; } + UIFontChars = FcCharSetCreate(); + UIFontRequested = FcCharSetCreate(); + if (!UIFontChars || !UIFontRequested) + { + DEBUG_ERROR("FcCharSetCreate Failed"); + if (UIFontChars) + FcCharSetDestroy(UIFontChars); + if (UIFontRequested) + FcCharSetDestroy(UIFontRequested); + UIFontChars = NULL; + UIFontRequested = NULL; + FcConfigDestroy(FontConfig); + FontConfig = NULL; + FcFini(); + return false; + } + + LG_LOCK_INIT(UIFontLock); + UIFontLockInitialized = true; return true; } @@ -264,11 +504,22 @@ char * util_getUIFont(const char * fontName) goto fail_parse; } + FcPatternDestroy(UIFontQuery); + FcPatternDestroy(UIFontPrimary); + UIFontQuery = FcPatternDuplicate(pat); + UIFontPrimary = FcPatternDuplicate(match); + if (!UIFontQuery || !UIFontPrimary) + { + DEBUG_ERROR("Failed to save the UI font pattern"); + goto fail_match; + } + if (FcPatternGetString(match, FC_FILE, 0, &file) == FcResultMatch) ttf = strdup((char *) file); else DEBUG_ERROR("Failed to locate the requested font: %s", fontName); +fail_match: FcPatternDestroy(match); fail_parse: @@ -276,11 +527,97 @@ fail_parse: return ttf; } +bool util_uiFontAddText(const char * text) +{ + if (!UIFontChars || !text) + return false; + + bool changed = false; + LG_LOCK(UIFontLock); + + const char * pos = text; + while (*pos) + { + unsigned int c; + const int length = igImTextCharFromUtf8(&c, pos, NULL); + if (length <= 0) + break; + pos += length; + + if (c < 0x20 || c > IM_UNICODE_CODEPOINT_MAX) + continue; + + FcCharSetAddChar(UIFontRequested, c); + if (!FcCharSetHasChar(UIFontChars, c) && + FcCharSetAddChar(UIFontChars, c)) + changed = true; + } + + LG_UNLOCK(UIFontLock); + return changed; +} + +bool util_buildUIFontAtlas( + ImFontAtlas * atlas, float size, ImFont ** large) +{ + if (!atlas || !UIFontQuery || !UIFontPrimary || !UIFontChars) + return false; + + bool result = false; + LG_LOCK(UIFontLock); + + ImFontAtlas_Clear(atlas); + if (!uiFontBuildFaces(atlas)) + { + DEBUG_ERROR("Failed to resolve UI fonts"); + goto done; + } + + if (!uiFontAddSize(atlas, size)) + { + DEBUG_ERROR("Failed to add the primary UI font"); + goto done; + } + + *large = uiFontAddSize(atlas, size * 1.3f); + if (!*large) + { + DEBUG_ERROR("Failed to add the large UI font"); + goto done; + } + + result = ImFontAtlas_Build(atlas); + +done: + LG_UNLOCK(UIFontLock); + return result; +} + void util_freeUIFonts(void) { if (!FontConfig) return; + if (UIFontLockInitialized) + LG_LOCK(UIFontLock); + + uiFontClearFaces(); + FcCharSetDestroy(UIFontChars); + FcCharSetDestroy(UIFontRequested); + FcPatternDestroy(UIFontQuery); + FcPatternDestroy(UIFontPrimary); + UIFontChars = NULL; + UIFontRequested = NULL; + UIFontQuery = NULL; + UIFontPrimary = NULL; + + if (UIFontLockInitialized) + { + LG_UNLOCK(UIFontLock); + LG_LOCK_FREE(UIFontLock); + UIFontLockInitialized = false; + } + FcConfigDestroy(FontConfig); FontConfig = NULL; FcFini();