From 78645a7dbab53bb9953d7ef98480dd1de0f0a8dd Mon Sep 17 00:00:00 2001 From: omar Date: Fri, 7 Nov 2014 14:45:56 +0900 Subject: [PATCH] Added word-wrapping API TextWrapped(), PushTextWrapPos(), PopTextWrapPos() Added word-wrapping sample in the test window. Added IsItemFocused() to tell if last widget is being focused for keyboard input. --- imgui.cpp | 498 ++++++++++++++++++++++++++++++++++++++++-------------- imgui.h | 36 ++-- 2 files changed, 396 insertions(+), 138 deletions(-) diff --git a/imgui.cpp b/imgui.cpp index 082565e9d..5018e209f 100644 --- a/imgui.cpp +++ b/imgui.cpp @@ -122,7 +122,7 @@ some functions like TreeNode() implicitly creates a scope for you by calling PushID() - when dealing with trees, ID are important because you want to preserve the opened/closed state of tree nodes. depending on your use cases you may want to use strings, indices or pointers as ID. experiment and see what makes more sense! - e.g. When displaying a single object, using a static string as ID will preserve your node open/closed state when the targetted object change + e.g. When displaying a single object, using a static string as ID will preserve your node open/closed state when the targeted object change e.g. When displaying a list of objects, using indices or pointers as ID will preserve the node open/closed state per object - when passing a label you can optionally specify extra unique ID information within the same string using "##". This helps solving the simpler collision cases. e.g. "Label" display "Label" and uses "Label" as ID @@ -144,7 +144,7 @@ - 2014/09/24 (1.12) moved IM_MALLOC/IM_REALLOC/IM_FREE preprocessor defines to IO.MemAllocFn/IO.MemReallocFn/IO.MemFreeFn - 2014/08/30 (1.09) removed IO.FontHeight (now computed automatically) - 2014/08/30 (1.09) moved IMGUI_FONT_TEX_UV_FOR_WHITE preprocessor define to IO.FontTexUvForWhite - - 2014/08/28 (1.09) changed the behaviour of IO.PixelCenterOffset following various rendering fixes + - 2014/08/28 (1.09) changed the behavior of IO.PixelCenterOffset following various rendering fixes ISSUES & TODO-LIST ================== @@ -225,7 +225,7 @@ namespace ImGui static bool ButtonBehaviour(const ImGuiAabb& bb, const ImGuiID& id, bool* out_hovered, bool* out_held, bool allow_key_modifiers, bool repeat = false); static void LogText(const ImVec2& ref_pos, const char* text, const char* text_end = NULL); -static void RenderText(ImVec2 pos, const char* text, const char* text_end = NULL, const bool hide_text_after_hash = true); +static void RenderText(ImVec2 pos, const char* text, const char* text_end = NULL, bool hide_text_after_hash = true, float wrap_width = 0.0f); static void RenderFrame(ImVec2 p_min, ImVec2 p_max, ImU32 fill_col, bool border = true, float rounding = 0.0f); static void RenderCollapseTriangle(ImVec2 p_min, bool open, float scale = 1.0f, bool shadow = false); @@ -590,9 +590,11 @@ struct ImGuiDrawContext int TreeDepth; ImGuiAabb LastItemAabb; bool LastItemHovered; + bool LastItemFocused; ImVector ChildWindows; ImVector AllowKeyboardFocus; ImVector ItemWidth; + ImVector TextWrapPos; ImVector ColorModifiers; ImGuiColorEditMode ColorEditMode; ImGuiStorage* StateStorage; @@ -613,6 +615,7 @@ struct ImGuiDrawContext TreeDepth = 0; LastItemAabb = ImGuiAabb(0.0f,0.0f,0.0f,0.0f); LastItemHovered = false; + LastItemFocused = true; StateStorage = NULL; OpenNextNode = -1; @@ -1068,6 +1071,9 @@ bool ImGuiWindow::FocusItemRegister(bool is_active) if (allow_keyboard_focus) FocusIdxTabCounter++; + if (is_active) + window->DC.LastItemFocused = true; + // Process keyboard input at this point: TAB, Shift-TAB switch focus // We can always TAB out of a widget that doesn't allow tabbing in. if (FocusIdxAllRequestNext == IM_INT_MAX && FocusIdxTabRequestNext == IM_INT_MAX && is_active && ImGui::IsKeyPressedMap(ImGuiKey_Tab)) @@ -1649,8 +1655,24 @@ static void LogText(const ImVec2& ref_pos, const char* text, const char* text_en } } +static float CalcWrapWidthForPos(const ImVec2& pos, float wrap_pos_x) +{ + if (wrap_pos_x < 0.0f) + return 0.0f; + + ImGuiWindow* window = GetCurrentWindow(); + if (wrap_pos_x == 0.0f) + wrap_pos_x = GetWindowContentRegionMax().x; + if (wrap_pos_x > 0.0f) + wrap_pos_x += window->Pos.x; // wrap_pos_x is provided is window local space + + const float wrap_width = wrap_pos_x > 0.0f ? ImMax(wrap_pos_x - pos.x, 0.00001f) : 0.0f; + return wrap_width; +} + // Internal ImGui function to render text (called from ImGui::Text(), ImGui::TextUnformatted(), etc.) -static void RenderText(ImVec2 pos, const char* text, const char* text_end, const bool hide_text_after_hash) +// ImGui::RenderText() calls ImDrawList::AddText() calls ImBitmapFont::RenderText() +static void RenderText(ImVec2 pos, const char* text, const char* text_end, bool hide_text_after_hash, float wrap_width) { ImGuiState& g = GImGui; ImGuiWindow* window = GetCurrentWindow(); @@ -1669,13 +1691,12 @@ static void RenderText(ImVec2 pos, const char* text, const char* text_end, const } const int text_len = (int)(text_display_end - text); - //IM_ASSERT(text_len >= 0 && text_len < 10000); // Suspicious text length if (text_len > 0) { // Render - window->DrawList->AddText(window->Font(), window->FontSize(), pos, window->Color(ImGuiCol_Text), text, text + text_len); + window->DrawList->AddText(window->Font(), window->FontSize(), pos, window->Color(ImGuiCol_Text), text, text + text_len, wrap_width); - // Log as text. We split text into individual lines to add the tree level padding + // Log as text. We split text into individual lines to add current tree level padding if (g.LogEnabled) LogText(pos, text, text_display_end); } @@ -1689,7 +1710,7 @@ static void RenderFrame(ImVec2 p_min, ImVec2 p_max, ImU32 fill_col, bool border, window->DrawList->AddRectFilled(p_min, p_max, fill_col, rounding); if (border && (window->Flags & ImGuiWindowFlags_ShowBorders)) { - // FIXME: I have no idea how this is working correctly but it is the best I've found that works on multiple rendering + // FIXME: This is the best I've found that works on multiple renderer/back ends. Rather dodgy. const float offset = GImGui.IO.PixelCenterOffset; window->DrawList->AddRect(p_min+ImVec2(1.5f-offset,1.5f-offset), p_max+ImVec2(1.0f-offset*2,1.0f-offset*2), window->Color(ImGuiCol_BorderShadow), rounding); window->DrawList->AddRect(p_min+ImVec2(0.5f-offset,0.5f-offset), p_max+ImVec2(0.0f-offset*2,0.0f-offset*2), window->Color(ImGuiCol_Border), rounding); @@ -1727,7 +1748,7 @@ static void RenderCollapseTriangle(ImVec2 p_min, bool open, float scale, bool sh // Calculate text size. Text can be multi-line. Optionally ignore text after a ## marker. // CalcTextSize("") should return ImVec2(0.0f, GImGui.FontSize) -ImVec2 CalcTextSize(const char* text, const char* text_end, const bool hide_text_after_hash) +ImVec2 CalcTextSize(const char* text, const char* text_end, bool hide_text_after_hash, float wrap_width) { ImGuiWindow* window = GetCurrentWindow(); @@ -1737,8 +1758,8 @@ ImVec2 CalcTextSize(const char* text, const char* text_end, const bool hide_text else text_display_end = text_end; - const ImVec2 size = window->Font()->CalcTextSizeA(window->FontSize(), 0, text, text_display_end, NULL); - return size; + const ImVec2 text_size = window->Font()->CalcTextSizeA(window->FontSize(), FLT_MAX, wrap_width, text, text_display_end, NULL); + return text_size; } // Find window given position, search front-to-back @@ -1864,6 +1885,12 @@ bool IsHovered() return window->DC.LastItemHovered; } +bool IsItemFocused() +{ + ImGuiWindow* window = GetCurrentWindow(); + return window->DC.LastItemFocused; +} + ImVec2 GetItemBoxMin() { ImGuiWindow* window = GetCurrentWindow(); @@ -2299,6 +2326,8 @@ bool Begin(const char* name, bool* open, ImVec2 size, float fill_alpha, ImGuiWin window->DC.ItemWidth.push_back(window->ItemWidthDefault); window->DC.AllowKeyboardFocus.resize(0); window->DC.AllowKeyboardFocus.push_back(true); + window->DC.TextWrapPos.resize(0); + window->DC.TextWrapPos.push_back(-1.0f); // disabled window->DC.ColorModifiers.resize(0); window->DC.ColorEditMode = ImGuiColorEditMode_UserSelect; window->DC.ColumnCurrent = 0; @@ -2464,6 +2493,18 @@ void PopAllowKeyboardFocus() window->DC.AllowKeyboardFocus.pop_back(); } +void PushTextWrapPos(float wrap_x) +{ + ImGuiWindow* window = GetCurrentWindow(); + window->DC.TextWrapPos.push_back(wrap_x); +} + +void PopTextWrapPos() +{ + ImGuiWindow* window = GetCurrentWindow(); + window->DC.TextWrapPos.pop_back(); +} + void PushStyleColor(ImGuiCol idx, const ImVec4& col) { ImGuiState& g = GImGui; @@ -2583,6 +2624,7 @@ ImVec2 GetWindowContentRegionMin() return ImVec2(0, window->TitleBarHeight()) + window->WindowPadding(); } +// FIXME: Provide an equivalent that gives the min/max region considering columns. ImVec2 GetWindowContentRegionMax() { ImGuiWindow* window = GetCurrentWindow(); @@ -2718,6 +2760,21 @@ void TextColored(const ImVec4& col, const char* fmt, ...) va_end(args); } +void TextWrappedV(const char* fmt, va_list args) +{ + ImGui::PushTextWrapPos(0.0f); + TextV(fmt, args); + ImGui::PopTextWrapPos(); +} + +void TextWrapped(const char* fmt, ...) +{ + va_list args; + va_start(args, fmt); + TextWrappedV(fmt, args); + va_end(args); +} + void TextUnformatted(const char* text, const char* text_end) { ImGuiState& g = GImGui; @@ -2730,11 +2787,14 @@ void TextUnformatted(const char* text, const char* text_end) if (text_end == NULL) text_end = text + strlen(text); - if (text_end - text > 2000) + const float wrap_pos_x = window->DC.TextWrapPos.back(); + const bool wrap_enabled = wrap_pos_x >= 0.0f; + if (text_end - text > 2000 && !wrap_enabled) { // Long text! // Perform manual coarse clipping to optimize for long multi-line text // From this point we will only compute the width of lines that are visible. + // Optimization only available when word-wrapping is disabled. const char* line = text; const float line_height = ImGui::GetTextLineHeight(); const ImVec2 start_pos = window->DC.CursorPos; @@ -2804,7 +2864,8 @@ void TextUnformatted(const char* text, const char* text_end) } else { - const ImVec2 text_size = CalcTextSize(text_begin, text_end, false); + const float wrap_width = wrap_enabled ? CalcWrapWidthForPos(window->DC.CursorPos, wrap_pos_x) : 0.0f; + const ImVec2 text_size = CalcTextSize(text_begin, text_end, false, wrap_width); ImGuiAabb bb(window->DC.CursorPos, window->DC.CursorPos + text_size); ItemSize(bb.GetSize(), &bb.Min); @@ -2813,7 +2874,7 @@ void TextUnformatted(const char* text, const char* text_end) // Render // We don't hide text after ## in this end-user function. - RenderText(bb.Min, text_begin, text_end, false); + RenderText(bb.Min, text_begin, text_end, false, wrap_width); } } @@ -3359,8 +3420,6 @@ bool SliderFloat(const char* label, float* v, float v_min, float v_max, const ch } } - const bool tab_focus_requested = window->FocusItemRegister(g.ActiveId == id); - const ImVec2 text_size = CalcTextSize(label); const ImGuiAabb frame_bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, text_size.y) + style.FramePadding*2.0f); const ImGuiAabb slider_bb(frame_bb.Min+g.Style.FramePadding, frame_bb.Max-g.Style.FramePadding); @@ -3373,6 +3432,8 @@ bool SliderFloat(const char* label, float* v, float v_min, float v_max, const ch return false; } + const bool tab_focus_requested = window->FocusItemRegister(g.ActiveId == id); + const bool is_unbound = v_min == -FLT_MAX || v_min == FLT_MAX || v_max == -FLT_MAX || v_max == FLT_MAX; const float grab_size_in_units = 1.0f; // In 'v' units. Probably needs to be parametrized, based on a 'v_step' value? decimal precision? @@ -3879,7 +3940,7 @@ bool RadioButton(const char* label, int* v, int v_button) // Wrapper for stb_textedit.h to edit text (our wrapper is for: statically sized buffer, single-line, ASCII, fixed-width font) int STB_TEXTEDIT_STRINGLEN(const STB_TEXTEDIT_STRING* obj) { return (int)ImStrlenW(obj->Text); } ImWchar STB_TEXTEDIT_GETCHAR(const STB_TEXTEDIT_STRING* obj, int idx) { return obj->Text[idx]; } -float STB_TEXTEDIT_GETWIDTH(STB_TEXTEDIT_STRING* obj, int line_start_idx, int char_idx) { (void)line_start_idx; return obj->Font->CalcTextSizeW(obj->FontSize, 0, &obj->Text[char_idx], &obj->Text[char_idx]+1, NULL).x; } +float STB_TEXTEDIT_GETWIDTH(STB_TEXTEDIT_STRING* obj, int line_start_idx, int char_idx) { (void)line_start_idx; return obj->Font->CalcTextSizeW(obj->FontSize, FLT_MAX, &obj->Text[char_idx], &obj->Text[char_idx]+1, NULL).x; } int STB_TEXTEDIT_KEYTOTEXT(int key) { return key >= 0x10000 ? 0 : key; } ImWchar STB_TEXTEDIT_NEWLINE = '\n'; void STB_TEXTEDIT_LAYOUTROW(StbTexteditRow* r, STB_TEXTEDIT_STRING* obj, int line_start_idx) @@ -3947,7 +4008,7 @@ void ImGuiTextEditState::UpdateScrollOffset() { // Scroll in chunks of quarter width const float scroll_x_increment = Width * 0.25f; - const float cursor_offset_x = Font->CalcTextSizeW(FontSize, 0, Text, Text+StbState.cursor, NULL).x; + const float cursor_offset_x = Font->CalcTextSizeW(FontSize, FLT_MAX, Text, Text+StbState.cursor, NULL).x; if (ScrollX > cursor_offset_x) ScrollX = ImMax(0.0f, cursor_offset_x - scroll_x_increment); else if (ScrollX < cursor_offset_x - Width) @@ -3971,7 +4032,7 @@ const char* ImGuiTextEditState::GetTextPointerClippedA(ImFont font, float font_s return text; const char* text_clipped_end = NULL; - const ImVec2 text_size = font->CalcTextSizeA(font_size, width, text, NULL, &text_clipped_end); + const ImVec2 text_size = font->CalcTextSizeA(font_size, width, 0.0f, text, NULL, &text_clipped_end); if (out_text_size) *out_text_size = text_size; return text_clipped_end; @@ -4835,6 +4896,7 @@ static bool ClipAdvance(const ImGuiAabb& bb) { ImGuiWindow* window = GetCurrentWindow(); window->DC.LastItemAabb = bb; + window->DC.LastItemFocused = false; if (ImGui::IsClipped(bb)) { window->DC.LastItemHovered = false; @@ -5331,7 +5393,7 @@ void ImDrawList::AddCircleFilled(const ImVec2& centre, float radius, ImU32 col, } } -void ImDrawList::AddText(ImFont font, float font_size, const ImVec2& pos, ImU32 col, const char* text_begin, const char* text_end) +void ImDrawList::AddText(ImFont font, float font_size, const ImVec2& pos, ImU32 col, const char* text_begin, const char* text_end, float wrap_width) { if ((col >> 24) == 0) return; @@ -5345,7 +5407,7 @@ void ImDrawList::AddText(ImFont font, float font_size, const ImVec2& pos, ImU32 const size_t vtx_begin = vtx_buffer.size(); ReserveVertices(vtx_count_max); - font->RenderText(font_size, pos, col, clip_rect_stack.back(), text_begin, text_end, vtx_write); + font->RenderText(font_size, pos, col, clip_rect_stack.back(), text_begin, text_end, vtx_write, wrap_width); // give back unused vertices vtx_buffer.resize((size_t)(vtx_write - &vtx_buffer.front())); @@ -5491,7 +5553,7 @@ void ImBitmapFont::BuildLookupTable() IndexLookup[Glyphs[i].Id] = (int)i; } -const ImBitmapFont::FntGlyph* ImBitmapFont::FindGlyph(unsigned short c) const +const ImBitmapFont::FntGlyph* ImBitmapFont::FindGlyph(unsigned short c, const ImBitmapFont::FntGlyph* fallback) const { if (c < (int)IndexLookup.size()) { @@ -5499,7 +5561,7 @@ const ImBitmapFont::FntGlyph* ImBitmapFont::FindGlyph(unsigned short c) const if (i >= 0 && i < (int)GlyphsCount) return &Glyphs[i]; } - return NULL; + return fallback; } // Convert UTF-8 to 32-bits character, process single character input. @@ -5635,10 +5697,110 @@ static ptrdiff_t ImTextStrToUtf8(char* buf, size_t buf_size, const ImWchar* in_t return buf_out - buf; } -ImVec2 ImBitmapFont::CalcTextSizeA(float size, float max_width, const char* text_begin, const char* text_end, const char** remaining) const +const char* ImBitmapFont::CalcWordWrapPositionA(float scale, const char* text, const char* text_end, float wrap_width, const FntGlyph* fallback_glyph) const +{ + // Simple word-wrapping for English, not full-featured. Please submit failing cases! + // FIXME: Much possible improvements (don't cut things like "word !", "word!!!" but cut within "word,,,,", more sensible support for punctuations, support for Unicode punctuations, etc.) + + // For references, possible wrap point marked with ^ + // "aaa bbb, ccc,ddd. eee fff. ggg!" + // ^ ^ ^ ^ ^__ ^ ^ + + // List of hardcoded separators: .,;!?'" + + // Skip extra blanks after a line returns (that includes not counting them in width computation) + // e.g. "Hello world" + // --> + // "Hello" + // "world" + + // Cut words that cannot possibly fit within one line. + // e.g.: "The tropical fish" with ~5 characters worth of width + // --> + // "The tr" + // "opical" + // "fish" + + float line_width = 0.0f; + float word_width = 0.0f; + float blank_width = 0.0f; + + const char* word_end = text; + const char* prev_word_end = NULL; + bool inside_word = true; + + const char* s = text; + while (s < text_end) + { + unsigned int c; + const int bytes_count = ImTextCharFromUtf8(&c, s, text_end); + const char* next_s = s + (bytes_count > 0 ? bytes_count : 1); + + if (c == '\n') + { + line_width = word_width = blank_width = 0.0f; + inside_word = true; + s = next_s; + continue; + } + + float char_width = 0.0f; + if (c == '\t') + { + if (const FntGlyph* glyph = FindGlyph((unsigned short)' ')) + char_width = (glyph->XAdvance + Info->SpacingHoriz) * 4 * scale; + } + else + { + if (const FntGlyph* glyph = FindGlyph((unsigned short)c, fallback_glyph)) + char_width = (glyph->XAdvance + Info->SpacingHoriz) * scale; + } + + if (c == ' ' || c == '\t') + { + if (inside_word) + { + line_width += blank_width; + blank_width = 0.0f; + } + blank_width += char_width; + inside_word = false; + } + else + { + word_width += char_width; + if (inside_word) + { + word_end = next_s; + } + else + { + prev_word_end = word_end; + line_width += word_width + blank_width; + word_width = blank_width = 0.0f; + } + + // Allow wrapping after punctuation. + inside_word = !(c == '.' || c == ',' || c == ';' || c == '!' || c == '?' || c == '\'' || c == '\"'); + } + + // We ignore blank width at the end of the line (they can be skipped) + if (line_width + word_width >= wrap_width) + { + // Words that cannot possibly fit within an entire line will be cut anywhere. + if (word_width < wrap_width) + s = prev_word_end ? prev_word_end : word_end; + break; + } + + s = next_s; + } + + return s; +} + +ImVec2 ImBitmapFont::CalcTextSizeA(float size, float max_width, float wrap_width, const char* text_begin, const char* text_end, const char** remaining) const { - if (max_width == 0.0f) - max_width = FLT_MAX; if (!text_end) text_end = text_begin + strlen(text_begin); @@ -5649,40 +5811,70 @@ ImVec2 ImBitmapFont::CalcTextSizeA(float size, float max_width, const char* text ImVec2 text_size = ImVec2(0,0); float line_width = 0.0f; + const bool word_wrap_enabled = (wrap_width > 0.0f); + const char* word_wrap_eol = NULL; + const char* s = text_begin; while (s < text_end) { + if (word_wrap_enabled) + { + // Calculate how far we can render. Requires two passes on the string data but keeps the code simple and not intrusive for what's essentially an uncommon feature. + if (!word_wrap_eol) + { + word_wrap_eol = CalcWordWrapPositionA(scale, s, text_end, wrap_width - line_width, fallback_glyph); + if (word_wrap_eol == s) // Wrap_width is too small to fit anything. Force displaying 1 character to minimize the height discontinuity. + word_wrap_eol++; // +1 may not be a character start point in UTF-8 but it's ok because we use s >= word_wrap_eol below + } + + if (s >= word_wrap_eol) + { + if (text_size.x < line_width) + text_size.x = line_width; + text_size.y += line_height; + line_width = 0.0f; + word_wrap_eol = NULL; + + // Wrapping skips upcoming blanks + while (s < text_end) + { + const char c = *s; + if (c == ' ' || c == '\t') { s++; } else if (c == '\n') { s++; break; } else { break; } + } + continue; + } + } + + // Decode and advance source (handle unlikely UTF-8 decoding failure by skipping to the next byte) unsigned int c; const int bytes_count = ImTextCharFromUtf8(&c, s, text_end); - s += bytes_count > 0 ? bytes_count : 1; // Handle decoding failure by skipping to next byte - + s += bytes_count > 0 ? bytes_count : 1; + if (c == '\n') { if (text_size.x < line_width) text_size.x = line_width; text_size.y += line_height; - line_width = 0; + line_width = 0.0f; + continue; } - else if (c == '\t') + + float char_width = 0.0f; + if (c == '\t') { - // FIXME: Better TAB handling needed. + // FIXME: Better TAB handling if (const FntGlyph* glyph = FindGlyph((unsigned short)' ')) - line_width += (glyph->XAdvance + Info->SpacingHoriz) * 4 * scale; + char_width = (glyph->XAdvance + Info->SpacingHoriz) * 4 * scale; } - else + else if (const FntGlyph* glyph = FindGlyph((unsigned short)c, fallback_glyph)) { - const FntGlyph* glyph = FindGlyph((unsigned short)c); - if (!glyph) - glyph = fallback_glyph; - if (glyph) - { - const float char_width = (glyph->XAdvance + Info->SpacingHoriz) * scale; - //const float char_extend = (glyph->XOffset + glyph->Width * scale); - if (line_width + char_width >= max_width) - break; - line_width += char_width; - } + char_width = (glyph->XAdvance + Info->SpacingHoriz) * scale; } + + if (line_width + char_width >= max_width) + break; + + line_width += char_width; } if (line_width > 0 || text_size.y == 0.0f) @@ -5700,8 +5892,6 @@ ImVec2 ImBitmapFont::CalcTextSizeA(float size, float max_width, const char* text ImVec2 ImBitmapFont::CalcTextSizeW(float size, float max_width, const ImWchar* text_begin, const ImWchar* text_end, const ImWchar** remaining) const { - if (max_width == 0.0f) - max_width = FLT_MAX; if (!text_end) text_end = text_begin + ImStrlenW(text_begin); @@ -5722,28 +5912,27 @@ ImVec2 ImBitmapFont::CalcTextSizeW(float size, float max_width, const ImWchar* t if (text_size.x < line_width) text_size.x = line_width; text_size.y += line_height; - line_width = 0; + line_width = 0.0f; + continue; } - else if (c == '\t') + + float char_width = 0.0f; + if (c == '\t') { - // FIXME: Better TAB handling needed. + // FIXME: Better TAB handling if (const FntGlyph* glyph = FindGlyph((unsigned short)' ')) - line_width += (glyph->XAdvance + Info->SpacingHoriz) * 4 * scale; + char_width = (glyph->XAdvance + Info->SpacingHoriz) * 4 * scale; } else { - const FntGlyph* glyph = FindGlyph((unsigned short)c); - if (!glyph) - glyph = fallback_glyph; - if (glyph) - { - const float char_width = (glyph->XAdvance + Info->SpacingHoriz) * scale; - //const float char_extend = (glyph->XOffset + glyph->Width * scale); - if (line_width + char_width >= max_width) - break; - line_width += char_width; - } + if (const FntGlyph* glyph = FindGlyph((unsigned short)c, fallback_glyph)) + char_width = (glyph->XAdvance + Info->SpacingHoriz) * scale; } + + if (line_width + char_width >= max_width) + break; + + line_width += char_width; } if (line_width > 0 || text_size.y == 0.0f) @@ -5759,7 +5948,7 @@ ImVec2 ImBitmapFont::CalcTextSizeW(float size, float max_width, const ImWchar* t return text_size; } -void ImBitmapFont::RenderText(float size, ImVec2 pos, ImU32 col, const ImVec4& clip_rect_ref, const char* text_begin, const char* text_end, ImDrawVert*& out_vertices) const +void ImBitmapFont::RenderText(float size, ImVec2 pos, ImU32 col, const ImVec4& clip_rect_ref, const char* text_begin, const char* text_end, ImDrawVert*& out_vertices, float wrap_width) const { if (!text_end) text_end = text_begin + strlen(text_begin); @@ -5775,17 +5964,46 @@ void ImBitmapFont::RenderText(float size, ImVec2 pos, ImU32 col, const ImVec4& c pos.x = (float)(int)pos.x; pos.y = (float)(int)pos.y + GImGui.IO.FontYOffset; - const ImVec4 clip_rect = clip_rect_ref; + const bool word_wrap_enabled = (wrap_width > 0.0f); + const char* word_wrap_eol = NULL; + const ImVec4 clip_rect = clip_rect_ref; float x = pos.x; float y = pos.y; - for (const char* s = text_begin; s < text_end; ) + + const char* s = text_begin; + while (s < text_end) { + if (word_wrap_enabled) + { + // Calculate how far we can render. Requires two passes on the string data but keeps the code simple and not intrusive for what's essentially an uncommon feature. + if (!word_wrap_eol) + { + word_wrap_eol = CalcWordWrapPositionA(scale, s, text_end, wrap_width - (x - pos.x), fallback_glyph); + if (word_wrap_eol == s) // Wrap_width is too small to fit anything. Force displaying 1 character to minimize the height discontinuity. + word_wrap_eol++; // +1 may not be a character start point in UTF-8 but it's ok because we use s >= word_wrap_eol below + } + + if (s >= word_wrap_eol) + { + x = pos.x; + y += line_height * scale; + word_wrap_eol = NULL; + + // Wrapping skips upcoming blanks + while (s < text_end) + { + const char c = *s; + if (c == ' ' || c == '\t') { s++; } else if (c == '\n') { s++; break; } else { break; } + } + continue; + } + } + + // Decode and advance source (handle unlikely UTF-8 decoding failure by skipping to the next byte) unsigned int c; const int bytes_count = ImTextCharFromUtf8(&c, s, text_end); - s += bytes_count > 0 ? bytes_count : 1; // Handle decoding failure by skipping to next byte - if (c >= 0x10000) - continue; + s += bytes_count > 0 ? bytes_count : 1; if (c == '\n') { @@ -5794,67 +6012,59 @@ void ImBitmapFont::RenderText(float size, ImVec2 pos, ImU32 col, const ImVec4& c continue; } - const FntGlyph* glyph = FindGlyph((unsigned short)c); - if (!glyph) - glyph = fallback_glyph; - if (glyph) + float char_width = 0.0f; + if (c == '\t') { - const float char_width = (glyph->XAdvance + Info->SpacingHoriz) * scale; - //const float char_extend = (glyph->XOffset + glyph->Width * scale); - - if (c != ' ' && c != '\n') + // FIXME: Better TAB handling + if (const FntGlyph* glyph = FindGlyph((unsigned short)' ')) + char_width += (glyph->XAdvance + Info->SpacingHoriz) * 4 * scale; + } + else if (const FntGlyph* glyph = FindGlyph((unsigned short)c, fallback_glyph)) + { + char_width = (glyph->XAdvance + Info->SpacingHoriz) * scale; + if (c != ' ') { - // Clipping due to Y limits is more likely + // Clipping on Y is more likely const float y1 = (float)(y + (glyph->YOffset + outline*2) * scale); const float y2 = (float)(y1 + glyph->Height * scale); - if (y1 > clip_rect.w || y2 < clip_rect.y) + if (y1 <= clip_rect.w && y2 >= clip_rect.y) { - x += char_width; - continue; + const float x1 = (float)(x + (glyph->XOffset + outline) * scale); + const float x2 = (float)(x1 + glyph->Width * scale); + if (x1 <= clip_rect.z && x2 >= clip_rect.x) + { + // Render a character + const float s1 = (glyph->X) * tex_scale_x; + const float t1 = (glyph->Y) * tex_scale_y; + const float s2 = (glyph->X + glyph->Width) * tex_scale_x; + const float t2 = (glyph->Y + glyph->Height) * tex_scale_y; + + out_vertices[0].pos = ImVec2(x1, y1); + out_vertices[0].uv = ImVec2(s1, t1); + out_vertices[0].col = col; + + out_vertices[1].pos = ImVec2(x2, y1); + out_vertices[1].uv = ImVec2(s2, t1); + out_vertices[1].col = col; + + out_vertices[2].pos = ImVec2(x2, y2); + out_vertices[2].uv = ImVec2(s2, t2); + out_vertices[2].col = col; + + out_vertices[3] = out_vertices[0]; + out_vertices[4] = out_vertices[2]; + + out_vertices[5].pos = ImVec2(x1, y2); + out_vertices[5].uv = ImVec2(s1, t2); + out_vertices[5].col = col; + + out_vertices += 6; + } } - - const float x1 = (float)(x + (glyph->XOffset + outline) * scale); - const float x2 = (float)(x1 + glyph->Width * scale); - if (x1 > clip_rect.z || x2 < clip_rect.x) - { - x += char_width; - continue; - } - - const float s1 = (glyph->X) * tex_scale_x; - const float t1 = (glyph->Y) * tex_scale_y; - const float s2 = (glyph->X + glyph->Width) * tex_scale_x; - const float t2 = (glyph->Y + glyph->Height) * tex_scale_y; - - out_vertices[0].pos = ImVec2(x1, y1); - out_vertices[0].uv = ImVec2(s1, t1); - out_vertices[0].col = col; - - out_vertices[1].pos = ImVec2(x2, y1); - out_vertices[1].uv = ImVec2(s2, t1); - out_vertices[1].col = col; - - out_vertices[2].pos = ImVec2(x2, y2); - out_vertices[2].uv = ImVec2(s2, t2); - out_vertices[2].col = col; - - out_vertices[3] = out_vertices[0]; - out_vertices[4] = out_vertices[2]; - - out_vertices[5].pos = ImVec2(x1, y2); - out_vertices[5].uv = ImVec2(s1, t2); - out_vertices[5].col = col; - - out_vertices += 6; } + } - x += char_width; - } - else if (c == '\t') - { - if (const FntGlyph* glyph = FindGlyph((unsigned short)' ')) - x += (glyph->XAdvance + Info->SpacingHoriz) * 4 * scale; - } + x += char_width; } } @@ -5948,7 +6158,7 @@ void ShowUserGuide() ImGui::BulletText("Mouse Wheel to scroll."); if (g.IO.FontAllowUserScaling) ImGui::BulletText("CTRL+Mouse Wheel to zoom window contents."); - ImGui::BulletText("TAB/SHIFT+TAB to cycle thru keyboard editable fields."); + ImGui::BulletText("TAB/SHIFT+TAB to cycle through keyboard editable fields."); ImGui::BulletText("CTRL+Click on a slider to input text."); ImGui::BulletText( "While editing text:\n" @@ -6114,20 +6324,45 @@ void ShowTestWindow(bool* open) if (ImGui::TreeNode("Colored Text")) { - // This is a merely a shortcut, you can use PushStyleColor()/PopStyleColor() for more flexibility. + // Using shortcut. You can use PushStyleColor()/PopStyleColor() for more flexibility. ImGui::TextColored(ImVec4(1.0f,0.0f,1.0f,1.0f), "Pink"); ImGui::TextColored(ImVec4(1.0f,1.0f,0.0f,1.0f), "Yellow"); ImGui::TreePop(); } + if (ImGui::TreeNode("Word Wrapping")) + { + // Using shortcut. You can use PushTextWrapPos()/PopTextWrapPos() for more flexibility. + ImGui::TextWrapped("This is a long paragraph. The text should automatically wrap on the edge of the window. The current implementation follows simple rules that works for English and possibly other languages."); + ImGui::Spacing(); + + static float wrap_width = 200.0f; + ImGui::SliderFloat("Wrap width", &wrap_width, -20, 600, "%.0f"); + + ImGui::Text("Test paragraph 1:"); + ImGui::GetWindowDrawList()->AddRectFilled(ImGui::GetCursorScreenPos() + ImVec2(wrap_width, 0.0f), ImGui::GetCursorScreenPos() + ImVec2(wrap_width+10, ImGui::GetTextLineHeight()), 0xFFFF00FF); + ImGui::PushTextWrapPos(ImGui::GetCursorPos().x + wrap_width); + ImGui::Text("lazy dog. This paragraph is made to fit within %.0f pixels. The quick brown fox jumps over the lazy dog.", wrap_width); + ImGui::GetWindowDrawList()->AddRect(ImGui::GetItemBoxMin(), ImGui::GetItemBoxMax(), 0xFF00FFFF); + ImGui::PopTextWrapPos(); + + ImGui::Text("Test paragraph 2:"); + ImGui::GetWindowDrawList()->AddRectFilled(ImGui::GetCursorScreenPos() + ImVec2(wrap_width, 0.0f), ImGui::GetCursorScreenPos() + ImVec2(wrap_width+10, ImGui::GetTextLineHeight()), 0xFFFF00FF); + ImGui::PushTextWrapPos(ImGui::GetCursorPos().x + wrap_width); + ImGui::Text("aaaaaaaa bbbbbbbb, cccccccc,dddddddd. eeeeeeee ffffffff. gggggggg!hhhhhhhh"); + ImGui::GetWindowDrawList()->AddRect(ImGui::GetItemBoxMin(), ImGui::GetItemBoxMax(), 0xFF00FFFF); + ImGui::PopTextWrapPos(); + + ImGui::TreePop(); + } + if (ImGui::TreeNode("UTF-8 Text")) { // UTF-8 test (need a suitable font, try extra_fonts/mplus* files for example) // Most compiler appears to support UTF-8 in source code (with Visual Studio you need to save your file as 'UTF-8 without signature') - // However for the sake for maximum portability here we are *not* including raw UTF-8 character in this source file, instead we encode the string with with hexadecimal constants. - // In your own application please be reasonable and use UTF-8 in the source or get the data from external files. :) - //const char* utf8_string = "\xe3\x81\x8b\xe3\x81\x8d\xe3\x81\x8f\xe3\x81\x91\xe3\x81\x93\x20\xe6\x97\xa5\xe6\x9c\xac\xe8\xaa\x9e"; // Japanese text for "Kakikukeo" (Hiragana) followed by "Nihongo" (kanji) - ImGui::Text("(CJK text will only appears if the font supports it. Please check in\nthe extra_fonts/ folder if you intend to use non-ASCII characters.\nNote that characters values are preserved even if the font cannot be\ndisplayed, so you can safely copy & paste garbled characters.)"); + // However for the sake for maximum portability here we are *not* including raw UTF-8 character in this source file, instead we encode the string with hexadecimal constants. + // In your own application please be reasonable and use UTF-8 in the source or get the data from external files! :) + ImGui::TextWrapped("(CJK text will only appears if the font supports it. Please check in the extra_fonts/ folder if you intend to use non-ASCII characters. Note that characters values are preserved even if the font cannot be displayed, so you can safely copy & paste garbled characters.)"); ImGui::Text("Hiragana: \xe3\x81\x8b\xe3\x81\x8d\xe3\x81\x8f\xe3\x81\x91\xe3\x81\x93 (kakikukeko)"); ImGui::Text("Kanjis: \xe6\x97\xa5\xe6\x9c\xac\xe8\xaa\x9e (nihongo)"); static char buf[32] = "\xe6\x97\xa5\xe6\x9c\xac\xe8\xaa\x9e"; @@ -6462,15 +6697,26 @@ void ShowTestWindow(bool* open) bool focus_1 = ImGui::Button("Focus on 1"); ImGui::SameLine(); bool focus_2 = ImGui::Button("Focus on 2"); ImGui::SameLine(); bool focus_3 = ImGui::Button("Focus on 3"); + int has_focus = 0; static char buf[128] = "click on a button to set focus"; + if (focus_1) ImGui::SetKeyboardFocusHere(); ImGui::InputText("1", buf, IM_ARRAYSIZE(buf)); + if (ImGui::IsItemFocused()) has_focus = 1; + if (focus_2) ImGui::SetKeyboardFocusHere(); ImGui::InputText("2", buf, IM_ARRAYSIZE(buf)); + if (ImGui::IsItemFocused()) has_focus = 2; + ImGui::PushAllowKeyboardFocus(false); if (focus_3) ImGui::SetKeyboardFocusHere(); ImGui::InputText("3 (tab skip)", buf, IM_ARRAYSIZE(buf)); + if (ImGui::IsItemFocused()) has_focus = 3; ImGui::PopAllowKeyboardFocus(); + if (has_focus) + ImGui::Text("Item with focus: %d", has_focus); + else + ImGui::Text("Item with focus: "); ImGui::TreePop(); } } diff --git a/imgui.h b/imgui.h index 715a20615..05b5d3f0c 100644 --- a/imgui.h +++ b/imgui.h @@ -159,13 +159,16 @@ namespace ImGui void SetKeyboardFocusHere(int offset = 0); // focus keyboard on the next widget. Use 'offset' to access sub components of a multiple component widget. void SetTreeStateStorage(ImGuiStorage* tree); // replace tree state storage with our own (if you want to manipulate it yourself, typically clear subsection of it). ImGuiStorage* GetTreeStateStorage(); - void PushItemWidth(float item_width); + + void PushItemWidth(float item_width); // width of items for the common item+label case. default to ~2/3 of windows width. void PopItemWidth(); float GetItemWidth(); void PushAllowKeyboardFocus(bool v); // allow focusing using TAB/Shift-TAB, enabled by default but you can disable it for certain widgets. void PopAllowKeyboardFocus(); void PushStyleColor(ImGuiCol idx, const ImVec4& col); void PopStyleColor(); + void PushTextWrapPos(float wrap_pos_x); // word-wrapping for Text*() commands. < 0.0f: no wrapping; 0.0f: wrap to end of window (or column); > 0.0f: wrap at 'wrap_pos_x' position in window local space. + void PopTextWrapPos(); // Tooltip void SetTooltip(const char* fmt, ...); // set tooltip under mouse-cursor, typically use with ImGui::IsHovered(). last call wins. @@ -200,10 +203,12 @@ namespace ImGui // Widgets void Text(const char* fmt, ...); void TextV(const char* fmt, va_list args); - void TextColored(const ImVec4& col, const char* fmt, ...); // shortcut to doing PushStyleColor(ImGuiCol_Text, col); Text(fmt, ...); PopStyleColor(); + void TextColored(const ImVec4& col, const char* fmt, ...); // shortcut for PushStyleColor(ImGuiCol_Text, col); Text(fmt, ...); PopStyleColor(); void TextColoredV(const ImVec4& col, const char* fmt, va_list args); - void TextUnformatted(const char* text, const char* text_end = NULL); // doesn't require null terminated string if 'text_end' is specified. no copy done to any bounded stack buffer, better for long chunks of text. - void LabelText(const char* label, const char* fmt, ...); + void TextWrapped(const char* fmt, ...); // shortcut for PushTextWrapPos(0.0f); Text(fmt, ...); PopTextWrapPos(); + void TextWrappedV(const char* fmt, va_list args); + void TextUnformatted(const char* text, const char* text_end = NULL); // doesn't require null terminated string if 'text_end' is specified. no copy done to any bounded stack buffer, recommended for long chunks of text. + void LabelText(const char* label, const char* fmt, ...); // display text+label aligned the same way as value+label widgets void LabelTextV(const char* label, const char* fmt, va_list args); void BulletText(const char* fmt, ...); void BulletTextV(const char* fmt, va_list args); @@ -265,9 +270,10 @@ namespace ImGui // Utilities void SetNewWindowDefaultPos(const ImVec2& pos); // set position of window that do bool IsHovered(); // was the last item active area hovered by mouse? + bool IsItemFocused(); // was the last item focused for keyboard input? ImVec2 GetItemBoxMin(); // get bounding box of last item ImVec2 GetItemBoxMax(); // get bounding box of last item - bool IsClipped(const ImVec2& item_size); // to perform coarse clipping on user's side (as an optimisation) + bool IsClipped(const ImVec2& item_size); // to perform coarse clipping on user's side (as an optimization) bool IsKeyPressed(int key_index, bool repeat = true); // key_index into the keys_down[512] array, imgui doesn't know the semantic of each entry bool IsMouseClicked(int button, bool repeat = false); bool IsMouseDoubleClicked(int button); @@ -280,7 +286,7 @@ namespace ImGui int GetFrameCount(); const char* GetStyleColorName(ImGuiCol idx); void GetDefaultFontData(const void** fnt_data, unsigned int* fnt_size, const void** png_data, unsigned int* png_size); - ImVec2 CalcTextSize(const char* text, const char* text_end = NULL, const bool hide_text_after_hash = true); + ImVec2 CalcTextSize(const char* text, const char* text_end = NULL, bool hide_text_after_hash = true, float wrap_width = -1.0f); } // namespace ImGui @@ -542,6 +548,7 @@ struct ImGuiTextBuffer ImVector Buf; ImGuiTextBuffer() { Buf.push_back(0); } + ~ImGuiTextBuffer() { clear(); } const char* begin() const { return Buf.begin(); } const char* end() const { return Buf.end()-1; } size_t size() const { return Buf.size()-1; } @@ -619,8 +626,8 @@ struct ImDrawList void AddTriangleFilled(const ImVec2& a, const ImVec2& b, const ImVec2& c, ImU32 col); void AddCircle(const ImVec2& centre, float radius, ImU32 col, int num_segments = 12); void AddCircleFilled(const ImVec2& centre, float radius, ImU32 col, int num_segments = 12); - void AddArc(const ImVec2& center, float rad, ImU32 col, int a_min, int a_max, bool tris=false, const ImVec2& third_point_offset = ImVec2(0,0)); - void AddText(ImFont font, float font_size, const ImVec2& pos, ImU32 col, const char* text_begin, const char* text_end); + void AddArc(const ImVec2& center, float rad, ImU32 col, int a_min, int a_max, bool tris = false, const ImVec2& third_point_offset = ImVec2(0,0)); + void AddText(ImFont font, float font_size, const ImVec2& pos, ImU32 col, const char* text_begin, const char* text_end, float wrap_width = 0.0f); }; // Optional bitmap font data loader & renderer into vertices @@ -696,11 +703,16 @@ struct ImBitmapFont bool LoadFromFile(const char* filename); void Clear(); void BuildLookupTable(); - const FntGlyph * FindGlyph(unsigned short c) const; + const FntGlyph * FindGlyph(unsigned short c, const FntGlyph* fallback = NULL) const; float GetFontSize() const { return (float)Info->FontSize; } bool IsLoaded() const { return Info != NULL && Common != NULL && Glyphs != NULL; } - ImVec2 CalcTextSizeA(float size, float max_width, const char* text_begin, const char* text_end, const char** remaining = NULL) const; // utf8 - ImVec2 CalcTextSizeW(float size, float max_width, const ImWchar* text_begin, const ImWchar* text_end, const ImWchar** remaining = NULL) const; // wchar - void RenderText(float size, ImVec2 pos, ImU32 col, const ImVec4& clip_rect, const char* text_begin, const char* text_end, ImDrawVert*& out_vertices) const; + // 'max_width' stops rendering after a certain width (could be turned into a 2d size). FLT_MAX to disable. + // 'wrap_width' enable automatic word-wrapping across multiple lines to fit into given width. 0.0f to disable. + ImVec2 CalcTextSizeA(float size, float max_width, float wrap_width, const char* text_begin, const char* text_end, const char** remaining = NULL) const; // utf8 + ImVec2 CalcTextSizeW(float size, float max_width, const ImWchar* text_begin, const ImWchar* text_end, const ImWchar** remaining = NULL) const; // wchar + void RenderText(float size, ImVec2 pos, ImU32 col, const ImVec4& clip_rect, const char* text_begin, const char* text_end, ImDrawVert*& out_vertices, float wrap_width = 0.0f) const; + +private: + const char* CalcWordWrapPositionA(float scale, const char* text, const char* text_end, float wrap_width, const FntGlyph* fallback_glyph) const; };