Font: implement a way to draw narrow ellipsis without relying on hardcoded 1 pixel dots. (#2775)

This changeset implements several pieces of the puzzle that add up to a narrow ellipsis rendering.

## EllipsisCodePoint

`ImFontConfig` and `ImFont` received `ImWchar EllipsisCodePoint = -1;` field. User may configure `ImFontConfig::EllipsisCodePoint` a unicode codepoint that will be used for rendering narrow ellipsis. Not setting this field will automatically detect a suitable character or fall back to rendering 3 dots with minimal spacing between them. Autodetection prefers codepoint 0x2026 (narrow ellipsis) and falls back to 0x0085 (NEXT LINE) when missing. Wikipedia indicates that codepoint 0x0085 was used as ellipsis in some older windows fonts. So does default Dear ImGui font. When user is merging fonts - first configured and present ellipsis codepoint will be used, ellipsis characters from subsequently merged fonts will be ignored.

## Narrow ellipsis

Rendering a narrow ellipsis is surprisingly not straightforward task. There are cases when ellipsis is bigger than the last visible character therefore `RenderTextEllipsis()` has to hide last two characters. In a subset of those cases ellipsis is as big as last visible character + space before it. `RenderTextEllipsis()` tries to work around this case by taking free space between glyph edges into account. Code responsible for this functionality is within `if (text_end_ellipsis != text_end_full) { ... }`.

## Fallback (manually rendered dots)

There are cases when font does not have ellipsis character defined. In this case RenderTextEllipsis() falls back to rendering ellipsis as 3 dots, but with reduced spacing between them. 1 pixel space is used in all cases. This results in a somewhat wider ellipsis, but avoids issues where spaces between dots are uneven (visible in larger/monospace fonts) or squish dots way too much (visible in default font where dot is essentially a pixel). This fallback method obsoleted `RenderPixelEllipsis()` and this function was removed. Note that fallback ellipsis will always be somewhat wider than it could be, however it will fit in visually into every font used unlike what `RenderPixelEllipsis()` produced.
diff --git a/imgui.cpp b/imgui.cpp
index c5039d6..7921b1d 100644
--- a/imgui.cpp
+++ b/imgui.cpp
@@ -2489,7 +2489,7 @@
 // Another overly complex function until we reorganize everything into a nice all-in-one helper.
 // This is made more complex because we have dissociated the layout rectangle (pos_min..pos_max) which define _where_ the ellipsis is, from actual clipping of text and limit of the ellipsis display.
 // This is because in the context of tabs we selectively hide part of the text when the Close Button appears, but we don't want the ellipsis to move. 
-void    ImGui::RenderTextEllipsis(ImDrawList* draw_list, const ImVec2& pos_min, const ImVec2& pos_max, float clip_max_x, float ellipsis_max_x, const char* text, const char* text_end_full, const ImVec2* text_size_if_known)
+void ImGui::RenderTextEllipsis(ImDrawList* draw_list, const ImVec2& pos_min, const ImVec2& pos_max, float clip_max_x, float ellipsis_max_x, const char* text, const char* text_end_full, const ImVec2* text_size_if_known)
 {
     ImGuiContext& g = *GImGui;
     if (text_end_full == NULL)
@@ -2503,15 +2503,42 @@
         // min   max   ellipsis_max
         //          <-> this is generally some padding value
 
-        // FIXME-STYLE: RenderPixelEllipsis() style should use actual font data.
         const ImFont* font = draw_list->_Data->Font;
         const float font_size = draw_list->_Data->FontSize;
-        const int ellipsis_dot_count = 3;
-        const float ellipsis_width = (1.0f + 1.0f) * ellipsis_dot_count - 1.0f;
         const char* text_end_ellipsis = NULL;
+        const ImFontGlyph* glyph;
+        int ellipsis_char_num = 1;
+        ImWchar ellipsis_codepoint = font->EllipsisCodePoint;
 
+        if (ellipsis_codepoint != (ImWchar)-1)
+            glyph = font->FindGlyph(ellipsis_codepoint);
+        else
+        {
+            ellipsis_codepoint = (ImWchar)'.';
+            glyph = font->FindGlyph(ellipsis_codepoint);
+            ellipsis_char_num = 3;
+        }
+
+        float ellipsis_glyph_width = glyph->X1;                      // Width of the glyph with no padding on either side
+        float ellipsis_width = ellipsis_glyph_width;                 // Full width of entire ellipsis
+        float push_left = 1.f;
+        
+        if (ellipsis_char_num > 1)
+        {
+            const float spacing_between_dots = 1.f * (draw_list->_Data->FontSize / font->FontSize);
+            ellipsis_glyph_width = glyph->X1 - glyph->X0 + spacing_between_dots;
+            // Full ellipsis size without free spacing after it.
+            ellipsis_width = ellipsis_glyph_width * (float)ellipsis_char_num - spacing_between_dots;
+            if (glyph->X0 > 1.f)
+            {
+                // Pushing ellipsis to the left will be accomplished by rendering the dot (X0).
+                push_left = 0.f;
+            }
+        }
+        
         float text_width = ImMax((pos_max.x - ellipsis_width) - pos_min.x, 1.0f);
         float text_size_clipped_x = font->CalcTextSizeA(font_size, text_width, 0.0f, text, text_end_full, &text_end_ellipsis).x;
+
         if (text == text_end_ellipsis && text_end_ellipsis < text_end_full)
         {
             // Always display at least 1 character if there's no room for character + ellipsis
@@ -2524,11 +2551,66 @@
             text_end_ellipsis--;
             text_size_clipped_x -= font->CalcTextSizeA(font_size, FLT_MAX, 0.0f, text_end_ellipsis, text_end_ellipsis + 1).x; // Ascii blanks are always 1 byte
         }
+
+        if (text_end_ellipsis != text_end_full)
+        {
+            //   +---- First invisible character we arrived at.
+            //  /  +-- Character that we hope to be first invisible.
+            // [l][i]
+            //  ||||
+            //   \ \__ extra_spacing when two characters got hidden
+            //    \___ extra_spacing when one character got hidden
+            unsigned c = 0;
+            float extra_spacing = 0;
+            const char* text_end_ellipsis_prev = text_end_ellipsis;
+            text_end_ellipsis += ImTextCharFromUtf8(&c, text_end_ellipsis, text_end_full);
+            if (c && !ImCharIsBlankW(c))
+            {
+                const ImFontGlyph* hidden_glyph = font->FindGlyph(c);
+                // Free space after first invisible glyph
+                extra_spacing = hidden_glyph->AdvanceX - hidden_glyph->X1;
+                c = 0;
+                text_end_ellipsis += ImTextCharFromUtf8(&c, text_end_ellipsis, text_end_full);
+                if (c && !ImCharIsBlankW(c))
+                {
+                    hidden_glyph = font->FindGlyph(text_end_ellipsis[1]);
+                    // Space before next invisible glyph. This intentionally ignores space from the first invisible 
+                    // glyph as that space will serve as spacing between ellipsis and last visible character. Without
+                    // doing this we may get into awkward situations where ellipsis pretty much sticks to the last 
+                    // visible character. This issue manifests with the default font for word "Brocolli" there both i 
+                    // and l are very thin. Unfortunately this makes fonts with wider gaps (like monospace) look a bit 
+                    // worse, but it is a fair middle ground.
+                    extra_spacing = hidden_glyph->X0;
+                }
+            }
+
+            if (extra_spacing > 0)
+            {
+                // Repeat calculation hoping that we will get extra character visible
+                text_width += extra_spacing;
+                // Text length calculation is essentially an optimized version of this:
+                //   text_size_clipped_x = font->CalcTextSizeA(font_size, text_width, 0.0f, text, text_end_full, &text_end_ellipsis).x;
+                // It avoids calculating entire width of the string.
+                text_size_clipped_x += font->CalcTextSizeA(font_size, text_width - text_size_clipped_x, 0.0f, text_end_ellipsis_prev, text_end_full, &text_end_ellipsis).x;
+            }
+            else
+                text_end_ellipsis = text_end_ellipsis_prev;
+        }
+
         RenderTextClippedEx(draw_list, pos_min, ImVec2(clip_max_x, pos_max.y), text, text_end_ellipsis, &text_size, ImVec2(0.0f, 0.0f));
 
-        const float ellipsis_x = pos_min.x + text_size_clipped_x + 1.0f;
-        if (ellipsis_x + ellipsis_width - 1.0f <= ellipsis_max_x)
-            RenderPixelEllipsis(draw_list, ImVec2(ellipsis_x, pos_min.y), GetColorU32(ImGuiCol_Text), ellipsis_dot_count);
+        // This variable pushes ellipsis to the left from last visible character. This is mostly useful when rendering
+        // ellipsis character contained in the font. If we render ellipsis manually space is already adequate and extra
+        // spacing is not needed.
+        float ellipsis_x = pos_min.x + text_size_clipped_x + push_left;
+        if (ellipsis_x + ellipsis_width - push_left <= ellipsis_max_x)
+        {
+            for (int i = 0; i < ellipsis_char_num; i++)
+            {
+                font->RenderChar(draw_list, font_size, ImVec2(ellipsis_x, pos_min.y), GetColorU32(ImGuiCol_Text), ellipsis_codepoint);
+                ellipsis_x += ellipsis_glyph_width;
+            }
+        }
     }
     else
     {
diff --git a/imgui.h b/imgui.h
index 42ee534..d4a81c3 100644
--- a/imgui.h
+++ b/imgui.h
@@ -2011,6 +2011,7 @@
     bool            MergeMode;              // false    // Merge into previous ImFont, so you can combine multiple inputs font into one ImFont (e.g. ASCII font + icons + Japanese glyphs). You may want to use GlyphOffset.y when merge font of different heights.
     unsigned int    RasterizerFlags;        // 0x00     // Settings for custom font rasterizer (e.g. ImGuiFreeType). Leave as zero if you aren't using one.
     float           RasterizerMultiply;     // 1.0f     // Brighten (>1.0f) or darken (<1.0f) font output. Brightening small fonts may be a good workaround to make them more readable.
+    ImWchar         EllipsisCodePoint;      // -1       // Explicitly specify unicode codepoint of ellipsis character. When fonts are being merged first specified ellipsis will be used.
 
     // [Internal]
     char            Name[40];               // Name (strictly to ease debugging)
@@ -2192,6 +2193,7 @@
     float                       Ascent, Descent;    // 4+4   // out //            // Ascent: distance from top to bottom of e.g. 'A' [0..FontSize]
     int                         MetricsTotalSurface;// 4     // out //            // Total surface in pixels to get an idea of the font rasterization/texture cost (not exact, we approximate the cost of padding between glyphs)
     bool                        DirtyLookupTables;  // 1     // out //
+    ImWchar                     EllipsisCodePoint;  // -1    // out //            // Override a codepoint used for ellipsis rendering.
 
     // Methods
     IMGUI_API ImFont();
diff --git a/imgui_draw.cpp b/imgui_draw.cpp
index 3eb1067..b4801c2 100644
--- a/imgui_draw.cpp
+++ b/imgui_draw.cpp
@@ -1428,6 +1428,7 @@
     RasterizerMultiply = 1.0f;
     memset(Name, 0, sizeof(Name));
     DstFont = NULL;
+    EllipsisCodePoint = (ImWchar)-1;
 }
 
 //-----------------------------------------------------------------------------
@@ -1618,6 +1619,9 @@
         memcpy(new_font_cfg.FontData, font_cfg->FontData, (size_t)new_font_cfg.FontDataSize);
     }
 
