diff --git a/imgui.cpp b/imgui.cpp index c5039d63..7921b1d1 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 42ee5340..d4a81c3b 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 3eb1067e..b4801c26 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 9c87d2a1..ea8a4099 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.