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.
This commit is contained in:
Rokas Kupstys 2019-09-05 12:59:43 +03:00 committed by omar
parent 404dc0367e
commit 45405f0dc9
4 changed files with 117 additions and 21 deletions

View File

@ -2503,15 +2503,42 @@ void ImGui::RenderTextEllipsis(ImDrawList* draw_list, const ImVec2& pos_min,
// min max ellipsis_max // min max ellipsis_max
// <-> this is generally some padding value // <-> this is generally some padding value
// FIXME-STYLE: RenderPixelEllipsis() style should use actual font data.
const ImFont* font = draw_list->_Data->Font; const ImFont* font = draw_list->_Data->Font;
const float font_size = draw_list->_Data->FontSize; 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 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_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_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) 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 // 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_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 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)); 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; // This variable pushes ellipsis to the left from last visible character. This is mostly useful when rendering
if (ellipsis_x + ellipsis_width - 1.0f <= ellipsis_max_x) // ellipsis character contained in the font. If we render ellipsis manually space is already adequate and extra
RenderPixelEllipsis(draw_list, ImVec2(ellipsis_x, pos_min.y), GetColorU32(ImGuiCol_Text), ellipsis_dot_count); // 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 else
{ {

View File

@ -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. 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. 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. 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] // [Internal]
char Name[40]; // Name (strictly to ease debugging) 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] 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) 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 // bool DirtyLookupTables; // 1 // out //
ImWchar EllipsisCodePoint; // -1 // out // // Override a codepoint used for ellipsis rendering.
// Methods // Methods
IMGUI_API ImFont(); IMGUI_API ImFont();

View File

@ -1428,6 +1428,7 @@ ImFontConfig::ImFontConfig()
RasterizerMultiply = 1.0f; RasterizerMultiply = 1.0f;
memset(Name, 0, sizeof(Name)); memset(Name, 0, sizeof(Name));
DstFont = NULL; 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); 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 // Invalidate texture
ClearTexData(); ClearTexData();
return new_font_cfg.DstFont; return new_font_cfg.DstFont;
@ -1652,6 +1656,7 @@ ImFont* ImFontAtlas::AddFontDefault(const ImFontConfig* font_cfg_template)
font_cfg.SizePixels = 13.0f * 1.0f; font_cfg.SizePixels = 13.0f * 1.0f;
if (font_cfg.Name[0] == '\0') if (font_cfg.Name[0] == '\0')
ImFormatString(font_cfg.Name, IM_ARRAYSIZE(font_cfg.Name), "ProggyClean.ttf, %dpx", (int)font_cfg.SizePixels); 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 char* ttf_compressed_base85 = GetDefaultCompressedFontDataTTFBase85();
const ImWchar* glyph_ranges = font_cfg.GlyphRanges != NULL ? font_cfg.GlyphRanges : GetGlyphRangesDefault(); 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++) for (int i = 0; i < atlas->Fonts.Size; i++)
if (atlas->Fonts[i]->DirtyLookupTables) if (atlas->Fonts[i]->DirtyLookupTables)
atlas->Fonts[i]->BuildLookupTable(); 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) // Retrieve list of range (2 int per range, values are inclusive)
@ -2474,6 +2499,7 @@ ImFont::ImFont()
Scale = 1.0f; Scale = 1.0f;
Ascent = Descent = 0.0f; Ascent = Descent = 0.0f;
MetricsTotalSurface = 0; MetricsTotalSurface = 0;
EllipsisCodePoint = (ImWchar)-1;
} }
ImFont::~ImFont() ImFont::~ImFont()
@ -3012,7 +3038,6 @@ void ImFont::RenderText(ImDrawList* draw_list, float size, ImVec2 pos, ImU32 col
// - RenderMouseCursor() // - RenderMouseCursor()
// - RenderArrowPointingAt() // - RenderArrowPointingAt()
// - RenderRectFilledRangeH() // - RenderRectFilledRangeH()
// - RenderPixelEllipsis()
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
void ImGui::RenderMouseCursor(ImDrawList* draw_list, ImVec2 pos, float scale, ImGuiMouseCursor mouse_cursor) 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); 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 // [SECTION] Decompression code
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------

View File

@ -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 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 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 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 #ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
// 2019/06/07: Updating prototypes of some of the internal functions. Leaving those for reference for a short while. // 2019/06/07: Updating prototypes of some of the internal functions. Leaving those for reference for a short while.