From 5e5658e68fae85dde414327eb7e462a4f6a23388 Mon Sep 17 00:00:00 2001 From: Harry Mander Date: Wed, 10 Sep 2025 16:38:10 +1200 Subject: [PATCH 01/11] Debug Tools: fixed assertion failure when opening a combo box while using io.ConfigDebugBeginReturnValueOnce/ConfigDebugBeginReturnValueLoop. (#8931) --- docs/CHANGELOG.txt | 2 ++ imgui_widgets.cpp | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/CHANGELOG.txt b/docs/CHANGELOG.txt index 6ac099fcf..7994e79e1 100644 --- a/docs/CHANGELOG.txt +++ b/docs/CHANGELOG.txt @@ -85,6 +85,8 @@ Other Changes: is now skipped. (#8904, #4631) - Debug Tools: ID Stack Tool: added option to hex-encode non-ASCII characters in output path. (#8904, #4631) +- Debug Tools: Fixed assertion failure when opening a combo box while using + io.ConfigDebugBeginReturnValueOnce/ConfigDebugBeginReturnValueLoop. (#8931) [@harrymander] - Demo: tweaked ShowFontSelector() and ShowStyleSelector() to update selection while navigating and to not close popup automatically. - Examples: Android: Android+OpenGL3: update Gradle project (#8888, #8878) [@scribam] diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 783c49fe2..dd24b74a6 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -2032,7 +2032,8 @@ bool ImGui::BeginComboPopup(ImGuiID popup_id, const ImRect& bb, ImGuiComboFlags if (!ret) { EndPopup(); - IM_ASSERT(0); // This should never happen as we tested for IsPopupOpen() above + if (!g.IO.ConfigDebugBeginReturnValueOnce && !g.IO.ConfigDebugBeginReturnValueLoop) // Begin may only return false with those debug tools activated. + IM_ASSERT(0); // This should never happen as we tested for IsPopupOpen() above return false; } g.BeginComboDepth++; From 55f590c1d17cf8bf1130e7c87c62eeface9001ac Mon Sep 17 00:00:00 2001 From: ocornut Date: Wed, 10 Sep 2025 22:29:42 +0200 Subject: [PATCH 02/11] Selectable: ImGuiSelectableFlags_SelectOnNav doesn't select when holding Ctrl, to be consistent with multi-select. Amend e66afbb + remove needless line in CloseCurrentPopup() block --- docs/CHANGELOG.txt | 2 +- imgui.h | 2 +- imgui_widgets.cpp | 5 ++--- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/CHANGELOG.txt b/docs/CHANGELOG.txt index 7994e79e1..2658bdb69 100644 --- a/docs/CHANGELOG.txt +++ b/docs/CHANGELOG.txt @@ -73,7 +73,7 @@ Other Changes: on InputTextMultiline() fields with ImGuiInputTextFlags_AllowTabInput, since they normally inhibit activation to allow tabbing through multiple items. (#8928) - Selectable: added ImGuiSelectableFlags_SelectOnNav to auto-select an item when - moved into (automatic when in a BeginMultiSelect() block). + moved into, unless Ctrl is held. (automatic when in a BeginMultiSelect() block). - TabBar: fixed an issue were forcefully selecting a tab using internal API would be ignored on first/appearing frame before tabs are submitted (#8929, #6681) - DrawList: fixed CloneOutput() unnecessarily taking a copy of the ImDrawListSharedData diff --git a/imgui.h b/imgui.h index a087c9e4d..e50ab814f 100644 --- a/imgui.h +++ b/imgui.h @@ -1341,7 +1341,7 @@ enum ImGuiSelectableFlags_ ImGuiSelectableFlags_Disabled = 1 << 3, // Cannot be selected, display grayed out text ImGuiSelectableFlags_AllowOverlap = 1 << 4, // (WIP) Hit testing to allow subsequent widgets to overlap this one ImGuiSelectableFlags_Highlight = 1 << 5, // Make the item be displayed as if it is hovered - ImGuiSelectableFlags_SelectOnNav = 1 << 6, // Auto-select when moved into. Automatic when in a BeginMultiSelect() block. + ImGuiSelectableFlags_SelectOnNav = 1 << 6, // Auto-select when moved into, unless Ctrl is held. Automatic when in a BeginMultiSelect() block. #ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS ImGuiSelectableFlags_DontClosePopups = ImGuiSelectableFlags_NoAutoClosePopups, // Renamed in 1.91.0 diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index dd24b74a6..55410b1f3 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7366,7 +7366,7 @@ bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags fl // - (2) usage will fail with clipped items // The multi-select API aim to fix those issues, e.g. may be replaced with a BeginSelection() API. if ((flags & ImGuiSelectableFlags_SelectOnNav) && g.NavJustMovedToId != 0 && g.NavJustMovedToFocusScopeId == g.CurrentFocusScopeId) - if (g.NavJustMovedToId == id) + if (g.NavJustMovedToId == id && (g.NavJustMovedToKeyMods & ImGuiMod_Ctrl) == 0) selected = pressed = auto_selected = true; } @@ -7419,8 +7419,7 @@ bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags fl // Automatically close popups if (pressed && !auto_selected && (window->Flags & ImGuiWindowFlags_Popup) && !(flags & ImGuiSelectableFlags_NoAutoClosePopups) && (g.LastItemData.ItemFlags & ImGuiItemFlags_AutoClosePopups)) - if (!(flags & ImGuiSelectableFlags_SelectOnNav) || g.NavJustMovedToId != id) - CloseCurrentPopup(); + CloseCurrentPopup(); if (disabled_item && !disabled_global) EndDisabled(); From 8eb22ea6203f2653c317670277d4b9604cbae068 Mon Sep 17 00:00:00 2001 From: ocornut Date: Wed, 10 Sep 2025 22:39:17 +0200 Subject: [PATCH 03/11] Demo: ShowStyleSelector(), ShowFontSelector(): remove ImGuiSelectableFlags_NoAutoClosePopups for now. In this situation we kinda want keyboard Enter to select and close but ideally not click. We don't have separate options yet. --- imgui.cpp | 2 +- imgui_demo.cpp | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/imgui.cpp b/imgui.cpp index f5f018c84..13c011ca9 100644 --- a/imgui.cpp +++ b/imgui.cpp @@ -17875,7 +17875,7 @@ void ImGui::ShowFontSelector(const char* label) for (ImFont* font : io.Fonts->Fonts) { PushID((void*)font); - if (Selectable(font->GetDebugName(), font == font_current, ImGuiSelectableFlags_SelectOnNav | ImGuiSelectableFlags_NoAutoClosePopups)) + if (Selectable(font->GetDebugName(), font == font_current, ImGuiSelectableFlags_SelectOnNav)) io.FontDefault = font; if (font == font_current) SetItemDefaultFocus(); diff --git a/imgui_demo.cpp b/imgui_demo.cpp index 8950e814e..26641f6b5 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -8224,17 +8224,18 @@ void ImGui::ShowAboutWindow(bool* p_open) //----------------------------------------------------------------------------- // Demo helper function to select among default colors. See ShowStyleEditor() for more advanced options. -// Here we use the simplified Combo() api that packs items into a single literal string. -// Useful for quick combo boxes where the choices are known locally. bool ImGui::ShowStyleSelector(const char* label) { + // FIXME: This is a bit tricky to get right as style are functions, they don't register a name nor the fact that one is active. + // So we keep track of last active one among our limited selection. static int style_idx = -1; const char* style_names[] = { "Dark", "Light", "Classic" }; bool ret = false; if (ImGui::BeginCombo(label, (style_idx >= 0 && style_idx < IM_ARRAYSIZE(style_names)) ? style_names[style_idx] : "")) { for (int n = 0; n < IM_ARRAYSIZE(style_names); n++) - if (ImGui::Selectable(style_names[n], style_idx == n, ImGuiSelectableFlags_SelectOnNav | ImGuiSelectableFlags_NoAutoClosePopups)) + { + if (ImGui::Selectable(style_names[n], style_idx == n, ImGuiSelectableFlags_SelectOnNav)) { style_idx = n; ret = true; @@ -8245,6 +8246,9 @@ bool ImGui::ShowStyleSelector(const char* label) case 2: ImGui::StyleColorsClassic(); break; } } + else if (style_idx == n) + ImGui::SetItemDefaultFocus(); + } ImGui::EndCombo(); } return ret; From e2f314d613b416a5126fb1385b2af67f9b74cf14 Mon Sep 17 00:00:00 2001 From: ocornut Date: Thu, 11 Sep 2025 15:05:53 +0200 Subject: [PATCH 04/11] InputText: fixed misassignment to unused Scroll.y variable when using ImGuiInputTextFlags_NoHorizontalScroll. Amend d474ed7f7 (#7913, #383) --- imgui_widgets.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 55410b1f3..ebdee29b1 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -5439,7 +5439,7 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ } else { - state->Scroll.y = 0.0f; + state->Scroll.x = 0.0f; } // Vertical scroll From 271f476d08320aae06d9d67e610f55c5e0608440 Mon Sep 17 00:00:00 2001 From: ocornut Date: Thu, 11 Sep 2025 15:12:33 +0200 Subject: [PATCH 05/11] CI: disable pvs-studio 28 days warning. --- .github/workflows/static-analysis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index 53db04764..a9e8bedf0 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -41,6 +41,6 @@ jobs: exit 0 fi cd examples/example_null - pvs-studio-analyzer trace -- make WITH_EXTRA_WARNINGS=1 - pvs-studio-analyzer analyze -e ../../imstb_rectpack.h -e ../../imstb_textedit.h -e ../../imstb_truetype.h -l ../../pvs-studio.lic -o pvs-studio.log + pvs-studio-analyzer trace --disableLicenseExpirationCheck -- make WITH_EXTRA_WARNINGS=1 + pvs-studio-analyzer analyze --disableLicenseExpirationCheck -e ../../imstb_rectpack.h -e ../../imstb_textedit.h -e ../../imstb_truetype.h -l ../../pvs-studio.lic -o pvs-studio.log plog-converter -a 'GA:1,2;OP:1' -d V1071 -t errorfile -w pvs-studio.log From e2b7d84e96d6578b41dc5f4ffe66cdb5b7f9a645 Mon Sep 17 00:00:00 2001 From: ocornut Date: Thu, 11 Sep 2025 15:15:48 +0200 Subject: [PATCH 06/11] CI: disable pvs-studio 28 days warning (amend). --- .github/workflows/static-analysis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index a9e8bedf0..a69c5110c 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -41,6 +41,6 @@ jobs: exit 0 fi cd examples/example_null - pvs-studio-analyzer trace --disableLicenseExpirationCheck -- make WITH_EXTRA_WARNINGS=1 + pvs-studio-analyzer trace -- make WITH_EXTRA_WARNINGS=1 pvs-studio-analyzer analyze --disableLicenseExpirationCheck -e ../../imstb_rectpack.h -e ../../imstb_textedit.h -e ../../imstb_truetype.h -l ../../pvs-studio.lic -o pvs-studio.log plog-converter -a 'GA:1,2;OP:1' -d V1071 -t errorfile -w pvs-studio.log From f36c65661c5534e215120eb96b7a2455ef1efc82 Mon Sep 17 00:00:00 2001 From: ocornut Date: Thu, 11 Sep 2025 18:44:50 +0200 Subject: [PATCH 07/11] InputText: fixed pressing End (without Shift) in a multi-line selection from mistakenly moving cursor based on selection start. --- docs/CHANGELOG.txt | 2 ++ imstb_textedit.h | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/CHANGELOG.txt b/docs/CHANGELOG.txt index 2658bdb69..c195ed1eb 100644 --- a/docs/CHANGELOG.txt +++ b/docs/CHANGELOG.txt @@ -69,6 +69,8 @@ Other Changes: - 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. +- InputText: fixed pressing End (without Shift) in a multi-line selection from + mistakenly moving cursor based on selection start. - Focus, InputText: fixed an issue where SetKeyboardFocusHere() did not work on InputTextMultiline() fields with ImGuiInputTextFlags_AllowTabInput, since they normally inhibit activation to allow tabbing through multiple items. (#8928) diff --git a/imstb_textedit.h b/imstb_textedit.h index 844be3b2b..c41078c8a 100644 --- a/imstb_textedit.h +++ b/imstb_textedit.h @@ -1154,7 +1154,7 @@ retry: #endif case STB_TEXTEDIT_K_LINEEND: { stb_textedit_clamp(str, state); - stb_textedit_move_to_first(state); + stb_textedit_move_to_last(str, state); state->cursor = STB_TEXTEDIT_MOVELINEEND(str, state, state->cursor); state->has_preferred_x = 0; break; From 67085d732aa06547800ec3c2c43ecf9bbfe76115 Mon Sep 17 00:00:00 2001 From: ocornut Date: Thu, 11 Sep 2025 15:17:22 +0200 Subject: [PATCH 08/11] ImGuiTextIndex: rename member. --- imgui.cpp | 4 ++-- imgui_internal.h | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/imgui.cpp b/imgui.cpp index 13c011ca9..b66474b0a 100644 --- a/imgui.cpp +++ b/imgui.cpp @@ -3080,11 +3080,11 @@ void ImGuiTextIndex::append(const char* base, int old_size, int new_size) if (old_size == new_size) return; if (EndOffset == 0 || base[EndOffset - 1] == '\n') - LineOffsets.push_back(EndOffset); + Offsets.push_back(EndOffset); const char* base_end = base + new_size; for (const char* p = base + old_size; (p = (const char*)ImMemchr(p, '\n', base_end - p)) != 0; ) if (++p < base_end) // Don't push a trailing offset on last \n - LineOffsets.push_back((int)(intptr_t)(p - base)); + Offsets.push_back((int)(intptr_t)(p - base)); EndOffset = ImMax(EndOffset, new_size); } diff --git a/imgui_internal.h b/imgui_internal.h index 4d54789f5..a206357b2 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -804,13 +804,13 @@ struct ImChunkStream // Maintain a line index for a text buffer. This is a strong candidate to be moved into the public API. struct ImGuiTextIndex { - ImVector LineOffsets; + ImVector Offsets; int EndOffset = 0; // Because we don't own text buffer we need to maintain EndOffset (may bake in LineOffsets?) - void clear() { LineOffsets.clear(); EndOffset = 0; } - int size() { return LineOffsets.Size; } - const char* get_line_begin(const char* base, int n) { return base + LineOffsets[n]; } - const char* get_line_end(const char* base, int n) { return base + (n + 1 < LineOffsets.Size ? (LineOffsets[n + 1] - 1) : EndOffset); } + void clear() { Offsets.clear(); EndOffset = 0; } + int size() { return Offsets.Size; } + const char* get_line_begin(const char* base, int n) { return base + Offsets[n]; } + const char* get_line_end(const char* base, int n) { return base + (n + 1 < Offsets.Size ? (Offsets[n + 1] - 1) : EndOffset); } void append(const char* base, int old_size, int new_size); }; From 1e52e7b90c07d347cd41822aeb0b2a96f51027e9 Mon Sep 17 00:00:00 2001 From: ocornut Date: Thu, 11 Sep 2025 19:05:26 +0200 Subject: [PATCH 09/11] InputText: Added a line index. Refactored cursor and selection rendering, now simpler, easier to reason about, and faster. (#3237, #952, #1062, #7363) --- docs/CHANGELOG.txt | 2 + imgui.cpp | 11 ++ imgui_internal.h | 4 +- imgui_widgets.cpp | 348 +++++++++++++++++++++------------------------ 4 files changed, 180 insertions(+), 185 deletions(-) diff --git a/docs/CHANGELOG.txt b/docs/CHANGELOG.txt index c195ed1eb..b589eeb2f 100644 --- a/docs/CHANGELOG.txt +++ b/docs/CHANGELOG.txt @@ -66,6 +66,8 @@ 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: refactored internals to simplify and optimizing rendering of selection. + Very large selection (e.g. 1 MB) now take less overhead. - 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. diff --git a/imgui.cpp b/imgui.cpp index b66474b0a..5c0b11014 100644 --- a/imgui.cpp +++ b/imgui.cpp @@ -3074,6 +3074,7 @@ void ImGuiTextBuffer::appendfv(const char* fmt, va_list args) va_end(args_copy); } +IM_MSVC_RUNTIME_CHECKS_OFF void ImGuiTextIndex::append(const char* base, int old_size, int new_size) { IM_ASSERT(old_size >= 0 && new_size >= old_size && new_size >= EndOffset); @@ -3087,6 +3088,7 @@ void ImGuiTextIndex::append(const char* base, int old_size, int new_size) Offsets.push_back((int)(intptr_t)(p - base)); EndOffset = ImMax(EndOffset, new_size); } +IM_MSVC_RUNTIME_CHECKS_RESTORE //----------------------------------------------------------------------------- // [SECTION] ImGuiListClipper @@ -3414,6 +3416,13 @@ bool ImGuiListClipper::Step() return ret; } +// Generic helper, equivalent to old ImGui::CalcListClipping() but statelesss +void ImGui::CalcClipRectVisibleItemsY(const ImRect& clip_rect, const ImVec2& pos, float items_height, int* out_visible_start, int* out_visible_end) +{ + *out_visible_start = ImMax((int)((clip_rect.Min.y - pos.y) / items_height), 0); + *out_visible_end = ImMax((int)ImCeil((clip_rect.Max.y - pos.y) / items_height), *out_visible_start); +} + //----------------------------------------------------------------------------- // [SECTION] STYLING //----------------------------------------------------------------------------- @@ -4374,6 +4383,7 @@ void ImGui::Shutdown() g.ClipboardHandlerData.clear(); g.MenusIdSubmittedThisFrame.clear(); g.InputTextState.ClearFreeMemory(); + g.InputTextLineIndex.clear(); g.InputTextDeactivatedState.ClearFreeMemory(); g.SettingsWindows.clear(); @@ -4489,6 +4499,7 @@ void ImGui::GcCompactTransientMiscBuffers() ImGuiContext& g = *GImGui; g.ItemFlagsStack.clear(); g.GroupStack.clear(); + g.InputTextLineIndex.clear(); g.MultiSelectTempDataStacked = 0; g.MultiSelectTempData.clear_destruct(); TableGcCompactSettings(); diff --git a/imgui_internal.h b/imgui_internal.h index a206357b2..021a24d98 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -809,7 +809,7 @@ struct ImGuiTextIndex void clear() { Offsets.clear(); EndOffset = 0; } int size() { return Offsets.Size; } - const char* get_line_begin(const char* base, int n) { return base + Offsets[n]; } + const char* get_line_begin(const char* base, int n) { return base + (Offsets.Size != 0 ? Offsets[n] : 0); } const char* get_line_end(const char* base, int n) { return base + (n + 1 < Offsets.Size ? (Offsets[n + 1] - 1) : EndOffset); } void append(const char* base, int old_size, int new_size); }; @@ -2430,6 +2430,7 @@ struct ImGuiContext // Widget state ImGuiInputTextState InputTextState; + ImGuiTextIndex InputTextLineIndex; // Temporary storage ImGuiInputTextDeactivatedState InputTextDeactivatedState; ImFontBaked InputTextPasswordFontBackupBaked; ImFontFlags InputTextPasswordFontBackupFlags; @@ -3258,6 +3259,7 @@ namespace ImGui IMGUI_API float CalcWrapWidthForPos(const ImVec2& pos, float wrap_pos_x); IMGUI_API void PushMultiItemsWidths(int components, float width_full); IMGUI_API void ShrinkWidths(ImGuiShrinkWidthItem* items, int count, float width_excess, float width_min); + IMGUI_API void CalcClipRectVisibleItemsY(const ImRect& clip_rect, const ImVec2& pos, float items_height, int* out_visible_start, int* out_visible_end); // Parameter stacks (shared) IMGUI_API const ImGuiStyleVarInfo* GetStyleVarInfo(ImGuiStyleVar idx); diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index ebdee29b1..f506b111e 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -135,7 +135,6 @@ 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(ImGuiContext* ctx, const char* text_begin, const char** out_text_end, float wrap_width); static ImVec2 InputTextCalcTextSize(ImGuiContext* ctx, const char* text_begin, const char* text_end_display, const char* text_end, const char** out_remaining = NULL, ImVec2* out_offset = NULL, ImDrawTextFlags flags = 0); //------------------------------------------------------------------------- @@ -3938,46 +3937,6 @@ bool ImGui::InputTextWithHint(const char* label, const char* hint, char* buf, si return InputTextEx(label, hint, buf, (int)buf_size, ImVec2(0, 0), flags, callback, user_data); } -// This is only used in the path where the multiline widget is inactive. -static int InputTextCalcTextLenAndLineCount(ImGuiContext* ctx, const char* text_begin, const char** out_text_end, float wrap_width) -{ - int line_count = 0; - const char* s = text_begin; - if (wrap_width == 0.0f) - { - while (true) - { - const char* s_eol = strchr(s, '\n'); - line_count++; - if (s_eol == NULL) - { - s = s + ImStrlen(s); - break; - } - s = s_eol + 1; - } - } - else - { - // FIXME-WORDWRAP, FIXME-OPT: This is very suboptimal. - // We basically want both text_end and text_size, they could more optimally be emitted from a RenderText call that uses word-wrapping. - ImGuiContext& g = *ctx; - ImFont* font = g.Font; - const char* text_end = text_begin + strlen(text_begin); - while (s < text_end) - { - s = ImFontCalcWordWrapPositionEx(font, g.FontSize, s, text_end, wrap_width, ImDrawTextFlags_WrapKeepBlanks); - s = (*s == '\n') ? s + 1 : s; - line_count++; - } - if (text_end > text_begin && text_end[-1] == '\n') - line_count++; - IM_ASSERT(s == text_end); - } - *out_text_end = s; - return line_count; -} - static ImVec2 InputTextCalcTextSize(ImGuiContext* ctx, const char* text_begin, const char* text_end_display, const char* text_end, const char** out_remaining, ImVec2* out_offset, ImDrawTextFlags flags) { ImGuiContext& g = *ctx; @@ -4555,6 +4514,83 @@ void ImGui::InputTextDeactivateHook(ImGuiID id) } } +static int* ImLowerBound(int* in_begin, int* in_end, int v) +{ + int* in_p = in_begin; + for (size_t count = (size_t)(in_end - in_p); count > 0; ) + { + size_t count2 = count >> 1; + int* mid = in_p + count2; + if (*mid < v) + { + in_p = ++mid; + count -= count2 + 1; + } + else + { + count = count2; + } + } + return in_p; +} + +// FIXME-WORDWRAP: Bundle some of this into ImGuiTextIndex and/or extract as a different tool? +// 'max_output_buffer_size' happens to be a meaningful optimization to avoid writing the full line_index when not necessarily needed (e.g. very large buffer, scrolled up, inactive) +static int InputTextLineIndexBuild(ImGuiInputTextFlags flags, ImGuiTextIndex* line_index, const char* buf, const char* buf_end, float wrap_width, int max_output_buffer_size) +{ + ImGuiContext& g = *GImGui; + int size = 0; + if (flags & ImGuiInputTextFlags_WordWrap) + { + for (const char* s = buf; s < buf_end; ) + { + if (size++ <= max_output_buffer_size) + line_index->Offsets.push_back((int)(s - buf)); + s = ImFontCalcWordWrapPositionEx(g.Font, g.FontSize, s, buf_end, wrap_width, ImDrawTextFlags_WrapKeepBlanks); + s = (*s == '\n') ? s + 1 : s; + } + } + else + { + for (const char* s = buf; s < buf_end; ) + { + if (size++ <= max_output_buffer_size) + line_index->Offsets.push_back((int)(s - buf)); + s = (const char*)ImMemchr(s, '\n', buf_end - s); + s = s ? s + 1 : buf_end; + } + } + if (size == 0) + { + line_index->Offsets.push_back(0); + size++; + } + if (buf_end > buf && buf_end[-1] == '\n' && size <= max_output_buffer_size) + { + line_index->Offsets.push_back((int)(buf_end - buf)); + size++; + } + return size; +} + +static ImVec2 InputTextLineIndexGetPosOffset(ImGuiContext& g, ImGuiInputTextState* state, ImGuiTextIndex* line_index, const char* buf, const char* buf_end, int cursor_n) +{ + const char* cursor_ptr = buf + cursor_n; + int* it_begin = line_index->Offsets.begin(); + int* it_end = line_index->Offsets.end(); + const int* it = ImLowerBound(it_begin, it_end, cursor_n); + if (it > it_begin) + if (it == it_end || *it != cursor_n || (cursor_ptr[-1] != '\n' && cursor_ptr[-1] != 0 && state != NULL && state->LastMoveDirectionLR == ImGuiDir_Right)) + it--; + + const int line_no = (it == it_begin) ? 0 : line_index->Offsets.index_from_ptr(it); + const char* line_start = line_index->get_line_begin(buf, line_no); + ImVec2 offset; + offset.x = InputTextCalcTextSize(&g, line_start, cursor_ptr, buf_end, NULL, NULL, ImDrawTextFlags_WrapKeepBlanks).x; + offset.y = (line_no + 1) * g.FontSize; + return offset; +} + // Edit a string of text // - buf_size account for the zero-terminator, so a buf_size of 6 can hold "Hello" but not "Hello!". // This is so we can easily call InputText() on static arrays using ARRAYSIZE() and to match @@ -5327,15 +5363,40 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ buf_display = hint; buf_display_end = hint + ImStrlen(hint); } + else + { + if (render_cursor || render_selection || g.ActiveId == id) + buf_display_end = buf_display + state->TextLen; //-V595 + else + buf_display_end = buf_display + ImStrlen(buf_display); // FIXME-OPT: For multi-line path this would optimally be folded into the InputTextLineIndex build below. + } + + // Calculate visibility + int line_visible_n0 = 0, line_visible_n1 = 1; + if (is_multiline) + CalcClipRectVisibleItemsY(clip_rect, draw_pos, g.FontSize, &line_visible_n0, &line_visible_n1); + + // Build line index for easy data access (makes code below simpler and faster) + ImGuiTextIndex* line_index = &g.InputTextLineIndex; + line_index->Offsets.resize(0); + line_index->EndOffset = (int)(buf_display_end - buf_display); + int line_count = 1; + if (is_multiline) + line_count = InputTextLineIndexBuild(flags, line_index, buf_display, buf_display_end, wrap_width, (render_cursor && state && state->CursorFollow) ? INT_MAX : line_visible_n1 + 1); + line_visible_n1 = ImMin(line_visible_n1, line_count); + + // Store text height (we don't need width) + text_size = ImVec2(inner_size.x, line_count * g.FontSize); + //GetForegroundDrawList()->AddRect(draw_pos + ImVec2(0, line_visible_n0 * g.FontSize), draw_pos + ImVec2(frame_size.x, line_visible_n1 * g.FontSize), IM_COL32(255, 0, 0, 255)); + + // Calculate blinking cursor position + const ImVec2 cursor_offset = render_cursor && state ? InputTextLineIndexGetPosOffset(g, state, line_index, buf_display, buf_display_end, state->Stb->cursor) : ImVec2(0.0f, 0.0f); + ImVec2 draw_scroll; // Render text. We currently only render selection when the widget is active or while scrolling. - // FIXME: This is one of the messiest piece of the whole codebase. + const ImU32 text_col = GetColorU32(is_displaying_hint ? ImGuiCol_TextDisabled : ImGuiCol_Text); if (render_cursor || render_selection) { - IM_ASSERT(state != NULL); - if (!is_displaying_hint) - buf_display_end = buf_display + state->TextLen; - // Render text (with cursor and selection) // This is going to be messy. We need to: // - Display the text (this alone can be more easily clipped) @@ -5343,85 +5404,8 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ // - Measure text height (for scrollbar) // We are attempting to do most of that in **one main pass** to minimize the computation cost (non-negligible for large amount of text) + 2nd pass for selection rendering (we could merge them by an extra refactoring effort) // FIXME: This should occur on buf_display but we'd need to maintain cursor/select_start/select_end for UTF-8. - const char* text_begin = buf_display; - const char* text_end = text_begin + state->TextLen; - ImVec2 cursor_offset; - float select_start_offset_y = 0.0f; // Offset of beginning of non-wrapped line for selection. - - { - // Find lines numbers straddling cursor and selection min position - int cursor_line_no = render_cursor ? -1 : -1000; - int selmin_line_no = render_selection ? -1 : -1000; - const char* cursor_ptr = render_cursor ? text_begin + state->Stb->cursor : NULL; - const char* selmin_ptr = render_selection ? text_begin + ImMin(state->Stb->select_start, state->Stb->select_end) : NULL; - const char* cursor_line_start = NULL; - const char* selmin_line_start = NULL; - bool cursor_straddle_word_wrap = false; - - // 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) - { - if (!is_wordwrap) - { - 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++; - } - } - else - { - bool is_start_of_non_wrapped_line = true; - int line_count_for_non_wrapped_line = 1; - for (const char* s = text_begin; s < text_end; s = (*s == '\n') ? s + 1 : s) - { - const char* s_eol = ImFontCalcWordWrapPositionEx(g.Font, g.FontSize, s, text_end, wrap_width, ImDrawTextFlags_WrapKeepBlanks); - const char* s_prev = s; - s = s_eol; - if (cursor_line_no == -1 && s >= cursor_ptr) { cursor_line_start = s_prev; cursor_line_no = line_count; } - if (selmin_line_no == -1 && s >= selmin_ptr) { selmin_line_start = s_prev; selmin_line_no = line_count_for_non_wrapped_line; } - if (s == cursor_ptr && *cursor_ptr != '\n' && *cursor_ptr != 0) - cursor_straddle_word_wrap = true; - is_start_of_non_wrapped_line = (*s == '\n'); - line_count++; - if (is_start_of_non_wrapped_line) - line_count_for_non_wrapped_line = line_count; - } - } - //IMGUI_DEBUG_LOG("%d\n", selmin_line_no); - } - if (cursor_line_no == -1) - cursor_line_no = line_count; - if (cursor_line_start == NULL) - cursor_line_start = ImStrbol(cursor_ptr, text_begin); - if (selmin_line_no == -1) - selmin_line_no = line_count; - if (selmin_line_start == NULL) - selmin_line_start = ImStrbol(cursor_ptr, text_begin); - - // Calculate 2d position by finding the beginning of the line and measuring distance - if (render_cursor) - { - cursor_offset.x = InputTextCalcTextSize(&g, cursor_line_start, cursor_ptr, text_end, NULL, NULL, ImDrawTextFlags_WrapKeepBlanks).x; - cursor_offset.y = cursor_line_no * g.FontSize; - if (is_multiline && cursor_straddle_word_wrap && state->LastMoveDirectionLR == ImGuiDir_Left) - cursor_offset = ImVec2(0.0f, cursor_offset.y + g.FontSize); - } - if (selmin_line_no >= 0) - select_start_offset_y = selmin_line_no * g.FontSize; - - // Store text height (note that we haven't calculated text width at all, see GitHub issues #383, #1224) - if (is_multiline) - { - if (is_wordwrap && text_end > text_begin && text_end[-1] != '\n') - line_count--; - text_size = ImVec2(inner_size.x, line_count * g.FontSize); - } - state->LineCount = line_count; - } + IM_ASSERT(state != NULL); + state->LineCount = line_count; // Scroll float new_scroll_y = scroll_y; @@ -5447,7 +5431,7 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ { // Test if cursor is vertically visible if (cursor_offset.y - g.FontSize < scroll_y) - new_scroll_y = ImMax(0.0f, cursor_offset.y - g.FontSize); + new_scroll_y = ImMax(0.0f, cursor_offset.y - g.FontSize); else if (cursor_offset.y - (inner_size.y - style.FramePadding.y * 2.0f) >= scroll_y) new_scroll_y = cursor_offset.y - inner_size.y + style.FramePadding.y * 2.0f; } @@ -5466,57 +5450,60 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ scroll_y = ImClamp(new_scroll_y, 0.0f, scroll_max_y); draw_pos.y += (draw_window->Scroll.y - scroll_y); // Manipulate cursor pos immediately avoid a frame of lag draw_window->Scroll.y = scroll_y; + CalcClipRectVisibleItemsY(clip_rect, draw_pos, g.FontSize, &line_visible_n0, &line_visible_n1); + line_visible_n1 = ImMin(line_visible_n1, line_count); } // Draw selection - const ImVec2 draw_scroll = ImVec2(state->Scroll.x, 0.0f); + draw_scroll.x = state->Scroll.x; if (render_selection) { - const char* text_selected_begin = text_begin + ImMin(state->Stb->select_start, state->Stb->select_end); - const char* text_selected_end = text_begin + ImMax(state->Stb->select_start, state->Stb->select_end); + const ImU32 bg_color = GetColorU32(ImGuiCol_TextSelectedBg, render_cursor ? 1.0f : 0.6f); // FIXME: current code flow mandate that render_cursor is always true here, we are leaving the transparent one for tests. + const float bg_offy_up = is_multiline ? 0.0f : -1.0f; // FIXME: those offsets should be part of the style? they don't play so well with multi-line selection. + const float bg_offy_dn = is_multiline ? 0.0f : 2.0f; + const float bg_eol_width = IM_TRUNC(g.FontBaked->GetCharAdvance((ImWchar)' ') * 0.50f); // So we can see selected empty lines - ImU32 bg_color = GetColorU32(ImGuiCol_TextSelectedBg, render_cursor ? 1.0f : 0.6f); // FIXME: current code flow mandate that render_cursor is always true here, we are leaving the transparent one for tests. - float bg_offy_up = is_multiline ? 0.0f : -1.0f; // FIXME: those offsets should be part of the style? they don't play so well with multi-line selection. - float bg_offy_dn = is_multiline ? 0.0f : 2.0f; - float bg_min_width = IM_TRUNC(g.FontBaked->GetCharAdvance((ImWchar)' ') * 0.50f); // So we can see selected empty lines - ImVec2 rect_pos = draw_pos - draw_scroll; - rect_pos.y += select_start_offset_y; - for (const char* p = ImStrbol(text_selected_begin, text_begin); p < text_selected_end; rect_pos.y += g.FontSize) + const char* text_selected_begin = buf_display + ImMin(state->Stb->select_start, state->Stb->select_end); + const char* text_selected_end = buf_display + ImMax(state->Stb->select_start, state->Stb->select_end); + for (int line_n = line_visible_n0; line_n < line_visible_n1; line_n++) { - if (rect_pos.y > clip_rect.Max.y + g.FontSize) - break; - const char* p_eol = is_wordwrap ? ImFontCalcWordWrapPositionEx(g.Font, g.FontSize, p, text_end, wrap_width, ImDrawTextFlags_WrapKeepBlanks) : (const char*)ImMemchr((void*)p, '\n', text_selected_end - p); - if (p_eol == NULL) - p_eol = text_selected_end; - const char* p_next = is_wordwrap ? (*p_eol == '\n' ? p_eol + 1 : p_eol) : (p_eol + 1); - if (rect_pos.y >= clip_rect.Min.y) - { - const char* line_selected_begin = (text_selected_begin > p) ? text_selected_begin : p; - const char* line_selected_end = (text_selected_end < p_eol) ? text_selected_end : p_eol; - if ((*p_eol == '\n' && text_selected_begin <= p_eol) || (text_selected_begin < p_eol)) - { - ImVec2 rect_offset = CalcTextSize(p, line_selected_begin); - ImVec2 rect_size = CalcTextSize(line_selected_begin, line_selected_end); - rect_size.x = ImMax(rect_size.x, bg_min_width); // So we can see selected empty lines - ImRect rect(rect_pos + ImVec2(rect_offset.x, bg_offy_up - g.FontSize), rect_pos + ImVec2(rect_offset.x + rect_size.x, bg_offy_dn)); - rect.ClipWith(clip_rect); - if (rect.Overlaps(clip_rect)) - draw_window->DrawList->AddRectFilled(rect.Min, rect.Max, bg_color); - } - } - p = p_next; + const char* p = line_index->get_line_begin(buf_display, line_n); + const char* p_eol = line_index->get_line_end(buf_display, line_n); + const bool p_eol_is_wrap = (p_eol < buf_display_end && *p_eol != '\n'); + if (p_eol_is_wrap) + p_eol++; + const char* line_selected_begin = (text_selected_begin > p) ? text_selected_begin : p; + const char* line_selected_end = (text_selected_end < p_eol) ? text_selected_end : p_eol; + + float rect_width = 0.0f; + if (line_selected_begin < line_selected_end) + rect_width += CalcTextSize(line_selected_begin, line_selected_end).x; + if (text_selected_begin <= p_eol && text_selected_end > p_eol && !p_eol_is_wrap) + rect_width += bg_eol_width; // So we can see selected empty lines + if (rect_width == 0.0f) + continue; + + ImRect rect; + rect.Min.x = draw_pos.x - draw_scroll.x + CalcTextSize(p, line_selected_begin).x; + rect.Min.y = draw_pos.y - draw_scroll.y + line_n * g.FontSize; + rect.Max.x = rect.Min.x + rect_width; + rect.Max.y = rect.Min.y + bg_offy_dn + g.FontSize; + rect.Min.y -= bg_offy_up; + rect.ClipWith(clip_rect); + draw_window->DrawList->AddRectFilled(rect.Min, rect.Max, bg_color); } } + // Render text // We test for 'buf_display_max_length' as a way to avoid some pathological cases (e.g. single-line 1 MB string) which would make ImDrawList crash. // FIXME-OPT: Multiline could submit a smaller amount of contents to AddText() since we already iterated through it. - if (is_multiline || (buf_display_end - buf_display) < buf_display_max_length) - { - ImU32 col = GetColorU32(is_displaying_hint ? ImGuiCol_TextDisabled : ImGuiCol_Text); - if (col & IM_COL32_A_MASK) - g.Font->RenderText(draw_window->DrawList, g.FontSize, draw_pos - draw_scroll, col, clip_rect.AsVec4(), buf_display, buf_display_end, wrap_width, ImDrawTextFlags_WrapKeepBlanks); - //draw_window->DrawList->AddText(g.Font, g.FontSize, draw_pos - draw_scroll, col, buf_display, buf_display_end, wrap_width, is_multiline ? NULL : &clip_rect.AsVec4()); - } + if ((is_multiline || (buf_display_end - buf_display) < buf_display_max_length) && (text_col & IM_COL32_A_MASK) && (line_visible_n0 < line_visible_n1)) + g.Font->RenderText(draw_window->DrawList, g.FontSize, + draw_pos - draw_scroll + ImVec2(0.0f, line_visible_n0 * g.FontSize), + text_col, clip_rect.AsVec4(), + line_index->get_line_begin(buf_display, line_visible_n0), + line_index->get_line_end(buf_display, line_visible_n1 - 1), + wrap_width, ImDrawTextFlags_WrapKeepBlanks); // Draw blinking cursor if (render_cursor) @@ -5544,26 +5531,19 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ } else { + // Find render position for right alignment (single-line only) + if (flags & ImGuiInputTextFlags_ElideLeft) + draw_pos.x = ImMin(draw_pos.x, frame_bb.Max.x - CalcTextSize(buf_display, NULL).x - style.FramePadding.x); + // Render text only (no selection, no cursor) - if (is_multiline) - text_size = ImVec2(inner_size.x, InputTextCalcTextLenAndLineCount(&g, buf_display, &buf_display_end, wrap_width) * 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) - buf_display_end = buf_display + ImStrlen(buf_display); - - if (is_multiline || (buf_display_end - buf_display) < buf_display_max_length) - { - // Find render position for right alignment - if (flags & ImGuiInputTextFlags_ElideLeft) - draw_pos.x = ImMin(draw_pos.x, frame_bb.Max.x - CalcTextSize(buf_display, NULL).x - style.FramePadding.x); - - 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); - if (col & IM_COL32_A_MASK) - g.Font->RenderText(draw_window->DrawList, g.FontSize, draw_pos - draw_scroll, col, clip_rect.AsVec4(), buf_display, buf_display_end, wrap_width, ImDrawTextFlags_WrapKeepBlanks); - //draw_window->DrawList->AddText(g.Font, g.FontSize, draw_pos - draw_scroll, col, buf_display, buf_display_end, wrap_width, is_multiline ? NULL : &clip_rect.AsVec4()); - } + //draw_scroll.x = state->Scroll.x; // Preserve scroll when inactive? + if ((is_multiline || (buf_display_end - buf_display) < buf_display_max_length) && (text_col & IM_COL32_A_MASK) && (line_visible_n0 < line_visible_n1)) + g.Font->RenderText(draw_window->DrawList, g.FontSize, + draw_pos - draw_scroll + ImVec2(0.0f, line_visible_n0 * g.FontSize), + text_col, clip_rect.AsVec4(), + line_index->get_line_begin(buf_display, line_visible_n0), + line_index->get_line_end(buf_display, line_visible_n1 - 1), + wrap_width, ImDrawTextFlags_WrapKeepBlanks); } if (is_password && !is_displaying_hint) From ae832ce532046a1953023af34e9041b77f84369c Mon Sep 17 00:00:00 2001 From: ocornut Date: Thu, 11 Sep 2025 20:19:17 +0200 Subject: [PATCH 10/11] InputText: moved blocks so same text rendering code is now used for active and inactive states. (ignore whitespace to visualize this change easily) --- imgui_widgets.cpp | 85 ++++++++++++++++++++--------------------------- 1 file changed, 36 insertions(+), 49 deletions(-) diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index f506b111e..b5980761a 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -5493,57 +5493,44 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ draw_window->DrawList->AddRectFilled(rect.Min, rect.Max, bg_color); } } - - // Render text - // We test for 'buf_display_max_length' as a way to avoid some pathological cases (e.g. single-line 1 MB string) which would make ImDrawList crash. - // FIXME-OPT: Multiline could submit a smaller amount of contents to AddText() since we already iterated through it. - if ((is_multiline || (buf_display_end - buf_display) < buf_display_max_length) && (text_col & IM_COL32_A_MASK) && (line_visible_n0 < line_visible_n1)) - g.Font->RenderText(draw_window->DrawList, g.FontSize, - draw_pos - draw_scroll + ImVec2(0.0f, line_visible_n0 * g.FontSize), - text_col, clip_rect.AsVec4(), - line_index->get_line_begin(buf_display, line_visible_n0), - line_index->get_line_end(buf_display, line_visible_n1 - 1), - wrap_width, ImDrawTextFlags_WrapKeepBlanks); - - // Draw blinking cursor - if (render_cursor) - { - state->CursorAnim += io.DeltaTime; - bool cursor_is_visible = (!g.IO.ConfigInputTextCursorBlink) || (state->CursorAnim <= 0.0f) || ImFmod(state->CursorAnim, 1.20f) <= 0.80f; - ImVec2 cursor_screen_pos = ImTrunc(draw_pos + cursor_offset - draw_scroll); - ImRect cursor_screen_rect(cursor_screen_pos.x, cursor_screen_pos.y - g.FontSize + 0.5f, cursor_screen_pos.x + 1.0f, cursor_screen_pos.y - 1.5f); - if (cursor_is_visible && cursor_screen_rect.Overlaps(clip_rect)) - draw_window->DrawList->AddLine(cursor_screen_rect.Min, cursor_screen_rect.GetBL(), GetColorU32(ImGuiCol_InputTextCursor), 1.0f); // FIXME-DPI: Cursor thickness (#7031) - - // Notify OS of text input position for advanced IME (-1 x offset so that Windows IME can cover our cursor. Bit of an extra nicety.) - // This is required for some backends (SDL3) to start emitting character/text inputs. - // As per #6341, make sure we don't set that on the deactivating frame. - if (!is_readonly && g.ActiveId == id) - { - ImGuiPlatformImeData* ime_data = &g.PlatformImeData; // (this is a public struct, passed to io.Platform_SetImeDataFn() handler) - ime_data->WantVisible = true; - ime_data->WantTextInput = true; - ime_data->InputPos = ImVec2(cursor_screen_pos.x - 1.0f, cursor_screen_pos.y - g.FontSize); - ime_data->InputLineHeight = g.FontSize; - ime_data->ViewportId = window->Viewport->ID; - } - } } - else - { - // Find render position for right alignment (single-line only) - if (flags & ImGuiInputTextFlags_ElideLeft) - draw_pos.x = ImMin(draw_pos.x, frame_bb.Max.x - CalcTextSize(buf_display, NULL).x - style.FramePadding.x); - // Render text only (no selection, no cursor) - //draw_scroll.x = state->Scroll.x; // Preserve scroll when inactive? - if ((is_multiline || (buf_display_end - buf_display) < buf_display_max_length) && (text_col & IM_COL32_A_MASK) && (line_visible_n0 < line_visible_n1)) - g.Font->RenderText(draw_window->DrawList, g.FontSize, - draw_pos - draw_scroll + ImVec2(0.0f, line_visible_n0 * g.FontSize), - text_col, clip_rect.AsVec4(), - line_index->get_line_begin(buf_display, line_visible_n0), - line_index->get_line_end(buf_display, line_visible_n1 - 1), - wrap_width, ImDrawTextFlags_WrapKeepBlanks); + // Find render position for right alignment (single-line only) + if (g.ActiveId != id && flags & ImGuiInputTextFlags_ElideLeft) + draw_pos.x = ImMin(draw_pos.x, frame_bb.Max.x - CalcTextSize(buf_display, NULL).x - style.FramePadding.x); + //draw_scroll.x = state->Scroll.x; // Preserve scroll when inactive? + + // Render text + if ((is_multiline || (buf_display_end - buf_display) < buf_display_max_length) && (text_col & IM_COL32_A_MASK) && (line_visible_n0 < line_visible_n1)) + g.Font->RenderText(draw_window->DrawList, g.FontSize, + draw_pos - draw_scroll + ImVec2(0.0f, line_visible_n0 * g.FontSize), + text_col, clip_rect.AsVec4(), + line_index->get_line_begin(buf_display, line_visible_n0), + line_index->get_line_end(buf_display, line_visible_n1 - 1), + wrap_width, ImDrawTextFlags_WrapKeepBlanks); + + // Render blinking cursor + if (render_cursor) + { + state->CursorAnim += io.DeltaTime; + bool cursor_is_visible = (!g.IO.ConfigInputTextCursorBlink) || (state->CursorAnim <= 0.0f) || ImFmod(state->CursorAnim, 1.20f) <= 0.80f; + ImVec2 cursor_screen_pos = ImTrunc(draw_pos + cursor_offset - draw_scroll); + ImRect cursor_screen_rect(cursor_screen_pos.x, cursor_screen_pos.y - g.FontSize + 0.5f, cursor_screen_pos.x + 1.0f, cursor_screen_pos.y - 1.5f); + if (cursor_is_visible && cursor_screen_rect.Overlaps(clip_rect)) + draw_window->DrawList->AddLine(cursor_screen_rect.Min, cursor_screen_rect.GetBL(), GetColorU32(ImGuiCol_InputTextCursor), 1.0f); // FIXME-DPI: Cursor thickness (#7031) + + // Notify OS of text input position for advanced IME (-1 x offset so that Windows IME can cover our cursor. Bit of an extra nicety.) + // This is required for some backends (SDL3) to start emitting character/text inputs. + // As per #6341, make sure we don't set that on the deactivating frame. + if (!is_readonly && g.ActiveId == id) + { + ImGuiPlatformImeData* ime_data = &g.PlatformImeData; // (this is a public struct, passed to io.Platform_SetImeDataFn() handler) + ime_data->WantVisible = true; + ime_data->WantTextInput = true; + ime_data->InputPos = ImVec2(cursor_screen_pos.x - 1.0f, cursor_screen_pos.y - g.FontSize); + ime_data->InputLineHeight = g.FontSize; + ime_data->ViewportId = window->Viewport->ID; + } } if (is_password && !is_displaying_hint) From 8a944222469ab23559bc01a563f4e9b516e4e701 Mon Sep 17 00:00:00 2001 From: ocornut Date: Thu, 11 Sep 2025 21:18:52 +0200 Subject: [PATCH 11/11] InputText: optimize inactive path by avoiding an early ImStrlen(). --- imgui_widgets.cpp | 34 +++++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index b5980761a..c5359044e 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -4536,30 +4536,44 @@ static int* ImLowerBound(int* in_begin, int* in_end, int v) // FIXME-WORDWRAP: Bundle some of this into ImGuiTextIndex and/or extract as a different tool? // 'max_output_buffer_size' happens to be a meaningful optimization to avoid writing the full line_index when not necessarily needed (e.g. very large buffer, scrolled up, inactive) -static int InputTextLineIndexBuild(ImGuiInputTextFlags flags, ImGuiTextIndex* line_index, const char* buf, const char* buf_end, float wrap_width, int max_output_buffer_size) +static int InputTextLineIndexBuild(ImGuiInputTextFlags flags, ImGuiTextIndex* line_index, const char* buf, const char* buf_end, float wrap_width, int max_output_buffer_size, const char** out_buf_end) { ImGuiContext& g = *GImGui; int size = 0; + const char* s; if (flags & ImGuiInputTextFlags_WordWrap) { - for (const char* s = buf; s < buf_end; ) + for (s = buf; s < buf_end; s = (*s == '\n') ? s + 1 : s) { if (size++ <= max_output_buffer_size) line_index->Offsets.push_back((int)(s - buf)); s = ImFontCalcWordWrapPositionEx(g.Font, g.FontSize, s, buf_end, wrap_width, ImDrawTextFlags_WrapKeepBlanks); - s = (*s == '\n') ? s + 1 : s; } } - else + else if (buf_end != NULL) { - for (const char* s = buf; s < buf_end; ) + for (s = buf; s < buf_end; s = s ? s + 1 : buf_end) { if (size++ <= max_output_buffer_size) line_index->Offsets.push_back((int)(s - buf)); s = (const char*)ImMemchr(s, '\n', buf_end - s); - s = s ? s + 1 : buf_end; } } + else + { + const char* s_eol; + for (s = buf; ; s = s_eol + 1) + { + if (size++ <= max_output_buffer_size) + line_index->Offsets.push_back((int)(s - buf)); + if ((s_eol = strchr(s, '\n')) != NULL) + continue; + s += strlen(s); + break; + } + } + if (out_buf_end != NULL) + *out_buf_end = buf_end = s; if (size == 0) { line_index->Offsets.push_back(0); @@ -5367,8 +5381,10 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ { if (render_cursor || render_selection || g.ActiveId == id) buf_display_end = buf_display + state->TextLen; //-V595 + else if (is_multiline && !is_wordwrap) + buf_display_end = NULL; // Inactive multi-line: end of buffer will be output by InputTextLineIndexBuild() special strchr() path. else - buf_display_end = buf_display + ImStrlen(buf_display); // FIXME-OPT: For multi-line path this would optimally be folded into the InputTextLineIndex build below. + buf_display_end = buf_display + ImStrlen(buf_display); } // Calculate visibility @@ -5379,10 +5395,10 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ // Build line index for easy data access (makes code below simpler and faster) ImGuiTextIndex* line_index = &g.InputTextLineIndex; line_index->Offsets.resize(0); - line_index->EndOffset = (int)(buf_display_end - buf_display); int line_count = 1; if (is_multiline) - line_count = InputTextLineIndexBuild(flags, line_index, buf_display, buf_display_end, wrap_width, (render_cursor && state && state->CursorFollow) ? INT_MAX : line_visible_n1 + 1); + line_count = InputTextLineIndexBuild(flags, line_index, buf_display, buf_display_end, wrap_width, (render_cursor && state && state->CursorFollow) ? INT_MAX : line_visible_n1 + 1, buf_display_end ? NULL : &buf_display_end); + line_index->EndOffset = (int)(buf_display_end - buf_display); line_visible_n1 = ImMin(line_visible_n1, line_count); // Store text height (we don't need width)