diff --git a/imgui_demo.cpp b/imgui_demo.cpp index 6063a3c6c790..827f16b02d97 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -3598,7 +3598,10 @@ static void DemoWindowWidgetsText() ImGui::SliderFloat("Wrap width", &wrap_width, -20, 600, "%.0f"); ImDrawList* draw_list = ImGui::GetWindowDrawList(); - for (int n = 0; n < 2; n++) + static char user_input[2048]; + + ImGui::InputText("Input Any.", user_input, 2048); + for (int n = 0; n < 3; n++) { ImGui::Text("Test paragraph %d:", n); ImVec2 pos = ImGui::GetCursorScreenPos(); @@ -3607,9 +3610,11 @@ static void DemoWindowWidgetsText() ImGui::PushTextWrapPos(ImGui::GetCursorPos().x + wrap_width); if (n == 0) ImGui::Text("The lazy dog is a good dog. This paragraph should fit within %.0f pixels. Testing a 1 character word. The quick brown fox jumps over the lazy dog.", wrap_width); - else + else if (n == 1) ImGui::Text("aaaaaaaa bbbbbbbb, c cccccccc,dddddddd. d eeeeeeee ffffffff. gggggggg!hhhhhhhh"); - + else + ImGui::Text(user_input); + // Draw actual text bounding box, following by marker of our expected limit (should not overlap!) draw_list->AddRect(ImGui::GetItemRectMin(), ImGui::GetItemRectMax(), IM_COL32(255, 255, 0, 255)); draw_list->AddRectFilled(marker_min, marker_max, IM_COL32(255, 0, 255, 255)); diff --git a/imgui_draw.cpp b/imgui_draw.cpp index ed6a91ab3b2f..432661daafb9 100644 --- a/imgui_draw.cpp +++ b/imgui_draw.cpp @@ -5353,17 +5353,8 @@ static inline const char* CalcWordWrapNextLineStartA(const char* text, const cha // 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.) const char* ImFont::CalcWordWrapPosition(float size, const char* text, const char* text_end, float wrap_width) { - // 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" + // Refactored word wrapping method detects wrapping points by looking for at most 3 consecutive characters. + // (Currently only 2.) ImFontBaked* baked = GetFontBaked(size); const float scale = size / baked->Size; @@ -5374,87 +5365,91 @@ const char* ImFont::CalcWordWrapPosition(float size, const char* text, const cha wrap_width /= scale; // We work with unscaled widths to avoid scaling every characters const char* word_end = text; - const char* prev_word_end = NULL; - bool inside_word = true; - const char* s = text; + const char* prev_s = NULL; + const char* s = NULL; + const char* next_s = text; + unsigned int prev_c = 0; + unsigned int c = 0; + unsigned int next_c; +#define IM_ADVANCE_WORD() \ +do {\ + word_end = s; \ + line_width += word_width + blank_width; \ + word_width = blank_width = 0.0f;\ +} while (0) IM_ASSERT(text_end != NULL); while (s < text_end) { - unsigned int c = (unsigned int)*s; - const char* next_s; - if (c < 0x80) - next_s = s + 1; + // prev_s is the END of prev_c, which actually points to c + // same for s and next_s. + prev_s = s; + s = next_s; + prev_c = c; + c = next_c; + next_c = (unsigned int)*next_s; + if (next_c < 0x80) + next_s = next_s + 1; else - next_s = s + ImTextCharFromUtf8(&c, s, text_end); + next_s = next_s + ImTextCharFromUtf8(&next_c, next_s, text_end); + if (next_s > text_end) + next_c = 0; - if (c < 32) + if (prev_s == NULL) { + continue; + } + if (c < 0x20) { if (c == '\n') { line_width = word_width = blank_width = 0.0f; - inside_word = true; - s = next_s; continue; } if (c == '\r') - { - s = next_s; continue; - } - } + } // Optimized inline version of 'float char_width = GetCharAdvance((ImWchar)c);' float char_width = (c < (unsigned int)baked->IndexAdvanceX.Size) ? baked->IndexAdvanceX.Data[c] : -1.0f; if (char_width < 0.0f) char_width = BuildLoadGlyphGetAdvanceOrFallback(baked, c); - if (ImCharIsBlankW(c)) - { - if (inside_word) - { - line_width += blank_width; - blank_width = 0.0f; - word_end = s; - } 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 != 0x3001 && c != 0x3002); + word_width += char_width + blank_width; + blank_width = 0.0f; } - // 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; + s = word_end; + else + s = prev_s; break; } - s = next_s; + if (!next_c) { + IM_ADVANCE_WORD(); + } + else if (c && next_c) + { + if (prev_c >= '0' && prev_c <= '9' && next_c >= '0' && next_c <= '9' && !ImCharIsLineBreakableW(c)) + continue; + if (ImCharIsLineBreakableW(next_c) && !ImCharIsHeadProhibited(next_c) && !ImCharIsTailProhibited(c)) + IM_ADVANCE_WORD(); + if ((ImCharIsHeadProhibited(c) || ImCharIsLineBreakableW(c)) && !ImCharIsHeadProhibited(next_c)) + IM_ADVANCE_WORD(); + } } - +#undef IM_ADVANCE_WORD // Wrap_width is too small to fit anything. Force displaying 1 character to minimize the height discontinuity. // +1 may not be a character start point in UTF-8 but it's ok because caller loops use (text >= word_wrap_eol). if (s == text && text < text_end) return s + ImTextCountUtf8BytesFromChar(s, text_end); - return s; + return s > text_end ? text_end : s; } ImVec2 ImFont::CalcTextSizeA(float size, float max_width, float wrap_width, const char* text_begin, const char* text_end, const char** remaining) @@ -5480,7 +5475,7 @@ ImVec2 ImFont::CalcTextSizeA(float size, float max_width, float wrap_width, cons // 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 = CalcWordWrapPosition(size, s, text_end, wrap_width - line_width); - + IM_ASSERT(word_wrap_eol <= text_end); if (s >= word_wrap_eol) { if (text_size.x < line_width) @@ -5488,7 +5483,6 @@ ImVec2 ImFont::CalcTextSizeA(float size, float max_width, float wrap_width, cons text_size.y += line_height; line_width = 0.0f; word_wrap_eol = NULL; - s = CalcWordWrapNextLineStartA(s, text_end); // Wrapping skips upcoming blanks continue; } } diff --git a/imgui_internal.h b/imgui_internal.h index e92ab7b5c709..7488ce3a00e8 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -401,6 +401,15 @@ IM_MSVC_RUNTIME_CHECKS_OFF inline char ImToUpper(char c) { return (c >= 'a' && c <= 'z') ? c &= ~32 : c; } inline bool ImCharIsBlankA(char c) { return c == ' ' || c == '\t'; } inline bool ImCharIsBlankW(unsigned int c) { return c == ' ' || c == '\t' || c == 0x3000; } +inline bool ImCharIsHeadProhibitedA(char c) { return c == ' ' || c == '\t' || c == '}' || c == ')' || c == ']' || c == '?' || c == '!' || c == '|' || c == '/' || c == '&' || c == '.' || c == ',' || c == ':' || c == ';';} +const unsigned int HeadProhibitedW[] = { 0xa2, 0xb0, 0x2032, 0x2033, 0x2030, 0x2103, 0x3001, 0x3002, 0xff61, 0xff64, 0xffe0, 0xff0c, 0xff0e, 0xff1a, 0xff1b, 0xff1f, 0xff01, 0xff05, 0x30fb, 0xff65, 0x309d, 0x309e, 0x30fd, 0x30fe, 0x30fc, 0x30a1, 0x30a3, 0x30a5, 0x30a7, 0x30a9, 0x30c3, 0x30e3, 0x30e5, 0x30e7, 0x30ee, 0x30f5, 0x30f6, 0x3041, 0x3043, 0x3045, 0x3047, 0x3049, 0x3063, 0x3083, 0x3085, 0x3087, 0x308e, 0x3095, 0x3096, 0x31f0, 0x31f1, 0x31f2, 0x31f3, 0x31f4, 0x31f5, 0x31f6, 0x31f7, 0x31f8, 0x31f9, 0x31fa, 0x31fb, 0x31fc, 0x31fd, 0x31fe, 0x31ff, 0x3005, 0x303b, 0xff67, 0xff68, 0xff69, 0xff6a, 0xff6b, 0xff6c, 0xff6d, 0xff6e, 0xff6f, 0xff70, 0x201d, 0x3009, 0x300b, 0x300d, 0x300f, 0x3011, 0x3015, 0xff09, 0xff3d, 0xff5d, 0xff63}; +inline bool ImCharIsHeadProhibitedW(unsigned int c) { for (int i = 0; i < IM_ARRAYSIZE(HeadProhibitedW); i++) if (c == HeadProhibitedW[i]) return true; return false;} +inline bool ImCharIsHeadProhibited(unsigned int c) { return (c < 128 && ImCharIsHeadProhibitedA(c)) || ImCharIsHeadProhibitedW(c); } +inline bool ImCharIsTailProhibitedA(unsigned int c) { return c == '(' || c == '[' || c == '{' || c == '+'; } +const unsigned int TailProhibitedW[] = { 0x2018, 0x201c, 0x3008, 0x300a, 0x300c, 0x300e, 0x3010, 0x3014, 0xff08, 0xff3b, 0xff5b, 0xff62, 0xa3, 0xa5, 0xff04, 0xffe1, 0xffe5, 0xff0b }; +inline bool ImCharIsTailProhibitedW(unsigned int c) { for (int i = 0; i < IM_ARRAYSIZE(TailProhibitedW); i++) if (c == TailProhibitedW[i]) return true; return false; } +inline bool ImCharIsTailProhibited(unsigned int c) { return (c < 128 && ImCharIsTailProhibitedA(c)) || ImCharIsTailProhibitedW(c); } +inline bool ImCharIsLineBreakableW(unsigned int c) { return (c >= 0x3040 && c <= 0x9fff) || (c >= 0x3400 && c <= 0x4dbf) || (c >= 0x20000 && c <= 0xdffff) || (c >= 0x3040 && c <= 0x30ff) || (c >= 0xac00 && c <= 0xd7ff) || (c >= 0xf900 && c <= 0xfaff) || (c >= 0x1100 && c <= 0x11ff) || (c >= 0x2e80 && c <= 0x2fff); } inline bool ImCharIsXdigitA(char c) { return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f'); } IM_MSVC_RUNTIME_CHECKS_RESTORE