From 45405f0dc9f2a3a08883748319aeba9a5afed6e3 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Thu, 5 Sep 2019 12:59:43 +0300 Subject: [PATCH 1/3] 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. --- imgui.cpp | 96 ++++++++++++++++++++++++++++++++++++++++++++---- imgui.h | 2 + imgui_draw.cpp | 39 +++++++++++++------- imgui_internal.h | 1 - 4 files changed, 117 insertions(+), 21 deletions(-) diff --git a/imgui.cpp b/imgui.cpp index c5039d637..7921b1d13 100644 --- a/imgui.cpp +++ b/imgui.cpp @@ -2489,7 +2489,7 @@ void ImGui::RenderTextClipped(const ImVec2& pos_min, const ImVec2& pos_max, cons // 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 @@ void ImGui::RenderTextEllipsis(ImDrawList* draw_list, const ImVec2& pos_min, // 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 @@ void ImGui::RenderTextEllipsis(ImDrawList* draw_list, const ImVec2& pos_min, 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 42ee53406..d4a81c3bb 100644 --- a/imgui.h +++ b/imgui.h @@ -2011,6 +2011,7 @@ struct ImFontConfig 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 @@ struct ImFont 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 3eb1067ef..b4801c26a 100644 --- a/imgui_draw.cpp +++ b/imgui_draw.cpp @@ -1428,6 +1428,7 @@ ImFontConfig::ImFontConfig() RasterizerMultiply = 1.0f; memset(Name, 0, sizeof(Name)); DstFont = NULL; + EllipsisCodePoint = (ImWchar)-1; } //----------------------------------------------------------------------------- @@ -1618,6 +1619,9 @@ ImFont* ImFontAtlas::AddFont(const ImFontConfig* font_cfg) 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 @@ ImFont* ImFontAtlas::AddFontDefault(const ImFontConfig* font_cfg_template) 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 @@ void ImFontAtlasBuildFinish(ImFontAtlas* atlas) 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 @@ ImFont::ImFont() Scale = 1.0f; Ascent = Descent = 0.0f; MetricsTotalSurface = 0; + EllipsisCodePoint = (ImWchar)-1; } ImFont::~ImFont() @@ -3012,7 +3038,6 @@ void ImFont::RenderText(ImDrawList* draw_list, float size, ImVec2 pos, ImU32 col // - RenderMouseCursor() // - RenderArrowPointingAt() // - RenderRectFilledRangeH() -// - RenderPixelEllipsis() //----------------------------------------------------------------------------- void ImGui::RenderMouseCursor(ImDrawList* draw_list, ImVec2 pos, float scale, ImGuiMouseCursor mouse_cursor) @@ -3122,18 +3147,6 @@ void ImGui::RenderRectFilledRangeH(ImDrawList* draw_list, const ImRect& rect, Im 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 9c87d2a14..ea8a40990 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -1635,7 +1635,6 @@ namespace ImGui 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. From 57623c15ddae10a6b0ba6b206d2502e7951dd705 Mon Sep 17 00:00:00 2001 From: omar Date: Tue, 17 Sep 2019 10:00:28 +0200 Subject: [PATCH 2/3] Font: Narrow ellipsis: various minor stylistic tweaks (#2775) --- docs/CHANGELOG.txt | 4 +++ docs/TODO.txt | 1 + imgui.cpp | 61 ++++++++++++++++++++-------------------------- imgui.h | 6 ++--- imgui_demo.cpp | 3 ++- imgui_draw.cpp | 29 ++++++++++------------ 6 files changed, 49 insertions(+), 55 deletions(-) diff --git a/docs/CHANGELOG.txt b/docs/CHANGELOG.txt index 6e487ae5c..36ba83ba8 100644 --- a/docs/CHANGELOG.txt +++ b/docs/CHANGELOG.txt @@ -50,6 +50,10 @@ Other Changes: - SliderScalar: Improved assert when using U32 or U64 types with a large v_max value. (#2765) [@loicmouton] - DragInt, DragFloat, DragScalar: Using (v_min > v_max) allows locking any edit to the value. - DragScalar: Fixed dragging of unsigned values on ARM cpu. (#2780) [@dBagrat] +- Font: Better ellipsis drawing implementation. Instead of drawing three pixel-ey dots (which was glaringly + unfitting with many types of fonts) we first attempt to find a standard ellipsis glyphs within the loaded set. + Otherwise we render ellipsis using '.' from the font from where we trim excessive spacing to make it as narrow + as possible. (#2775) [@rokups] - ImDrawList: clarified the name of many parameters so reading the code is a little easier. (#2740) - Using offsetof() when available in C++11. Avoids Clang sanitizer complaining about old-style macros. (#94) - Added a mechanism to compact/free the larger allocations of unused windows (buffers are compacted when diff --git a/docs/TODO.txt b/docs/TODO.txt index 70c64c40f..df413b7e3 100644 --- a/docs/TODO.txt +++ b/docs/TODO.txt @@ -275,6 +275,7 @@ It's mostly a bunch of personal notes, probably incomplete. Feel free to query i - font: MergeMode: flags to select overwriting or not (this is now very easy with refactored ImFontAtlasBuildWithStbTruetype) - font: free the Alpha buffer if user only requested RGBA. !- font: better CalcTextSizeA() API, at least for simple use cases. current one is horrible (perhaps have simple vs extended versions). + - font: for the purpose of RenderTextEllipsis(), it might be useful that CalcTextSizeA() can ignore the trailing padding? - font: a CalcTextHeight() helper could run faster than CalcTextSize().y - font: enforce monospace through ImFontConfig (for icons?) + create dual ImFont output from same input, reusing rasterized data but with different glyphs/AdvanceX - font: finish CustomRectRegister() to allow mapping Unicode codepoint to custom texture data diff --git a/imgui.cpp b/imgui.cpp index 7921b1d13..4a606daa3 100644 --- a/imgui.cpp +++ b/imgui.cpp @@ -2506,38 +2506,32 @@ void ImGui::RenderTextEllipsis(ImDrawList* draw_list, const ImVec2& pos_min, con const ImFont* font = draw_list->_Data->Font; const float font_size = draw_list->_Data->FontSize; 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 + ImWchar ellipsis_char = font->EllipsisChar; + int ellipsis_char_count = 1; + if (ellipsis_char == (ImWchar)-1) { - ellipsis_codepoint = (ImWchar)'.'; - glyph = font->FindGlyph(ellipsis_codepoint); - ellipsis_char_num = 3; + ellipsis_char = (ImWchar)'.'; + ellipsis_char_count = 3; } + const ImFontGlyph* glyph = font->FindGlyph(ellipsis_char); - 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; + float ellipsis_glyph_width = glyph->X1; // Width of the glyph with no padding on either side + float ellipsis_total_width = ellipsis_glyph_width; // Full width of entire ellipsis + float push_left = 1.0f; - if (ellipsis_char_num > 1) + if (ellipsis_char_count > 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; - } + const float spacing_between_dots = 1.0f * (draw_list->_Data->FontSize / font->FontSize); + ellipsis_glyph_width = glyph->X1 - glyph->X0 + spacing_between_dots; + ellipsis_total_width = ellipsis_glyph_width * (float)ellipsis_char_count - spacing_between_dots; + if (glyph->X0 > 1.0f) + push_left = 0.0f; // Pushing ellipsis to the left will be accomplished by rendering the dot (X0). } - 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; + float text_avail_width = ImMax((pos_max.x - ellipsis_total_width) - pos_min.x, 1.0f); + float text_size_clipped_x = font->CalcTextSizeA(font_size, text_avail_width, 0.0f, text, text_end_full, &text_end_ellipsis).x; if (text == text_end_ellipsis && text_end_ellipsis < text_end_full) { @@ -2547,7 +2541,7 @@ void ImGui::RenderTextEllipsis(ImDrawList* draw_list, const ImVec2& pos_min, con } while (text_end_ellipsis > text && ImCharIsBlankA(text_end_ellipsis[-1])) { - // Trim trailing space before ellipsis + // Trim trailing space before ellipsis (FIXME: Supporting non-ascii blanks would be nice, for this we need a function to backtrack in UTF-8 text) 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 } @@ -2560,16 +2554,15 @@ void ImGui::RenderTextEllipsis(ImDrawList* draw_list, const ImVec2& pos_min, con // |||| // \ \__ extra_spacing when two characters got hidden // \___ extra_spacing when one character got hidden - unsigned c = 0; - float extra_spacing = 0; + unsigned int c = 0; + float extra_spacing = 0.0f; 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 + const ImFontGlyph* hidden_glyph = font->FindGlyph((ImWchar)c); 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)) { @@ -2587,11 +2580,11 @@ void ImGui::RenderTextEllipsis(ImDrawList* draw_list, const ImVec2& pos_min, con if (extra_spacing > 0) { // Repeat calculation hoping that we will get extra character visible - text_width += extra_spacing; + text_avail_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; + text_size_clipped_x += font->CalcTextSizeA(font_size, text_avail_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; @@ -2603,14 +2596,12 @@ void ImGui::RenderTextEllipsis(ImDrawList* draw_list, const ImVec2& pos_min, con // 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++) + if (ellipsis_x + ellipsis_total_width - push_left <= ellipsis_max_x) + for (int i = 0; i < ellipsis_char_count; i++) { - font->RenderChar(draw_list, font_size, ImVec2(ellipsis_x, pos_min.y), GetColorU32(ImGuiCol_Text), ellipsis_codepoint); + font->RenderChar(draw_list, font_size, ImVec2(ellipsis_x, pos_min.y), GetColorU32(ImGuiCol_Text), ellipsis_char); ellipsis_x += ellipsis_glyph_width; } - } } else { diff --git a/imgui.h b/imgui.h index d4a81c3bb..9e875f897 100644 --- a/imgui.h +++ b/imgui.h @@ -2011,7 +2011,7 @@ struct ImFontConfig 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. + ImWchar EllipsisChar; // -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) @@ -2188,12 +2188,12 @@ struct ImFont ImFontAtlas* ContainerAtlas; // 4-8 // out // // What we has been loaded into const ImFontConfig* ConfigData; // 4-8 // in // // Pointer within ContainerAtlas->ConfigData short ConfigDataCount; // 2 // in // ~ 1 // Number of ImFontConfig involved in creating this font. Bigger than 1 when merging multiple font sources into one ImFont. - ImWchar FallbackChar; // 2 // in // = '?' // Replacement glyph if one isn't found. Only set via SetFallbackChar() + ImWchar FallbackChar; // 2 // in // = '?' // Replacement character if a glyph isn't found. Only set via SetFallbackChar() + ImWchar EllipsisChar; // 2 // out // = -1 // Character used for ellipsis rendering. float Scale; // 4 // in // = 1.f // Base font scale, multiplied by the per-window font scale which you can adjust with SetWindowFontScale() 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_demo.cpp b/imgui_demo.cpp index fd1d85a3d..e8e874698 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -3283,7 +3283,8 @@ void ImGui::ShowStyleEditor(ImGuiStyle* ref) ImGui::SameLine(); HelpMarker("Note than the default embedded font is NOT meant to be scaled.\n\nFont are currently rendered into bitmaps at a given size at the time of building the atlas. You may oversample them to get some flexibility with scaling. You can also render at multiple sizes and select which one to use at runtime.\n\n(Glimmer of hope: the atlas system should hopefully be rewritten in the future to make scaling more natural and automatic.)"); ImGui::InputFloat("Font offset", &font->DisplayOffset.y, 1, 1, "%.0f"); ImGui::Text("Ascent: %f, Descent: %f, Height: %f", font->Ascent, font->Descent, font->Ascent - font->Descent); - ImGui::Text("Fallback character: '%c' (%d)", font->FallbackChar, font->FallbackChar); + ImGui::Text("Fallback character: '%c' (U+%04X)", font->FallbackChar, font->FallbackChar); + ImGui::Text("Ellipsis character: '%c' (U+%04X)", font->EllipsisChar); const float surface_sqrt = sqrtf((float)font->MetricsTotalSurface); ImGui::Text("Texture surface: %d pixels (approx) ~ %dx%d", font->MetricsTotalSurface, (int)surface_sqrt, (int)surface_sqrt); for (int config_i = 0; config_i < font->ConfigDataCount; config_i++) diff --git a/imgui_draw.cpp b/imgui_draw.cpp index b4801c26a..dd21523e4 100644 --- a/imgui_draw.cpp +++ b/imgui_draw.cpp @@ -1426,9 +1426,9 @@ ImFontConfig::ImFontConfig() MergeMode = false; RasterizerFlags = 0x00; RasterizerMultiply = 1.0f; + EllipsisChar = (ImWchar)-1; memset(Name, 0, sizeof(Name)); DstFont = NULL; - EllipsisCodePoint = (ImWchar)-1; } //----------------------------------------------------------------------------- @@ -1619,8 +1619,8 @@ ImFont* ImFontAtlas::AddFont(const ImFontConfig* font_cfg) 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; + if (new_font_cfg.DstFont->EllipsisChar == (ImWchar)-1) + new_font_cfg.DstFont->EllipsisChar = font_cfg->EllipsisChar; // Invalidate texture ClearTexData(); @@ -1656,7 +1656,7 @@ ImFont* ImFontAtlas::AddFontDefault(const ImFontConfig* font_cfg_template) 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; + font_cfg.EllipsisChar = (ImWchar)0x0085; const char* ttf_compressed_base85 = GetDefaultCompressedFontDataTTFBase85(); const ImWchar* glyph_ranges = font_cfg.GlyphRanges != NULL ? font_cfg.GlyphRanges : GetGlyphRangesDefault(); @@ -2204,22 +2204,19 @@ void ImFontAtlasBuildFinish(ImFontAtlas* atlas) // 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. + // FIXME: Also note that 0x2026 is currently seldomly included in our font ranges. Because of this we are more likely to use three individual dots. 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++) + if (font->EllipsisChar != (ImWchar)-1) + continue; + const ImWchar ellipsis_variants[] = { (ImWchar)0x2026, (ImWchar)0x0085 }; + for (int j = 0; j < IM_ARRAYSIZE(ellipsis_variants); j++) + if (font->FindGlyphNoFallback(ellipsis_variants[j]) != NULL) // Verify glyph exists { - ImWchar ellipsis_codepoint = ellipsis_variants[j]; - if (font->FindGlyph(ellipsis_codepoint) != font->FallbackGlyph) // Verify glyph exists - { - font->EllipsisCodePoint = ellipsis_codepoint; - break; - } + font->EllipsisChar = ellipsis_variants[j]; + break; } - } } } @@ -2490,6 +2487,7 @@ ImFont::ImFont() FontSize = 0.0f; FallbackAdvanceX = 0.0f; FallbackChar = (ImWchar)'?'; + EllipsisChar = (ImWchar)-1; DisplayOffset = ImVec2(0.0f, 0.0f); FallbackGlyph = NULL; ContainerAtlas = NULL; @@ -2499,7 +2497,6 @@ ImFont::ImFont() Scale = 1.0f; Ascent = Descent = 0.0f; MetricsTotalSurface = 0; - EllipsisCodePoint = (ImWchar)-1; } ImFont::~ImFont() From 1c951dca977e24018110aff7374d7cb3a062e5da Mon Sep 17 00:00:00 2001 From: omar Date: Tue, 17 Sep 2019 11:11:14 +0200 Subject: [PATCH 3/3] Font: Narrow ellipsis: once we know an ellipsis is going to be drawn, we can claim the space between pos_max.x and ellipsis_max.x which gives us enough extra space to not requires the further (and otherwise valid) optimizations. Gets us vastly simplified code, yay. (#2775) --- imgui.cpp | 64 ++++++++----------------------------------------------- 1 file changed, 9 insertions(+), 55 deletions(-) diff --git a/imgui.cpp b/imgui.cpp index 4a606daa3..80e8a280d 100644 --- a/imgui.cpp +++ b/imgui.cpp @@ -2496,6 +2496,10 @@ void ImGui::RenderTextEllipsis(ImDrawList* draw_list, const ImVec2& pos_min, con text_end_full = FindRenderedTextEnd(text); const ImVec2 text_size = text_size_if_known ? *text_size_if_known : CalcTextSize(text, text_end_full, false, 0.0f); + //draw_list->AddLine(ImVec2(pos_max.x, pos_min.y - 4), ImVec2(pos_max.x, pos_max.y + 4), IM_COL32(0, 0, 255, 255)); + //draw_list->AddLine(ImVec2(ellipsis_max_x, pos_min.y-2), ImVec2(ellipsis_max_x, pos_max.y+2), IM_COL32(0, 255, 0, 255)); + //draw_list->AddLine(ImVec2(clip_max_x, pos_min.y), ImVec2(clip_max_x, pos_max.y), IM_COL32(255, 0, 0, 255)); + // FIXME: We could technically remove (last_glyph->AdvanceX - last_glyph->X1) from text_size.x here and save a few pixels. if (text_size.x > pos_max.x - pos_min.x) { // Hello wo... @@ -2518,7 +2522,6 @@ void ImGui::RenderTextEllipsis(ImDrawList* draw_list, const ImVec2& pos_min, con float ellipsis_glyph_width = glyph->X1; // Width of the glyph with no padding on either side float ellipsis_total_width = ellipsis_glyph_width; // Full width of entire ellipsis - float push_left = 1.0f; if (ellipsis_char_count > 1) { @@ -2526,13 +2529,11 @@ void ImGui::RenderTextEllipsis(ImDrawList* draw_list, const ImVec2& pos_min, con const float spacing_between_dots = 1.0f * (draw_list->_Data->FontSize / font->FontSize); ellipsis_glyph_width = glyph->X1 - glyph->X0 + spacing_between_dots; ellipsis_total_width = ellipsis_glyph_width * (float)ellipsis_char_count - spacing_between_dots; - if (glyph->X0 > 1.0f) - push_left = 0.0f; // Pushing ellipsis to the left will be accomplished by rendering the dot (X0). } - float text_avail_width = ImMax((pos_max.x - ellipsis_total_width) - pos_min.x, 1.0f); + // We can now claim the space between pos_max.x and ellipsis_max.x + const float text_avail_width = ImMax((ImMax(pos_max.x, ellipsis_max_x) - ellipsis_total_width) - pos_min.x, 1.0f); float text_size_clipped_x = font->CalcTextSizeA(font_size, text_avail_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 @@ -2546,57 +2547,10 @@ void ImGui::RenderTextEllipsis(ImDrawList* draw_list, const ImVec2& pos_min, con 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 int c = 0; - float extra_spacing = 0.0f; - const char* text_end_ellipsis_prev = text_end_ellipsis; - text_end_ellipsis += ImTextCharFromUtf8(&c, text_end_ellipsis, text_end_full); - if (c && !ImCharIsBlankW(c)) - { - // Free space after first invisible glyph - const ImFontGlyph* hidden_glyph = font->FindGlyph((ImWchar)c); - extra_spacing = hidden_glyph->AdvanceX - hidden_glyph->X1; - 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_avail_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_avail_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; - } - + // Render text, render ellipsis RenderTextClippedEx(draw_list, pos_min, ImVec2(clip_max_x, pos_max.y), text, text_end_ellipsis, &text_size, ImVec2(0.0f, 0.0f)); - - // 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_total_width - push_left <= ellipsis_max_x) + float ellipsis_x = pos_min.x + text_size_clipped_x; + if (ellipsis_x + ellipsis_total_width <= ellipsis_max_x) for (int i = 0; i < ellipsis_char_count; i++) { font->RenderChar(draw_list, font_size, ImVec2(ellipsis_x, pos_min.y), GetColorU32(ImGuiCol_Text), ellipsis_char);