From 22ffa3d6d34db5fad26a1ceeeef6c5c009a80adf Mon Sep 17 00:00:00 2001 From: ocornut Date: Thu, 18 Dec 2025 00:12:32 +0100 Subject: [PATCH] Text: rewrite word-wrapping logic. (#8990, #3237, #8503, #8139, #8439, #9094, #3002, #9066, #8838) --- docs/CHANGELOG.txt | 7 +++- imgui.h | 2 +- imgui_draw.cpp | 84 ++++++++++++++++++++++++++++------------------ 3 files changed, 59 insertions(+), 34 deletions(-) diff --git a/docs/CHANGELOG.txt b/docs/CHANGELOG.txt index 8931aa070..f9ee3a143 100644 --- a/docs/CHANGELOG.txt +++ b/docs/CHANGELOG.txt @@ -93,7 +93,12 @@ Other Changes: - Added ImGuiSliderFlags_ColorMarkers to opt-in adding R/G/B/A color markers next to each components, in multi-components functions. - Added a way to select a specific marker color. -- Text: +- Text, InputText: + - Reworked word-wrapping logic: + - Try to not wrap in the middle of contiguous punctuations. (#8139, #8439, #9094) + - Try to not wrap between a punctuation and a digit. (#8503) + - Inside InputTextMultiline() with _WordWrap: prefer keeping blanks at the + end of a line rather than at the beginning of next line. (#8990, #3237) - Fixed low-level word-wrapping function reading from *text_end when passed a string range. (#9107) [@achabense] - Scrollbar: fixed a codepath leading to a divide-by-zero (which would not be diff --git a/imgui.h b/imgui.h index a336ccd06..275cf566b 100644 --- a/imgui.h +++ b/imgui.h @@ -30,7 +30,7 @@ // Library Version // (Integer encoded as XYYZZ for use in #if preprocessor conditionals, e.g. '#if IMGUI_VERSION_NUM >= 12345') #define IMGUI_VERSION "1.92.6 WIP" -#define IMGUI_VERSION_NUM 19256 +#define IMGUI_VERSION_NUM 19257 #define IMGUI_HAS_TABLE // Added BeginTable() - from IMGUI_VERSION_NUM >= 18000 #define IMGUI_HAS_TEXTURES // Added ImGuiBackendFlags_RendererHasTextures - from IMGUI_VERSION_NUM >= 19198 diff --git a/imgui_draw.cpp b/imgui_draw.cpp index 058b3724b..06134e3aa 100644 --- a/imgui_draw.cpp +++ b/imgui_draw.cpp @@ -17,7 +17,7 @@ Index of this file: // [SECTION] ImFontAtlas: backend for stb_truetype // [SECTION] ImFontAtlas: glyph ranges helpers // [SECTION] ImFontGlyphRangesBuilder -// [SECTION] ImFont +// [SECTION] ImFontBaked, ImFont // [SECTION] ImGui Internal Render Helpers // [SECTION] Decompression code // [SECTION] Default font data (ProggyClean.ttf) @@ -5068,7 +5068,7 @@ void ImFontGlyphRangesBuilder::BuildRanges(ImVector* out_ranges) } //----------------------------------------------------------------------------- -// [SECTION] ImFont +// [SECTION] ImFontBaked, ImFont //----------------------------------------------------------------------------- ImFontBaked::ImFontBaked() @@ -5371,6 +5371,12 @@ const char* ImTextCalcWordWrapNextLineStart(const char* text, const char* text_e return text; } +// Character classification for word-wrapping logic +enum +{ + ImWcharClass_Blank, ImWcharClass_Punct, ImWcharClass_Other +}; + // Simple word-wrapping for English, not full-featured. Please submit failing cases! // This will return the next location to wrap from. If no wrapping if necessary, this will fast-forward to e.g. text_end. // 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.) @@ -5392,16 +5398,20 @@ const char* ImFontCalcWordWrapPositionEx(ImFont* font, float size, const char* t const float scale = size / baked->Size; float line_width = 0.0f; - float word_width = 0.0f; float blank_width = 0.0f; 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; IM_ASSERT(text_end != NULL); + + int prev_type = ImWcharClass_Other; + const bool keep_blanks = (flags & ImDrawTextFlags_WrapKeepBlanks) != 0; + + // Find next wrapping point + //const char* span_begin = s; + const char* span_end = s; + float span_width = 0.0f; + while (s < text_end) { unsigned int c = (unsigned int)*s; @@ -5417,7 +5427,7 @@ const char* ImFontCalcWordWrapPositionEx(ImFont* font, float size, const char* t return s; // Direct return, skip "Wrap_width is too small to fit anything" path. if (c == '\r') { - s = next_s; + s = next_s; // Fast-skip continue; } } @@ -5427,46 +5437,56 @@ const char* ImFontCalcWordWrapPositionEx(ImFont* font, float size, const char* t if (char_width < 0.0f) char_width = BuildLoadGlyphGetAdvanceOrFallback(baked, c); - if (ImCharIsBlankW(c)) + // Classify current character + int curr_type; + if (c == ' ' || c == '\t' || c == 0x3000) // Inline version of ImCharIsBlankW(c) + curr_type = ImWcharClass_Blank; + else if (c == '.' || c == ',' || c == ';' || c == '!' || c == '?' || c == '\"' || c == 0x3001 || c == 0x3002) + curr_type = ImWcharClass_Punct; + else + curr_type = ImWcharClass_Other; + + if (curr_type == ImWcharClass_Blank) { - if (inside_word) + // End span: 'A ' or '. ' + if (prev_type != ImWcharClass_Blank && !keep_blanks) { - line_width += blank_width; - blank_width = 0.0f; - word_end = s; + span_end = s; + line_width += span_width; + span_width = 0.0f; } blank_width += char_width; - inside_word = false; } else { - word_width += char_width; - if (inside_word) + // End span: '.X' unless X is a digit + if (prev_type == ImWcharClass_Punct && curr_type != ImWcharClass_Punct && !(c >= '0' && c <= '9')) { - word_end = next_s; + span_end = s; + line_width += span_width + blank_width; + span_width = blank_width = 0.0f; } - else + // End span: 'A ' or '. ' + else if (prev_type == ImWcharClass_Blank && keep_blanks) { - prev_word_end = word_end; - line_width += word_width + blank_width; - if ((flags & ImDrawTextFlags_WrapKeepBlanks) && line_width <= wrap_width) - prev_word_end = s; - word_width = blank_width = 0.0f; + span_end = s; + line_width += span_width + blank_width; + span_width = blank_width = 0.0f; } - - // Allow wrapping after punctuation. - inside_word = (c != '.' && c != ',' && c != ';' && c != '!' && c != '?' && c != '\"' && c != 0x3001 && c != 0x3002); + span_width += char_width; } - // We ignore blank width at the end of the line (they can be skipped) - if (line_width + word_width > wrap_width) + if (span_width + blank_width + line_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; + if (span_width + blank_width > wrap_width) + break; + // FIXME: Narrow wrapping e.g. "A quick brown" -> "Quic|k br|own", would require knowing if span is going to be longer than wrap_width. + //if (span_width > wrap_width && !is_blank && !was_blank) + // return s; + return span_end; } + prev_type = curr_type; s = next_s; }