+    if (new_font_cfg.DstFont->EllipsisCodePoint == (ImWchar)-1)
+        new_font_cfg.DstFont->EllipsisCodePoint = font_cfg->EllipsisCodePoint;
+
     // Invalidate texture
     ClearTexData();
     return new_font_cfg.DstFont;
@@ -1652,6 +1656,7 @@
         font_cfg.SizePixels = 13.0f * 1.0f;
     if (font_cfg.Name[0] == '\0')
         ImFormatString(font_cfg.Name, IM_ARRAYSIZE(font_cfg.Name), "ProggyClean.ttf, %dpx", (int)font_cfg.SizePixels);
+    font_cfg.EllipsisCodePoint = (ImWchar)0x0085;
 
     const char* ttf_compressed_base85 = GetDefaultCompressedFontDataTTFBase85();
     const ImWchar* glyph_ranges = font_cfg.GlyphRanges != NULL ? font_cfg.GlyphRanges : GetGlyphRangesDefault();
@@ -2196,6 +2201,26 @@
     for (int i = 0; i < atlas->Fonts.Size; i++)
         if (atlas->Fonts[i]->DirtyLookupTables)
             atlas->Fonts[i]->BuildLookupTable();
+
+    // Ellipsis character is required for rendering elided text. We prefer using U+2026 (horizontal ellipsis).
+    // However some old fonts may contain ellipsis at U+0085. Here we auto-detect most suitable ellipsis character.
+    for (int i = 0; i < atlas->Fonts.size(); i++)
+    {
+        ImFont* font = atlas->Fonts[i];
+        if (font->EllipsisCodePoint == (ImWchar)-1)
+        {
+            const ImWchar ellipsis_variants[] = {(ImWchar)0x2026, (ImWchar)0x0085, (ImWchar)0};
+            for (int j = 0; ellipsis_variants[j] != (ImWchar) 0; j++)
+            {
+                ImWchar ellipsis_codepoint = ellipsis_variants[j];
+                if (font->FindGlyph(ellipsis_codepoint) != font->FallbackGlyph)     // Verify glyph exists
+                {
+                    font->EllipsisCodePoint = ellipsis_codepoint;
+                    break;
+                }
+            }
+        }
+    }
 }
 
 // Retrieve list of range (2 int per range, values are inclusive)
