ocornut / imgui

Dear ImGui: Bloat-free Graphical User interface for C++ with minimal dependencies
MIT License
59.55k stars 10.15k forks source link

Merging icon font doesn't work #7993

Closed Durobot closed 2 days ago

Durobot commented 2 days ago

Version/Branch of Dear ImGui:

Version 1.91.1, Branch: docking (via cimgui)

Back-ends:

imgui_impl_glfw.cpp + imgui_impl_opengl3.cpp

Compiler, OS:

Arch Linux + gcc 14.2.1 20240910

Full config/build information:

Dear ImGui 1.91.1 (19110)
--------------------------------
sizeof(size_t): 8, sizeof(ImDrawIdx): 2, sizeof(ImDrawVert): 20
define: __cplusplus=201103
define: IMGUI_DISABLE_OBSOLETE_FUNCTIONS
define: IMGUI_DISABLE_OBSOLETE_KEYIO
define: __linux__
define: __GNUC__=14
define: IMGUI_HAS_VIEWPORT
define: IMGUI_HAS_DOCK
--------------------------------
io.BackendPlatformName: imgui_impl_glfw
io.BackendRendererName: imgui_impl_opengl3
io.ConfigFlags: 0x00000081
 NavEnableKeyboard
 DockingEnable
io.ConfigViewportsNoDecoration
io.ConfigInputTextCursorBlink
io.ConfigWindowsResizeFromEdges
io.ConfigMemoryCompactTimer = 60.0
io.BackendFlags: 0x00001C0E
 HasMouseCursors
 HasSetMousePos
 PlatformHasViewports
 HasMouseHoveredViewport
 RendererHasVtxOffset
 RendererHasViewports
--------------------------------
io.Fonts: 1 fonts, Flags: 0x00000000, TexSize: 512,64
io.DisplaySize: 1024.00,768.00
io.DisplayFramebufferScale: 1.00,1.00
--------------------------------
style.WindowPadding: 8.00,8.00
style.WindowBorderSize: 1.00
style.FramePadding: 4.00,3.00
style.FrameRounding: 0.00
style.FrameBorderSize: 0.00
style.ItemSpacing: 8.00,4.00
style.ItemInnerSpacing: 4.00,4.00

Details:

I'm trying to merge an icon font into the standard built-in ProggyClean.ttf to be able to add icons to tree nodes / leaves, checkboxes, buttons, etc.

Currently I don't plan on replacing ProggyClean.ttf, although at some point in the future I probably will have to think about my "font strategy" in the context of internationalization. For now, 13 pixel ProggyClean.ttf is fine.

Also, I don't have any particular icon font in mind at the moment, I will probably have to make my own, but for now I am experimenting with fontawesome-webfont.ttf v4.7.0 I have downloaded from https://www.cdnpkg.com/font-awesome/file/fontawesome-webfont.ttf/.

My test program below is written in C and I am using Dear Imgui via cimgui. My actual development takes place in Zig, also via cimgui, directly. I don't use any type of bindings, as Zig can compile, link C and C++ and also import C into Zig, translate C to Zig, so bindings are more of a syntactic sugar, not that this is relevant to my problem.

I'm following this example, but when I merge fontawesome-webfont.ttf with the default font (?), my test control,

igText(ICON_FA_GLASS ICON_FA_SEARCH " find a glass");

shows up empty.

When I don't merge fontawesome-webfont.ttf and instead switch my font by calling igPushFont(p_fnt_fa); / igPopFont(); around igText(ICON_FA_GLASS ICON_FA_SEARCH " find a glass");, the glass and search icons show up - see the screenshots.

To switch between these behaviors, comment out / uncomment #define MERGE_FONTS in the test code below.

When I merge fontawesome-webfont.ttf, in the Style Editor I can see one font, "ProggyClean.ttf, 13px" with two inputs:

Imput 0: 'ProggyClean.ttf, 13px', Oversample: (1, 1), PixelSnapH: 1, Offset: (0.0, 1.0)
Input 1: 'fontawesome-webfont.ttf, 13px', Oversample: (0, 0), PixelSnapH: 0, Offset: (0.0, 0.0)

There are 227 glyphs: the U+0000..U+00FF region (224 glyphs), which looks fine, and U+F000..U+F0FF (3 glyphs), which looks empty. See the screenshot below.

When I don't merge fontawesome-webfont.ttf (by compiling the example code with #define MERGE_FONTS commented out), I can see two fonts in the Style Editor:

