From abd07f6d30736c00fba899d41043a78a5de0f765 Mon Sep 17 00:00:00 2001 From: alektron Date: Tue, 27 Aug 2024 01:37:50 +0200 Subject: [PATCH 01/12] InputText: added native support for UTF-8 text editing and god rid of the wchar buffer. (#7925) WIP (requires subsequent commits for fixes) --- imgui.cpp | 2 +- imgui_internal.h | 11 +- imgui_widgets.cpp | 278 +++++++++++++++++++++++++--------------------- imstb_textedit.h | 54 +++++---- 4 files changed, 189 insertions(+), 156 deletions(-) diff --git a/imgui.cpp b/imgui.cpp index e6cf5ca45..71b1b1011 100644 --- a/imgui.cpp +++ b/imgui.cpp @@ -1975,7 +1975,7 @@ const char* ImStreolRange(const char* str, const char* str_end) return p ? p : str_end; } -const ImWchar* ImStrbolW(const ImWchar* buf_mid_line, const ImWchar* buf_begin) // find beginning-of-line +const char* ImStrbol(const char* buf_mid_line, const char* buf_begin) // find beginning-of-line { while (buf_mid_line > buf_begin && buf_mid_line[-1] != '\n') buf_mid_line--; diff --git a/imgui_internal.h b/imgui_internal.h index 04dfba3b3..ec4c069eb 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -366,7 +366,7 @@ IMGUI_API const char* ImStristr(const char* haystack, const char* haystack_end IMGUI_API void ImStrTrimBlanks(char* str); // Remove leading and trailing blanks from a buffer. IMGUI_API const char* ImStrSkipBlank(const char* str); // Find first non-blank character. IMGUI_API int ImStrlenW(const ImWchar* str); // Computer string length (ImWchar string) -IMGUI_API const ImWchar*ImStrbolW(const ImWchar* buf_mid_line, const ImWchar* buf_begin); // Find beginning-of-line (ImWchar string) +IMGUI_API const char* ImStrbol(const char* buf_mid_line, const char* buf_begin); // Find beginning-of-line IM_MSVC_RUNTIME_CHECKS_OFF static inline char ImToUpper(char c) { return (c >= 'a' && c <= 'z') ? c &= ~32 : c; } static inline bool ImCharIsBlankA(char c) { return c == ' ' || c == '\t'; } @@ -1097,7 +1097,7 @@ struct IMGUI_API ImGuiInputTextDeactivatedState #undef IMSTB_TEXTEDIT_STRING #undef IMSTB_TEXTEDIT_CHARTYPE #define IMSTB_TEXTEDIT_STRING ImGuiInputTextState -#define IMSTB_TEXTEDIT_CHARTYPE ImWchar +#define IMSTB_TEXTEDIT_CHARTYPE char #define IMSTB_TEXTEDIT_GETWIDTH_NEWLINE (-1.0f) #define IMSTB_TEXTEDIT_UNDOSTATECOUNT 99 #define IMSTB_TEXTEDIT_UNDOCHARCOUNT 999 @@ -1111,8 +1111,7 @@ struct IMGUI_API ImGuiInputTextState ImGuiContext* Ctx; // parent UI context (needs to be set explicitly by parent). ImStbTexteditState* Stb; // State for stb_textedit.h ImGuiID ID; // widget id owning the text state - int CurLenW, CurLenA; // we need to maintain our buffer length in both UTF-8 and wchar format. UTF-8 length is valid even if TextA is not. - ImVector TextW; // edit buffer, we need to persist but can't guarantee the persistence of the user-provided buffer. so we copy into own buffer. + int CurLenA; // UTF-8 length of the string in TextA (in bytes) ImVector TextA; // temporary UTF8 buffer for callbacks and other operations. this is not updated in every code-path! size=capacity. ImVector InitialTextA; // value to revert to when pressing Escape = backup of end-user buffer at the time of focus (in UTF-8, unaltered) bool TextAIsValid; // temporary UTF8 buffer is not initially valid before we make the widget active (until then we pull the data from user argument) @@ -1129,8 +1128,8 @@ struct IMGUI_API ImGuiInputTextState ImGuiInputTextState(); ~ImGuiInputTextState(); - void ClearText() { CurLenW = CurLenA = 0; TextW[0] = 0; TextA[0] = 0; CursorClamp(); } - void ClearFreeMemory() { TextW.clear(); TextA.clear(); InitialTextA.clear(); } + void ClearText() { CurLenA = 0; TextA[0] = 0; CursorClamp(); } + void ClearFreeMemory() { TextA.clear(); InitialTextA.clear(); } void OnKeyPressed(int key); // Cannot be inline because we call in code in stb_textedit.h implementation // Cursor & Selection diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 33af0686e..5a8721c78 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -128,7 +128,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 ImVec2 InputTextCalcTextSizeW(ImGuiContext* ctx, const ImWchar* text_begin, const ImWchar* text_end, const ImWchar** remaining = NULL, ImVec2* out_offset = NULL, bool stop_on_new_line = false); +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); //------------------------------------------------------------------------- // [SECTION] Widgets: Text, etc. @@ -3836,7 +3836,7 @@ static int InputTextCalcTextLenAndLineCount(const char* text_begin, const char** return line_count; } -static ImVec2 InputTextCalcTextSizeW(ImGuiContext* ctx, const ImWchar* text_begin, const ImWchar* text_end, const ImWchar** remaining, ImVec2* out_offset, bool stop_on_new_line) +static ImVec2 InputTextCalcTextSize(ImGuiContext* ctx, const char* text_begin, const char* text_end, const char** remaining, ImVec2* out_offset, bool stop_on_new_line) { ImGuiContext& g = *ctx; ImFont* font = g.Font; @@ -3846,10 +3846,11 @@ static ImVec2 InputTextCalcTextSizeW(ImGuiContext* ctx, const ImWchar* text_begi ImVec2 text_size = ImVec2(0, 0); float line_width = 0.0f; - const ImWchar* s = text_begin; + const char* s = text_begin; while (s < text_end) { - unsigned int c = (unsigned int)(*s++); + unsigned int c; + s += ImTextCharFromUtf8(&c, s, text_end); if (c == '\n') { text_size.x = ImMax(text_size.x, line_width); @@ -3885,22 +3886,21 @@ static ImVec2 InputTextCalcTextSizeW(ImGuiContext* ctx, const ImWchar* text_begi namespace ImStb { -static int STB_TEXTEDIT_STRINGLEN(const ImGuiInputTextState* obj) { return obj->CurLenW; } -static ImWchar STB_TEXTEDIT_GETCHAR(const ImGuiInputTextState* obj, int idx) { IM_ASSERT(idx <= obj->CurLenW); return obj->TextW[idx]; } -static float STB_TEXTEDIT_GETWIDTH(ImGuiInputTextState* obj, int line_start_idx, int char_idx) { ImWchar c = obj->TextW[line_start_idx + char_idx]; if (c == '\n') return IMSTB_TEXTEDIT_GETWIDTH_NEWLINE; ImGuiContext& g = *obj->Ctx; return g.Font->GetCharAdvance(c) * g.FontScale; } +static int STB_TEXTEDIT_STRINGLEN(const ImGuiInputTextState* obj) { return obj->CurLenA; } +static char STB_TEXTEDIT_GETCHAR(const ImGuiInputTextState* obj, int idx) { IM_ASSERT(idx <= obj->CurLenA); return obj->TextA[idx]; } +static float STB_TEXTEDIT_GETWIDTH(ImGuiInputTextState* obj, int line_start_idx, int char_idx) { unsigned int c; ImTextCharFromUtf8(&c, obj->TextA.Data + line_start_idx + char_idx, obj->TextA.Data + obj->TextA.Size); if ((ImWchar)c == '\n') return IMSTB_TEXTEDIT_GETWIDTH_NEWLINE; ImGuiContext& g = *obj->Ctx; return g.Font->GetCharAdvance((ImWchar)c) * g.FontScale; } static int STB_TEXTEDIT_KEYTOTEXT(int key) { return key >= 0x200000 ? 0 : key; } -static ImWchar STB_TEXTEDIT_NEWLINE = '\n'; +static char STB_TEXTEDIT_NEWLINE = '\n'; static void STB_TEXTEDIT_LAYOUTROW(StbTexteditRow* r, ImGuiInputTextState* obj, int line_start_idx) { - const ImWchar* text = obj->TextW.Data; - const ImWchar* text_remaining = NULL; - const ImVec2 size = InputTextCalcTextSizeW(obj->Ctx, text + line_start_idx, text + obj->CurLenW, &text_remaining, NULL, true); + const char* text_remaining = NULL; + const ImVec2 size = InputTextCalcTextSize(obj->Ctx, obj->TextA.Data + line_start_idx, obj->TextA.Data + obj->CurLenA, &text_remaining, NULL, true); r->x0 = 0.0f; r->x1 = size.x; r->baseline_y_delta = size.y; r->ymin = 0.0f; r->ymax = size.y; - r->num_chars = (int)(text_remaining - (text + line_start_idx)); + r->num_chars = (int)(text_remaining - (obj->TextA.Data + line_start_idx)); } static bool ImCharIsSeparatorW(unsigned int c) @@ -3923,10 +3923,14 @@ static int is_word_boundary_from_right(ImGuiInputTextState* obj, int idx) if ((obj->Flags & ImGuiInputTextFlags_Password) || idx <= 0) return 0; - bool prev_white = ImCharIsBlankW(obj->TextW[idx - 1]); - bool prev_separ = ImCharIsSeparatorW(obj->TextW[idx - 1]); - bool curr_white = ImCharIsBlankW(obj->TextW[idx]); - bool curr_separ = ImCharIsSeparatorW(obj->TextW[idx]); + const char* prevPtr = ImTextFindPreviousUtf8Codepoint(obj->TextA.Data, obj->TextA.Data + idx); + unsigned int curr; ImTextCharFromUtf8(&curr, obj->TextA.Data + idx, obj->TextA.Data + obj->TextA.Size); + unsigned int prev; ImTextCharFromUtf8(&prev, prevPtr, obj->TextA.Data + obj->TextA.Size); + + bool prev_white = ImCharIsBlankW(prev); + bool prev_separ = ImCharIsSeparatorW(prev); + bool curr_white = ImCharIsBlankW(curr); + bool curr_separ = ImCharIsSeparatorW(curr); return ((prev_white || prev_separ) && !(curr_separ || curr_white)) || (curr_separ && !prev_separ); } static int is_word_boundary_from_left(ImGuiInputTextState* obj, int idx) @@ -3934,63 +3938,108 @@ static int is_word_boundary_from_left(ImGuiInputTextState* obj, int idx) if ((obj->Flags & ImGuiInputTextFlags_Password) || idx <= 0) return 0; - bool prev_white = ImCharIsBlankW(obj->TextW[idx]); - bool prev_separ = ImCharIsSeparatorW(obj->TextW[idx]); - bool curr_white = ImCharIsBlankW(obj->TextW[idx - 1]); - bool curr_separ = ImCharIsSeparatorW(obj->TextW[idx - 1]); + const char* prevPtr = ImTextFindPreviousUtf8Codepoint(obj->TextA.Data, obj->TextA.Data + idx); + unsigned int prev; ImTextCharFromUtf8(&prev, obj->TextA.Data + idx, obj->TextA.Data + obj->TextA.Size); + unsigned int curr; ImTextCharFromUtf8(&curr, prevPtr, obj->TextA.Data + obj->TextA.Size); + + bool prev_white = ImCharIsBlankW(prev); + bool prev_separ = ImCharIsSeparatorW(prev); + bool curr_white = ImCharIsBlankW(curr); + bool curr_separ = ImCharIsSeparatorW(curr); return ((prev_white) && !(curr_separ || curr_white)) || (curr_separ && !prev_separ); } static int STB_TEXTEDIT_MOVEWORDLEFT_IMPL(ImGuiInputTextState* obj, int idx) { idx--; while (idx >= 0 && !is_word_boundary_from_right(obj, idx)) idx--; return idx < 0 ? 0 : idx; } -static int STB_TEXTEDIT_MOVEWORDRIGHT_MAC(ImGuiInputTextState* obj, int idx) { idx++; int len = obj->CurLenW; while (idx < len && !is_word_boundary_from_left(obj, idx)) idx++; return idx > len ? len : idx; } -static int STB_TEXTEDIT_MOVEWORDRIGHT_WIN(ImGuiInputTextState* obj, int idx) { idx++; int len = obj->CurLenW; while (idx < len && !is_word_boundary_from_right(obj, idx)) idx++; return idx > len ? len : idx; } +static int STB_TEXTEDIT_MOVEWORDRIGHT_MAC(ImGuiInputTextState* obj, int idx) { idx++; int len = obj->CurLenA; while (idx < len && !is_word_boundary_from_left(obj, idx)) idx++; return idx > len ? len : idx; } +static int STB_TEXTEDIT_MOVEWORDRIGHT_WIN(ImGuiInputTextState* obj, int idx) { idx++; int len = obj->CurLenA; while (idx < len && !is_word_boundary_from_right(obj, idx)) idx++; return idx > len ? len : idx; } static int STB_TEXTEDIT_MOVEWORDRIGHT_IMPL(ImGuiInputTextState* obj, int idx) { ImGuiContext& g = *obj->Ctx; if (g.IO.ConfigMacOSXBehaviors) return STB_TEXTEDIT_MOVEWORDRIGHT_MAC(obj, idx); else return STB_TEXTEDIT_MOVEWORDRIGHT_WIN(obj, idx); } #define STB_TEXTEDIT_MOVEWORDLEFT STB_TEXTEDIT_MOVEWORDLEFT_IMPL // They need to be #define for stb_textedit.h #define STB_TEXTEDIT_MOVEWORDRIGHT STB_TEXTEDIT_MOVEWORDRIGHT_IMPL +#define IMSTB_TEXTEDIT_GETNEXTCHARINDEX IMSTB_TEXTEDIT_GETNEXTCHARINDEX_IMPL +#define IMSTB_TEXTEDIT_GETPREVIOUSCHARINDEX IMSTB_TEXTEDIT_GETPREVIOUSCHARINDEX_IMPL + +static int IMSTB_TEXTEDIT_GETNEXTCHARINDEX_IMPL(ImGuiInputTextState* obj, int idx) +{ + unsigned int c; + return idx + ImTextCharFromUtf8(&c, obj->TextA.Data + idx, obj->TextA.Data + obj->TextA.Size); +} + +static int CountLeadingHighBits(unsigned char b) +{ + for (int i = 0; i < (int)sizeof(b) * 8; i++) + { + bool set = (b >> (7 - i)) & 1; + if (!set) + return i; + } + + return sizeof(b) * 8; +} + +static int IMSTB_TEXTEDIT_GETPREVIOUSCHARINDEX_IMPL(ImGuiInputTextState* obj, int idx) +{ + //Backwards check/count for UTF-8 multi byte sequence + int num_seq_bytes = 0; + for (int i = idx - 1; i >= 0; i -= 1) + { + bool is_seq_byte = (obj->TextA.Data[i] & 0x80) == 0x80 && (obj->TextA.Data[i] & 0x40) == 0; + num_seq_bytes += is_seq_byte; + if (!is_seq_byte) + { + if (num_seq_bytes > 0) + { + char initial_byte = obj->TextA.Data[i]; + char num_leading_bits = (char)CountLeadingHighBits(initial_byte); + bool is_multi_byte_seq = num_leading_bits == num_seq_bytes + 1; + if (is_multi_byte_seq) + { + return idx - (num_seq_bytes + 1); + } + } + break; + } + } + return idx - 1; +} static void STB_TEXTEDIT_DELETECHARS(ImGuiInputTextState* obj, int pos, int n) { - ImWchar* dst = obj->TextW.Data + pos; + char* dst = obj->TextA.Data + pos; - // We maintain our buffer length in both UTF-8 and wchar formats obj->Edited = true; - obj->CurLenA -= ImTextCountUtf8BytesFromStr(dst, dst + n); - obj->CurLenW -= n; + obj->CurLenA -= n; // Offset remaining text (FIXME-OPT: Use memmove) - const ImWchar* src = obj->TextW.Data + pos + n; - while (ImWchar c = *src++) + const char* src = obj->TextA.Data + pos + n; + while (char c = *src++) *dst++ = c; *dst = '\0'; } -static bool STB_TEXTEDIT_INSERTCHARS(ImGuiInputTextState* obj, int pos, const ImWchar* new_text, int new_text_len) +static bool STB_TEXTEDIT_INSERTCHARS(ImGuiInputTextState* obj, int pos, const char* new_text, int new_text_len) { const bool is_resizable = (obj->Flags & ImGuiInputTextFlags_CallbackResize) != 0; - const int text_len = obj->CurLenW; + const int text_len = obj->CurLenA; IM_ASSERT(pos <= text_len); - const int new_text_len_utf8 = ImTextCountUtf8BytesFromStr(new_text, new_text + new_text_len); - if (!is_resizable && (new_text_len_utf8 + obj->CurLenA + 1 > obj->BufCapacityA)) + if (!is_resizable && (new_text_len + obj->CurLenA + 1 > obj->BufCapacityA)) return false; // Grow internal buffer if needed - if (new_text_len + text_len + 1 > obj->TextW.Size) + if (new_text_len + text_len + 1 > obj->TextA.Size) { if (!is_resizable) return false; - IM_ASSERT(text_len < obj->TextW.Size); - obj->TextW.resize(text_len + ImClamp(new_text_len * 4, 32, ImMax(256, new_text_len)) + 1); + obj->TextA.resize(text_len + ImClamp(new_text_len, 32, ImMax(256, new_text_len)) + 1); } - ImWchar* text = obj->TextW.Data; + char* text = obj->TextA.Data; if (pos != text_len) - memmove(text + pos + new_text_len, text + pos, (size_t)(text_len - pos) * sizeof(ImWchar)); - memcpy(text + pos, new_text, (size_t)new_text_len * sizeof(ImWchar)); + memmove(text + pos + new_text_len, text + pos, (size_t)(text_len - pos)); + memcpy(text + pos, new_text, (size_t)new_text_len); obj->Edited = true; - obj->CurLenW += new_text_len; - obj->CurLenA += new_text_len_utf8; - obj->TextW[obj->CurLenW] = '\0'; + obj->CurLenA += new_text_len; + obj->TextA[obj->CurLenA] = '\0'; return true; } @@ -4022,8 +4071,8 @@ static bool STB_TEXTEDIT_INSERTCHARS(ImGuiInputTextState* obj, int pos, const Im // the stb_textedit_paste() function creates two separate records, so we perform it manually. (FIXME: Report to nothings/stb?) static void stb_textedit_replace(ImGuiInputTextState* str, STB_TexteditState* state, const IMSTB_TEXTEDIT_CHARTYPE* text, int text_len) { - stb_text_makeundo_replace(str, state, 0, str->CurLenW, text_len); - ImStb::STB_TEXTEDIT_DELETECHARS(str, 0, str->CurLenW); + stb_text_makeundo_replace(str, state, 0, str->CurLenA, text_len); + ImStb::STB_TEXTEDIT_DELETECHARS(str, 0, str->CurLenA); state->cursor = state->select_start = state->select_end = 0; if (text_len <= 0) return; @@ -4052,20 +4101,25 @@ ImGuiInputTextState::~ImGuiInputTextState() void ImGuiInputTextState::OnKeyPressed(int key) { - stb_textedit_key(this, Stb, key); + //We prematurely convert the key to a UTF8 byte sequence, even for keys where that doesn't even make sense (e.g. arrow keys). + //Not optimal but stb_textedit_key will only use the UTF8 values for valid character keys anyways. + //The changes we had to make to stb_textedit_key make it very much UTF8 specific which is not too great. + char utf8[5]; + ImTextCharToUtf8(utf8, key); + stb_textedit_key(this, Stb, key, utf8, (int)strlen(utf8)); CursorFollow = true; CursorAnimReset(); } // Those functions are not inlined in imgui_internal.h, allowing us to hide ImStbTexteditState from that header. void ImGuiInputTextState::CursorAnimReset() { CursorAnim = -0.30f; } // After a user-input the cursor stays on for a while without blinking -void ImGuiInputTextState::CursorClamp() { Stb->cursor = ImMin(Stb->cursor, CurLenW); Stb->select_start = ImMin(Stb->select_start, CurLenW); Stb->select_end = ImMin(Stb->select_end, CurLenW); } +void ImGuiInputTextState::CursorClamp() { Stb->cursor = ImMin(Stb->cursor, CurLenA); Stb->select_start = ImMin(Stb->select_start, CurLenA); Stb->select_end = ImMin(Stb->select_end, CurLenA); } bool ImGuiInputTextState::HasSelection() const { return Stb->select_start != Stb->select_end; } void ImGuiInputTextState::ClearSelection() { Stb->select_start = Stb->select_end = Stb->cursor; } int ImGuiInputTextState::GetCursorPos() const { return Stb->cursor; } int ImGuiInputTextState::GetSelectionStart() const { return Stb->select_start; } int ImGuiInputTextState::GetSelectionEnd() const { return Stb->select_end; } -void ImGuiInputTextState::SelectAll() { Stb->select_start = 0; Stb->cursor = Stb->select_end = CurLenW; Stb->has_preferred_x = 0; } +void ImGuiInputTextState::SelectAll() { Stb->select_start = 0; Stb->cursor = Stb->select_end = CurLenA; Stb->has_preferred_x = 0; } void ImGuiInputTextState::ReloadUserBufAndSelectAll() { ReloadUserBuf = true; ReloadSelectionStart = 0; ReloadSelectionEnd = INT_MAX; } void ImGuiInputTextState::ReloadUserBufAndKeepSelection() { ReloadUserBuf = true; ReloadSelectionStart = Stb->select_start; ReloadSelectionEnd = Stb->select_end; } void ImGuiInputTextState::ReloadUserBufAndMoveToEnd() { ReloadUserBuf = true; ReloadSelectionStart = ReloadSelectionEnd = INT_MAX; } @@ -4238,26 +4292,21 @@ static bool InputTextFilterCharacter(ImGuiContext* ctx, unsigned int* p_char, Im // FIXME: Ideally we should transition toward (1) making InsertChars()/DeleteChars() update undo-stack (2) discourage (and keep reconcile) or obsolete (and remove reconcile) accessing buffer directly. static void InputTextReconcileUndoStateAfterUserCallback(ImGuiInputTextState* state, const char* new_buf_a, int new_length_a) { - ImGuiContext& g = *GImGui; - const ImWchar* old_buf = state->TextW.Data; - const int old_length = state->CurLenW; - const int new_length = ImTextCountCharsFromUtf8(new_buf_a, new_buf_a + new_length_a); - g.TempBuffer.reserve_discard((new_length + 1) * sizeof(ImWchar)); - ImWchar* new_buf = (ImWchar*)(void*)g.TempBuffer.Data; - ImTextStrFromUtf8(new_buf, new_length + 1, new_buf_a, new_buf_a + new_length_a); + const char* old_buf = state->TextA.Data; + const int old_length = state->CurLenA; - const int shorter_length = ImMin(old_length, new_length); + const int shorter_length = ImMin(old_length, new_length_a); int first_diff; for (first_diff = 0; first_diff < shorter_length; first_diff++) - if (old_buf[first_diff] != new_buf[first_diff]) + if (old_buf[first_diff] != new_buf_a[first_diff]) break; - if (first_diff == old_length && first_diff == new_length) + if (first_diff == old_length && first_diff == new_length_a) return; - int old_last_diff = old_length - 1; - int new_last_diff = new_length - 1; + int old_last_diff = old_length - 1; + int new_last_diff = new_length_a - 1; for (; old_last_diff >= first_diff && new_last_diff >= first_diff; old_last_diff--, new_last_diff--) - if (old_buf[old_last_diff] != new_buf[new_last_diff]) + if (old_buf[old_last_diff] != new_buf_a[new_last_diff]) break; const int insert_len = new_last_diff - first_diff + 1; @@ -4434,13 +4483,11 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ recycle_state = false; // Start edition - const char* buf_end = NULL; state->ID = id; - state->TextW.resize(buf_size + 1); // wchar count <= UTF-8 count. we use +1 to make sure that .Data is always pointing to at least an empty string. - state->TextA.resize(0); - state->TextAIsValid = false; // TextA is not valid yet (we will display buf until then) - state->CurLenW = ImTextStrFromUtf8(state->TextW.Data, buf_size, buf, NULL, &buf_end); - state->CurLenA = (int)(buf_end - buf); // We can't get the result from ImStrncpy() above because it is not UTF-8 aware. Here we'll cut off malformed UTF-8. + state->TextA.resize(buf_size + 1); // we use +1 to make sure that .Data is always pointing to at least an empty string. + state->TextAIsValid = true; + state->CurLenA = (int)strlen(buf); + memcpy(state->TextA.Data, buf, state->CurLenA + 1); if (recycle_state) { @@ -4522,18 +4569,6 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ bool value_changed = false; bool validated = false; - // When read-only we always use the live data passed to the function - // FIXME-OPT: Because our selection/cursor code currently needs the wide text we need to convert it when active, which is not ideal :( - if (is_readonly && state != NULL && (render_cursor || render_selection)) - { - const char* buf_end = NULL; - state->TextW.resize(buf_size + 1); - state->CurLenW = ImTextStrFromUtf8(state->TextW.Data, state->TextW.Size, buf, NULL, &buf_end); - state->CurLenA = (int)(buf_end - buf); - state->CursorClamp(); - render_selection &= state->HasSelection(); - } - // Select the buffer to render. const bool buf_display_from_state = (render_cursor || render_selection || g.ActiveId == id) && !is_readonly && state && state->TextAIsValid; const bool is_displaying_hint = (hint != NULL && (buf_display_from_state ? state->TextA.Data : buf)[0] == 0); @@ -4786,12 +4821,12 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ if (g.PlatformIO.Platform_SetClipboardTextFn != NULL) { const int ib = state->HasSelection() ? ImMin(state->Stb->select_start, state->Stb->select_end) : 0; - const int ie = state->HasSelection() ? ImMax(state->Stb->select_start, state->Stb->select_end) : state->CurLenW; - const int clipboard_data_len = ImTextCountUtf8BytesFromStr(state->TextW.Data + ib, state->TextW.Data + ie) + 1; - char* clipboard_data = (char*)IM_ALLOC(clipboard_data_len * sizeof(char)); - ImTextStrToUtf8(clipboard_data, clipboard_data_len, state->TextW.Data + ib, state->TextW.Data + ie); - SetClipboardText(clipboard_data); - MemFree(clipboard_data); + const int ie = state->HasSelection() ? ImMax(state->Stb->select_start, state->Stb->select_end) : state->CurLenA; + + char backup = state->TextA.Data[ie]; + state->TextA.Data[ie] = 0; // A bit of a hack since SetClipboardText only takes null terminated strings + SetClipboardText(state->TextA.Data + ib); + state->TextA.Data[ie] = backup; } if (is_cut) { @@ -4807,15 +4842,17 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ { // Filter pasted buffer const int clipboard_len = (int)strlen(clipboard); - ImWchar* clipboard_filtered = (ImWchar*)IM_ALLOC((clipboard_len + 1) * sizeof(ImWchar)); + char* clipboard_filtered = (char*)IM_ALLOC((clipboard_len + 1)); int clipboard_filtered_len = 0; for (const char* s = clipboard; *s != 0; ) { unsigned int c; - s += ImTextCharFromUtf8(&c, s, NULL); + int len = ImTextCharFromUtf8(&c, s, NULL); if (!InputTextFilterCharacter(&g, &c, flags, callback, callback_user_data, true)) continue; - clipboard_filtered[clipboard_filtered_len++] = (ImWchar)c; + memcpy(clipboard_filtered + clipboard_filtered_len, s, len); + clipboard_filtered_len += len; + s += len; } clipboard_filtered[clipboard_filtered_len] = 0; if (clipboard_filtered_len > 0) // If everything was filtered, ignore the pasting operation @@ -4851,27 +4888,14 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ } else if (strcmp(buf, state->InitialTextA.Data) != 0) { - // Restore initial value. Only return true if restoring to the initial value changes the current buffer contents. - // Push records into the undo stack so we can CTRL+Z the revert operation itself apply_new_text = state->InitialTextA.Data; apply_new_text_length = state->InitialTextA.Size - 1; - value_changed = true; - ImVector w_text; - if (apply_new_text_length > 0) - { - w_text.resize(ImTextCountCharsFromUtf8(apply_new_text, apply_new_text + apply_new_text_length) + 1); - ImTextStrFromUtf8(w_text.Data, w_text.Size, apply_new_text, apply_new_text + apply_new_text_length); - } - stb_textedit_replace(state, state->Stb, w_text.Data, (apply_new_text_length > 0) ? (w_text.Size - 1) : 0); - } - } - // Apply ASCII value - if (!is_readonly) - { - state->TextAIsValid = true; - state->TextA.resize(state->TextW.Size * 4 + 1); - ImTextStrToUtf8(state->TextA.Data, state->TextA.Size, state->TextW.Data, NULL); + // Restore initial value. Only return true if restoring to the initial value changes the current buffer contents. + // Push records into the undo stack so we can CTRL+Z the revert operation itself + value_changed = true; + stb_textedit_replace(state, state->Stb, state->InitialTextA.Data, state->InitialTextA.Size - 1); + } } // When using 'ImGuiInputTextFlags_EnterReturnsTrue' as a special case we reapply the live buffer back to the input buffer @@ -4935,11 +4959,9 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ callback_data.BufSize = state->BufCapacityA; callback_data.BufDirty = false; - // We have to convert from wchar-positions to UTF-8-positions, which can be pretty slow (an incentive to ditch the ImWchar buffer, see https://github.com/nothings/stb/issues/188) - ImWchar* text = state->TextW.Data; - const int utf8_cursor_pos = callback_data.CursorPos = ImTextCountUtf8BytesFromStr(text, text + state->Stb->cursor); - const int utf8_selection_start = callback_data.SelectionStart = ImTextCountUtf8BytesFromStr(text, text + state->Stb->select_start); - const int utf8_selection_end = callback_data.SelectionEnd = ImTextCountUtf8BytesFromStr(text, text + state->Stb->select_end); + const int utf8_cursor_pos = callback_data.CursorPos = state->Stb->cursor; + const int utf8_selection_start = callback_data.SelectionStart = state->Stb->select_start; + const int utf8_selection_end = state->Stb->select_end; // Call user code callback(&callback_data); @@ -4950,17 +4972,18 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ IM_ASSERT(callback_data.BufSize == state->BufCapacityA); IM_ASSERT(callback_data.Flags == flags); const bool buf_dirty = callback_data.BufDirty; - if (callback_data.CursorPos != utf8_cursor_pos || buf_dirty) { state->Stb->cursor = ImTextCountCharsFromUtf8(callback_data.Buf, callback_data.Buf + callback_data.CursorPos); state->CursorFollow = true; } - if (callback_data.SelectionStart != utf8_selection_start || buf_dirty) { state->Stb->select_start = (callback_data.SelectionStart == callback_data.CursorPos) ? state->Stb->cursor : ImTextCountCharsFromUtf8(callback_data.Buf, callback_data.Buf + callback_data.SelectionStart); } - if (callback_data.SelectionEnd != utf8_selection_end || buf_dirty) { state->Stb->select_end = (callback_data.SelectionEnd == callback_data.SelectionStart) ? state->Stb->select_start : ImTextCountCharsFromUtf8(callback_data.Buf, callback_data.Buf + callback_data.SelectionEnd); } + if (callback_data.CursorPos != utf8_cursor_pos || buf_dirty) { state->Stb->cursor = callback_data.CursorPos; state->CursorFollow = true; } + if (callback_data.SelectionStart != utf8_selection_start || buf_dirty) { state->Stb->select_start = (callback_data.SelectionStart == callback_data.CursorPos) ? state->Stb->cursor : callback_data.SelectionStart; } + if (callback_data.SelectionEnd != utf8_selection_end || buf_dirty) { state->Stb->select_end = (callback_data.SelectionEnd == callback_data.SelectionStart) ? state->Stb->select_start : callback_data.SelectionEnd; } if (buf_dirty) { // Callback may update buffer and thus set buf_dirty even in read-only mode. IM_ASSERT(callback_data.BufTextLen == (int)strlen(callback_data.Buf)); // You need to maintain BufTextLen if you change the text! InputTextReconcileUndoStateAfterUserCallback(state, callback_data.Buf, callback_data.BufTextLen); // FIXME: Move the rest of this block inside function and rename to InputTextReconcileStateAfterUserCallback() ? if (callback_data.BufTextLen > backup_current_text_length && is_resizable) - state->TextW.resize(state->TextW.Size + (callback_data.BufTextLen - backup_current_text_length)); // Worse case scenario resize - state->CurLenW = ImTextStrFromUtf8(state->TextW.Data, state->TextW.Size, callback_data.Buf, NULL); + state->TextA.resize(state->TextA.Size + (callback_data.BufTextLen - backup_current_text_length)); // Worse case scenario resize + + memcpy(state->TextA.Data, callback_data.Buf, callback_data.BufTextLen); state->CurLenA = callback_data.BufTextLen; // Assume correct length and valid UTF-8 from user, saves us an extra strlen() state->CursorAnimReset(); } @@ -5064,12 +5087,12 @@ 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 ImWchar* text_begin = state->TextW.Data; + const char* text_begin = state->TextA.Data; ImVec2 cursor_offset, select_start_offset; { // Find lines numbers straddling 'cursor' (slot 0) and 'select_start' (slot 1) positions. - const ImWchar* searches_input_ptr[2] = { NULL, NULL }; + const char* searches_input_ptr[2] = { NULL, NULL }; int searches_result_line_no[2] = { -1000, -1000 }; int searches_remaining = 0; if (render_cursor) @@ -5090,7 +5113,7 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ searches_remaining += is_multiline ? 1 : 0; int line_count = 0; //for (const ImWchar* s = text_begin; (s = (const ImWchar*)wcschr((const wchar_t*)s, (wchar_t)'\n')) != NULL; s++) // FIXME-OPT: Could use this when wchar_t are 16-bit - for (const ImWchar* s = text_begin; *s != 0; s++) + for (const char* s = text_begin; *s != 0; s++) if (*s == '\n') { line_count++; @@ -5104,11 +5127,11 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ searches_result_line_no[1] = line_count; // Calculate 2d position by finding the beginning of the line and measuring distance - cursor_offset.x = InputTextCalcTextSizeW(&g, ImStrbolW(searches_input_ptr[0], text_begin), searches_input_ptr[0]).x; + cursor_offset.x = InputTextCalcTextSize(&g, ImStrbol(searches_input_ptr[0], text_begin), searches_input_ptr[0]).x; cursor_offset.y = searches_result_line_no[0] * g.FontSize; if (searches_result_line_no[1] >= 0) { - select_start_offset.x = InputTextCalcTextSizeW(&g, ImStrbolW(searches_input_ptr[1], text_begin), searches_input_ptr[1]).x; + select_start_offset.x = InputTextCalcTextSize(&g, ImStrbol(searches_input_ptr[1], text_begin), searches_input_ptr[1]).x; select_start_offset.y = searches_result_line_no[1] * g.FontSize; } @@ -5156,14 +5179,14 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ const ImVec2 draw_scroll = ImVec2(state->Scroll.x, 0.0f); if (render_selection) { - const ImWchar* text_selected_begin = text_begin + ImMin(state->Stb->select_start, state->Stb->select_end); - const ImWchar* text_selected_end = text_begin + ImMax(state->Stb->select_start, state->Stb->select_end); + 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); 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; ImVec2 rect_pos = draw_pos + select_start_offset - draw_scroll; - for (const ImWchar* p = text_selected_begin; p < text_selected_end; ) + for (const char* p = text_selected_begin; p < text_selected_end; ) { if (rect_pos.y > clip_rect.w + g.FontSize) break; @@ -5177,7 +5200,7 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ } else { - ImVec2 rect_size = InputTextCalcTextSizeW(&g, p, text_selected_end, &p, NULL, true); + ImVec2 rect_size = InputTextCalcTextSize(&g, p, text_selected_end, &p, NULL, true); if (rect_size.x <= 0.0f) rect_size.x = IM_TRUNC(g.Font->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); @@ -5282,7 +5305,7 @@ void ImGui::DebugNodeInputTextState(ImGuiInputTextState* state) ImStb::StbUndoState* undo_state = &stb_state->undostate; Text("ID: 0x%08X, ActiveID: 0x%08X", state->ID, g.ActiveId); DebugLocateItemOnHover(state->ID); - Text("CurLenW: %d, CurLenA: %d, Cursor: %d, Selection: %d..%d", state->CurLenW, state->CurLenA, stb_state->cursor, stb_state->select_start, stb_state->select_end); + Text("CurLenA: %d, Cursor: %d, Selection: %d..%d", state->CurLenA, stb_state->cursor, stb_state->select_start, stb_state->select_end); Text("has_preferred_x: %d (%.2f)", stb_state->has_preferred_x, stb_state->preferred_x); Text("undo_point: %d, redo_point: %d, undo_char_point: %d, redo_char_point: %d", undo_state->undo_point, undo_state->redo_point, undo_state->undo_char_point, undo_state->redo_char_point); if (BeginChild("undopoints", ImVec2(0.0f, GetTextLineHeight() * 10), ImGuiChildFlags_Borders | ImGuiChildFlags_ResizeY)) // Visualize undo state @@ -5294,11 +5317,8 @@ void ImGui::DebugNodeInputTextState(ImGuiInputTextState* state) const char undo_rec_type = (n < undo_state->undo_point) ? 'u' : (n >= undo_state->redo_point) ? 'r' : ' '; if (undo_rec_type == ' ') BeginDisabled(); - char buf[64] = ""; - if (undo_rec_type != ' ' && undo_rec->char_storage != -1) - ImTextStrToUtf8(buf, IM_ARRAYSIZE(buf), undo_state->undo_char + undo_rec->char_storage, undo_state->undo_char + undo_rec->char_storage + undo_rec->insert_length); - Text("%c [%02d] where %03d, insert %03d, delete %03d, char_storage %03d \"%s\"", - undo_rec_type, n, undo_rec->where, undo_rec->insert_length, undo_rec->delete_length, undo_rec->char_storage, buf); + Text("%c [%02d] where %03d, insert %03d, delete %03d, char_storage %03d \"%.*s\"", + undo_rec_type, n, undo_rec->where, undo_rec->insert_length, undo_rec->delete_length, undo_rec->char_storage, undo_rec->insert_length, undo_state->undo_char + undo_rec->char_storage); if (undo_rec_type == ' ') EndDisabled(); } diff --git a/imstb_textedit.h b/imstb_textedit.h index faaf518f4..28508607b 100644 --- a/imstb_textedit.h +++ b/imstb_textedit.h @@ -4,6 +4,7 @@ // - Fix in stb_textedit_discard_redo (see https://github.com/nothings/stb/issues/321) // - Fix in stb_textedit_find_charpos to handle last line (see https://github.com/ocornut/imgui/issues/6000 + #6783) // - Added name to struct or it may be forward declared in our code. +// - Added UTF8 support https://github.com/nothings/stb/issues/188 // Grep for [DEAR IMGUI] to find the changes. // - Also renamed macros used or defined outside of IMSTB_TEXTEDIT_IMPLEMENTATION block from STB_TEXTEDIT_* to IMSTB_TEXTEDIT_* @@ -439,13 +440,13 @@ static int stb_text_locate_coord(IMSTB_TEXTEDIT_STRING *str, float x, float y) if (x < r.x1) { // search characters in row for one that straddles 'x' prev_x = r.x0; - for (k=0; k < r.num_chars; ++k) { + for (k=0; k < r.num_chars; k = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(str, i + k) - i) { float w = STB_TEXTEDIT_GETWIDTH(str, i, k); if (x < prev_x+w) { if (x < prev_x+w/2) return k+i; else - return k+i+1; + return IMSTB_TEXTEDIT_GETNEXTCHARINDEX(str, i + k); } prev_x += w; } @@ -564,7 +565,7 @@ static void stb_textedit_find_charpos(StbFindState *find, IMSTB_TEXTEDIT_STRING // now scan to find xpos find->x = r.x0; - for (i=0; first+i < n; ++i) + for (i=0; first+i < n; i = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(str, first + i) - first) find->x += STB_TEXTEDIT_GETWIDTH(str, first, i); } @@ -641,6 +642,17 @@ static void stb_textedit_move_to_last(IMSTB_TEXTEDIT_STRING *str, STB_TexteditSt } } +//[DEAR IMGUI] +//Functions must be implemented for UTF8 support +//Code in this file that uses them, is modified for [DEAR IMGUI] and deviates from the original stb_textedit. +//There is not necessarily a '[DEAR IMGUI]' at the usage sites +#ifndef IMSTB_TEXTEDIT_GETPREVIOUSCHARINDEX +#define IMSTB_TEXTEDIT_GETPREVIOUSCHARINDEX(obj, idx) idx - 1 +#endif +#ifndef IMSTB_TEXTEDIT_GETNEXTCHARINDEX +#define IMSTB_TEXTEDIT_GETNEXTCHARINDEX(obj, idx) idx + 1 +#endif + #ifdef STB_TEXTEDIT_IS_SPACE static int is_word_boundary( IMSTB_TEXTEDIT_STRING *str, int idx ) { @@ -722,15 +734,16 @@ static int stb_textedit_paste_internal(IMSTB_TEXTEDIT_STRING *str, STB_TexteditS #endif // API key: process a keyboard input -static void stb_textedit_key(IMSTB_TEXTEDIT_STRING *str, STB_TexteditState *state, STB_TEXTEDIT_KEYTYPE key) +//[DEAR IMGUI] In addition to the key we also pass in the decoded UTF8 byte sequence, if it is a character key. +//This is a bit ugly and only makes sense for UTF8. One could think of other solutions that wouldn't make this function so UTF8 specific. +//If the idea is to push the changes upstream to stb_textedit it might be worth thinking about but since this is just for [DEAR IMGUI], it might not be worth the complication +static void stb_textedit_key(IMSTB_TEXTEDIT_STRING *str, STB_TexteditState *state, STB_TEXTEDIT_KEYTYPE key, const IMSTB_TEXTEDIT_CHARTYPE* decoded, int decoded_size) { retry: switch (key) { default: { int c = STB_TEXTEDIT_KEYTOTEXT(key); if (c > 0) { - IMSTB_TEXTEDIT_CHARTYPE ch = (IMSTB_TEXTEDIT_CHARTYPE) c; - // can't add newline in single-line mode if (c == '\n' && state->single_line) break; @@ -738,15 +751,15 @@ retry: if (state->insert_mode && !STB_TEXT_HAS_SELECTION(state) && state->cursor < STB_TEXTEDIT_STRINGLEN(str)) { stb_text_makeundo_replace(str, state, state->cursor, 1, 1); STB_TEXTEDIT_DELETECHARS(str, state->cursor, 1); - if (STB_TEXTEDIT_INSERTCHARS(str, state->cursor, &ch, 1)) { - ++state->cursor; + if (STB_TEXTEDIT_INSERTCHARS(str, state->cursor, decoded, decoded_size)) { + state->cursor += decoded_size; state->has_preferred_x = 0; } } else { stb_textedit_delete_selection(str,state); // implicitly clamps - if (STB_TEXTEDIT_INSERTCHARS(str, state->cursor, &ch, 1)) { - stb_text_makeundo_insert(state, state->cursor, 1); - ++state->cursor; + if (STB_TEXTEDIT_INSERTCHARS(str, state->cursor, decoded, decoded_size)) { + stb_text_makeundo_insert(state, state->cursor, decoded_size); + state->cursor += decoded_size; state->has_preferred_x = 0; } } @@ -776,7 +789,7 @@ retry: stb_textedit_move_to_first(state); else if (state->cursor > 0) - --state->cursor; + state->cursor = IMSTB_TEXTEDIT_GETPREVIOUSCHARINDEX(str, state->cursor); state->has_preferred_x = 0; break; @@ -785,7 +798,7 @@ retry: if (STB_TEXT_HAS_SELECTION(state)) stb_textedit_move_to_last(str, state); else - ++state->cursor; + state->cursor = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(str, state->cursor); stb_textedit_clamp(str, state); state->has_preferred_x = 0; break; @@ -795,7 +808,7 @@ retry: stb_textedit_prep_selection_at_cursor(state); // move selection left if (state->select_end > 0) - --state->select_end; + state->select_end = IMSTB_TEXTEDIT_GETPREVIOUSCHARINDEX(str, state->select_end); state->cursor = state->select_end; state->has_preferred_x = 0; break; @@ -845,7 +858,7 @@ retry: case STB_TEXTEDIT_K_RIGHT | STB_TEXTEDIT_K_SHIFT: stb_textedit_prep_selection_at_cursor(state); // move selection right - ++state->select_end; + state->select_end = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(str, state->select_end); stb_textedit_clamp(str, state); state->cursor = state->select_end; state->has_preferred_x = 0; @@ -901,7 +914,7 @@ retry: x += dx; if (x > goal_x) break; - ++state->cursor; + state->cursor = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(str, state->cursor); } stb_textedit_clamp(str, state); @@ -963,7 +976,7 @@ retry: x += dx; if (x > goal_x) break; - ++state->cursor; + state->cursor = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(str, state->cursor); } stb_textedit_clamp(str, state); @@ -991,7 +1004,7 @@ retry: else { int n = STB_TEXTEDIT_STRINGLEN(str); if (state->cursor < n) - stb_textedit_delete(str, state, state->cursor, 1); + stb_textedit_delete(str, state, state->cursor, IMSTB_TEXTEDIT_GETNEXTCHARINDEX(str, state->cursor) - state->cursor); } state->has_preferred_x = 0; break; @@ -1003,8 +1016,9 @@ retry: else { stb_textedit_clamp(str, state); if (state->cursor > 0) { - stb_textedit_delete(str, state, state->cursor-1, 1); - --state->cursor; + int prev = IMSTB_TEXTEDIT_GETPREVIOUSCHARINDEX(str, state->cursor); + stb_textedit_delete(str, state, prev, state->cursor - prev); + state->cursor = prev; } } state->has_preferred_x = 0; From d1b7817959245230e0ef24f8c9886c0e6d0d1396 Mon Sep 17 00:00:00 2001 From: ocornut Date: Thu, 5 Sep 2024 15:25:30 +0200 Subject: [PATCH 02/12] InputText: amends: renames, coding style. (#7925) --- imgui.h | 2 +- imgui_internal.h | 1 - imgui_widgets.cpp | 17 ++++++++--------- imstb_textedit.h | 22 +++++++++++----------- 4 files changed, 20 insertions(+), 22 deletions(-) diff --git a/imgui.h b/imgui.h index ebb156d4b..095c25002 100644 --- a/imgui.h +++ b/imgui.h @@ -29,7 +29,7 @@ // Library Version // (Integer encoded as XYYZZ for use in #if preprocessor conditionals, e.g. '#if IMGUI_VERSION_NUM >= 12345') #define IMGUI_VERSION "1.91.2 WIP" -#define IMGUI_VERSION_NUM 19113 +#define IMGUI_VERSION_NUM 19114 #define IMGUI_HAS_TABLE /* diff --git a/imgui_internal.h b/imgui_internal.h index ec4c069eb..4dfcfce9a 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -1114,7 +1114,6 @@ struct IMGUI_API ImGuiInputTextState int CurLenA; // UTF-8 length of the string in TextA (in bytes) ImVector TextA; // temporary UTF8 buffer for callbacks and other operations. this is not updated in every code-path! size=capacity. ImVector InitialTextA; // value to revert to when pressing Escape = backup of end-user buffer at the time of focus (in UTF-8, unaltered) - bool TextAIsValid; // temporary UTF8 buffer is not initially valid before we make the widget active (until then we pull the data from user argument) int BufCapacityA; // end-user buffer capacity ImVec2 Scroll; // horizontal offset (managed manually) + vertical scrolling (pulled from child window's own Scroll.y) float CursorAnim; // timer for cursor blink, reset on every user action so the cursor reappears immediately diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 5a8721c78..64e701407 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -3952,10 +3952,10 @@ static int STB_TEXTEDIT_MOVEWORDLEFT_IMPL(ImGuiInputTextState* obj, int idx) static int STB_TEXTEDIT_MOVEWORDRIGHT_MAC(ImGuiInputTextState* obj, int idx) { idx++; int len = obj->CurLenA; while (idx < len && !is_word_boundary_from_left(obj, idx)) idx++; return idx > len ? len : idx; } static int STB_TEXTEDIT_MOVEWORDRIGHT_WIN(ImGuiInputTextState* obj, int idx) { idx++; int len = obj->CurLenA; while (idx < len && !is_word_boundary_from_right(obj, idx)) idx++; return idx > len ? len : idx; } static int STB_TEXTEDIT_MOVEWORDRIGHT_IMPL(ImGuiInputTextState* obj, int idx) { ImGuiContext& g = *obj->Ctx; if (g.IO.ConfigMacOSXBehaviors) return STB_TEXTEDIT_MOVEWORDRIGHT_MAC(obj, idx); else return STB_TEXTEDIT_MOVEWORDRIGHT_WIN(obj, idx); } -#define STB_TEXTEDIT_MOVEWORDLEFT STB_TEXTEDIT_MOVEWORDLEFT_IMPL // They need to be #define for stb_textedit.h -#define STB_TEXTEDIT_MOVEWORDRIGHT STB_TEXTEDIT_MOVEWORDRIGHT_IMPL +#define STB_TEXTEDIT_MOVEWORDLEFT STB_TEXTEDIT_MOVEWORDLEFT_IMPL // They need to be #define for stb_textedit.h +#define STB_TEXTEDIT_MOVEWORDRIGHT STB_TEXTEDIT_MOVEWORDRIGHT_IMPL #define IMSTB_TEXTEDIT_GETNEXTCHARINDEX IMSTB_TEXTEDIT_GETNEXTCHARINDEX_IMPL -#define IMSTB_TEXTEDIT_GETPREVIOUSCHARINDEX IMSTB_TEXTEDIT_GETPREVIOUSCHARINDEX_IMPL +#define IMSTB_TEXTEDIT_GETPREVCHARINDEX IMSTB_TEXTEDIT_GETPREVCHARINDEX_IMPL static int IMSTB_TEXTEDIT_GETNEXTCHARINDEX_IMPL(ImGuiInputTextState* obj, int idx) { @@ -3975,9 +3975,9 @@ static int CountLeadingHighBits(unsigned char b) return sizeof(b) * 8; } -static int IMSTB_TEXTEDIT_GETPREVIOUSCHARINDEX_IMPL(ImGuiInputTextState* obj, int idx) +static int IMSTB_TEXTEDIT_GETPREVCHARINDEX_IMPL(ImGuiInputTextState* obj, int idx) { - //Backwards check/count for UTF-8 multi byte sequence + // Backwards check/count for UTF-8 multi byte sequence int num_seq_bytes = 0; for (int i = idx - 1; i >= 0; i -= 1) { @@ -4479,13 +4479,12 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ // Preserve cursor position and undo/redo stack if we come back to same widget // FIXME: Since we reworked this on 2022/06, may want to differentiate recycle_cursor vs recycle_undostate? bool recycle_state = (state->ID == id && !init_changed_specs && !init_reload_from_user_buf); - if (recycle_state && (state->CurLenA != buf_len || (state->TextAIsValid && strncmp(state->TextA.Data, buf, buf_len) != 0))) + if (recycle_state && (state->CurLenA != buf_len || (strncmp(state->TextA.Data, buf, buf_len) != 0))) recycle_state = false; // Start edition state->ID = id; state->TextA.resize(buf_size + 1); // we use +1 to make sure that .Data is always pointing to at least an empty string. - state->TextAIsValid = true; state->CurLenA = (int)strlen(buf); memcpy(state->TextA.Data, buf, state->CurLenA + 1); @@ -4570,7 +4569,7 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ bool validated = false; // Select the buffer to render. - const bool buf_display_from_state = (render_cursor || render_selection || g.ActiveId == id) && !is_readonly && state && state->TextAIsValid; + const bool buf_display_from_state = (render_cursor || render_selection || g.ActiveId == id) && !is_readonly && state; const bool is_displaying_hint = (hint != NULL && (buf_display_from_state ? state->TextA.Data : buf)[0] == 0); // Password pushes a temporary font with only a fallback glyph @@ -4842,7 +4841,7 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ { // Filter pasted buffer const int clipboard_len = (int)strlen(clipboard); - char* clipboard_filtered = (char*)IM_ALLOC((clipboard_len + 1)); + char* clipboard_filtered = (char*)IM_ALLOC(clipboard_len + 1); int clipboard_filtered_len = 0; for (const char* s = clipboard; *s != 0; ) { diff --git a/imstb_textedit.h b/imstb_textedit.h index 28508607b..22122c08b 100644 --- a/imstb_textedit.h +++ b/imstb_textedit.h @@ -4,7 +4,7 @@ // - Fix in stb_textedit_discard_redo (see https://github.com/nothings/stb/issues/321) // - Fix in stb_textedit_find_charpos to handle last line (see https://github.com/ocornut/imgui/issues/6000 + #6783) // - Added name to struct or it may be forward declared in our code. -// - Added UTF8 support https://github.com/nothings/stb/issues/188 +// - Added UTF-8 support (see https://github.com/nothings/stb/issues/188 + https://github.com/ocornut/imgui/pull/7925) // Grep for [DEAR IMGUI] to find the changes. // - Also renamed macros used or defined outside of IMSTB_TEXTEDIT_IMPLEMENTATION block from STB_TEXTEDIT_* to IMSTB_TEXTEDIT_* @@ -642,15 +642,15 @@ static void stb_textedit_move_to_last(IMSTB_TEXTEDIT_STRING *str, STB_TexteditSt } } -//[DEAR IMGUI] -//Functions must be implemented for UTF8 support -//Code in this file that uses them, is modified for [DEAR IMGUI] and deviates from the original stb_textedit. -//There is not necessarily a '[DEAR IMGUI]' at the usage sites -#ifndef IMSTB_TEXTEDIT_GETPREVIOUSCHARINDEX -#define IMSTB_TEXTEDIT_GETPREVIOUSCHARINDEX(obj, idx) idx - 1 +// [DEAR IMGUI] +// Functions must be implemented for UTF8 support +// Code in this file that uses those functions is modified for [DEAR IMGUI] and deviates from the original stb_textedit. +// There is not necessarily a '[DEAR IMGUI]' at the usage sites. +#ifndef IMSTB_TEXTEDIT_GETPREVCHARINDEX +#define IMSTB_TEXTEDIT_GETPREVCHARINDEX(obj, idx) (idx - 1) #endif #ifndef IMSTB_TEXTEDIT_GETNEXTCHARINDEX -#define IMSTB_TEXTEDIT_GETNEXTCHARINDEX(obj, idx) idx + 1 +#define IMSTB_TEXTEDIT_GETNEXTCHARINDEX(obj, idx) (idx + 1) #endif #ifdef STB_TEXTEDIT_IS_SPACE @@ -789,7 +789,7 @@ retry: stb_textedit_move_to_first(state); else if (state->cursor > 0) - state->cursor = IMSTB_TEXTEDIT_GETPREVIOUSCHARINDEX(str, state->cursor); + state->cursor = IMSTB_TEXTEDIT_GETPREVCHARINDEX(str, state->cursor); state->has_preferred_x = 0; break; @@ -808,7 +808,7 @@ retry: stb_textedit_prep_selection_at_cursor(state); // move selection left if (state->select_end > 0) - state->select_end = IMSTB_TEXTEDIT_GETPREVIOUSCHARINDEX(str, state->select_end); + state->select_end = IMSTB_TEXTEDIT_GETPREVCHARINDEX(str, state->select_end); state->cursor = state->select_end; state->has_preferred_x = 0; break; @@ -1016,7 +1016,7 @@ retry: else { stb_textedit_clamp(str, state); if (state->cursor > 0) { - int prev = IMSTB_TEXTEDIT_GETPREVIOUSCHARINDEX(str, state->cursor); + int prev = IMSTB_TEXTEDIT_GETPREVCHARINDEX(str, state->cursor); stb_textedit_delete(str, state, prev, state->cursor - prev); state->cursor = prev; } From 1674fe96ff57e4540b6c1860e61aa7fcc06642fe Mon Sep 17 00:00:00 2001 From: ocornut Date: Thu, 5 Sep 2024 16:31:41 +0200 Subject: [PATCH 03/12] InputText: amends: add stb_textedit_text() api. (#7925) It seems sensible to push this change in stb_textedit repo eventually. --- imgui_internal.h | 1 + imgui_widgets.cpp | 23 +++++++++++------- imstb_textedit.h | 61 ++++++++++++++++++++++++++++------------------- 3 files changed, 52 insertions(+), 33 deletions(-) diff --git a/imgui_internal.h b/imgui_internal.h index 4dfcfce9a..e6f3f8ec5 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -1130,6 +1130,7 @@ struct IMGUI_API ImGuiInputTextState void ClearText() { CurLenA = 0; TextA[0] = 0; CursorClamp(); } void ClearFreeMemory() { TextA.clear(); InitialTextA.clear(); } void OnKeyPressed(int key); // Cannot be inline because we call in code in stb_textedit.h implementation + void OnCharPressed(unsigned int c); // Cursor & Selection void CursorAnimReset(); diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 64e701407..f35563119 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -3889,7 +3889,6 @@ namespace ImStb static int STB_TEXTEDIT_STRINGLEN(const ImGuiInputTextState* obj) { return obj->CurLenA; } static char STB_TEXTEDIT_GETCHAR(const ImGuiInputTextState* obj, int idx) { IM_ASSERT(idx <= obj->CurLenA); return obj->TextA[idx]; } static float STB_TEXTEDIT_GETWIDTH(ImGuiInputTextState* obj, int line_start_idx, int char_idx) { unsigned int c; ImTextCharFromUtf8(&c, obj->TextA.Data + line_start_idx + char_idx, obj->TextA.Data + obj->TextA.Size); if ((ImWchar)c == '\n') return IMSTB_TEXTEDIT_GETWIDTH_NEWLINE; ImGuiContext& g = *obj->Ctx; return g.Font->GetCharAdvance((ImWchar)c) * g.FontScale; } -static int STB_TEXTEDIT_KEYTOTEXT(int key) { return key >= 0x200000 ? 0 : key; } static char STB_TEXTEDIT_NEWLINE = '\n'; static void STB_TEXTEDIT_LAYOUTROW(StbTexteditRow* r, ImGuiInputTextState* obj, int line_start_idx) { @@ -4101,12 +4100,18 @@ ImGuiInputTextState::~ImGuiInputTextState() void ImGuiInputTextState::OnKeyPressed(int key) { - //We prematurely convert the key to a UTF8 byte sequence, even for keys where that doesn't even make sense (e.g. arrow keys). - //Not optimal but stb_textedit_key will only use the UTF8 values for valid character keys anyways. - //The changes we had to make to stb_textedit_key make it very much UTF8 specific which is not too great. + stb_textedit_key(this, Stb, key); + CursorFollow = true; + CursorAnimReset(); +} + +void ImGuiInputTextState::OnCharPressed(unsigned int c) +{ + // Convert the key to a UTF8 byte sequence. + // The changes we had to make to stb_textedit_key made it very much UTF-8 specific which is not too great. char utf8[5]; - ImTextCharToUtf8(utf8, key); - stb_textedit_key(this, Stb, key, utf8, (int)strlen(utf8)); + ImTextCharToUtf8(utf8, c); + stb_textedit_text(this, Stb, utf8, (int)strlen(utf8)); CursorFollow = true; CursorAnimReset(); } @@ -4674,7 +4679,7 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ { unsigned int c = '\t'; // Insert TAB if (InputTextFilterCharacter(&g, &c, flags, callback, callback_user_data)) - state->OnKeyPressed((int)c); + state->OnCharPressed(c); } // FIXME: Implement Shift+Tab /* @@ -4697,7 +4702,7 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ if (c == '\t') // Skip Tab, see above. continue; if (InputTextFilterCharacter(&g, &c, flags, callback, callback_user_data)) - state->OnKeyPressed((int)c); + state->OnCharPressed(c); } // Consume characters @@ -4781,7 +4786,7 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ { unsigned int c = '\n'; // Insert new line if (InputTextFilterCharacter(&g, &c, flags, callback, callback_user_data)) - state->OnKeyPressed((int)c); + state->OnCharPressed(c); } } else if (is_cancel) diff --git a/imstb_textedit.h b/imstb_textedit.h index 22122c08b..b7a761c85 100644 --- a/imstb_textedit.h +++ b/imstb_textedit.h @@ -211,6 +211,7 @@ // int stb_textedit_cut(STB_TEXTEDIT_STRING *str, STB_TexteditState *state) // int stb_textedit_paste(STB_TEXTEDIT_STRING *str, STB_TexteditState *state, STB_TEXTEDIT_CHARTYPE *text, int len) // void stb_textedit_key(STB_TEXTEDIT_STRING *str, STB_TexteditState *state, STB_TEXEDIT_KEYTYPE key) +// void stb_textedit_text(STB_TEXTEDIT_STRING *str, STB_TexteditState *state, STB_TEXTEDIT_CHARTYPE *text, int text_len) // // Each of these functions potentially updates the string and updates the // state. @@ -245,7 +246,12 @@ // various definitions like STB_TEXTEDIT_K_LEFT have the is-key-event bit // set, and make STB_TEXTEDIT_KEYTOCHAR check that the is-key-event bit is // clear. STB_TEXTEDIT_KEYTYPE defaults to int, but you can #define it to -// anything other type you wante before including. +// anything other type you want before including. +// if the STB_TEXTEDIT_KEYTOTEXT function is defined, selected keys are +// transformed into text and stb_textedit_text() is automatically called. +// +// text: [DEAR IMGUI] added 2024-09 +// call this to text inputs sent to the textfield. // // // When rendering, you can read the cursor position and selection state from @@ -733,37 +739,44 @@ static int stb_textedit_paste_internal(IMSTB_TEXTEDIT_STRING *str, STB_TexteditS #define STB_TEXTEDIT_KEYTYPE int #endif +// [DEAR IMGUI] Added stb_textedit_text(), extracted out and called by stb_textedit_key() for backward compatibility. +static void stb_textedit_text(IMSTB_TEXTEDIT_STRING* str, STB_TexteditState* state, const IMSTB_TEXTEDIT_CHARTYPE* text, int text_len) +{ + // can't add newline in single-line mode + if (text[0] == '\n' && state->single_line) + return; + + if (state->insert_mode && !STB_TEXT_HAS_SELECTION(state) && state->cursor < STB_TEXTEDIT_STRINGLEN(str)) { + stb_text_makeundo_replace(str, state, state->cursor, 1, 1); + STB_TEXTEDIT_DELETECHARS(str, state->cursor, 1); + if (STB_TEXTEDIT_INSERTCHARS(str, state->cursor, text, text_len)) { + state->cursor += text_len; + state->has_preferred_x = 0; + } + } + else { + stb_textedit_delete_selection(str, state); // implicitly clamps + if (STB_TEXTEDIT_INSERTCHARS(str, state->cursor, text, text_len)) { + stb_text_makeundo_insert(state, state->cursor, text_len); + state->cursor += text_len; + state->has_preferred_x = 0; + } + } +} + // API key: process a keyboard input -//[DEAR IMGUI] In addition to the key we also pass in the decoded UTF8 byte sequence, if it is a character key. -//This is a bit ugly and only makes sense for UTF8. One could think of other solutions that wouldn't make this function so UTF8 specific. -//If the idea is to push the changes upstream to stb_textedit it might be worth thinking about but since this is just for [DEAR IMGUI], it might not be worth the complication -static void stb_textedit_key(IMSTB_TEXTEDIT_STRING *str, STB_TexteditState *state, STB_TEXTEDIT_KEYTYPE key, const IMSTB_TEXTEDIT_CHARTYPE* decoded, int decoded_size) +static void stb_textedit_key(IMSTB_TEXTEDIT_STRING *str, STB_TexteditState *state, STB_TEXTEDIT_KEYTYPE key) { retry: switch (key) { default: { +#ifdef STB_TEXTEDIT_KEYTOTEXT int c = STB_TEXTEDIT_KEYTOTEXT(key); if (c > 0) { - // can't add newline in single-line mode - if (c == '\n' && state->single_line) - break; - - if (state->insert_mode && !STB_TEXT_HAS_SELECTION(state) && state->cursor < STB_TEXTEDIT_STRINGLEN(str)) { - stb_text_makeundo_replace(str, state, state->cursor, 1, 1); - STB_TEXTEDIT_DELETECHARS(str, state->cursor, 1); - if (STB_TEXTEDIT_INSERTCHARS(str, state->cursor, decoded, decoded_size)) { - state->cursor += decoded_size; - state->has_preferred_x = 0; - } - } else { - stb_textedit_delete_selection(str,state); // implicitly clamps - if (STB_TEXTEDIT_INSERTCHARS(str, state->cursor, decoded, decoded_size)) { - stb_text_makeundo_insert(state, state->cursor, decoded_size); - state->cursor += decoded_size; - state->has_preferred_x = 0; - } - } + IMSTB_TEXTEDIT_CHARTYPE ch = (IMSTB_TEXTEDIT_CHARTYPE)c; + stb_textedit_text(str, state, &ch, 1); } +#endif break; } From e240bc151a3650be8af0ea9d426d4fb44914c7e0 Mon Sep 17 00:00:00 2001 From: ocornut Date: Thu, 5 Sep 2024 16:29:30 +0200 Subject: [PATCH 04/12] InputText: amends: add note about STB_TEXTEDIT_GETCHAR. tweak InputTextCalcTextSize() to use similar debug-friendly logic as ImFont:CalcTextSizeA(). misc small tidying up. (#7925) --- imgui_widgets.cpp | 51 ++++++++++++++++++++++++++++------------------- 1 file changed, 31 insertions(+), 20 deletions(-) diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index f35563119..6d701d03b 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -3836,6 +3836,7 @@ static int InputTextCalcTextLenAndLineCount(const char* text_begin, const char** return line_count; } +// FIXME: Ideally we'd share code with ImFont::CalcTextSizeA() static ImVec2 InputTextCalcTextSize(ImGuiContext* ctx, const char* text_begin, const char* text_end, const char** remaining, ImVec2* out_offset, bool stop_on_new_line) { ImGuiContext& g = *ctx; @@ -3849,8 +3850,12 @@ static ImVec2 InputTextCalcTextSize(ImGuiContext* ctx, const char* text_begin, c const char* s = text_begin; while (s < text_end) { - unsigned int c; - s += ImTextCharFromUtf8(&c, s, text_end); + unsigned int c = (unsigned int)*s; + if (c < 0x80) + s += 1; + else + s += ImTextCharFromUtf8(&c, s, text_end); + if (c == '\n') { text_size.x = ImMax(text_size.x, line_width); @@ -3863,7 +3868,7 @@ static ImVec2 InputTextCalcTextSize(ImGuiContext* ctx, const char* text_begin, c if (c == '\r') continue; - const float char_width = font->GetCharAdvance((ImWchar)c) * scale; + const float char_width = ((int)c < font->IndexAdvanceX.Size ? font->IndexAdvanceX.Data[c] : font->FallbackAdvanceX) * scale; line_width += char_width; } @@ -3883,23 +3888,27 @@ static ImVec2 InputTextCalcTextSize(ImGuiContext* ctx, const char* text_begin, c } // Wrapper for stb_textedit.h to edit text (our wrapper is for: statically sized buffer, single-line, wchar characters. InputText converts between UTF-8 and wchar) +// With our UTF-8 use of stb_textedit: +// - STB_TEXTEDIT_GETCHAR is nothing more than a a "GETBYTE". It's only used to compare to ascii or to copy blocks of text so we are fine. +// - One exception is the STB_TEXTEDIT_IS_SPACE feature which would expect a full char in order to handle full-width space such as 0x3000 (see ImCharIsBlankW). +// - ...but we don't use that feature. namespace ImStb { - static int STB_TEXTEDIT_STRINGLEN(const ImGuiInputTextState* obj) { return obj->CurLenA; } static char STB_TEXTEDIT_GETCHAR(const ImGuiInputTextState* obj, int idx) { IM_ASSERT(idx <= obj->CurLenA); return obj->TextA[idx]; } static float STB_TEXTEDIT_GETWIDTH(ImGuiInputTextState* obj, int line_start_idx, int char_idx) { unsigned int c; ImTextCharFromUtf8(&c, obj->TextA.Data + line_start_idx + char_idx, obj->TextA.Data + obj->TextA.Size); if ((ImWchar)c == '\n') return IMSTB_TEXTEDIT_GETWIDTH_NEWLINE; ImGuiContext& g = *obj->Ctx; return g.Font->GetCharAdvance((ImWchar)c) * g.FontScale; } static char STB_TEXTEDIT_NEWLINE = '\n'; static void STB_TEXTEDIT_LAYOUTROW(StbTexteditRow* r, ImGuiInputTextState* obj, int line_start_idx) { + const char* text = obj->TextA.Data; const char* text_remaining = NULL; - const ImVec2 size = InputTextCalcTextSize(obj->Ctx, obj->TextA.Data + line_start_idx, obj->TextA.Data + obj->CurLenA, &text_remaining, NULL, true); + const ImVec2 size = InputTextCalcTextSize(obj->Ctx, text + line_start_idx, text + obj->CurLenA, &text_remaining, NULL, true); r->x0 = 0.0f; r->x1 = size.x; r->baseline_y_delta = size.y; r->ymin = 0.0f; r->ymax = size.y; - r->num_chars = (int)(text_remaining - (obj->TextA.Data + line_start_idx)); + r->num_chars = (int)(text_remaining - (text + line_start_idx)); } static bool ImCharIsSeparatorW(unsigned int c) @@ -3922,14 +3931,15 @@ static int is_word_boundary_from_right(ImGuiInputTextState* obj, int idx) if ((obj->Flags & ImGuiInputTextFlags_Password) || idx <= 0) return 0; - const char* prevPtr = ImTextFindPreviousUtf8Codepoint(obj->TextA.Data, obj->TextA.Data + idx); - unsigned int curr; ImTextCharFromUtf8(&curr, obj->TextA.Data + idx, obj->TextA.Data + obj->TextA.Size); - unsigned int prev; ImTextCharFromUtf8(&prev, prevPtr, obj->TextA.Data + obj->TextA.Size); + const char* curr_p = obj->TextA.Data + idx; + const char* prev_p = ImTextFindPreviousUtf8Codepoint(obj->TextA.Data, curr_p); + unsigned int curr_c; ImTextCharFromUtf8(&curr_c, curr_p, obj->TextA.Data + obj->TextA.Size); + unsigned int prev_c; ImTextCharFromUtf8(&prev_c, prev_p, obj->TextA.Data + obj->TextA.Size); - bool prev_white = ImCharIsBlankW(prev); - bool prev_separ = ImCharIsSeparatorW(prev); - bool curr_white = ImCharIsBlankW(curr); - bool curr_separ = ImCharIsSeparatorW(curr); + bool prev_white = ImCharIsBlankW(prev_c); + bool prev_separ = ImCharIsSeparatorW(prev_c); + bool curr_white = ImCharIsBlankW(curr_c); + bool curr_separ = ImCharIsSeparatorW(curr_c); return ((prev_white || prev_separ) && !(curr_separ || curr_white)) || (curr_separ && !prev_separ); } static int is_word_boundary_from_left(ImGuiInputTextState* obj, int idx) @@ -3937,14 +3947,15 @@ static int is_word_boundary_from_left(ImGuiInputTextState* obj, int idx) if ((obj->Flags & ImGuiInputTextFlags_Password) || idx <= 0) return 0; - const char* prevPtr = ImTextFindPreviousUtf8Codepoint(obj->TextA.Data, obj->TextA.Data + idx); - unsigned int prev; ImTextCharFromUtf8(&prev, obj->TextA.Data + idx, obj->TextA.Data + obj->TextA.Size); - unsigned int curr; ImTextCharFromUtf8(&curr, prevPtr, obj->TextA.Data + obj->TextA.Size); + const char* curr_p = obj->TextA.Data + idx; + const char* prev_p = ImTextFindPreviousUtf8Codepoint(obj->TextA.Data, curr_p); + unsigned int prev_c; ImTextCharFromUtf8(&prev_c, curr_p, obj->TextA.Data + obj->TextA.Size); + unsigned int curr_c; ImTextCharFromUtf8(&curr_c, prev_p, obj->TextA.Data + obj->TextA.Size); - bool prev_white = ImCharIsBlankW(prev); - bool prev_separ = ImCharIsSeparatorW(prev); - bool curr_white = ImCharIsBlankW(curr); - bool curr_separ = ImCharIsSeparatorW(curr); + bool prev_white = ImCharIsBlankW(prev_c); + bool prev_separ = ImCharIsSeparatorW(prev_c); + bool curr_white = ImCharIsBlankW(curr_c); + bool curr_separ = ImCharIsSeparatorW(curr_c); return ((prev_white) && !(curr_separ || curr_white)) || (curr_separ && !prev_separ); } static int STB_TEXTEDIT_MOVEWORDLEFT_IMPL(ImGuiInputTextState* obj, int idx) { idx--; while (idx >= 0 && !is_word_boundary_from_right(obj, idx)) idx--; return idx < 0 ? 0 : idx; } From 3d1e593b5b1c6842001a5cf9db1845c93df07689 Mon Sep 17 00:00:00 2001 From: ocornut Date: Thu, 5 Sep 2024 19:56:12 +0200 Subject: [PATCH 05/12] InputText: amends: fixed next/prev word implementation. (#7925) + replace IMSTB_TEXTEDIT_GETPREVCHARINDEX code with ImTextFindPreviousUtf8Codepoint(). --- imgui_widgets.cpp | 91 ++++++++++++++++++++++------------------------- 1 file changed, 42 insertions(+), 49 deletions(-) diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 6d701d03b..24d1fda7e 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -3911,6 +3911,25 @@ static void STB_TEXTEDIT_LAYOUTROW(StbTexteditRow* r, ImGuiInputTextState* ob r->num_chars = (int)(text_remaining - (text + line_start_idx)); } +#define IMSTB_TEXTEDIT_GETNEXTCHARINDEX IMSTB_TEXTEDIT_GETNEXTCHARINDEX_IMPL +#define IMSTB_TEXTEDIT_GETPREVCHARINDEX IMSTB_TEXTEDIT_GETPREVCHARINDEX_IMPL + +static int IMSTB_TEXTEDIT_GETNEXTCHARINDEX_IMPL(ImGuiInputTextState* obj, int idx) +{ + if (idx >= obj->CurLenA) + return obj->CurLenA + 1; + unsigned int c; + return idx + ImTextCharFromUtf8(&c, obj->TextA.Data + idx, obj->TextA.Data + obj->TextA.Size); +} + +static int IMSTB_TEXTEDIT_GETPREVCHARINDEX_IMPL(ImGuiInputTextState* obj, int idx) +{ + if (idx <= 0) + return -1; + const char* p = ImTextFindPreviousUtf8Codepoint(obj->TextA.Data, obj->TextA.Data + idx); + return (int)(p - obj->TextA.Data); +} + static bool ImCharIsSeparatorW(unsigned int c) { static const unsigned int separator_list[] = @@ -3958,58 +3977,32 @@ static int is_word_boundary_from_left(ImGuiInputTextState* obj, int idx) bool curr_separ = ImCharIsSeparatorW(curr_c); return ((prev_white) && !(curr_separ || curr_white)) || (curr_separ && !prev_separ); } -static int STB_TEXTEDIT_MOVEWORDLEFT_IMPL(ImGuiInputTextState* obj, int idx) { idx--; while (idx >= 0 && !is_word_boundary_from_right(obj, idx)) idx--; return idx < 0 ? 0 : idx; } -static int STB_TEXTEDIT_MOVEWORDRIGHT_MAC(ImGuiInputTextState* obj, int idx) { idx++; int len = obj->CurLenA; while (idx < len && !is_word_boundary_from_left(obj, idx)) idx++; return idx > len ? len : idx; } -static int STB_TEXTEDIT_MOVEWORDRIGHT_WIN(ImGuiInputTextState* obj, int idx) { idx++; int len = obj->CurLenA; while (idx < len && !is_word_boundary_from_right(obj, idx)) idx++; return idx > len ? len : idx; } +static int STB_TEXTEDIT_MOVEWORDLEFT_IMPL(ImGuiInputTextState* obj, int idx) +{ + idx = IMSTB_TEXTEDIT_GETPREVCHARINDEX(obj, idx); + while (idx >= 0 && !is_word_boundary_from_right(obj, idx)) + idx = IMSTB_TEXTEDIT_GETPREVCHARINDEX(obj, idx); + return idx < 0 ? 0 : idx; +} +static int STB_TEXTEDIT_MOVEWORDRIGHT_MAC(ImGuiInputTextState* obj, int idx) +{ + int len = obj->CurLenA; + idx = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(obj, idx); + while (idx < len && !is_word_boundary_from_left(obj, idx)) + idx = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(obj, idx); + return idx > len ? len : idx; +} +static int STB_TEXTEDIT_MOVEWORDRIGHT_WIN(ImGuiInputTextState* obj, int idx) +{ + idx = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(obj, idx); + int len = obj->CurLenA; + while (idx < len && !is_word_boundary_from_right(obj, idx)) + idx = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(obj, idx); + return idx > len ? len : idx; +} static int STB_TEXTEDIT_MOVEWORDRIGHT_IMPL(ImGuiInputTextState* obj, int idx) { ImGuiContext& g = *obj->Ctx; if (g.IO.ConfigMacOSXBehaviors) return STB_TEXTEDIT_MOVEWORDRIGHT_MAC(obj, idx); else return STB_TEXTEDIT_MOVEWORDRIGHT_WIN(obj, idx); } #define STB_TEXTEDIT_MOVEWORDLEFT STB_TEXTEDIT_MOVEWORDLEFT_IMPL // They need to be #define for stb_textedit.h #define STB_TEXTEDIT_MOVEWORDRIGHT STB_TEXTEDIT_MOVEWORDRIGHT_IMPL -#define IMSTB_TEXTEDIT_GETNEXTCHARINDEX IMSTB_TEXTEDIT_GETNEXTCHARINDEX_IMPL -#define IMSTB_TEXTEDIT_GETPREVCHARINDEX IMSTB_TEXTEDIT_GETPREVCHARINDEX_IMPL - -static int IMSTB_TEXTEDIT_GETNEXTCHARINDEX_IMPL(ImGuiInputTextState* obj, int idx) -{ - unsigned int c; - return idx + ImTextCharFromUtf8(&c, obj->TextA.Data + idx, obj->TextA.Data + obj->TextA.Size); -} - -static int CountLeadingHighBits(unsigned char b) -{ - for (int i = 0; i < (int)sizeof(b) * 8; i++) - { - bool set = (b >> (7 - i)) & 1; - if (!set) - return i; - } - - return sizeof(b) * 8; -} - -static int IMSTB_TEXTEDIT_GETPREVCHARINDEX_IMPL(ImGuiInputTextState* obj, int idx) -{ - // Backwards check/count for UTF-8 multi byte sequence - int num_seq_bytes = 0; - for (int i = idx - 1; i >= 0; i -= 1) - { - bool is_seq_byte = (obj->TextA.Data[i] & 0x80) == 0x80 && (obj->TextA.Data[i] & 0x40) == 0; - num_seq_bytes += is_seq_byte; - if (!is_seq_byte) - { - if (num_seq_bytes > 0) - { - char initial_byte = obj->TextA.Data[i]; - char num_leading_bits = (char)CountLeadingHighBits(initial_byte); - bool is_multi_byte_seq = num_leading_bits == num_seq_bytes + 1; - if (is_multi_byte_seq) - { - return idx - (num_seq_bytes + 1); - } - } - break; - } - } - return idx - 1; -} static void STB_TEXTEDIT_DELETECHARS(ImGuiInputTextState* obj, int pos, int n) { From 19accb14a9e52f515624485ef8ea6eba8dbab7c3 Mon Sep 17 00:00:00 2001 From: ocornut Date: Wed, 11 Sep 2024 12:57:54 +0200 Subject: [PATCH 06/12] InputText: amends: fixed undo-stack reconcile. fixed metrics crash. fixes character filtering. (#7925) Refer to imgui_test_suite for tests. --- imgui_internal.h | 3 ++- imgui_widgets.cpp | 31 ++++++++++++++++--------------- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/imgui_internal.h b/imgui_internal.h index e6f3f8ec5..b54848635 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -1112,8 +1112,9 @@ struct IMGUI_API ImGuiInputTextState ImStbTexteditState* Stb; // State for stb_textedit.h ImGuiID ID; // widget id owning the text state int CurLenA; // UTF-8 length of the string in TextA (in bytes) - ImVector TextA; // temporary UTF8 buffer for callbacks and other operations. this is not updated in every code-path! size=capacity. + ImVector TextA; // main UTF8 buffer. ImVector InitialTextA; // value to revert to when pressing Escape = backup of end-user buffer at the time of focus (in UTF-8, unaltered) + ImVector CallbackTextBackup; // temporary storage for callback to support automatic reconcile of undo-stack int BufCapacityA; // end-user buffer capacity ImVec2 Scroll; // horizontal offset (managed manually) + vertical scrolling (pulled from child window's own Scroll.y) float CursorAnim; // timer for cursor blink, reset on every user action so the cursor reappears immediately diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 24d1fda7e..b5c69e73e 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -4301,8 +4301,8 @@ static bool InputTextFilterCharacter(ImGuiContext* ctx, unsigned int* p_char, Im // FIXME: Ideally we should transition toward (1) making InsertChars()/DeleteChars() update undo-stack (2) discourage (and keep reconcile) or obsolete (and remove reconcile) accessing buffer directly. static void InputTextReconcileUndoStateAfterUserCallback(ImGuiInputTextState* state, const char* new_buf_a, int new_length_a) { - const char* old_buf = state->TextA.Data; - const int old_length = state->CurLenA; + const char* old_buf = state->CallbackTextBackup.Data; + const int old_length = state->CallbackTextBackup.Size - 1; const int shorter_length = ImMin(old_length, new_length_a); int first_diff; @@ -4323,7 +4323,7 @@ static void InputTextReconcileUndoStateAfterUserCallback(ImGuiInputTextState* st if (insert_len > 0 || delete_len > 0) if (IMSTB_TEXTEDIT_CHARTYPE* p = stb_text_createundo(&state->Stb->undostate, first_diff, delete_len, insert_len)) for (int i = 0; i < delete_len; i++) - p[i] = ImStb::STB_TEXTEDIT_GETCHAR(state, first_diff + i); + p[i] = old_buf[first_diff + i]; } // As InputText() retain textual data and we currently provide a path for user to not retain it (via local variables) @@ -4598,11 +4598,9 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ } // Process mouse inputs and character inputs - int backup_current_text_length = 0; if (g.ActiveId == id) { IM_ASSERT(state != NULL); - backup_current_text_length = state->CurLenA; state->Edited = false; state->BufCapacityA = buf_size; state->Flags = flags; @@ -4856,11 +4854,11 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ { unsigned int c; int len = ImTextCharFromUtf8(&c, s, NULL); + s += len; if (!InputTextFilterCharacter(&g, &c, flags, callback, callback_user_data, true)) continue; - memcpy(clipboard_filtered + clipboard_filtered_len, s, len); + memcpy(clipboard_filtered + clipboard_filtered_len, s - len, len); clipboard_filtered_len += len; - s += len; } clipboard_filtered[clipboard_filtered_len] = 0; if (clipboard_filtered_len > 0) // If everything was filtered, ignore the pasting operation @@ -4960,6 +4958,10 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ callback_data.Flags = flags; callback_data.UserData = callback_user_data; + // FIXME-OPT: Undo stack reconcile needs a backup of the data until we rework API, see #7925 + state->CallbackTextBackup.resize(state->CurLenA + 1); + memcpy(state->CallbackTextBackup.Data, state->TextA.Data, state->CurLenA + 1); + char* callback_buf = is_readonly ? buf : state->TextA.Data; callback_data.EventKey = event_key; callback_data.Buf = callback_buf; @@ -4988,11 +4990,8 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ // Callback may update buffer and thus set buf_dirty even in read-only mode. IM_ASSERT(callback_data.BufTextLen == (int)strlen(callback_data.Buf)); // You need to maintain BufTextLen if you change the text! InputTextReconcileUndoStateAfterUserCallback(state, callback_data.Buf, callback_data.BufTextLen); // FIXME: Move the rest of this block inside function and rename to InputTextReconcileStateAfterUserCallback() ? - if (callback_data.BufTextLen > backup_current_text_length && is_resizable) - state->TextA.resize(state->TextA.Size + (callback_data.BufTextLen - backup_current_text_length)); // Worse case scenario resize - - memcpy(state->TextA.Data, callback_data.Buf, callback_data.BufTextLen); state->CurLenA = callback_data.BufTextLen; // Assume correct length and valid UTF-8 from user, saves us an extra strlen() + state->TextA.Size = state->CurLenA + 1; state->CursorAnimReset(); } } @@ -5024,9 +5023,9 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ // Copy result to user buffer. This can currently only happen when (g.ActiveId == id) if (apply_new_text != NULL) { - // We cannot test for 'backup_current_text_length != apply_new_text_length' here because we have no guarantee that the size - // of our owned buffer matches the size of the string object held by the user, and by design we allow InputText() to be used - // without any storage on user's side. + //// We cannot test for 'backup_current_text_length != apply_new_text_length' here because we have no guarantee that the size + //// of our owned buffer matches the size of the string object held by the user, and by design we allow InputText() to be used + //// without any storage on user's side. IM_ASSERT(apply_new_text_length >= 0); if (is_resizable) { @@ -5325,8 +5324,10 @@ void ImGui::DebugNodeInputTextState(ImGuiInputTextState* state) const char undo_rec_type = (n < undo_state->undo_point) ? 'u' : (n >= undo_state->redo_point) ? 'r' : ' '; if (undo_rec_type == ' ') BeginDisabled(); + const int buf_preview_len = (undo_rec_type != ' ' && undo_rec->char_storage != -1) ? undo_rec->insert_length : 0; + const char* buf_preview_str = undo_state->undo_char + undo_rec->char_storage; Text("%c [%02d] where %03d, insert %03d, delete %03d, char_storage %03d \"%.*s\"", - undo_rec_type, n, undo_rec->where, undo_rec->insert_length, undo_rec->delete_length, undo_rec->char_storage, undo_rec->insert_length, undo_state->undo_char + undo_rec->char_storage); + undo_rec_type, n, undo_rec->where, undo_rec->insert_length, undo_rec->delete_length, undo_rec->char_storage, buf_preview_len, buf_preview_str); if (undo_rec_type == ' ') EndDisabled(); } From 8807b01b28883dfbf7b971a4aa6c1199e4dea827 Mon Sep 17 00:00:00 2001 From: ocornut Date: Wed, 11 Sep 2024 14:15:23 +0200 Subject: [PATCH 07/12] InputText: amends: now can use memchr()/strchr() for line counting. Much faster! (#7925) --- docs/CHANGELOG.txt | 4 ++++ docs/TODO.txt | 1 - imgui_widgets.cpp | 24 ++++++++++-------------- 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/docs/CHANGELOG.txt b/docs/CHANGELOG.txt index 35c4c6743..1611bf59b 100644 --- a/docs/CHANGELOG.txt +++ b/docs/CHANGELOG.txt @@ -63,6 +63,10 @@ Other changes: #6223, #6364, #6387, #6567, #6692, #6724, #6939, #6984, #7246, #7270, #7375, #7421, #7434, #7472, #7581, #7724, #7926, #7937 and probably more..) - Nav: pressing any keyboard key while holding Alt disable toggling nav layer on Alt release. (#4439) +- InputText: internal refactoring to simplify and optimize the code. The ImWchar buffer has been + removed. Simplifications allowed to implement new optimizations for handling very large text buffers + (e.g. in our testing, handling of a 1 MB text buffer is now 3 times faster in VS2022 Debug build). + This is the first step toward more refactorig. (#7925) [@alektron, @ocornut] - InputText: added CJK double-width punctuation to list of separators considered for CTRL+Arrow. - TextLinkOpenURL(): modified tooltip to display a verb "Open 'xxxx'". (#7885, #7660) - Backends: SDL2: use SDL_Vulkan_GetDrawableSize() when available. (#7967, #3190) [@scribam] diff --git a/docs/TODO.txt b/docs/TODO.txt index 84a00bc6e..ddceb3b3d 100644 --- a/docs/TODO.txt +++ b/docs/TODO.txt @@ -65,7 +65,6 @@ It's mostly a bunch of personal notes, probably incomplete. Feel free to query i - selectable: generic BeginSelectable()/EndSelectable() mechanism. (work out alongside range-select branch) - selectable: a way to visualize partial/mixed selection (e.g. parent tree node has children with mixed selection) - - input text: clean up the mess caused by converting UTF-8 <> wchar. the code is rather inefficient right now and super fragile. (WIP branch) - input text: preserve scrolling when unfocused? - input text: reorganize event handling, allow CharFilter to modify buffers, allow multiple events? (#541) - input text: expose CursorPos in char filter event (#816) diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index b5c69e73e..94e1abff6 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -5119,14 +5119,12 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ // In multi-line mode, we never exit the loop until all lines are counted, so add one extra to the searches_remaining counter. searches_remaining += is_multiline ? 1 : 0; int line_count = 0; - //for (const ImWchar* s = text_begin; (s = (const ImWchar*)wcschr((const wchar_t*)s, (wchar_t)'\n')) != NULL; s++) // FIXME-OPT: Could use this when wchar_t are 16-bit - for (const char* s = text_begin; *s != 0; s++) - if (*s == '\n') - { - line_count++; - if (searches_result_line_no[0] == -1 && s >= searches_input_ptr[0]) { searches_result_line_no[0] = line_count; if (--searches_remaining <= 0) break; } - if (searches_result_line_no[1] == -1 && s >= searches_input_ptr[1]) { searches_result_line_no[1] = line_count; if (--searches_remaining <= 0) break; } - } + for (const char* s = text_begin; (s = strchr(s, '\n')) != NULL; s++) // FIXME-OPT: memchr() would be faster? + { + line_count++; + if (searches_result_line_no[0] == -1 && s >= searches_input_ptr[0]) { searches_result_line_no[0] = line_count; if (--searches_remaining <= 0) break; } + if (searches_result_line_no[1] == -1 && s >= searches_input_ptr[1]) { searches_result_line_no[1] = line_count; if (--searches_remaining <= 0) break; } + } line_count++; if (searches_result_line_no[0] == -1) searches_result_line_no[0] = line_count; @@ -5199,11 +5197,8 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ break; if (rect_pos.y < clip_rect.y) { - //p = (const ImWchar*)wmemchr((const wchar_t*)p, '\n', text_selected_end - p); // FIXME-OPT: Could use this when wchar_t are 16-bit - //p = p ? p + 1 : text_selected_end; - while (p < text_selected_end) - if (*p++ == '\n') - break; + p = (const char*)memchr((void*)p, '\n', text_selected_end - p); + p = p ? p + 1 : text_selected_end; } else { @@ -5213,13 +5208,14 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ rect.ClipWith(clip_rect); if (rect.Overlaps(clip_rect)) draw_window->DrawList->AddRectFilled(rect.Min, rect.Max, bg_color); + rect_pos.x = draw_pos.x - draw_scroll.x; } - rect_pos.x = draw_pos.x - draw_scroll.x; rect_pos.y += g.FontSize; } } // 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); From dc6346b763e4c92fe57887ee1cb4fe557517d4fa Mon Sep 17 00:00:00 2001 From: ocornut Date: Thu, 12 Sep 2024 19:22:09 +0200 Subject: [PATCH 08/12] Fonts: fixed ellipsis "..." rendering width miscalculation bug introduced in 1.91.0. (#7976) Amend 0f63d3e9 --- docs/CHANGELOG.txt | 1 + imgui.cpp | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/CHANGELOG.txt b/docs/CHANGELOG.txt index 1611bf59b..28b3cae26 100644 --- a/docs/CHANGELOG.txt +++ b/docs/CHANGELOG.txt @@ -68,6 +68,7 @@ Other changes: (e.g. in our testing, handling of a 1 MB text buffer is now 3 times faster in VS2022 Debug build). This is the first step toward more refactorig. (#7925) [@alektron, @ocornut] - InputText: added CJK double-width punctuation to list of separators considered for CTRL+Arrow. +- Fonts: fixed ellipsis "..." rendering width miscalculation bug introduced in 1.91.0. (#7976) [@DDeimos] - TextLinkOpenURL(): modified tooltip to display a verb "Open 'xxxx'". (#7885, #7660) - Backends: SDL2: use SDL_Vulkan_GetDrawableSize() when available. (#7967, #3190) [@scribam] diff --git a/imgui.cpp b/imgui.cpp index 71b1b1011..3c2fd6658 100644 --- a/imgui.cpp +++ b/imgui.cpp @@ -4007,7 +4007,7 @@ static void SetCurrentWindow(ImGuiWindow* window) if (window) { g.FontSize = g.DrawListSharedData.FontSize = window->CalcFontSize(); - g.FontScale = g.FontSize / g.Font->FontSize; + g.FontScale = g.DrawListSharedData.FontScale = g.FontSize / g.Font->FontSize; ImGui::NavUpdateCurrentWindowIsScrollPushableX(); } } From 52a6ab44b1f0387922ec802ad909cf32a5283f24 Mon Sep 17 00:00:00 2001 From: David Briscoe <43559+idbrii@users.noreply.github.com> Date: Thu, 12 Sep 2024 10:24:37 -0700 Subject: [PATCH 09/12] Doc: Fix typo. (#7974) --- imgui.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/imgui.cpp b/imgui.cpp index 3c2fd6658..59a918e86 100644 --- a/imgui.cpp +++ b/imgui.cpp @@ -9046,7 +9046,7 @@ bool ImGui::IsKeyPressed(ImGuiKey key, bool repeat) return IsKeyPressed(key, repeat ? ImGuiInputFlags_Repeat : ImGuiInputFlags_None, ImGuiKeyOwner_Any); } -// Important: unless legacy IsKeyPressed(ImGuiKey, bool repeat=true) which DEFAULT to repeat, this requires EXPLICIT repeat. +// Important: unlike legacy IsKeyPressed(ImGuiKey, bool repeat=true) which DEFAULT to repeat, this requires EXPLICIT repeat. bool ImGui::IsKeyPressed(ImGuiKey key, ImGuiInputFlags flags, ImGuiID owner_id) { const ImGuiKeyData* key_data = GetKeyData(key); From ac2ad79812e8f17ed4d76b8645c3f10e385018c8 Mon Sep 17 00:00:00 2001 From: Yan Pujante Date: Sat, 7 Sep 2024 10:22:45 -0700 Subject: [PATCH 10/12] Backends: GLFW+Emscripten: use OSX behaviors automatically when using contrib glfw port. (#7965, #7915) --- backends/imgui_impl_glfw.cpp | 22 +++++++++++++++++++++- docs/CHANGELOG.txt | 2 ++ examples/example_glfw_wgpu/CMakeLists.txt | 1 - 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/backends/imgui_impl_glfw.cpp b/backends/imgui_impl_glfw.cpp index 06b5142a1..9c6722f78 100644 --- a/backends/imgui_impl_glfw.cpp +++ b/backends/imgui_impl_glfw.cpp @@ -18,6 +18,11 @@ // - Documentation https://dearimgui.com/docs (same as your local docs/ folder). // - Introduction, links and more at the top of imgui.cpp +// About Emscripten support: +// - Emscripten provides its own GLFW (3.2.1) implementation (syntax: "-sUSE_GLFW=3"), but Joystick is broken and several features are not supported (multiple windows, clipboard, timer, etc.) +// - A third-party Emscripten GLFW (3.4.0) implementation (syntax: "--use-port=contrib.glfw3") fixes the Joystick issue and implements all relevant features for the browser. +// See https://github.com/pongasoft/emscripten-glfw/blob/master/docs/Comparison.md for details. + // CHANGELOG // (minor and older changes stripped away, please see git history for details) // 2024-08-22: moved some OS/backend related function pointers from ImGuiIO to ImGuiPlatformIO: @@ -560,7 +565,7 @@ void ImGui_ImplGlfw_SetCallbacksChainForAllWindows(bool chain_for_all_windows) } #ifdef __EMSCRIPTEN__ -#if EMSCRIPTEN_USE_PORT_CONTRIB_GLFW3 >= 3'4'0'20240817 +#if EMSCRIPTEN_USE_PORT_CONTRIB_GLFW3 >= 34020240817 void ImGui_ImplGlfw_EmscriptenOpenURL(const char* url) { if (url) emscripten::glfw3::OpenURL(url); } #else EM_JS(void, ImGui_ImplGlfw_EmscriptenOpenURL, (const char* url), { url = url ? UTF8ToString(url) : null; if (url) window.open(url, '_blank'); }); @@ -639,6 +644,21 @@ static bool ImGui_ImplGlfw_Init(GLFWwindow* window, bool install_callbacks, Glfw ::SetWindowLongPtrW((HWND)main_viewport->PlatformHandleRaw, GWLP_WNDPROC, (LONG_PTR)ImGui_ImplGlfw_WndProc); #endif + // Emscripten: the same application can run on various platforms, so we detect the Apple platform at runtime + // to override io.ConfigMacOSXBehaviors from its default (which is always false in Emscripten). +#if EMSCRIPTEN_USE_PORT_CONTRIB_GLFW3 >= 34020240817 + if (emscripten::glfw3::IsRuntimePlatformApple()) + { + ImGui::GetIO().ConfigMacOSXBehaviors = true; + + // Due to how the browser (poorly) handles the Meta Key, this line essentially disables repeats when used. + // This means that Meta + V only registers a single key-press, even if the keys are held. + // This is a compromise for dealing with this issue in ImGui since ImGui implements key repeat itself. + // See https://github.com/pongasoft/emscripten-glfw/blob/v3.4.0.20240817/docs/Usage.md#the-problem-of-the-super-key + emscripten::glfw3::SetSuperPlusKeyTimeouts(10, 10); + } +#endif + bd->ClientApi = client_api; return true; } diff --git a/docs/CHANGELOG.txt b/docs/CHANGELOG.txt index 28b3cae26..2189f2584 100644 --- a/docs/CHANGELOG.txt +++ b/docs/CHANGELOG.txt @@ -71,6 +71,8 @@ Other changes: - Fonts: fixed ellipsis "..." rendering width miscalculation bug introduced in 1.91.0. (#7976) [@DDeimos] - TextLinkOpenURL(): modified tooltip to display a verb "Open 'xxxx'". (#7885, #7660) - Backends: SDL2: use SDL_Vulkan_GetDrawableSize() when available. (#7967, #3190) [@scribam] +- Backends: GLFW+Emscripten: use OSX behaviors automatically when using contrib glfw port. (#7965, #7915) + [@ypujante] ----------------------------------------------------------------------- VERSION 1.91.1 (Released 2024-09-04) diff --git a/examples/example_glfw_wgpu/CMakeLists.txt b/examples/example_glfw_wgpu/CMakeLists.txt index 77b492fdc..6be95af44 100644 --- a/examples/example_glfw_wgpu/CMakeLists.txt +++ b/examples/example_glfw_wgpu/CMakeLists.txt @@ -91,7 +91,6 @@ if(EMSCRIPTEN) if("${IMGUI_EMSCRIPTEN_GLFW3}" STREQUAL "--use-port=contrib.glfw3") target_compile_options(example_glfw_wgpu PUBLIC "${IMGUI_EMSCRIPTEN_GLFW3}" - "-DEMSCRIPTEN_USE_PORT_CONTRIB_GLFW3" # unnecessary beyond emscripten 3.1.59 ) endif() message(STATUS "Using ${IMGUI_EMSCRIPTEN_GLFW3} GLFW implementation") From 6dcb7be35f8e9b54d4a3e3c75a97ab09589e4f38 Mon Sep 17 00:00:00 2001 From: Yan Pujante Date: Sat, 7 Sep 2024 10:23:20 -0700 Subject: [PATCH 11/12] CI: amend build tests for emscripten (#7965) + Backends: GLFW: minor preemptive fix. --- .github/workflows/build.yml | 16 +++++++++++++++- backends/imgui_impl_glfw.cpp | 2 ++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1d43da032..f95ff43c8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -527,6 +527,7 @@ jobs: emsdk-master/emsdk update emsdk-master/emsdk install latest emsdk-master/emsdk activate latest + sudo apt-get install build-essential - name: Build example_sdl2_opengl3 with Emscripten run: | @@ -535,13 +536,26 @@ jobs: popd make -C examples/example_sdl2_opengl3 -f Makefile.emscripten - - name: Build example_glfw_wgpu + # This build compiles example_glfw_wgpu using Makefile.emscripten and Emscripten GLFW built-in implementation (-sUSE_GLFW=3) + # This ensures 2 things: the make build works, and the GLFW built-in implementation is tested + - name: Build example_glfw_wgpu with Emscripten/Makefile run: | pushd emsdk-master source ./emsdk_env.sh popd make -C examples/example_glfw_wgpu -f Makefile.emscripten + # This build compiles example_glfw_wgpu using CMakeLists.txt and Emscripten GLFW contrib port (--use-port=contrib.glfw3) + # This ensures 2 things: the CMake build works, and the GLFW contrib port is tested + - name: Build example_glfw_wgpu with Emscripten/CMake + run: | + pushd emsdk-master + source ./emsdk_env.sh + popd + emcc -v + emcmake cmake -B build -DCMAKE_BUILD_TYPE=Release examples/example_glfw_wgpu + cmake --build build + Android: runs-on: ubuntu-24.04 steps: diff --git a/backends/imgui_impl_glfw.cpp b/backends/imgui_impl_glfw.cpp index 9c6722f78..30afaf4f0 100644 --- a/backends/imgui_impl_glfw.cpp +++ b/backends/imgui_impl_glfw.cpp @@ -646,6 +646,7 @@ static bool ImGui_ImplGlfw_Init(GLFWwindow* window, bool install_callbacks, Glfw // Emscripten: the same application can run on various platforms, so we detect the Apple platform at runtime // to override io.ConfigMacOSXBehaviors from its default (which is always false in Emscripten). +#ifdef __EMSCRIPTEN__ #if EMSCRIPTEN_USE_PORT_CONTRIB_GLFW3 >= 34020240817 if (emscripten::glfw3::IsRuntimePlatformApple()) { @@ -657,6 +658,7 @@ static bool ImGui_ImplGlfw_Init(GLFWwindow* window, bool install_callbacks, Glfw // See https://github.com/pongasoft/emscripten-glfw/blob/v3.4.0.20240817/docs/Usage.md#the-problem-of-the-super-key emscripten::glfw3::SetSuperPlusKeyTimeouts(10, 10); } +#endif #endif bd->ClientApi = client_api; From 4d00bf8add44155a4f6b489538bd77d6214a4432 Mon Sep 17 00:00:00 2001 From: ocornut Date: Thu, 12 Sep 2024 23:13:32 +0200 Subject: [PATCH 12/12] MultiSelect+Tables: fixed an issue where box-select would skip items while drag-scrolling in a table with outer borders. (#7970, #7821). See "widgets_multiselect_boxselect_2" test. --- docs/CHANGELOG.txt | 2 ++ imgui_widgets.cpp | 7 ++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/CHANGELOG.txt b/docs/CHANGELOG.txt index 2189f2584..903af40c3 100644 --- a/docs/CHANGELOG.txt +++ b/docs/CHANGELOG.txt @@ -63,6 +63,8 @@ Other changes: #6223, #6364, #6387, #6567, #6692, #6724, #6939, #6984, #7246, #7270, #7375, #7421, #7434, #7472, #7581, #7724, #7926, #7937 and probably more..) - Nav: pressing any keyboard key while holding Alt disable toggling nav layer on Alt release. (#4439) +- MultiSelect+Tables: fixed an issue where box-select would skip items while drag-scrolling + in a table with outer borders. (#7970, #7821). - InputText: internal refactoring to simplify and optimize the code. The ImWchar buffer has been removed. Simplifications allowed to implement new optimizations for handling very large text buffers (e.g. in our testing, handling of a 1 MB text buffer is now 3 times faster in VS2022 Debug build). diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 94e1abff6..14c5af3e4 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7403,6 +7403,7 @@ static void DebugLogMultiSelectRequests(const char* function, const ImGuiMultiSe static ImRect CalcScopeRect(ImGuiMultiSelectTempData* ms, ImGuiWindow* window) { + ImGuiContext& g = *GImGui; if (ms->Flags & ImGuiMultiSelectFlags_ScopeRect) { // Warning: this depends on CursorMaxPos so it means to be called by EndMultiSelect() only @@ -7410,8 +7411,12 @@ static ImRect CalcScopeRect(ImGuiMultiSelectTempData* ms, ImGuiWindow* window) } else { - // Add inner table decoration (#7821) // FIXME: Why not baking in InnerClipRect? + // When a table, pull HostClipRect, which allows us to predict ClipRect before first row/layout is performed. (#7970) ImRect scope_rect = window->InnerClipRect; + if (g.CurrentTable != NULL) + scope_rect = g.CurrentTable->HostClipRect; + + // Add inner table decoration (#7821) // FIXME: Why not baking in InnerClipRect? scope_rect.Min = ImMin(scope_rect.Min + ImVec2(window->DecoInnerSizeX1, window->DecoInnerSizeY1), scope_rect.Max); return scope_rect; }