From 9f969944d5c995cefa2c7069f7967f3a30d93e44 Mon Sep 17 00:00:00 2001 From: ocornut Date: Mon, 18 Aug 2025 17:37:40 +0200 Subject: [PATCH 1/7] stb_textedit: fixed misleading cursor-1 in STB_TEXTEDIT_K_LINESTART handlers. (#7925) `state->cursor - 1` in STB_TEXTEDIT_K_LINESTART handlers was technically misleadingly not UTF-8 compliant but things would naturally work anyhow. --- imstb_textedit.h | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/imstb_textedit.h b/imstb_textedit.h index 33eef7095..1a9975f3b 100644 --- a/imstb_textedit.h +++ b/imstb_textedit.h @@ -1100,8 +1100,12 @@ retry: stb_textedit_move_to_first(state); if (state->single_line) state->cursor = 0; - else while (state->cursor > 0 && STB_TEXTEDIT_GETCHAR(str, state->cursor-1) != STB_TEXTEDIT_NEWLINE) - state->cursor = IMSTB_TEXTEDIT_GETPREVCHARINDEX(str, state->cursor); + else while (state->cursor > 0) { + int prev = IMSTB_TEXTEDIT_GETPREVCHARINDEX(str, state->cursor); + if (STB_TEXTEDIT_GETCHAR(str, prev) == STB_TEXTEDIT_NEWLINE) + break; + state->cursor = prev; + } state->has_preferred_x = 0; break; @@ -1128,8 +1132,12 @@ retry: stb_textedit_prep_selection_at_cursor(state); if (state->single_line) state->cursor = 0; - else while (state->cursor > 0 && STB_TEXTEDIT_GETCHAR(str, state->cursor-1) != STB_TEXTEDIT_NEWLINE) - state->cursor = IMSTB_TEXTEDIT_GETPREVCHARINDEX(str, state->cursor); + else while (state->cursor > 0) { + int prev = IMSTB_TEXTEDIT_GETPREVCHARINDEX(str, state->cursor); + if (STB_TEXTEDIT_GETCHAR(str, prev) == STB_TEXTEDIT_NEWLINE) + break; + state->cursor = prev; + } state->select_end = state->cursor; state->has_preferred_x = 0; break; From 55cbc665081d31108420532bdf4435b9397a383f Mon Sep 17 00:00:00 2001 From: ocornut Date: Wed, 27 Aug 2025 20:21:31 +0200 Subject: [PATCH 2/7] InputText: allow passing an empty string with buf_size==0. (#8907) --- docs/CHANGELOG.txt | 6 +++++- imgui_widgets.cpp | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/CHANGELOG.txt b/docs/CHANGELOG.txt index 38b9898e9..b25af3124 100644 --- a/docs/CHANGELOG.txt +++ b/docs/CHANGELOG.txt @@ -51,7 +51,11 @@ Other Changes: disable the assumption that 1 clipper item == 1 table row, which breaks when e.g. using clipper with ItemsHeight=1 in order to clip in pixel units. (#8886) - Fixed Bullet() fixed tesselation amount which looked out of place in very large sizes. -- DrawList: Fixed CloneOutput() unnecessarily taking a copy of the ImDrawListSharedData +- InputText: allow passing an empty string with buf_size==0. (#8907) + In theory the buffer size should always account for a zero-terminator, but idioms + such as using InputTextMultiline() with ImGuiInputTextFlags_ReadOnly to display + a text blob are facilitated by allowing this. +- DrawList: fixed CloneOutput() unnecessarily taking a copy of the ImDrawListSharedData pointer, which could to issue when deleting the cloned list. (#8894, #1860) - Debug Tools: ID Stack Tool: fixed using fixed-size buffers preventing long identifiers from being displayed in the tool. (#8905, #4631) diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 4a64b2c21..e701e0293 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -4653,7 +4653,7 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ // Take a copy of the initial buffer value. // From the moment we focused we are normally ignoring the content of 'buf' (unless we are in read-only mode) const int buf_len = (int)ImStrlen(buf); - IM_ASSERT(buf_len + 1 <= buf_size && "Is your input buffer properly zero-terminated?"); + IM_ASSERT(((buf_len + 1 <= buf_size) || (buf_len == 0 && buf_size == 0)) && "Is your input buffer properly zero-terminated?"); state->TextToRevertTo.resize(buf_len + 1); // UTF-8. we use +1 to make sure that .Data is always pointing to at least an empty string. memcpy(state->TextToRevertTo.Data, buf, buf_len + 1); From 6351f00ff11cf926262200b9e9f2f9e13be79710 Mon Sep 17 00:00:00 2001 From: ocornut Date: Wed, 27 Aug 2025 20:31:05 +0200 Subject: [PATCH 3/7] Clipper, Tables: removed `row_increase >= 0` assert. (#8886) Seeing cases in my own tests that are not obvious so it seems like too much of a burden for the user to assert/crash, as the row count is not always useful anyhow. --- imgui.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/imgui.cpp b/imgui.cpp index 085575bbd..07537df87 100644 --- a/imgui.cpp +++ b/imgui.cpp @@ -3142,10 +3142,9 @@ static void ImGuiListClipper_SeekCursorAndSetupPrevLine(ImGuiListClipper* clippe { if (table->IsInsideRow) ImGui::TableEndRow(table); - if ((clipper->Flags & ImGuiListClipperFlags_NoSetTableRowCounters) == 0) + const int row_increase = (int)((off_y / line_height) + 0.5f); + if (row_increase > 0 && (clipper->Flags & ImGuiListClipperFlags_NoSetTableRowCounters) == 0) // If your clipper item height is != from actual table row height, consider using ImGuiListClipperFlags_NoSetTableRowCounters. See #8886. { - const int row_increase = (int)((off_y / line_height) + 0.5f); - IM_ASSERT(row_increase >= 0); // If your clipper item height is != from actual table row height, consider using ImGuiListClipperFlags_NoSetTableRowCounters. See #8886. table->CurrentRow += row_increase; table->RowBgColorCounter += row_increase; } From 771fae623d747cfac490c738c16bb288a9b45e82 Mon Sep 17 00:00:00 2001 From: ocornut Date: Wed, 27 Aug 2025 20:06:18 +0200 Subject: [PATCH 4/7] ImRect: added AsVec4() helper. Using ImRect in InputTextEx(). --- imgui_internal.h | 1 + imgui_widgets.cpp | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/imgui_internal.h b/imgui_internal.h index 873e7881e..739ae692c 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -592,6 +592,7 @@ struct IMGUI_API ImRect void Floor() { Min.x = IM_TRUNC(Min.x); Min.y = IM_TRUNC(Min.y); Max.x = IM_TRUNC(Max.x); Max.y = IM_TRUNC(Max.y); } bool IsInverted() const { return Min.x > Max.x || Min.y > Max.y; } ImVec4 ToVec4() const { return ImVec4(Min.x, Min.y, Max.x, Max.y); } + const ImVec4& AsVec4() const { return *(const ImVec4*)&Min.x; } }; // Helper: ImBitArray diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index e701e0293..5178d5448 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -5230,7 +5230,7 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ RenderFrame(frame_bb.Min, frame_bb.Max, GetColorU32(ImGuiCol_FrameBg), true, style.FrameRounding); } - const ImVec4 clip_rect(frame_bb.Min.x, frame_bb.Min.y, frame_bb.Min.x + inner_size.x, frame_bb.Min.y + inner_size.y); // Not using frame_bb.Max because we have adjusted size + const ImRect clip_rect(frame_bb.Min.x, frame_bb.Min.y, frame_bb.Min.x + inner_size.x, frame_bb.Min.y + inner_size.y); // Not using frame_bb.Max because we have adjusted size ImVec2 draw_pos = is_multiline ? draw_window->DC.CursorPos : frame_bb.Min + style.FramePadding; ImVec2 text_size(0.0f, 0.0f); @@ -5362,9 +5362,9 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ ImVec2 rect_pos = draw_pos + select_start_offset - draw_scroll; for (const char* p = text_selected_begin; p < text_selected_end; ) { - if (rect_pos.y > clip_rect.w + g.FontSize) + if (rect_pos.y > clip_rect.Max.y + g.FontSize) break; - if (rect_pos.y < clip_rect.y) + if (rect_pos.y < clip_rect.Min.y) { p = (const char*)ImMemchr((void*)p, '\n', text_selected_end - p); p = p ? p + 1 : text_selected_end; @@ -5388,7 +5388,7 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ if (is_multiline || (buf_display_end - buf_display) < buf_display_max_length) { ImU32 col = GetColorU32(is_displaying_hint ? ImGuiCol_TextDisabled : ImGuiCol_Text); - draw_window->DrawList->AddText(g.Font, g.FontSize, draw_pos - draw_scroll, col, buf_display, buf_display_end, 0.0f, is_multiline ? NULL : &clip_rect); + draw_window->DrawList->AddText(g.Font, g.FontSize, draw_pos - draw_scroll, col, buf_display, buf_display_end, 0.0f, is_multiline ? NULL : &clip_rect.AsVec4()); } // Draw blinking cursor @@ -5433,7 +5433,7 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ const ImVec2 draw_scroll = /*state ? ImVec2(state->Scroll.x, 0.0f) :*/ ImVec2(0.0f, 0.0f); // Preserve scroll when inactive? ImU32 col = GetColorU32(is_displaying_hint ? ImGuiCol_TextDisabled : ImGuiCol_Text); - draw_window->DrawList->AddText(g.Font, g.FontSize, draw_pos - draw_scroll, col, buf_display, buf_display_end, 0.0f, is_multiline ? NULL : &clip_rect); + draw_window->DrawList->AddText(g.Font, g.FontSize, draw_pos - draw_scroll, col, buf_display, buf_display_end, 0.0f, is_multiline ? NULL : &clip_rect.AsVec4()); } } From 5c92699f5f7a1295c8d5e1d578695f00dc329ce0 Mon Sep 17 00:00:00 2001 From: ocornut Date: Thu, 28 Aug 2025 18:43:41 +0200 Subject: [PATCH 5/7] stb_textedit: trim trailing blanks for simplicity. In theory your editorconfig has this disabled for this file but MSVC plugin doesn't seem to handle this properly. --- imstb_textedit.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/imstb_textedit.h b/imstb_textedit.h index 1a9975f3b..5049715af 100644 --- a/imstb_textedit.h +++ b/imstb_textedit.h @@ -181,10 +181,10 @@ // // To support UTF-8: // -// STB_TEXTEDIT_GETPREVCHARINDEX returns index of previous character -// STB_TEXTEDIT_GETNEXTCHARINDEX returns index of next character +// STB_TEXTEDIT_GETPREVCHARINDEX returns index of previous character +// STB_TEXTEDIT_GETNEXTCHARINDEX returns index of next character // Do NOT define STB_TEXTEDIT_KEYTOTEXT. -// Instead, call stb_textedit_text() directly for text contents. +// Instead, call stb_textedit_text() directly for text contents. // // Keyboard input must be encoded as a single integer value; e.g. a character code // and some bitflags that represent shift states. to simplify the interface, SHIFT must @@ -260,7 +260,7 @@ // // text: (added 2025) // call this to directly send text input the textfield, which is required -// for UTF-8 support, because stb_textedit_key() + STB_TEXTEDIT_KEYTOTEXT() +// for UTF-8 support, because stb_textedit_key() + STB_TEXTEDIT_KEYTOTEXT() // cannot infer text length. // // From 0ef9610e701ad8ca37928768dc54374a067eecf2 Mon Sep 17 00:00:00 2001 From: ocornut Date: Fri, 29 Aug 2025 17:55:45 +0200 Subject: [PATCH 6/7] InputText, stb_textedit: Revert special handling when pressing Down/PageDown on last line of a buffer without a trailing carriage return. Revert fbf70070bb. --- docs/CHANGELOG.txt | 3 +++ imstb_textedit.h | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/CHANGELOG.txt b/docs/CHANGELOG.txt index b25af3124..9d1710a6d 100644 --- a/docs/CHANGELOG.txt +++ b/docs/CHANGELOG.txt @@ -55,6 +55,9 @@ Other Changes: In theory the buffer size should always account for a zero-terminator, but idioms such as using InputTextMultiline() with ImGuiInputTextFlags_ReadOnly to display a text blob are facilitated by allowing this. +- InputText: revert a change in 1.79 where pressing Down or PageDown on the last line + of a multi-line buffer without a trailing carriage return would keep the cursor + unmoved. We revert back to move to the end of line in this situation. - DrawList: fixed CloneOutput() unnecessarily taking a copy of the ImDrawListSharedData pointer, which could to issue when deleting the cloned list. (#8894, #1860) - Debug Tools: ID Stack Tool: fixed using fixed-size buffers preventing long identifiers diff --git a/imstb_textedit.h b/imstb_textedit.h index 5049715af..ac4db0fb9 100644 --- a/imstb_textedit.h +++ b/imstb_textedit.h @@ -921,8 +921,8 @@ retry: // [DEAR IMGUI] // going down while being on the last line shouldn't bring us to that line end - if (STB_TEXTEDIT_GETCHAR(str, find.first_char + find.length - 1) != STB_TEXTEDIT_NEWLINE) - break; + //if (STB_TEXTEDIT_GETCHAR(str, find.first_char + find.length - 1) != STB_TEXTEDIT_NEWLINE) + // break; // now find character position down a row state->cursor = start; From 8dc457fda24806d69a1b291f6fa03b9924c41fdd Mon Sep 17 00:00:00 2001 From: ocornut Date: Mon, 18 Aug 2025 16:43:44 +0200 Subject: [PATCH 7/7] Internals: added indent, shallow tweaks + unused context pointer to InputTextCalcTextLenAndLineCount() to reduce noise in wip patch. Visualize this commit with white-space changes disabled. --- imgui.h | 2 +- imgui_draw.cpp | 6 +++--- imgui_widgets.cpp | 38 ++++++++++++++++++++++---------------- 3 files changed, 26 insertions(+), 20 deletions(-) diff --git a/imgui.h b/imgui.h index f0163f9f1..12f01f43f 100644 --- a/imgui.h +++ b/imgui.h @@ -3832,7 +3832,7 @@ struct ImFont // '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. IMGUI_API ImFontBaked* GetFontBaked(float font_size, float density = -1.0f); // Get or create baked data for given size - IMGUI_API ImVec2 CalcTextSizeA(float size, float max_width, float wrap_width, const char* text_begin, const char* text_end = NULL, const char** remaining = NULL); // utf8 + IMGUI_API ImVec2 CalcTextSizeA(float size, float max_width, float wrap_width, const char* text_begin, const char* text_end = NULL, const char** out_remaining = NULL); IMGUI_API const char* CalcWordWrapPosition(float size, const char* text, const char* text_end, float wrap_width); IMGUI_API void RenderChar(ImDrawList* draw_list, float size, const ImVec2& pos, ImU32 col, ImWchar c, const ImVec4* cpu_fine_clip = NULL); IMGUI_API void RenderText(ImDrawList* draw_list, float size, const ImVec2& pos, ImU32 col, const ImVec4& clip_rect, const char* text_begin, const char* text_end, float wrap_width = 0.0f, bool cpu_fine_clip = false); diff --git a/imgui_draw.cpp b/imgui_draw.cpp index 76d8474bf..e0d56612a 100644 --- a/imgui_draw.cpp +++ b/imgui_draw.cpp @@ -5458,7 +5458,7 @@ const char* ImFont::CalcWordWrapPosition(float size, const char* text, const cha return s; } -ImVec2 ImFont::CalcTextSizeA(float size, float max_width, float wrap_width, const char* text_begin, const char* text_end, const char** remaining) +ImVec2 ImFont::CalcTextSizeA(float size, float max_width, float wrap_width, const char* text_begin, const char* text_end, const char** out_remaining) { if (!text_end) text_end = text_begin + ImStrlen(text_begin); // FIXME-OPT: Need to avoid this. @@ -5536,8 +5536,8 @@ ImVec2 ImFont::CalcTextSizeA(float size, float max_width, float wrap_width, cons if (line_width > 0 || text_size.y == 0.0f) text_size.y += line_height; - if (remaining) - *remaining = s; + if (out_remaining != NULL) + *out_remaining = s; return text_size; } diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 5178d5448..3012404e0 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -135,7 +135,7 @@ static const ImU64 IM_U64_MAX = (2ULL * 9223372036854775807LL + 1); // For InputTextEx() static bool InputTextFilterCharacter(ImGuiContext* ctx, unsigned int* p_char, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data, bool input_source_is_clipboard = false); -static int InputTextCalcTextLenAndLineCount(const char* text_begin, const char** out_text_end); +static int InputTextCalcTextLenAndLineCount(ImGuiContext* ctx, const char* text_begin, const char** out_text_end); static ImVec2 InputTextCalcTextSize(ImGuiContext* ctx, const char* text_begin, const char* text_end, const char** remaining = NULL, ImVec2* out_offset = NULL, bool stop_on_new_line = false); //------------------------------------------------------------------------- @@ -3941,20 +3941,22 @@ bool ImGui::InputTextWithHint(const char* label, const char* hint, char* buf, si } // This is only used in the path where the multiline widget is inactive. -static int InputTextCalcTextLenAndLineCount(const char* text_begin, const char** out_text_end) +static int InputTextCalcTextLenAndLineCount(ImGuiContext*, const char* text_begin, const char** out_text_end) { int line_count = 0; const char* s = text_begin; - while (true) { - const char* s_eol = strchr(s, '\n'); - line_count++; - if (s_eol == NULL) + while (true) { - s = s + ImStrlen(s); - break; + const char* s_eol = strchr(s, '\n'); + line_count++; + if (s_eol == NULL) + { + s = s + ImStrlen(s); + break; + } + s = s_eol + 1; } - s = s_eol + 1; } *out_text_end = s; return line_count; @@ -5259,7 +5261,7 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ } // Render text. We currently only render selection when the widget is active or while scrolling. - // FIXME: We could remove the '&& render_cursor' to keep rendering selection when inactive. + // FIXME: This is one of the messiest piece of the whole codebase. if (render_cursor || render_selection) { IM_ASSERT(state != NULL); @@ -5285,14 +5287,17 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ const char* selmin_ptr = render_selection ? text_begin + ImMin(state->Stb->select_start, state->Stb->select_end) : NULL; // Count lines and find line number for cursor and selection ends + // FIXME: Switch to zero-based index to reduce confusion. int line_count = 1; if (is_multiline) { - for (const char* s = text_begin; (s = (const char*)ImMemchr(s, '\n', (size_t)(text_end - s))) != NULL; s++) { - if (cursor_line_no == -1 && s >= cursor_ptr) { cursor_line_no = line_count; } - if (selmin_line_no == -1 && s >= selmin_ptr) { selmin_line_no = line_count; } - line_count++; + for (const char* s = text_begin; (s = (const char*)ImMemchr(s, '\n', (size_t)(text_end - s))) != NULL; s++) + { + if (cursor_line_no == -1 && s >= cursor_ptr) { cursor_line_no = line_count; } + if (selmin_line_no == -1 && s >= selmin_ptr) { selmin_line_no = line_count; } + line_count++; + } } } if (cursor_line_no == -1) @@ -5372,7 +5377,8 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ else { ImVec2 rect_size = InputTextCalcTextSize(&g, p, text_selected_end, &p, NULL, true); - if (rect_size.x <= 0.0f) rect_size.x = IM_TRUNC(g.FontBaked->GetCharAdvance((ImWchar)' ') * 0.50f); // So we can see selected empty lines + if (rect_size.x <= 0.0f) + rect_size.x = IM_TRUNC(g.FontBaked->GetCharAdvance((ImWchar)' ') * 0.50f); // So we can see selected empty lines ImRect rect(rect_pos + ImVec2(0.0f, bg_offy_up - g.FontSize), rect_pos + ImVec2(rect_size.x, bg_offy_dn)); rect.ClipWith(clip_rect); if (rect.Overlaps(clip_rect)) @@ -5419,7 +5425,7 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ { // Render text only (no selection, no cursor) if (is_multiline) - text_size = ImVec2(inner_size.x, InputTextCalcTextLenAndLineCount(buf_display, &buf_display_end) * g.FontSize); // We don't need width + text_size = ImVec2(inner_size.x, InputTextCalcTextLenAndLineCount(&g, buf_display, &buf_display_end) * g.FontSize); // We don't need width else if (!is_displaying_hint && g.ActiveId == id) buf_display_end = buf_display + state->TextLen; else if (!is_displaying_hint)