Font: "ProggyClean.ttf, 13px"
13.00 px, 224 glyphs, 1 file(s)
Font: "fontawesome-webfont.ttf, 13px"
13.00 px, 3 glyphs, 1 file(s)

fontawesome-webfont.ttf shows 3 icons, just as expected. See the screenshots below.

I have tried calling ImFontAtlas_Build(ioptr->Fonts); after I load / merge fontawesome-webfont.ttf, but it doesn't seem to matter.

Screenshots/Video:

Merged fonts, test control doesn't show up: merged_fonts

Separate fonts, test control shows up (it doesn't show text, but shows the icons, as expected): separate_fonts

Merged fonts, U+F000..U+F0FF region is claimed to contain 3 glyphs, but they are empty: style_editor_merged_fonts

Merged fonts, font atlas does not contain the expected icons: style_editor_merged_fonts_atlas

Separate fonts, U+F000..U+F0FF region contains and shows 3 icon glyphs: style_editor_separate_fonts

Separate fonts, font atlas contains the icons: style_editor_separate_fonts_atlas

Minimal, Complete and Verifiable Example code:

// Comment out to use `fontawesome-webfont.ttf` as a separate font
#define MERGE_FONTS
// fontawesome-webfont.ttf version 4.7.0 downloaded from
// https://www.cdnpkg.com/font-awesome/file/fontawesome-webfont.ttf/

#define CIMGUI_DEFINE_ENUMS_AND_STRUCTS

#define CIMGUI_USE_OPENGL3
#define CIMGUI_USE_GLFW

#include "cimgui.h"
#include "cimgui_impl.h"
#include <GLFW/glfw3.h>
#include <stdio.h>
#ifdef _MSC_VER
#include <windows.h>
#endif
#include <GL/gl.h>

#ifdef IMGUI_HAS_IMSTR
#define igBegin igBegin_Str
#define igSliderFloat igSliderFloat_Str
#define igCheckbox igCheckbox_Str
#define igColorEdit3 igColorEdit3_Str
#define igButton igButton_Str
#endif

// From https://github.com/juliettef/IconFontCppHeaders/blob/main/IconsFontAwesome4.h
#define ICON_MIN_FA 0xf000
#define ICON_MAX_16_FA 0xf2e0
//#define ICON_MAX_FA 0xf2e0
#define ICON_MAX_FA 0xf002

#define ICON_FA_GLASS "\xef\x80\x80"      // U+f000
#define ICON_FA_MUSIC "\xef\x80\x81"      // U+f001
#define ICON_FA_SEARCH "\xef\x80\x82"     // U+f002
#define ICON_FA_ENVELOPE_O "\xef\x80\x83" // U+f003
#define ICON_FA_HEART "\xef\x80\x84"      // U+f004
#define ICON_FA_STAR "\xef\x80\x85"       // U+f005
#define ICON_FA_STAR_O "\xef\x80\x86"     // U+f006
#define ICON_FA_USER "\xef\x80\x87"       // U+f007
#define ICON_FA_FILM "\xef\x80\x88"       // U+f008
#define ICON_FA_TH_LARGE "\xef\x80\x89"   // U+f009
#define ICON_FA_TH "\xef\x80\x8a"         // U+f00a

GLFWwindow *window;

int main(int argc, char *argv[])
{
    if (!glfwInit())
        return -1;

    // Decide GL+GLSL versions
    glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GLFW_TRUE);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2);

#if __APPLE__
    // GL 3.2 Core + GLSL 150
    const char *glsl_version = "#version 150";
#else
    // GL 3.2 + GLSL 130
    const char *glsl_version = "#version 130";
#endif

    // just an extra window hint for resize
    glfwWindowHint(GLFW_RESIZABLE, GLFW_TRUE);

    window = glfwCreateWindow(1024, 768, "Hello World!", NULL, NULL);
    if (!window)
    {
        printf("Window creation failed, terminating...\n");
        glfwTerminate();
        return -1;
    }

    glfwMakeContextCurrent(window);

    // enable vsync
    glfwSwapInterval(1);

    // check opengl version sdl uses
    printf("::: OpenGL version: %s\n", (char *)glGetString(GL_VERSION));

    // setup imgui
    igCreateContext(NULL);

    // set docking
    ImGuiIO *ioptr = igGetIO();
    ioptr->ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;   // Enable Keyboard Controls
    //ioptr->ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad;  // Enable Gamepad Controls