@@ -2474,6 +2499,7 @@
     Scale = 1.0f;
     Ascent = Descent = 0.0f;
     MetricsTotalSurface = 0;
+    EllipsisCodePoint = (ImWchar)-1;
 }
 
 ImFont::~ImFont()
@@ -3012,7 +3038,6 @@
 // - RenderMouseCursor()
 // - RenderArrowPointingAt()
 // - RenderRectFilledRangeH()
-// - RenderPixelEllipsis()
 //-----------------------------------------------------------------------------
 
 void ImGui::RenderMouseCursor(ImDrawList* draw_list, ImVec2 pos, float scale, ImGuiMouseCursor mouse_cursor)
@@ -3122,18 +3147,6 @@
     draw_list->PathFillConvex(col);
 }
 
-// FIXME: Rendering an ellipsis "..." is a surprisingly tricky problem for us... we cannot rely on font glyph having it,
-// and regular dot are typically too wide. If we render a dot/shape ourselves it comes with the risk that it wouldn't match
-// the boldness or positioning of what the font uses...
-void ImGui::RenderPixelEllipsis(ImDrawList* draw_list, ImVec2 pos, ImU32 col, int count)
-{
-    ImFont* font = draw_list->_Data->Font;
-    const float font_scale = draw_list->_Data->FontSize / font->FontSize;
-    pos.y += (float)(int)(font->DisplayOffset.y + font->Ascent * font_scale + 0.5f - 1.0f);
-    for (int dot_n = 0; dot_n < count; dot_n++)
-        draw_list->AddRectFilled(ImVec2(pos.x + dot_n * 2.0f, pos.y), ImVec2(pos.x + dot_n * 2.0f + 1.0f, pos.y + 1.0f), col);
-}
-
 //-----------------------------------------------------------------------------
 // [SECTION] Decompression code
 //-----------------------------------------------------------------------------
diff --git a/imgui_internal.h b/imgui_internal.h
index 9c87d2a..ea8a409 100644
--- a/imgui_internal.h
+++ b/imgui_internal.h
@@ -1635,7 +1635,6 @@
     IMGUI_API void          RenderMouseCursor(ImDrawList* draw_list, ImVec2 pos, float scale, ImGuiMouseCursor mouse_cursor = ImGuiMouseCursor_Arrow);
     IMGUI_API void          RenderArrowPointingAt(ImDrawList* draw_list, ImVec2 pos, ImVec2 half_sz, ImGuiDir direction, ImU32 col);
     IMGUI_API void          RenderRectFilledRangeH(ImDrawList* draw_list, const ImRect& rect, ImU32 col, float x_start_norm, float x_end_norm, float rounding);
-    IMGUI_API void          RenderPixelEllipsis(ImDrawList* draw_list, ImVec2 pos, ImU32 col, int count);
 
 #ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
     // 2019/06/07: Updating prototypes of some of the internal functions. Leaving those for reference for a short while.