Skip to content

Commit

Permalink
InputText: use TextSrc more consistently to facilitate accessing user…
Browse files Browse the repository at this point in the history
… buffer in text processing code. (#8242)

Followup to e900571
Removed SetClipboardText() trick used in abd07f6 (#7925)
  • Loading branch information
ocornut committed Dec 18, 2024
1 parent e900571 commit 32f1140
Show file tree
Hide file tree
Showing 2 changed files with 36 additions and 44 deletions.
1 change: 1 addition & 0 deletions imgui_internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -1124,6 +1124,7 @@ struct IMGUI_API ImGuiInputTextState
ImGuiInputTextFlags Flags; // copy of InputText() flags. may be used to check if e.g. ImGuiInputTextFlags_Password is set.
ImGuiID ID; // widget id owning the text state
int TextLen; // UTF-8 length of the string in TextA (in bytes)
const char* TextSrc; // == TextA.Data unless read-only, in which case == buf passed to InputText(). Field only set and valid _inside_ the call InputText() call.
ImVector<char> TextA; // main UTF8 buffer. TextA.Size is a buffer size! Should always be >= buf_size passed by user (and of course >= CurLenA + 1).
ImVector<char> TextToRevertTo; // value to revert to when pressing Escape = backup of end-user buffer at the time of focus (in UTF-8, unaltered)
ImVector<char> CallbackTextBackup; // temporary storage for callback to support automatic reconcile of undo-stack
Expand Down
79 changes: 35 additions & 44 deletions imgui_widgets.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3938,12 +3938,12 @@ static ImVec2 InputTextCalcTextSize(ImGuiContext* ctx, const char* text_begin, c
namespace ImStb
{
static int STB_TEXTEDIT_STRINGLEN(const ImGuiInputTextState* obj) { return obj->TextLen; }
static char STB_TEXTEDIT_GETCHAR(const ImGuiInputTextState* obj, int idx) { IM_ASSERT(idx <= obj->TextLen); 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->TextLen); 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_GETCHAR(const ImGuiInputTextState* obj, int idx) { IM_ASSERT(idx <= obj->TextLen); return obj->TextSrc[idx]; }
static float STB_TEXTEDIT_GETWIDTH(ImGuiInputTextState* obj, int line_start_idx, int char_idx) { unsigned int c; ImTextCharFromUtf8(&c, obj->TextSrc + line_start_idx + char_idx, obj->TextSrc + obj->TextLen); 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 = obj->TextSrc;
const char* text_remaining = NULL;
const ImVec2 size = InputTextCalcTextSize(obj->Ctx, text + line_start_idx, text + obj->TextLen, &text_remaining, NULL, true);
r->x0 = 0.0f;
Expand All @@ -3962,15 +3962,15 @@ static int IMSTB_TEXTEDIT_GETNEXTCHARINDEX_IMPL(ImGuiInputTextState* obj, int id
if (idx >= obj->TextLen)
return obj->TextLen + 1;
unsigned int c;
return idx + ImTextCharFromUtf8(&c, obj->TextA.Data + idx, obj->TextA.Data + obj->TextLen);
return idx + ImTextCharFromUtf8(&c, obj->TextSrc + idx, obj->TextSrc + obj->TextLen);
}

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);
const char* p = ImTextFindPreviousUtf8Codepoint(obj->TextSrc, obj->TextSrc + idx);
return (int)(p - obj->TextSrc);
}

static bool ImCharIsSeparatorW(unsigned int c)
Expand All @@ -3993,10 +3993,10 @@ static int is_word_boundary_from_right(ImGuiInputTextState* obj, int idx)
if ((obj->Flags & ImGuiInputTextFlags_Password) || idx <= 0)
return 0;

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->TextLen);
unsigned int prev_c; ImTextCharFromUtf8(&prev_c, prev_p, obj->TextA.Data + obj->TextLen);
const char* curr_p = obj->TextSrc + idx;
const char* prev_p = ImTextFindPreviousUtf8Codepoint(obj->TextSrc, curr_p);
unsigned int curr_c; ImTextCharFromUtf8(&curr_c, curr_p, obj->TextSrc + obj->TextLen);
unsigned int prev_c; ImTextCharFromUtf8(&prev_c, prev_p, obj->TextSrc + obj->TextLen);

bool prev_white = ImCharIsBlankW(prev_c);
bool prev_separ = ImCharIsSeparatorW(prev_c);
Expand All @@ -4009,10 +4009,10 @@ static int is_word_boundary_from_left(ImGuiInputTextState* obj, int idx)
if ((obj->Flags & ImGuiInputTextFlags_Password) || idx <= 0)
return 0;

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->TextLen);
unsigned int curr_c; ImTextCharFromUtf8(&curr_c, prev_p, obj->TextA.Data + obj->TextLen);
const char* curr_p = obj->TextSrc + idx;
const char* prev_p = ImTextFindPreviousUtf8Codepoint(obj->TextSrc, curr_p);
unsigned int prev_c; ImTextCharFromUtf8(&prev_c, curr_p, obj->TextSrc + obj->TextLen);
unsigned int curr_c; ImTextCharFromUtf8(&curr_c, prev_p, obj->TextSrc + obj->TextLen);

bool prev_white = ImCharIsBlankW(prev_c);
bool prev_separ = ImCharIsSeparatorW(prev_c);
Expand Down Expand Up @@ -4050,6 +4050,7 @@ static int STB_TEXTEDIT_MOVEWORDRIGHT_IMPL(ImGuiInputTextState* obj, int idx)
static void STB_TEXTEDIT_DELETECHARS(ImGuiInputTextState* obj, int pos, int n)
{
// Offset remaining text (+ copy zero terminator)
IM_ASSERT(obj->TextSrc == obj->TextA.Data);
char* dst = obj->TextA.Data + pos;
char* src = obj->TextA.Data + pos + n;
memmove(dst, src, obj->TextLen - n - pos + 1);
Expand All @@ -4067,11 +4068,13 @@ static bool STB_TEXTEDIT_INSERTCHARS(ImGuiInputTextState* obj, int pos, const ch
return false;

// Grow internal buffer if needed
IM_ASSERT(obj->TextSrc == obj->TextA.Data);
if (new_text_len + text_len + 1 > obj->TextA.Size)
{
if (!is_resizable)
return false;
obj->TextA.resize(text_len + ImClamp(new_text_len, 32, ImMax(256, new_text_len)) + 1);
obj->TextSrc = obj->TextA.Data;
}

char* text = obj->TextA.Data;
Expand Down Expand Up @@ -4218,6 +4221,7 @@ void ImGuiInputTextCallbackData::InsertChars(int pos, const char* new_text, cons
IM_ASSERT(Buf == edit_state->TextA.Data);
int new_buf_size = BufTextLen + ImClamp(new_text_len * 4, 32, ImMax(256, new_text_len)) + 1;
edit_state->TextA.resize(new_buf_size + 1);
edit_state->TextSrc = edit_state->TextA.Data;
Buf = edit_state->TextA.Data;
BufSize = edit_state->BufCapacity = new_buf_size;
}
Expand Down Expand Up @@ -4507,7 +4511,6 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_
const bool init_changed_specs = (state != NULL && state->Stb->single_line != !is_multiline); // state != NULL means its our state.
const bool init_make_active = (user_clicked || user_scroll_finish || input_requested_by_nav);
const bool init_state = (init_make_active || user_scroll_active);
bool readonly_swapped_text_data = false;
if (init_reload_from_user_buf)
{
int new_len = (int)strlen(buf);
Expand Down Expand Up @@ -4612,22 +4615,15 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_
// Expose scroll in a manner that is agnostic to us using a child window
if (is_multiline && state != NULL)
state->Scroll.y = draw_window->Scroll.y;
}

if (g.ActiveId == id && is_readonly)
{
// FIXME: Refresh buffer because cursor/selection code uses that data (see repro #8242)
// The "simple" way would be to copy buf into state->TextA, like in the block above.
// But because we like to live dangerously, we do a little swap....
// Removing the swap and only doing a TextA.clear() is a way to identify who's using TextA.Data.
state->TextLen = (int)strlen(buf);
state->TextA.clear();
state->TextA.Data = buf; // Ouch
state->TextA.Size = state->TextLen + 1;
readonly_swapped_text_data = true; // Need to always ensure that every code path below lead to this being handled
//state->TextA.resize(buf_size + 1);
//memcpy(state->TextA.Data, buf, state->TextLen + 1);
// Read-only mode always ever read from source buffer. Refresh TextLen when active.
if (is_readonly && state != NULL)
state->TextLen = (int)strlen(buf);
//if (is_readonly && state != NULL)
// state->TextA.clear(); // Uncomment to facilitate debugging, but we otherwise prefer to keep/amortize th allocation.
}
if (state != NULL)
state->TextSrc = is_readonly ? buf : state->TextA.Data;

// We have an edge case if ActiveId was set through another widget (e.g. widget being swapped), clear id immediately (don't wait until the end of the function)
if (g.ActiveId == id && state == NULL)
Expand Down Expand Up @@ -4892,13 +4888,13 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_
// Cut, Copy
if (g.PlatformIO.Platform_SetClipboardTextFn != NULL)
{
// SetClipboardText() only takes null terminated strings + state->TextSrc may point to read-only user buffer, so we need to make a copy.
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->TextLen;

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;
g.TempBuffer.reserve(ie - ib + 1);
memcpy(g.TempBuffer.Data, state->TextSrc, ie - ib);
g.TempBuffer.Data[ie] = 0;
SetClipboardText(g.TempBuffer.Data);
}
if (is_cut)
{
Expand Down Expand Up @@ -5028,7 +5024,7 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_

// FIXME-OPT: Undo stack reconcile needs a backup of the data until we rework API, see #7925
char* callback_buf = is_readonly ? buf : state->TextA.Data;

IM_ASSERT(callback_buf == state->TextSrc);
state->CallbackTextBackup.resize(state->TextLen + 1);
memcpy(state->CallbackTextBackup.Data, callback_buf, state->TextLen + 1);

Expand Down Expand Up @@ -5066,9 +5062,9 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_
}

// Will copy result string if modified
if (!is_readonly && strcmp(state->TextA.Data, buf) != 0)
if (!is_readonly && strcmp(state->TextSrc, buf) != 0)
{
apply_new_text = state->TextA.Data;
apply_new_text = state->TextSrc;
apply_new_text_length = state->TextLen;
value_changed = true;
}
Expand Down Expand Up @@ -5324,13 +5320,6 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_
if (is_password && !is_displaying_hint)
PopFont();

if (readonly_swapped_text_data)
{
IM_ASSERT(state->TextA.Data == buf);
state->TextA.Size = 0;
state->TextA.Data = NULL;
}

if (is_multiline)
{
// For focus requests to work on our multiline we need to ensure our child ItemAdd() call specifies the ImGuiItemFlags_Inputable (see #4761, #7870)...
Expand All @@ -5349,6 +5338,8 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_
g.LastItemData.StatusFlags = item_data_backup.StatusFlags;
}
}
if (state)
state->TextSrc = NULL;

// Log as text
if (g.LogEnabled && (!is_password || is_displaying_hint))
Expand Down

0 comments on commit 32f1140

Please sign in to comment.