#ifdef IMGUI_HAS_DOCK
    ioptr->ConfigFlags |= ImGuiConfigFlags_DockingEnable;       // Enable Docking
    // ioptr->ConfigFlags |= ImGuiConfigFlags_ViewportsEnable;     // Enable Multi-Viewport / Platform Windows
#endif

    ImGui_ImplGlfw_InitForOpenGL(window, true);
    ImGui_ImplOpenGL3_Init(glsl_version);

    igStyleColorsDark(NULL);
    // ImFontAtlas_AddFontDefault(io.Fonts, NULL);

    bool showDemoWindow = true;
    bool showAnotherWindow = false;
    ImVec4 clearColor;
    clearColor.x = 0.45f;
    clearColor.y = 0.55f;
    clearColor.z = 0.60f;
    clearColor.w = 1.00f;

    // ------------------------------------------------------------------------------------------
    // https://github.com/ocornut/imgui/blob/master/docs/FONTS.md#using-icon-fonts
    ImFontAtlas_AddFontDefault(ioptr->Fonts, NULL); // NULL is default param value in imgui.h

    static const ImWchar icon_ranges[] = { ICON_MIN_FA, ICON_MAX_FA, 0 };
#ifdef MERGE_FONTS
    // If there are fewer initializers in a brace-enclosed list than there are elements or members of an aggregate,
    // or fewer characters in a string literal used to initialize an array of known size than there are elements in the array,
    // the remainder of the aggregate shall be initialized implicitly the same as objects that have static storage duration.
    ImFontConfig config = { 0 };
    config.FontData = NULL; // To be extra certain :)
    config.MergeMode = true;
    config.GlyphMinAdvanceX = 13.0f; // Use if you want to make the icon monospaced
    ImFontAtlas_AddFontFromFileTTF(ioptr->Fonts, "/home/archie/projects/c-playground/cimgui-02/fonts/fontawesome-webfont.ttf", 13.0f, &config, icon_ranges);
#else
    ImFont* p_fnt_fa =
        ImFontAtlas_AddFontFromFileTTF(ioptr->Fonts, "/home/archie/projects/c-playground/cimgui-02/fonts/fontawesome-webfont.ttf", 13.0f, NULL, icon_ranges);
#endif
    //ImFontAtlas_Build(ioptr->Fonts); // -- Must we do this? It doesn't seem to make any difference.
    // -------------------------------------------------------------------------------------------

    // main event loop
    // bool quit = false;
    while (!glfwWindowShouldClose(window))
    {
        glfwPollEvents();

        // start imgui frame
        ImGui_ImplOpenGL3_NewFrame();
        ImGui_ImplGlfw_NewFrame();
        igNewFrame();

        if (showDemoWindow)
            igShowDemoWindow(&showDemoWindow);

        // show a simple window that we created ourselves.
        static float f = 0.0f;
        static int counter = 0;

        igBegin("Hello, world!", NULL, 0);
        igText("This is some useful text");

#ifndef MERGE_FONTS
        igPushFont(p_fnt_fa);
#endif
        igText(ICON_FA_GLASS ICON_FA_SEARCH " find a glass"); // (""); //\xef\x80\x80\xef\x80\x8a");
#ifndef MERGE_FONTS
        igPopFont();
#endif

        igCheckbox("Demo window", &showDemoWindow);
        igCheckbox("Another window", &showAnotherWindow);

        igSliderFloat("Float", &f, 0.0f, 1.0f, "%.3f", 0);
        igColorEdit3("clear color", (float *)&clearColor, 0);

        ImVec2 buttonSize;
        buttonSize.x = 0;
        buttonSize.y = 0;
        if (igButton("Button", buttonSize))
            counter++;
        igSameLine(0.0f, -1.0f);
        igText("counter = %d", counter);

        igText("Application average %.3f ms/frame (%.1f FPS)",
              1000.0f / igGetIO()->Framerate, igGetIO()->Framerate);
        igEnd();

        if (showAnotherWindow)
        {
            igBegin("imgui Another Window", &showAnotherWindow, 0);
            igText("Hello from imgui");
            ImVec2 buttonSize;
            buttonSize.x = 0;
            buttonSize.y = 0;
            if (igButton("Close me", buttonSize))
                showAnotherWindow = false;

            igEnd();
        }

        // render
        igRender();
        glfwMakeContextCurrent(window);
        glViewport(0, 0, (int)ioptr->DisplaySize.x, (int)ioptr->DisplaySize.y);
        glClearColor(clearColor.x, clearColor.y, clearColor.z, clearColor.w);
        glClear(GL_COLOR_BUFFER_BIT);
        ImGui_ImplOpenGL3_RenderDrawData(igGetDrawData());
#ifdef IMGUI_HAS_DOCK
        if (ioptr->ConfigFlags & ImGuiConfigFlags_ViewportsEnable)
        {
            GLFWwindow *backup_current_window = glfwGetCurrentContext();
            igUpdatePlatformWindows();
            igRenderPlatformWindowsDefault(NULL, NULL);
            glfwMakeContextCurrent(backup_current_window);
        }
#endif
        glfwSwapBuffers(window);
    }

    // clean up
    ImGui_ImplOpenGL3_Shutdown();
    ImGui_ImplGlfw_Shutdown();
    igDestroyContext(NULL);

    glfwDestroyWindow(window);
    glfwTerminate();

    return 0;
}
ocornut commented 2 days ago

Input 1: 'fontawesome-webfont.ttf, 13px', Oversample: (0, 0), PixelSnapH: 0, Offset: (0.0, 0.0)

ImFontConfig config = { 0 };

I believe that your problem and it may be due to your ImFontConfig structure being zero initialized instead of constructed with our default values.

The default constructor does this:

ImFontConfig::ImFontConfig()
{
    memset(this, 0, sizeof(*this));
    FontDataOwnedByAtlas = true;
    OversampleH = 2;
    OversampleV = 1;
    GlyphMaxAdvanceX = FLT_MAX;
    RasterizerMultiply = 1.0f;
    RasterizerDensity = 1.0f;
    EllipsisChar = (ImWchar)-1;
}

I believe you need to look into how to initialize structures with cimgui.

Durobot commented 2 days ago

Looks like that was indeed what was causing my problem. After a cursory search for ImFontConfig constructor equivalent in cimgui and failing to find one - only found an allocator function:

CIMGUI_API ImFontConfig* ImFontConfig_ImFontConfig(void)
{
    return IM_NEW(ImFontConfig)();
}

I have added initialization code into my example directly:

    ImFontConfig config = { 0 };
    // Dear ImGui standard init (from ImFontConfig::ImFontConfig() in imgui_draw.cpp)
    config.FontDataOwnedByAtlas = true;
    config.OversampleH = 2;
    config.OversampleV = 1;
    config.GlyphMaxAdvanceX = FLT_MAX;
    config.RasterizerMultiply = 1.0f;
    config.RasterizerDensity = 1.0f;
    config.EllipsisChar = (ImWchar)-1;

    config.FontData = NULL; // To be extra certain :)
    config.MergeMode = true;
    config.GlyphMinAdvanceX = 13.0f; // Use if you want to make the icon monospaced
    ImFontAtlas_AddFontFromFileTTF(ioptr->Fonts, "/home/archie/projects/c-playground/cimgui-02/fonts/fontawesome-webfont.ttf", 13.0f, &config, icon_ranges);

and voila! merged_fonts_success

Thank you so much!

Durobot commented 2 days ago

I'll keep my eyes open for Dear ImGui constructors / default initialization that has to be done manually with cimgui.

ocornut commented 2 days ago

We noted it was an issue with dear bindings too and will try to address it. I think you may ask cimgui to provide a constructor function for some type. This one is clearly the most common one.

I have added an assert 6ce26ef to detect your issue in the future.

sonoro1234 commented 1 day ago

I'll keep my eyes open for Dear ImGui constructors / default initialization that has to be done manually with cimgui.

you need ImFontConfig *config = ImFontConfig_ImFontConfig();

ocornut commented 1 day ago

you need ImFontConfig *config = ImFontConfig_ImFontConfig();

As mentioned in https://github.com/cimgui/cimgui/issues/278, this seems to needlessly allocate on the heap. Whereas this solution wouldn't: https://github.com/cimgui/cimgui/issues/278#issuecomment-2356300849 And would be more idiomatic.

Durobot commented 16 hours ago

They have added a new parameter to cimgui's generator.sh / generator.bat scripts, "constructors". So, for example, ./generator.sh -t "internal noimstrv constructors" now outputs cimgui.h and cimgui.cpp files with *_Construct() functions that call Dear ImGui constructors.