1502 lines
74 KiB
C++
1502 lines
74 KiB
C++
#include "../headers/includes.h"
|
|
|
|
namespace ImStb
|
|
{
|
|
#include "imstb_textedit.h"
|
|
}
|
|
|
|
static int input_text_calc_text_len_and_line_count(const char* text_begin, const char** out_text_end)
|
|
{
|
|
int line_count = 0;
|
|
const char* s = text_begin;
|
|
while (true)
|
|
{
|
|
const char* s_eol = strchr(s, '\n');
|
|
line_count++;
|
|
if (s_eol == NULL)
|
|
{
|
|
s = s + strlen(s);
|
|
break;
|
|
}
|
|
s = s_eol + 1;
|
|
}
|
|
*out_text_end = s;
|
|
return line_count;
|
|
}
|
|
|
|
static ImVec2 input_text_calc_text_size(ImGuiContext* ctx, const char* text_begin, const char* text_end, const char** remaining = NULL, ImVec2* out_offset = NULL, bool stop_on_new_line = false)
|
|
{
|
|
ImGuiContext& g = *ctx;
|
|
ImFont* font = g.Font;
|
|
const float line_height = g.FontSize;
|
|
const float scale = line_height / font->FontSize;
|
|
|
|
ImVec2 text_size = ImVec2(0, 0);
|
|
float line_width = 0.0f;
|
|
|
|
const char* s = text_begin;
|
|
while (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);
|
|
text_size.y += line_height;
|
|
line_width = 0.0f;
|
|
if (stop_on_new_line)
|
|
break;
|
|
continue;
|
|
}
|
|
if (c == '\r')
|
|
continue;
|
|
|
|
const float char_width = ((int)c < font->IndexAdvanceX.Size ? font->IndexAdvanceX.Data[c] : font->FallbackAdvanceX) * scale;
|
|
line_width += char_width;
|
|
}
|
|
|
|
if (text_size.x < line_width)
|
|
text_size.x = line_width;
|
|
|
|
if (out_offset)
|
|
*out_offset = ImVec2(line_width, text_size.y + line_height); // offset allow for the possibility of sitting after a trailing \n
|
|
|
|
if (line_width > 0 || text_size.y == 0.0f) // whereas size.y will ignore the trailing \n
|
|
text_size.y += line_height;
|
|
|
|
if (remaining)
|
|
*remaining = s;
|
|
|
|
return text_size;
|
|
}
|
|
|
|
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 = input_text_calc_text_size(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 - (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[] =
|
|
{
|
|
',', 0x3001, '.', 0x3002, ';', 0xFF1B, '(', 0xFF08, ')', 0xFF09, '{', 0xFF5B, '}', 0xFF5D,
|
|
'[', 0x300C, ']', 0x300D, '|', 0xFF5C, '!', 0xFF01, '\\', 0xFFE5, '/', 0x30FB, 0xFF0F,
|
|
'\n', '\r',
|
|
};
|
|
for (unsigned int separator : separator_list)
|
|
if (c == separator)
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
static int is_word_boundary_from_right(ImGuiInputTextState* obj, int idx)
|
|
{
|
|
// When ImGuiInputTextFlags_Password is set, we don't want actions such as CTRL+Arrow to leak the fact that underlying data are blanks or separators.
|
|
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->TextA.Size);
|
|
unsigned int prev_c; ImTextCharFromUtf8(&prev_c, prev_p, obj->TextA.Data + obj->TextA.Size);
|
|
|
|
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)
|
|
{
|
|
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->TextA.Size);
|
|
unsigned int curr_c; ImTextCharFromUtf8(&curr_c, prev_p, obj->TextA.Data + obj->TextA.Size);
|
|
|
|
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 = 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
|
|
|
|
static void STB_TEXTEDIT_DELETECHARS(ImGuiInputTextState* obj, int pos, int n)
|
|
{
|
|
char* dst = obj->TextA.Data + pos;
|
|
|
|
obj->Edited = true;
|
|
obj->CurLenA -= n;
|
|
|
|
// Offset remaining text (FIXME-OPT: Use memmove)
|
|
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 char* new_text, int new_text_len)
|
|
{
|
|
const bool is_resizable = (obj->Flags & ImGuiInputTextFlags_CallbackResize) != 0;
|
|
const int text_len = obj->CurLenA;
|
|
IM_ASSERT(pos <= text_len);
|
|
|
|
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->TextA.Size)
|
|
{
|
|
if (!is_resizable)
|
|
return false;
|
|
obj->TextA.resize(text_len + ImClamp(new_text_len, 32, ImMax(256, new_text_len)) + 1);
|
|
}
|
|
|
|
char* text = obj->TextA.Data;
|
|
if (pos != text_len)
|
|
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->CurLenA += new_text_len;
|
|
obj->TextA[obj->CurLenA] = '\0';
|
|
|
|
return true;
|
|
}
|
|
|
|
// We don't use an enum so we can build even with conflicting symbols (if another user of stb_textedit.h leak their STB_TEXTEDIT_K_* symbols)
|
|
#define STB_TEXTEDIT_K_LEFT 0x200000 // keyboard input to move cursor left
|
|
#define STB_TEXTEDIT_K_RIGHT 0x200001 // keyboard input to move cursor right
|
|
#define STB_TEXTEDIT_K_UP 0x200002 // keyboard input to move cursor up
|
|
#define STB_TEXTEDIT_K_DOWN 0x200003 // keyboard input to move cursor down
|
|
#define STB_TEXTEDIT_K_LINESTART 0x200004 // keyboard input to move cursor to start of line
|
|
#define STB_TEXTEDIT_K_LINEEND 0x200005 // keyboard input to move cursor to end of line
|
|
#define STB_TEXTEDIT_K_TEXTSTART 0x200006 // keyboard input to move cursor to start of text
|
|
#define STB_TEXTEDIT_K_TEXTEND 0x200007 // keyboard input to move cursor to end of text
|
|
#define STB_TEXTEDIT_K_DELETE 0x200008 // keyboard input to delete selection or character under cursor
|
|
#define STB_TEXTEDIT_K_BACKSPACE 0x200009 // keyboard input to delete selection or character left of cursor
|
|
#define STB_TEXTEDIT_K_UNDO 0x20000A // keyboard input to perform undo
|
|
#define STB_TEXTEDIT_K_REDO 0x20000B // keyboard input to perform redo
|
|
#define STB_TEXTEDIT_K_WORDLEFT 0x20000C // keyboard input to move cursor left one word
|
|
#define STB_TEXTEDIT_K_WORDRIGHT 0x20000D // keyboard input to move cursor right one word
|
|
#define STB_TEXTEDIT_K_PGUP 0x20000E // keyboard input to move cursor up a page
|
|
#define STB_TEXTEDIT_K_PGDOWN 0x20000F // keyboard input to move cursor down a page
|
|
#define STB_TEXTEDIT_K_SHIFT 0x400000
|
|
|
|
#define IMSTB_TEXTEDIT_IMPLEMENTATION
|
|
#define IMSTB_TEXTEDIT_memmove memmove
|
|
#include "imstb_textedit.h"
|
|
|
|
// stb_textedit internally allows for a single undo record to do addition and deletion, but somehow, calling
|
|
// 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->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;
|
|
if (ImStb::STB_TEXTEDIT_INSERTCHARS(str, 0, text, text_len))
|
|
{
|
|
state->cursor = state->select_start = state->select_end = text_len;
|
|
state->has_preferred_x = 0;
|
|
return;
|
|
}
|
|
IM_ASSERT(0); // Failed to insert character, normally shouldn't happen because of how we currently use stb_textedit_replace()
|
|
}
|
|
|
|
} // namespace ImStb
|
|
|
|
static bool input_text_filter_character(ImGuiContext* ctx, unsigned int* p_char, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data, bool input_source_is_clipboard = false)
|
|
{
|
|
unsigned int c = *p_char;
|
|
|
|
// Filter non-printable (NB: isprint is unreliable! see #2467)
|
|
bool apply_named_filters = true;
|
|
if (c < 0x20)
|
|
{
|
|
bool pass = false;
|
|
pass |= (c == '\n') && (flags & ImGuiInputTextFlags_Multiline) != 0; // Note that an Enter KEY will emit \r and be ignored (we poll for KEY in InputText() code)
|
|
pass |= (c == '\t') && (flags & ImGuiInputTextFlags_AllowTabInput) != 0;
|
|
if (!pass)
|
|
return false;
|
|
apply_named_filters = false; // Override named filters below so newline and tabs can still be inserted.
|
|
}
|
|
|
|
if (input_source_is_clipboard == false)
|
|
{
|
|
// We ignore Ascii representation of delete (emitted from Backspace on OSX, see #2578, #2817)
|
|
if (c == 127)
|
|
return false;
|
|
|
|
// Filter private Unicode range. GLFW on OSX seems to send private characters for special keys like arrow keys (FIXME)
|
|
if (c >= 0xE000 && c <= 0xF8FF)
|
|
return false;
|
|
}
|
|
|
|
// Filter Unicode ranges we are not handling in this build
|
|
if (c > IM_UNICODE_CODEPOINT_MAX)
|
|
return false;
|
|
|
|
// Generic named filters
|
|
if (apply_named_filters && (flags & (ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_CharsHexadecimal | ImGuiInputTextFlags_CharsUppercase | ImGuiInputTextFlags_CharsNoBlank | ImGuiInputTextFlags_CharsScientific | (ImGuiInputTextFlags)ImGuiInputTextFlags_LocalizeDecimalPoint)))
|
|
{
|
|
// The libc allows overriding locale, with e.g. 'setlocale(LC_NUMERIC, "de_DE.UTF-8");' which affect the output/input of printf/scanf to use e.g. ',' instead of '.'.
|
|
// The standard mandate that programs starts in the "C" locale where the decimal point is '.'.
|
|
// We don't really intend to provide widespread support for it, but out of empathy for people stuck with using odd API, we support the bare minimum aka overriding the decimal point.
|
|
// Change the default decimal_point with:
|
|
// ImGui::GetPlatformIO()->Platform_LocaleDecimalPoint = *localeconv()->decimal_point;
|
|
// Users of non-default decimal point (in particular ',') may be affected by word-selection logic (is_word_boundary_from_right/is_word_boundary_from_left) functions.
|
|
ImGuiContext& g = *ctx;
|
|
const unsigned c_decimal_point = (unsigned int)g.PlatformIO.Platform_LocaleDecimalPoint;
|
|
if (flags & (ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_CharsScientific | (ImGuiInputTextFlags)ImGuiInputTextFlags_LocalizeDecimalPoint))
|
|
if (c == '.' || c == ',')
|
|
c = c_decimal_point;
|
|
|
|
// Full-width -> half-width conversion for numeric fields (https://en.wikipedia.org/wiki/Halfwidth_and_Fullwidth_Forms_(Unicode_block)
|
|
// While this is mostly convenient, this has the side-effect for uninformed users accidentally inputting full-width characters that they may
|
|
// scratch their head as to why it works in numerical fields vs in generic text fields it would require support in the font.
|
|
if (flags & (ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_CharsScientific | ImGuiInputTextFlags_CharsHexadecimal))
|
|
if (c >= 0xFF01 && c <= 0xFF5E)
|
|
c = c - 0xFF01 + 0x21;
|
|
|
|
// Allow 0-9 . - + * /
|
|
if (flags & ImGuiInputTextFlags_CharsDecimal)
|
|
if (!(c >= '0' && c <= '9') && (c != c_decimal_point) && (c != '-') && (c != '+') && (c != '*') && (c != '/'))
|
|
return false;
|
|
|
|
// Allow 0-9 . - + * / e E
|
|
if (flags & ImGuiInputTextFlags_CharsScientific)
|
|
if (!(c >= '0' && c <= '9') && (c != c_decimal_point) && (c != '-') && (c != '+') && (c != '*') && (c != '/') && (c != 'e') && (c != 'E'))
|
|
return false;
|
|
|
|
// Allow 0-9 a-F A-F
|
|
if (flags & ImGuiInputTextFlags_CharsHexadecimal)
|
|
if (!(c >= '0' && c <= '9') && !(c >= 'a' && c <= 'f') && !(c >= 'A' && c <= 'F'))
|
|
return false;
|
|
|
|
// Turn a-z into A-Z
|
|
if (flags & ImGuiInputTextFlags_CharsUppercase)
|
|
if (c >= 'a' && c <= 'z')
|
|
c += (unsigned int)('A' - 'a');
|
|
|
|
if (flags & ImGuiInputTextFlags_CharsNoBlank)
|
|
if (ImCharIsBlankW(c))
|
|
return false;
|
|
|
|
*p_char = c;
|
|
}
|
|
|
|
// Custom callback filter
|
|
if (flags & ImGuiInputTextFlags_CallbackCharFilter)
|
|
{
|
|
ImGuiContext& g = *GImGui;
|
|
ImGuiInputTextCallbackData callback_data;
|
|
callback_data.Ctx = &g;
|
|
callback_data.EventFlag = ImGuiInputTextFlags_CallbackCharFilter;
|
|
callback_data.EventChar = (ImWchar)c;
|
|
callback_data.Flags = flags;
|
|
callback_data.UserData = user_data;
|
|
if (callback(&callback_data) != 0)
|
|
return false;
|
|
*p_char = callback_data.EventChar;
|
|
if (!callback_data.EventChar)
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static void input_text_reconcile_undo_state_after_user_callback(ImGuiInputTextState* state, const char* new_buf_a, int new_length_a)
|
|
{
|
|
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;
|
|
for (first_diff = 0; first_diff < shorter_length; first_diff++)
|
|
if (old_buf[first_diff] != new_buf_a[first_diff])
|
|
break;
|
|
if (first_diff == old_length && first_diff == new_length_a)
|
|
return;
|
|
|
|
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_a[new_last_diff])
|
|
break;
|
|
|
|
const int insert_len = new_last_diff - first_diff + 1;
|
|
const int delete_len = old_last_diff - first_diff + 1;
|
|
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] = old_buf[first_diff + i];
|
|
}
|
|
|
|
bool text_field_ex(std::string_view label, std::string_view hint, char* buf, int buf_size, float text_padding, float line_padding, ImU32 text_col, bool centered, const ImRect& rect, bool* active, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* callback_user_data, bool user_clicked)
|
|
{
|
|
struct text_field_state
|
|
{
|
|
float offset{ 0 };
|
|
float alpha{ 0 };
|
|
float buf_size{ 0 };
|
|
float cursor_alpha{ 0 };
|
|
ImVec2 get_draw_scroll{ 0, 0 };
|
|
ImVec2 get_cursor_offset{ 0, 0 };
|
|
};
|
|
|
|
ImGuiWindow* window = GetCurrentWindow();
|
|
if (window->SkipItems)
|
|
return false;
|
|
|
|
IM_ASSERT(buf != NULL && buf_size >= 0);
|
|
IM_ASSERT(!((flags & ImGuiInputTextFlags_CallbackHistory) && (flags & ImGuiInputTextFlags_Multiline))); // Can't use both together (they both use up/down keys)
|
|
IM_ASSERT(!((flags & ImGuiInputTextFlags_CallbackCompletion) && (flags & ImGuiInputTextFlags_AllowTabInput))); // Can't use both together (they both use tab key)
|
|
|
|
ImGuiContext& g = *GImGui;
|
|
ImGuiIO& io = g.IO;
|
|
const ImGuiStyle& style = g.Style;
|
|
|
|
const bool RENDER_SELECTION_WHEN_INACTIVE = false;
|
|
const bool is_multiline = (flags & ImGuiInputTextFlags_Multiline) != 0;
|
|
|
|
if (is_multiline)
|
|
gui->begin_group();
|
|
const ImGuiID id = window->GetID(label.data());
|
|
|
|
const ImVec2 pos = window->DC.CursorPos;
|
|
|
|
text_field_state* animstate = gui->anim_container<text_field_state>(id);
|
|
|
|
ImGuiWindow* draw_window = window;
|
|
ImVec2 inner_size = rect.GetSize();
|
|
ImGuiLastItemData item_data_backup;
|
|
if (is_multiline)
|
|
{
|
|
ImVec2 backup_pos = window->DC.CursorPos;
|
|
gui->item_size(rect, style.FramePadding.y);
|
|
if (!gui->item_add(rect, id, &rect, ImGuiItemFlags_Inputable))
|
|
{
|
|
gui->end_group();
|
|
return false;
|
|
}
|
|
item_data_backup = g.LastItemData;
|
|
window->DC.CursorPos = backup_pos;
|
|
|
|
// Prevent NavActivation from Tabbing when our widget accepts Tab inputs: this allows cycling through widgets without stopping.
|
|
if (g.NavActivateId == id && (g.NavActivateFlags & ImGuiActivateFlags_FromTabbing) && (flags & ImGuiInputTextFlags_AllowTabInput))
|
|
g.NavActivateId = 0;
|
|
|
|
// Prevent NavActivate reactivating in BeginChild() when we are already active.
|
|
const ImGuiID backup_activate_id = g.NavActivateId;
|
|
if (g.ActiveId == id) // Prevent reactivation
|
|
g.NavActivateId = 0;
|
|
|
|
// We reproduce the contents of BeginChildFrame() in order to provide 'label' so our window internal data are easier to read/debug.
|
|
bool child_visible = gui->begin_def_child(label.data(), rect.GetSize(), ImGuiChildFlags_Borders, ImGuiWindowFlags_NoMove);
|
|
g.NavActivateId = backup_activate_id;
|
|
if (!child_visible)
|
|
{
|
|
gui->end_def_child();
|
|
gui->end_group();
|
|
return false;
|
|
}
|
|
draw_window = g.CurrentWindow; // Child window
|
|
draw_window->DC.NavLayersActiveMaskNext |= (1 << draw_window->DC.NavLayerCurrent); // This is to ensure that EndChild() will display a navigation highlight so we can "enter" into it.
|
|
draw_window->DC.CursorPos += style.FramePadding;
|
|
inner_size.x -= draw_window->ScrollbarSizes.x;
|
|
}
|
|
else
|
|
{
|
|
// Support for internal ImGuiInputTextFlags_MergedItem flag, which could be redesigned as an ItemFlags if needed (with test performed in ItemAdd)
|
|
/*gui->item_size(rect, style.FramePadding.y);
|
|
if (!(flags & ImGuiInputTextFlags_MergedItem))
|
|
if (!gui->item_add(rect, id, &rect, ImGuiItemFlags_Inputable))
|
|
return false;*/
|
|
}
|
|
const bool hovered = gui->item_hoverable(rect, id, g.LastItemData.InFlags);
|
|
gui->push_font(g.Font);
|
|
|
|
// We are only allowed to access the state if we are already the active widget.
|
|
ImGuiInputTextState* state = GetInputTextState(id);
|
|
|
|
if (g.LastItemData.InFlags & ImGuiItemFlags_ReadOnly)
|
|
flags |= ImGuiInputTextFlags_ReadOnly;
|
|
const bool is_readonly = (flags & ImGuiInputTextFlags_ReadOnly) != 0;
|
|
const bool is_password = (flags & ImGuiInputTextFlags_Password) != 0;
|
|
const bool is_undoable = (flags & ImGuiInputTextFlags_NoUndoRedo) == 0;
|
|
const bool is_resizable = (flags & ImGuiInputTextFlags_CallbackResize) != 0;
|
|
if (is_resizable)
|
|
IM_ASSERT(callback != NULL); // Must provide a callback if you set the ImGuiInputTextFlags_CallbackResize flag!
|
|
|
|
const bool input_requested_by_nav = (g.ActiveId != id) && ((g.NavActivateId == id) && ((g.NavActivateFlags & ImGuiActivateFlags_PreferInput) || (g.NavInputSource == ImGuiInputSource_Keyboard)));
|
|
|
|
const bool user_scroll_finish = is_multiline && state != NULL && g.ActiveId == 0 && g.ActiveIdPreviousFrame == GetWindowScrollbarID(draw_window, ImGuiAxis_Y);
|
|
const bool user_scroll_active = is_multiline && state != NULL && g.ActiveId == GetWindowScrollbarID(draw_window, ImGuiAxis_Y);
|
|
bool clear_active_id = false;
|
|
bool select_all = false;
|
|
|
|
float scroll_y = is_multiline ? draw_window->Scroll.y : FLT_MAX;
|
|
|
|
const bool init_reload_from_user_buf = (state != NULL && state->ReloadUserBuf);
|
|
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);
|
|
if ((init_state && g.ActiveId != id) || init_changed_specs || init_reload_from_user_buf)
|
|
{
|
|
// Access state even if we don't own it yet.
|
|
state = &g.InputTextState;
|
|
state->CursorAnimReset();
|
|
state->ReloadUserBuf = false;
|
|
|
|
// Backup state of deactivating item so they'll have a chance to do a write to output buffer on the same frame they report IsItemDeactivatedAfterEdit (#4714)
|
|
InputTextDeactivateHook(state->ID);
|
|
|
|
// From the moment we focused we are normally ignoring the content of 'buf' (unless we are in read-only mode)
|
|
const int buf_len = (int)strlen(buf);
|
|
if (!init_reload_from_user_buf)
|
|
{
|
|
// Take a copy of the initial buffer value.
|
|
state->InitialTextA.resize(buf_len + 1); // UTF-8. we use +1 to make sure that .Data is always pointing to at least an empty string.
|
|
memcpy(state->InitialTextA.Data, buf, buf_len + 1);
|
|
}
|
|
|
|
// 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 || (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->CurLenA = (int)strlen(buf);
|
|
memcpy(state->TextA.Data, buf, state->CurLenA + 1);
|
|
|
|
if (recycle_state)
|
|
{
|
|
// Recycle existing cursor/selection/undo stack but clamp position
|
|
// Note a single mouse click will override the cursor/position immediately by calling stb_textedit_click handler.
|
|
state->CursorClamp();
|
|
}
|
|
else
|
|
{
|
|
state->Scroll = ImVec2(0.0f, 0.0f);
|
|
stb_textedit_initialize_state(state->Stb, !is_multiline);
|
|
}
|
|
|
|
if (init_reload_from_user_buf)
|
|
{
|
|
state->Stb->select_start = state->ReloadSelectionStart;
|
|
state->Stb->cursor = state->Stb->select_end = state->ReloadSelectionEnd;
|
|
state->CursorClamp();
|
|
}
|
|
else if (!is_multiline)
|
|
{
|
|
if (flags & ImGuiInputTextFlags_AutoSelectAll)
|
|
select_all = true;
|
|
if (input_requested_by_nav && (!recycle_state || !(g.NavActivateFlags & ImGuiActivateFlags_TryToPreserveState)))
|
|
select_all = true;
|
|
if (user_clicked && io.KeyCtrl)
|
|
select_all = true;
|
|
}
|
|
|
|
if (flags & ImGuiInputTextFlags_AlwaysOverwrite)
|
|
state->Stb->insert_mode = 1; // stb field name is indeed incorrect (see #2863)
|
|
}
|
|
|
|
const bool is_osx = io.ConfigMacOSXBehaviors;
|
|
if (g.ActiveId != id && init_make_active)
|
|
{
|
|
IM_ASSERT(state && state->ID == id);
|
|
SetActiveID(id, window);
|
|
SetFocusID(id, window);
|
|
FocusWindow(window);
|
|
}
|
|
if (g.ActiveId == id)
|
|
{
|
|
// Declare some inputs, the other are registered and polled via Shortcut() routing system.
|
|
if (user_clicked)
|
|
SetKeyOwner(ImGuiKey_MouseLeft, id);
|
|
g.ActiveIdUsingNavDirMask |= (1 << ImGuiDir_Left) | (1 << ImGuiDir_Right);
|
|
if (is_multiline || (flags & ImGuiInputTextFlags_CallbackHistory))
|
|
g.ActiveIdUsingNavDirMask |= (1 << ImGuiDir_Up) | (1 << ImGuiDir_Down);
|
|
SetKeyOwner(ImGuiKey_Enter, id);
|
|
SetKeyOwner(ImGuiKey_KeypadEnter, id);
|
|
SetKeyOwner(ImGuiKey_Home, id);
|
|
SetKeyOwner(ImGuiKey_End, id);
|
|
if (is_multiline)
|
|
{
|
|
SetKeyOwner(ImGuiKey_PageUp, id);
|
|
SetKeyOwner(ImGuiKey_PageDown, id);
|
|
}
|
|
// FIXME: May be a problem to always steal Alt on OSX, would ideally still allow an uninterrupted Alt down-up to toggle menu
|
|
if (is_osx)
|
|
SetKeyOwner(ImGuiMod_Alt, id);
|
|
|
|
// 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;
|
|
}
|
|
|
|
// 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)
|
|
ClearActiveID();
|
|
|
|
// Release focus when we click outside
|
|
if (g.ActiveId == id && io.MouseClicked[0] && !init_state && !init_make_active) //-V560
|
|
clear_active_id = true;
|
|
|
|
// Lock the decision of whether we are going to take the path displaying the cursor or selection
|
|
bool render_cursor = (g.ActiveId == id) || (state && user_scroll_active);
|
|
bool render_selection = state && (state->HasSelection() || select_all) && (RENDER_SELECTION_WHEN_INACTIVE || render_cursor);
|
|
bool value_changed = false;
|
|
bool validated = false;
|
|
|
|
const bool buf_display_from_state = (render_cursor || render_selection || g.ActiveId == id) && !is_readonly && state;
|
|
const bool is_displaying_hint = false;
|
|
|
|
if (is_password && !is_displaying_hint)
|
|
{
|
|
const ImFontGlyph* glyph = g.Font->FindGlyph('D');
|
|
ImFont* password_font = &g.InputTextPasswordFont;
|
|
password_font->FontSize = g.Font->FontSize;
|
|
password_font->Scale = g.Font->Scale;
|
|
password_font->Ascent = g.Font->Ascent;
|
|
password_font->Descent = g.Font->Descent;
|
|
password_font->ContainerAtlas = g.Font->ContainerAtlas;
|
|
password_font->FallbackGlyph = glyph;
|
|
password_font->FallbackAdvanceX = glyph->AdvanceX;
|
|
IM_ASSERT(password_font->Glyphs.empty() && password_font->IndexAdvanceX.empty() && password_font->IndexLookup.empty());
|
|
PushFont(password_font);
|
|
}
|
|
|
|
if (g.ActiveId == id)
|
|
{
|
|
IM_ASSERT(state != NULL);
|
|
state->Edited = false;
|
|
state->BufCapacityA = buf_size;
|
|
state->Flags = flags;
|
|
|
|
// Although we are active we don't prevent mouse from hovering other elements unless we are interacting right now with the widget.
|
|
// Down the line we should have a cleaner library-wide concept of Selected vs Active.
|
|
g.ActiveIdAllowOverlap = !io.MouseDown[0];
|
|
|
|
const float text_width = animstate->buf_size;
|
|
|
|
// Edit in progress
|
|
const float mouse_x = centered ? (io.MousePos.x - rect.Min.x - style.FramePadding.x) + state->Scroll.x - ((rect.GetWidth() - text_width) / 2.0f) : (io.MousePos.x - rect.Min.x - text_padding) + state->Scroll.x;
|
|
const float mouse_y = (is_multiline ? (io.MousePos.y - draw_window->DC.CursorPos.y) : (g.FontSize * 0.5f));
|
|
|
|
if (select_all)
|
|
{
|
|
state->SelectAll();
|
|
state->SelectedAllMouseLock = true;
|
|
}
|
|
else if (hovered && io.MouseClickedCount[0] >= 2 && !io.KeyShift)
|
|
{
|
|
stb_textedit_click(state, state->Stb, mouse_x, mouse_y);
|
|
const int multiclick_count = (io.MouseClickedCount[0] - 2);
|
|
if ((multiclick_count % 2) == 0)
|
|
{
|
|
// Double-click: Select word
|
|
// We always use the "Mac" word advance for double-click select vs CTRL+Right which use the platform dependent variant:
|
|
// FIXME: There are likely many ways to improve this behavior, but there's no "right" behavior (depends on use-case, software, OS)
|
|
const bool is_bol = (state->Stb->cursor == 0) || ImStb::STB_TEXTEDIT_GETCHAR(state, state->Stb->cursor - 1) == '\n';
|
|
if (STB_TEXT_HAS_SELECTION(state->Stb) || !is_bol)
|
|
state->OnKeyPressed(STB_TEXTEDIT_K_WORDLEFT);
|
|
//state->OnKeyPressed(STB_TEXTEDIT_K_WORDRIGHT | STB_TEXTEDIT_K_SHIFT);
|
|
if (!STB_TEXT_HAS_SELECTION(state->Stb))
|
|
ImStb::stb_textedit_prep_selection_at_cursor(state->Stb);
|
|
state->Stb->cursor = ImStb::STB_TEXTEDIT_MOVEWORDRIGHT_MAC(state, state->Stb->cursor);
|
|
state->Stb->select_end = state->Stb->cursor;
|
|
ImStb::stb_textedit_clamp(state, state->Stb);
|
|
}
|
|
else
|
|
{
|
|
// Triple-click: Select line
|
|
const bool is_eol = ImStb::STB_TEXTEDIT_GETCHAR(state, state->Stb->cursor) == '\n';
|
|
state->OnKeyPressed(STB_TEXTEDIT_K_LINESTART);
|
|
state->OnKeyPressed(STB_TEXTEDIT_K_LINEEND | STB_TEXTEDIT_K_SHIFT);
|
|
state->OnKeyPressed(STB_TEXTEDIT_K_RIGHT | STB_TEXTEDIT_K_SHIFT);
|
|
if (!is_eol && is_multiline)
|
|
{
|
|
ImSwap(state->Stb->select_start, state->Stb->select_end);
|
|
state->Stb->cursor = state->Stb->select_end;
|
|
}
|
|
state->CursorFollow = false;
|
|
}
|
|
state->CursorAnimReset();
|
|
}
|
|
else if (io.MouseClicked[0] && !state->SelectedAllMouseLock)
|
|
{
|
|
if (hovered)
|
|
{
|
|
if (io.KeyShift)
|
|
stb_textedit_drag(state, state->Stb, mouse_x, mouse_y);
|
|
else
|
|
stb_textedit_click(state, state->Stb, mouse_x, mouse_y);
|
|
state->CursorAnimReset();
|
|
}
|
|
}
|
|
else if (io.MouseDown[0] && !state->SelectedAllMouseLock && (io.MouseDelta.x != 0.0f || io.MouseDelta.y != 0.0f))
|
|
{
|
|
stb_textedit_drag(state, state->Stb, mouse_x, mouse_y);
|
|
state->CursorAnimReset();
|
|
state->CursorFollow = true;
|
|
}
|
|
if (state->SelectedAllMouseLock && !io.MouseDown[0])
|
|
state->SelectedAllMouseLock = false;
|
|
|
|
// We expect backends to emit a Tab key but some also emit a Tab character which we ignore (#2467, #1336)
|
|
// (For Tab and Enter: Win32/SFML/Allegro are sending both keys and chars, GLFW and SDL are only sending keys. For Space they all send all threes)
|
|
if ((flags & ImGuiInputTextFlags_AllowTabInput) && !is_readonly)
|
|
{
|
|
if (Shortcut(ImGuiKey_Tab, ImGuiInputFlags_Repeat, id))
|
|
{
|
|
unsigned int c = '\t'; // Insert TAB
|
|
if (input_text_filter_character(&g, &c, flags, callback, callback_user_data))
|
|
state->OnCharPressed(c);
|
|
}
|
|
// FIXME: Implement Shift+Tab
|
|
/*
|
|
if (Shortcut(ImGuiKey_Tab | ImGuiMod_Shift, ImGuiInputFlags_Repeat, id))
|
|
{
|
|
}
|
|
*/
|
|
}
|
|
|
|
// Process regular text input (before we check for Return because using some IME will effectively send a Return?)
|
|
// We ignore CTRL inputs, but need to allow ALT+CTRL as some keyboards (e.g. German) use AltGR (which _is_ Alt+Ctrl) to input certain characters.
|
|
const bool ignore_char_inputs = (io.KeyCtrl && !io.KeyAlt) || (is_osx && io.KeyCtrl);
|
|
if (io.InputQueueCharacters.Size > 0)
|
|
{
|
|
if (!ignore_char_inputs && !is_readonly && !input_requested_by_nav)
|
|
for (int n = 0; n < io.InputQueueCharacters.Size; n++)
|
|
{
|
|
// Insert character if they pass filtering
|
|
unsigned int c = (unsigned int)io.InputQueueCharacters[n];
|
|
if (c == '\t') // Skip Tab, see above.
|
|
continue;
|
|
if (input_text_filter_character(&g, &c, flags, callback, callback_user_data))
|
|
state->OnCharPressed(c);
|
|
}
|
|
|
|
// Consume characters
|
|
io.InputQueueCharacters.resize(0);
|
|
}
|
|
}
|
|
|
|
// Process other shortcuts/key-presses
|
|
bool revert_edit = false;
|
|
if (g.ActiveId == id && !g.ActiveIdIsJustActivated && !clear_active_id)
|
|
{
|
|
IM_ASSERT(state != NULL);
|
|
|
|
const int row_count_per_page = ImMax((int)((inner_size.y - style.FramePadding.y) / g.FontSize), 1);
|
|
state->Stb->row_count_per_page = row_count_per_page;
|
|
|
|
const int k_mask = (io.KeyShift ? STB_TEXTEDIT_K_SHIFT : 0);
|
|
const bool is_wordmove_key_down = is_osx ? io.KeyAlt : io.KeyCtrl; // OS X style: Text editing cursor movement using Alt instead of Ctrl
|
|
const bool is_startend_key_down = is_osx && io.KeyCtrl && !io.KeySuper && !io.KeyAlt; // OS X style: Line/Text Start and End using Cmd+Arrows instead of Home/End
|
|
|
|
// Using Shortcut() with ImGuiInputFlags_RouteFocused (default policy) to allow routing operations for other code (e.g. calling window trying to use CTRL+A and CTRL+B: formet would be handled by InputText)
|
|
// Otherwise we could simply assume that we own the keys as we are active.
|
|
const ImGuiInputFlags f_repeat = ImGuiInputFlags_Repeat;
|
|
const bool is_cut = (Shortcut(ImGuiMod_Ctrl | ImGuiKey_X, f_repeat, id) || Shortcut(ImGuiMod_Shift | ImGuiKey_Delete, f_repeat, id)) && !is_readonly && !is_password && (!is_multiline || state->HasSelection());
|
|
const bool is_copy = (Shortcut(ImGuiMod_Ctrl | ImGuiKey_C, 0, id) || Shortcut(ImGuiMod_Ctrl | ImGuiKey_Insert, 0, id)) && !is_password && (!is_multiline || state->HasSelection());
|
|
const bool is_paste = (Shortcut(ImGuiMod_Ctrl | ImGuiKey_V, f_repeat, id) || Shortcut(ImGuiMod_Shift | ImGuiKey_Insert, f_repeat, id)) && !is_readonly;
|
|
const bool is_undo = (Shortcut(ImGuiMod_Ctrl | ImGuiKey_Z, f_repeat, id)) && !is_readonly && is_undoable;
|
|
const bool is_redo = (Shortcut(ImGuiMod_Ctrl | ImGuiKey_Y, f_repeat, id) || (is_osx && Shortcut(ImGuiMod_Ctrl | ImGuiMod_Shift | ImGuiKey_Z, f_repeat, id))) && !is_readonly && is_undoable;
|
|
const bool is_select_all = Shortcut(ImGuiMod_Ctrl | ImGuiKey_A, 0, id);
|
|
|
|
// We allow validate/cancel with Nav source (gamepad) to makes it easier to undo an accidental NavInput press with no keyboard wired, but otherwise it isn't very useful.
|
|
const bool nav_gamepad_active = (io.ConfigFlags & ImGuiConfigFlags_NavEnableGamepad) != 0 && (io.BackendFlags & ImGuiBackendFlags_HasGamepad) != 0;
|
|
const bool is_enter_pressed = IsKeyPressed(ImGuiKey_Enter, true) || IsKeyPressed(ImGuiKey_KeypadEnter, true);
|
|
const bool is_gamepad_validate = nav_gamepad_active && (IsKeyPressed(ImGuiKey_NavGamepadActivate, false) || IsKeyPressed(ImGuiKey_NavGamepadInput, false));
|
|
const bool is_cancel = Shortcut(ImGuiKey_Escape, f_repeat, id) || (nav_gamepad_active && Shortcut(ImGuiKey_NavGamepadCancel, f_repeat, id));
|
|
|
|
// FIXME: Should use more Shortcut() and reduce IsKeyPressed()+SetKeyOwner(), but requires modifiers combination to be taken account of.
|
|
// FIXME-OSX: Missing support for Alt(option)+Right/Left = go to end of line, or next line if already in end of line.
|
|
if (IsKeyPressed(ImGuiKey_LeftArrow)) { state->OnKeyPressed((is_startend_key_down ? STB_TEXTEDIT_K_LINESTART : is_wordmove_key_down ? STB_TEXTEDIT_K_WORDLEFT : STB_TEXTEDIT_K_LEFT) | k_mask); }
|
|
else if (IsKeyPressed(ImGuiKey_RightArrow)) { state->OnKeyPressed((is_startend_key_down ? STB_TEXTEDIT_K_LINEEND : is_wordmove_key_down ? STB_TEXTEDIT_K_WORDRIGHT : STB_TEXTEDIT_K_RIGHT) | k_mask); }
|
|
else if (IsKeyPressed(ImGuiKey_UpArrow) && is_multiline) { if (io.KeyCtrl) SetScrollY(draw_window, ImMax(draw_window->Scroll.y - g.FontSize, 0.0f)); else state->OnKeyPressed((is_startend_key_down ? STB_TEXTEDIT_K_TEXTSTART : STB_TEXTEDIT_K_UP) | k_mask); }
|
|
else if (IsKeyPressed(ImGuiKey_DownArrow) && is_multiline) { if (io.KeyCtrl) SetScrollY(draw_window, ImMin(draw_window->Scroll.y + g.FontSize, GetScrollMaxY())); else state->OnKeyPressed((is_startend_key_down ? STB_TEXTEDIT_K_TEXTEND : STB_TEXTEDIT_K_DOWN) | k_mask); }
|
|
else if (IsKeyPressed(ImGuiKey_PageUp) && is_multiline) { state->OnKeyPressed(STB_TEXTEDIT_K_PGUP | k_mask); scroll_y -= row_count_per_page * g.FontSize; }
|
|
else if (IsKeyPressed(ImGuiKey_PageDown) && is_multiline) { state->OnKeyPressed(STB_TEXTEDIT_K_PGDOWN | k_mask); scroll_y += row_count_per_page * g.FontSize; }
|
|
else if (IsKeyPressed(ImGuiKey_Home)) { state->OnKeyPressed(io.KeyCtrl ? STB_TEXTEDIT_K_TEXTSTART | k_mask : STB_TEXTEDIT_K_LINESTART | k_mask); }
|
|
else if (IsKeyPressed(ImGuiKey_End)) { state->OnKeyPressed(io.KeyCtrl ? STB_TEXTEDIT_K_TEXTEND | k_mask : STB_TEXTEDIT_K_LINEEND | k_mask); }
|
|
else if (IsKeyPressed(ImGuiKey_Delete) && !is_readonly && !is_cut)
|
|
{
|
|
if (!state->HasSelection())
|
|
{
|
|
// OSX doesn't seem to have Super+Delete to delete until end-of-line, so we don't emulate that (as opposed to Super+Backspace)
|
|
if (is_wordmove_key_down)
|
|
state->OnKeyPressed(STB_TEXTEDIT_K_WORDRIGHT | STB_TEXTEDIT_K_SHIFT);
|
|
}
|
|
state->OnKeyPressed(STB_TEXTEDIT_K_DELETE | k_mask);
|
|
}
|
|
else if (IsKeyPressed(ImGuiKey_Backspace) && !is_readonly)
|
|
{
|
|
if (!state->HasSelection())
|
|
{
|
|
if (is_wordmove_key_down)
|
|
state->OnKeyPressed(STB_TEXTEDIT_K_WORDLEFT | STB_TEXTEDIT_K_SHIFT);
|
|
else if (is_osx && io.KeyCtrl && !io.KeyAlt && !io.KeySuper)
|
|
state->OnKeyPressed(STB_TEXTEDIT_K_LINESTART | STB_TEXTEDIT_K_SHIFT);
|
|
}
|
|
state->OnKeyPressed(STB_TEXTEDIT_K_BACKSPACE | k_mask);
|
|
}
|
|
else if (is_enter_pressed || is_gamepad_validate)
|
|
{
|
|
// Determine if we turn Enter into a \n character
|
|
bool ctrl_enter_for_new_line = (flags & ImGuiInputTextFlags_CtrlEnterForNewLine) != 0;
|
|
if (!is_multiline || is_gamepad_validate || (ctrl_enter_for_new_line && !io.KeyCtrl) || (!ctrl_enter_for_new_line && io.KeyCtrl))
|
|
{
|
|
validated = true;
|
|
if (io.ConfigInputTextEnterKeepActive && !is_multiline)
|
|
state->SelectAll(); // No need to scroll
|
|
else
|
|
clear_active_id = true;
|
|
}
|
|
else if (!is_readonly)
|
|
{
|
|
unsigned int c = '\n'; // Insert new line
|
|
if (input_text_filter_character(&g, &c, flags, callback, callback_user_data))
|
|
state->OnCharPressed(c);
|
|
}
|
|
}
|
|
else if (is_cancel)
|
|
{
|
|
if (flags & ImGuiInputTextFlags_EscapeClearsAll)
|
|
{
|
|
if (buf[0] != 0)
|
|
{
|
|
revert_edit = true;
|
|
}
|
|
else
|
|
{
|
|
render_cursor = render_selection = false;
|
|
clear_active_id = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
clear_active_id = revert_edit = true;
|
|
render_cursor = render_selection = false;
|
|
}
|
|
}
|
|
else if (is_undo || is_redo)
|
|
{
|
|
state->OnKeyPressed(is_undo ? STB_TEXTEDIT_K_UNDO : STB_TEXTEDIT_K_REDO);
|
|
state->ClearSelection();
|
|
}
|
|
else if (is_select_all)
|
|
{
|
|
state->SelectAll();
|
|
state->CursorFollow = true;
|
|
}
|
|
else if (is_cut || is_copy)
|
|
{
|
|
// Cut, Copy
|
|
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->CurLenA;
|
|
|
|
char backup = state->TextA.Data[ie];
|
|
state->TextA.Data[ie] = 0; // Temporary terminator because SetClipboardText only takes null-terminated strings
|
|
SetClipboardText(state->TextA.Data + ib);
|
|
state->TextA.Data[ie] = backup;
|
|
}
|
|
if (is_cut)
|
|
{
|
|
if (!state->HasSelection())
|
|
state->SelectAll();
|
|
state->CursorFollow = true;
|
|
stb_textedit_cut(state, state->Stb);
|
|
}
|
|
}
|
|
else if (is_paste)
|
|
{
|
|
if (const char* clipboard = GetClipboardText())
|
|
{
|
|
// Filter pasted buffer
|
|
const int clipboard_len = (int)strlen(clipboard);
|
|
char* clipboard_filtered = (char*)IM_ALLOC(clipboard_len + 1);
|
|
int clipboard_filtered_len = 0;
|
|
for (const char* s = clipboard; *s != 0; )
|
|
{
|
|
unsigned int c;
|
|
int len = ImTextCharFromUtf8(&c, s, NULL);
|
|
s += len;
|
|
if (!input_text_filter_character(&g, &c, flags, callback, callback_user_data, true))
|
|
continue;
|
|
memcpy(clipboard_filtered + clipboard_filtered_len, s - len, len);
|
|
clipboard_filtered_len += len;
|
|
}
|
|
clipboard_filtered[clipboard_filtered_len] = 0;
|
|
if (clipboard_filtered_len > 0) // If everything was filtered, ignore the pasting operation
|
|
{
|
|
stb_textedit_paste(state, state->Stb, clipboard_filtered, clipboard_filtered_len);
|
|
state->CursorFollow = true;
|
|
}
|
|
MemFree(clipboard_filtered);
|
|
}
|
|
}
|
|
|
|
// Update render selection flag after events have been handled, so selection highlight can be displayed during the same frame.
|
|
render_selection |= state->HasSelection() && (RENDER_SELECTION_WHEN_INACTIVE || render_cursor);
|
|
}
|
|
|
|
// Process callbacks and apply result back to user's buffer.
|
|
const char* apply_new_text = NULL;
|
|
int apply_new_text_length = 0;
|
|
if (g.ActiveId == id)
|
|
{
|
|
IM_ASSERT(state != NULL);
|
|
if (revert_edit && !is_readonly)
|
|
{
|
|
if (flags & ImGuiInputTextFlags_EscapeClearsAll)
|
|
{
|
|
// Clear input
|
|
IM_ASSERT(buf[0] != 0);
|
|
apply_new_text = "";
|
|
apply_new_text_length = 0;
|
|
value_changed = true;
|
|
IMSTB_TEXTEDIT_CHARTYPE empty_string;
|
|
stb_textedit_replace(state, state->Stb, &empty_string, 0);
|
|
}
|
|
else if (strcmp(buf, state->InitialTextA.Data) != 0)
|
|
{
|
|
apply_new_text = state->InitialTextA.Data;
|
|
apply_new_text_length = state->InitialTextA.Size - 1;
|
|
|
|
// 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
|
|
// before clearing ActiveId, even though strictly speaking it wasn't modified on this frame.
|
|
// If we didn't do that, code like InputInt() with ImGuiInputTextFlags_EnterReturnsTrue would fail.
|
|
// This also allows the user to use InputText() with ImGuiInputTextFlags_EnterReturnsTrue without maintaining any user-side storage
|
|
// (please note that if you use this property along ImGuiInputTextFlags_CallbackResize you can end up with your temporary string object
|
|
// unnecessarily allocating once a frame, either store your string data, either if you don't then don't use ImGuiInputTextFlags_CallbackResize).
|
|
const bool apply_edit_back_to_user_buffer = !revert_edit || (validated && (flags & ImGuiInputTextFlags_EnterReturnsTrue) != 0);
|
|
if (apply_edit_back_to_user_buffer)
|
|
{
|
|
// Apply new value immediately - copy modified buffer back
|
|
// Note that as soon as the input box is active, the in-widget value gets priority over any underlying modification of the input buffer
|
|
// FIXME: We actually always render 'buf' when calling DrawList->AddText, making the comment above incorrect.
|
|
// FIXME-OPT: CPU waste to do this every time the widget is active, should mark dirty state from the stb_textedit callbacks.
|
|
|
|
// User callback
|
|
if ((flags & (ImGuiInputTextFlags_CallbackCompletion | ImGuiInputTextFlags_CallbackHistory | ImGuiInputTextFlags_CallbackEdit | ImGuiInputTextFlags_CallbackAlways)) != 0)
|
|
{
|
|
IM_ASSERT(callback != NULL);
|
|
|
|
// The reason we specify the usage semantic (Completion/History) is that Completion needs to disable keyboard TABBING at the moment.
|
|
ImGuiInputTextFlags event_flag = 0;
|
|
ImGuiKey event_key = ImGuiKey_None;
|
|
if ((flags & ImGuiInputTextFlags_CallbackCompletion) != 0 && Shortcut(ImGuiKey_Tab, 0, id))
|
|
{
|
|
event_flag = ImGuiInputTextFlags_CallbackCompletion;
|
|
event_key = ImGuiKey_Tab;
|
|
}
|
|
else if ((flags & ImGuiInputTextFlags_CallbackHistory) != 0 && IsKeyPressed(ImGuiKey_UpArrow))
|
|
{
|
|
event_flag = ImGuiInputTextFlags_CallbackHistory;
|
|
event_key = ImGuiKey_UpArrow;
|
|
}
|
|
else if ((flags & ImGuiInputTextFlags_CallbackHistory) != 0 && IsKeyPressed(ImGuiKey_DownArrow))
|
|
{
|
|
event_flag = ImGuiInputTextFlags_CallbackHistory;
|
|
event_key = ImGuiKey_DownArrow;
|
|
}
|
|
else if ((flags & ImGuiInputTextFlags_CallbackEdit) && state->Edited)
|
|
{
|
|
event_flag = ImGuiInputTextFlags_CallbackEdit;
|
|
}
|
|
else if (flags & ImGuiInputTextFlags_CallbackAlways)
|
|
{
|
|
event_flag = ImGuiInputTextFlags_CallbackAlways;
|
|
}
|
|
|
|
if (event_flag)
|
|
{
|
|
ImGuiInputTextCallbackData callback_data;
|
|
callback_data.Ctx = &g;
|
|
callback_data.EventFlag = event_flag;
|
|
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;
|
|
callback_data.BufTextLen = state->CurLenA;
|
|
callback_data.BufSize = state->BufCapacityA;
|
|
callback_data.BufDirty = false;
|
|
|
|
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 = callback_data.SelectionEnd = state->Stb->select_end;
|
|
|
|
// Call user code
|
|
callback(&callback_data);
|
|
|
|
// Read back what user may have modified
|
|
callback_buf = is_readonly ? buf : state->TextA.Data; // Pointer may have been invalidated by a resize callback
|
|
IM_ASSERT(callback_data.Buf == callback_buf); // Invalid to modify those fields
|
|
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 = 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!
|
|
input_text_reconcile_undo_state_after_user_callback(state, callback_data.Buf, callback_data.BufTextLen); // FIXME: Move the rest of this block inside function and rename to InputTextReconcileStateAfterUserCallback() ?
|
|
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();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Will copy result string if modified
|
|
if (!is_readonly && strcmp(state->TextA.Data, buf) != 0)
|
|
{
|
|
apply_new_text = state->TextA.Data;
|
|
apply_new_text_length = state->CurLenA;
|
|
value_changed = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Handle reapplying final data on deactivation (see InputTextDeactivateHook() for details)
|
|
if (g.InputTextDeactivatedState.ID == id)
|
|
{
|
|
if (g.ActiveId != id && IsItemDeactivatedAfterEdit() && !is_readonly && strcmp(g.InputTextDeactivatedState.TextA.Data, buf) != 0)
|
|
{
|
|
apply_new_text = g.InputTextDeactivatedState.TextA.Data;
|
|
apply_new_text_length = g.InputTextDeactivatedState.TextA.Size - 1;
|
|
value_changed = true;
|
|
//IMGUI_DEBUG_LOG("InputText(): apply Deactivated data for 0x%08X: \"%.*s\".\n", id, apply_new_text_length, apply_new_text);
|
|
}
|
|
g.InputTextDeactivatedState.ID = 0;
|
|
}
|
|
|
|
// 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.
|
|
IM_ASSERT(apply_new_text_length >= 0);
|
|
if (is_resizable)
|
|
{
|
|
ImGuiInputTextCallbackData callback_data;
|
|
callback_data.Ctx = &g;
|
|
callback_data.EventFlag = ImGuiInputTextFlags_CallbackResize;
|
|
callback_data.Flags = flags;
|
|
callback_data.Buf = buf;
|
|
callback_data.BufTextLen = apply_new_text_length;
|
|
callback_data.BufSize = ImMax(buf_size, apply_new_text_length + 1);
|
|
callback_data.UserData = callback_user_data;
|
|
callback(&callback_data);
|
|
buf = callback_data.Buf;
|
|
buf_size = callback_data.BufSize;
|
|
apply_new_text_length = ImMin(callback_data.BufTextLen, buf_size - 1);
|
|
IM_ASSERT(apply_new_text_length <= buf_size);
|
|
}
|
|
//IMGUI_DEBUG_PRINT("InputText(\"%s\"): apply_new_text length %d\n", label, apply_new_text_length);
|
|
|
|
// If the underlying buffer resize was denied or not carried to the next frame, apply_new_text_length+1 may be >= buf_size.
|
|
ImStrncpy(buf, apply_new_text, ImMin(apply_new_text_length + 1, buf_size));
|
|
}
|
|
|
|
// Release active ID at the end of the function (so e.g. pressing Return still does a final application of the value)
|
|
// Otherwise request text input ahead for next frame.
|
|
if (g.ActiveId == id && clear_active_id)
|
|
ClearActiveID();
|
|
else if (g.ActiveId == id)
|
|
g.WantTextInputNextFrame = 1;
|
|
|
|
const int buf_display_max_length = 2 * 1024 * 1024;
|
|
const char* buf_display = buf_display_from_state ? state->TextA.Data : buf; //-V595
|
|
const char* buf_display_end = NULL; // We have specialized paths below for setting the length
|
|
|
|
gui->easing(animstate->alpha, strlen(buf_display) > 0 ? 1.f : 0.f, 8.f, static_easing);
|
|
|
|
*active = render_cursor;
|
|
|
|
// Render frame
|
|
if (!is_multiline)
|
|
{
|
|
|
|
}
|
|
|
|
const ImVec2 buf_text_size = gui->text_size(g.Font, buf_display);
|
|
|
|
animstate->buf_size = buf_text_size.x;
|
|
|
|
const ImVec4 clip_rect(rect.Min.x, rect.Min.y, rect.Min.x + inner_size.x, rect.Min.y + inner_size.y); // Not using frame_bb.Max because we have adjusted size
|
|
ImVec2 draw_pos = centered ? ImVec2(rect.Min.x + (rect.GetWidth() - buf_text_size.x) / 2, rect.GetCenter().y - gui->text_size(g.Font, "A").y / 2) : ImVec2(rect.Min.x + text_padding, rect.GetCenter().y - gui->text_size(g.Font, "A").y / 2 - 1);
|
|
ImVec2 text_size(0.0f, 0.0f);
|
|
|
|
// Set upper limit of single-line InputTextEx() at 2 million characters strings. The current pathological worst case is a long line
|
|
// without any carriage return, which would makes ImFont::RenderText() reserve too many vertices and probably crash. Avoid it altogether.
|
|
// Note that we only use this limit on single-line InputText(), so a pathologically large line on a InputTextMultiline() would still crash.
|
|
if (is_displaying_hint)
|
|
{
|
|
buf_display = hint.data();
|
|
buf_display_end = hint.data() + strlen(hint.data());
|
|
}
|
|
|
|
// Render text. We currently only render selection when the widget is active or while scrolling.
|
|
// FIXME: We could remove the '&& render_cursor' to keep rendering selection when inactive.
|
|
if (render_cursor || render_selection)
|
|
{
|
|
IM_ASSERT(state != NULL);
|
|
if (!is_displaying_hint)
|
|
buf_display_end = buf_display + state->CurLenA;
|
|
|
|
// Render text (with cursor and selection)
|
|
// This is going to be messy. We need to:
|
|
// - Display the text (this alone can be more easily clipped)
|
|
// - Handle scrolling, highlight selection, display cursor (those all requires some form of 1d->2d cursor position calculation)
|
|
// - Measure text height (for scrollbar)
|
|
// We are attempting to do most of that in **one main pass** to minimize the computation cost (non-negligible for large amount of text) + 2nd pass for selection rendering (we could merge them by an extra refactoring effort)
|
|
// FIXME: This should occur on buf_display but we'd need to maintain cursor/select_start/select_end for UTF-8.
|
|
const char* text_begin = state->TextA.Data;
|
|
const char* text_end = text_begin + state->CurLenA;
|
|
ImVec2 cursor_offset, select_start_offset;
|
|
|
|
{
|
|
// Find lines numbers straddling cursor and selection min position
|
|
int cursor_line_no = render_cursor ? -1 : -1000;
|
|
int selmin_line_no = render_selection ? -1 : -1000;
|
|
const char* cursor_ptr = render_cursor ? text_begin + state->Stb->cursor : NULL;
|
|
const char* selmin_ptr = render_selection ? text_begin + ImMin(state->Stb->select_start, state->Stb->select_end) : NULL;
|
|
|
|
// Count lines and find line number for cursor and selection ends
|
|
int line_count = 1;
|
|
if (is_multiline)
|
|
{
|
|
for (const char* s = text_begin; (s = (const char*)memchr(s, '\n', (size_t)(text_end - s))) != NULL; s++)
|
|
{
|
|
if (cursor_line_no == -1 && s >= cursor_ptr) { cursor_line_no = line_count; }
|
|
if (selmin_line_no == -1 && s >= selmin_ptr) { selmin_line_no = line_count; }
|
|
line_count++;
|
|
}
|
|
}
|
|
if (cursor_line_no == -1)
|
|
cursor_line_no = line_count;
|
|
if (selmin_line_no == -1)
|
|
selmin_line_no = line_count;
|
|
|
|
// Calculate 2d position by finding the beginning of the line and measuring distance
|
|
cursor_offset.x = input_text_calc_text_size(&g, ImStrbol(cursor_ptr, text_begin), cursor_ptr).x;
|
|
cursor_offset.y = cursor_line_no * g.FontSize;
|
|
if (selmin_line_no >= 0)
|
|
{
|
|
select_start_offset.x = input_text_calc_text_size(&g, ImStrbol(selmin_ptr, text_begin), selmin_ptr).x;
|
|
select_start_offset.y = selmin_line_no * g.FontSize;
|
|
}
|
|
|
|
// Store text height (note that we haven't calculated text width at all, see GitHub issues #383, #1224)
|
|
if (is_multiline)
|
|
text_size = ImVec2(inner_size.x, line_count * g.FontSize);
|
|
}
|
|
|
|
// Scroll
|
|
if (render_cursor && state->CursorFollow)
|
|
{
|
|
// Horizontal scroll in chunks of quarter width
|
|
if (!(flags & ImGuiInputTextFlags_NoHorizontalScroll))
|
|
{
|
|
const float scroll_increment_x = inner_size.x * 0.25f;
|
|
const float visible_width = inner_size.x - text_padding;
|
|
|
|
if (!centered)
|
|
{
|
|
if (cursor_offset.x < state->Scroll.x)
|
|
state->Scroll.x = IM_TRUNC(ImMax(0.0f, cursor_offset.x - scroll_increment_x));
|
|
else if (cursor_offset.x - visible_width >= state->Scroll.x)
|
|
state->Scroll.x = IM_TRUNC(cursor_offset.x - visible_width + scroll_increment_x);
|
|
}
|
|
|
|
}
|
|
else
|
|
{
|
|
state->Scroll.y = 0.0f;
|
|
}
|
|
|
|
// Vertical scroll
|
|
if (is_multiline)
|
|
{
|
|
// Test if cursor is vertically visible
|
|
if (cursor_offset.y - g.FontSize < scroll_y)
|
|
scroll_y = ImMax(0.0f, cursor_offset.y - g.FontSize);
|
|
else if (cursor_offset.y - (inner_size.y - style.FramePadding.y * 2.0f) >= scroll_y)
|
|
scroll_y = cursor_offset.y - inner_size.y + style.FramePadding.y * 2.0f;
|
|
const float scroll_max_y = ImMax((text_size.y + style.FramePadding.y * 2.0f) - inner_size.y, 0.0f);
|
|
scroll_y = ImClamp(scroll_y, 0.0f, scroll_max_y);
|
|
draw_pos.y += (draw_window->Scroll.y - scroll_y); // Manipulate cursor pos immediately avoid a frame of lag
|
|
draw_window->Scroll.y = scroll_y;
|
|
}
|
|
|
|
state->CursorFollow = false;
|
|
}
|
|
|
|
// Draw selection
|
|
const ImVec2 draw_scroll = ImVec2(state->Scroll.x, 0.0f);
|
|
if (render_selection)
|
|
{
|
|
const char* text_selected_begin = text_begin + ImMin(state->Stb->select_start, state->Stb->select_end);
|
|
const char* text_selected_end = text_begin + ImMax(state->Stb->select_start, state->Stb->select_end);
|
|
|
|
ImVec2 rect_pos = draw_pos + select_start_offset - draw_scroll;
|
|
for (const char* p = text_selected_begin; p < text_selected_end; )
|
|
{
|
|
if (rect_pos.y > clip_rect.w + g.FontSize)
|
|
break;
|
|
if (rect_pos.y < clip_rect.y)
|
|
{
|
|
p = (const char*)memchr((void*)p, '\n', text_selected_end - p);
|
|
p = p ? p + 1 : text_selected_end;
|
|
}
|
|
else
|
|
{
|
|
ImVec2 rect_size = input_text_calc_text_size(&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 d(ImVec2(rect_pos.x, rect.Min.y + line_padding), ImVec2(rect_pos.x + rect_size.x, rect.Max.y - line_padding));
|
|
d.ClipWith(clip_rect);
|
|
if (d.Overlaps(clip_rect))
|
|
window->DrawList->AddRectFilled(d.Min, d.Max, draw->get_clr(clr->white, 0.1f), var->style.frame_rounding);
|
|
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);
|
|
window->DrawList->AddText(g.Font, g.FontSize, draw_pos - draw_scroll, text_col, buf_display, buf_display_end, 0.0f, is_multiline ? NULL : &clip_rect);
|
|
}
|
|
|
|
animstate->get_cursor_offset = cursor_offset;
|
|
animstate->get_draw_scroll = draw_scroll;
|
|
}
|
|
else
|
|
{
|
|
// Render text only (no selection, no cursor)
|
|
if (is_multiline)
|
|
text_size = ImVec2(inner_size.x, input_text_calc_text_len_and_line_count(buf_display, &buf_display_end) * g.FontSize); // We don't need width
|
|
else if (!is_displaying_hint && g.ActiveId == id)
|
|
buf_display_end = buf_display + state->CurLenA;
|
|
else if (!is_displaying_hint)
|
|
buf_display_end = buf_display + strlen(buf_display);
|
|
|
|
if (is_multiline || (buf_display_end - buf_display) < buf_display_max_length)
|
|
{
|
|
ImU32 col = GetColorU32(is_displaying_hint ? ImGuiCol_TextDisabled : ImGuiCol_Text);
|
|
window->DrawList->AddText(g.Font, g.FontSize, draw_pos, text_col, buf_display, buf_display_end, 0.0f, is_multiline ? NULL : &clip_rect);
|
|
}
|
|
}
|
|
|
|
gui->easing(animstate->cursor_alpha, g.ActiveId == id ? 1.f : 0.f, 7.f, static_easing);
|
|
|
|
if (animstate->cursor_alpha > 0.01f)
|
|
{
|
|
ImVec2 cursor_screen_pos = ImTrunc(draw_pos + animstate->get_cursor_offset - animstate->get_draw_scroll);
|
|
ImRect cursor_screen_rect(cursor_screen_pos.x, cursor_screen_pos.y - g.FontSize + 0.5f, cursor_screen_pos.x + 1.0f, cursor_screen_pos.y - 1.5f);
|
|
gui->easing(animstate->offset, round(cursor_screen_rect.Min.x - rect.Min.x), 24.f, dynamic_easing);
|
|
|
|
if (cursor_screen_rect.Overlaps(clip_rect))
|
|
draw->line(window->DrawList, rect.Min + ImVec2(animstate->offset, line_padding), ImVec2(rect.Min.x + animstate->offset, rect.Max.y - line_padding), draw->get_clr(clr->white, animstate->cursor_alpha), 1);
|
|
|
|
if (!is_readonly)
|
|
{
|
|
g.PlatformImeData.WantVisible = true;
|
|
g.PlatformImeData.InputPos = ImVec2(cursor_screen_pos.x - 1.0f, cursor_screen_pos.y - g.FontSize);
|
|
g.PlatformImeData.InputLineHeight = g.FontSize;
|
|
}
|
|
}
|
|
|
|
if (is_password && !is_displaying_hint)
|
|
PopFont();
|
|
|
|
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)...
|
|
Dummy(ImVec2(text_size.x, text_size.y + style.FramePadding.y));
|
|
g.NextItemData.ItemFlags |= (ImGuiItemFlags)ImGuiItemFlags_Inputable | ImGuiItemFlags_NoTabStop;
|
|
gui->end_def_child();
|
|
item_data_backup.StatusFlags |= (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_HoveredWindow);
|
|
|
|
// ...and then we need to undo the group overriding last item data, which gets a bit messy as EndGroup() tries to forward scrollbar being active...
|
|
// FIXME: This quite messy/tricky, should attempt to get rid of the child window.
|
|
gui->end_group();
|
|
if (g.LastItemData.ID == 0 || g.LastItemData.ID != GetWindowScrollbarID(draw_window, ImGuiAxis_Y))
|
|
{
|
|
g.LastItemData.ID = id;
|
|
g.LastItemData.InFlags = item_data_backup.InFlags;
|
|
g.LastItemData.StatusFlags = item_data_backup.StatusFlags;
|
|
}
|
|
}
|
|
|
|
// Log as text
|
|
if (g.LogEnabled && (!is_password || is_displaying_hint))
|
|
{
|
|
LogSetNextTextDecoration("{", "}");
|
|
LogRenderedText(&draw_pos, buf_display, buf_display_end);
|
|
}
|
|
|
|
if (value_changed && !(flags & ImGuiInputTextFlags_NoMarkEdited))
|
|
MarkItemEdited(id);
|
|
|
|
gui->pop_font();
|
|
|
|
IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Inputable);
|
|
if ((flags & ImGuiInputTextFlags_EnterReturnsTrue) != 0)
|
|
return validated;
|
|
else
|
|
return value_changed;
|
|
}
|
|
|
|
struct text_field_state
|
|
{
|
|
bool active;
|
|
float alpha;
|
|
float hover_alpha;
|
|
float focus_alpha;
|
|
float icon_shift;
|
|
float text_shift;
|
|
ImVec4 text;
|
|
ImVec4 icon;
|
|
};
|
|
|
|
static void draw_license_key_icon(ImDrawList* draw_list, c_vec2 center, ImU32 color)
|
|
{
|
|
const float thickness = s_(1.7f);
|
|
const c_vec2 bow = center + s_(-5, 2);
|
|
const c_vec2 shaft_start = bow + s_(3.2f, -2.8f);
|
|
const c_vec2 shaft_end = center + s_(8.5f, -8.5f);
|
|
|
|
draw->circle(draw_list, bow, s_(4.4f), color, s_(28), thickness);
|
|
draw->line(draw_list, shaft_start, shaft_end, color, thickness);
|
|
draw->line(draw_list, center + s_(4.7f, -4.8f), center + s_(8.2f, -1.3f), color, thickness);
|
|
draw->line(draw_list, center + s_(7.3f, -7.4f), center + s_(10.5f, -4.2f), color, thickness);
|
|
}
|
|
|
|
static void draw_search_icon(ImDrawList* draw_list, c_vec2 center, ImU32 color)
|
|
{
|
|
const float thickness = s_(1.55f);
|
|
draw->circle(draw_list, center + s_(-1.5f, -1.5f), s_(5.2f), color, 28, thickness);
|
|
draw->line(draw_list, center + s_(2.7f, 2.7f), center + s_(7.2f, 7.2f), color, thickness);
|
|
}
|
|
|
|
bool c_widgets::text_field(std::string name, char* buf, int buf_size, bool password, std::string icon)
|
|
{
|
|
c_window* window = gui->get_window();
|
|
if (window->SkipItems)
|
|
return false;
|
|
|
|
c_id id = window->GetID(name.data());
|
|
text_field_state* state = gui->anim_container<text_field_state>(window->GetID((std::stringstream{} << id << "text_field_state").str().data()));
|
|
|
|
c_vec2 pos = window->DC.CursorPos;
|
|
c_vec2 size = c_vec2(gui->content_avail().x, s_(66));
|
|
c_rect rect(pos, pos + size);
|
|
c_rect inner(rect.Min + s_(0, 8), rect.Max - s_(0, 8));
|
|
c_rect button(inner.GetBL() - s_(0, 32), inner.GetBR());
|
|
const bool has_icon = !icon.empty();
|
|
const float icon_slot_width = 42.f;
|
|
const float text_offset = has_icon ? 54.f : elements->padding.x;
|
|
|
|
gui->item_size(rect);
|
|
if (!gui->item_add(rect, id))
|
|
return false;
|
|
|
|
bool user_clicked = rect.Contains(GetMousePos()) && gui->mouse_clicked(0) && gui->is_window_hovered(0);
|
|
gui->easing(state->text, state->active ? clr->white.Value : clr->text.Value, 24.f, dynamic_easing);
|
|
gui->easing(state->alpha, strlen(buf) > 0 ? 0.f : 1.f, 24.f, dynamic_easing);
|
|
|
|
draw->rect_filled(window->DrawList, button.Min, button.Max, draw->get_clr(clr->widget), s_(8));
|
|
|
|
draw->text_clipped(window->DrawList, font->get(inter_semibold, 12), inner.Min, inner.Max, draw->get_clr(clr->white),
|
|
name.data(), 0, 0, { 0, 0 });
|
|
|
|
if (has_icon)
|
|
{
|
|
if (icon == "key")
|
|
draw_license_key_icon(window->DrawList, button.Min + s_(20, 18), draw->get_clr(clr->accent));
|
|
else
|
|
draw->text_clipped(window->DrawList, font->get(icon_font, 15), button.Min + s_(12, 0), button.Min + s_(30, 32),
|
|
draw->get_clr(state->text), icon.data(), 0, 0, { 0.5f, 0.5f });
|
|
|
|
const float separator_x = button.Min.x + s_(icon_slot_width);
|
|
draw->line(window->DrawList, c_vec2(separator_x, button.Min.y + s_(8)), c_vec2(separator_x, button.Max.y - s_(8)),
|
|
draw->get_clr(clr->border, 0.95f), s_(1));
|
|
}
|
|
|
|
draw->text_clipped(window->DrawList, font->get(inter_medium, 11), button.Min + s_(text_offset, 0) + ImVec2((button.GetWidth() / 2) * (1.f - state->alpha), 0), button.Max, draw->get_clr(clr->text, state->alpha), name.data(), 0, 0, { 0, 0.5 });
|
|
|
|
gui->push_var(style_var_frame_rounding, s_(8));
|
|
gui->push_font(font->get(inter_medium, 11));
|
|
|
|
ImGuiInputTextFlags flags = password ? ImGuiInputTextFlags_Password : 0;
|
|
bool value_changed = text_field_ex(name, "", buf, buf_size, s_(text_offset), s_(8), draw->get_clr(state->text), false, button, &state->active, flags, 0, 0, user_clicked);
|
|
|
|
gui->pop_font();
|
|
gui->pop_var();
|
|
|
|
if (gui->content_avail().y > 0)
|
|
{
|
|
draw->line(window->DrawList, rect.GetBL(), rect.GetBR(), draw->get_clr(clr->border));
|
|
}
|
|
|
|
return value_changed;
|
|
}
|
|
|
|
bool c_widgets::search_field(std::string name, char* buf, int buf_size, const c_vec2& size)
|
|
{
|
|
c_window* window = gui->get_window();
|
|
if (window->SkipItems)
|
|
return false;
|
|
|
|
c_id id = window->GetID(name.data());
|
|
text_field_state* state = gui->anim_container<text_field_state>(window->GetID((std::stringstream{} << id << "search_field_state").str().data()));
|
|
|
|
c_vec2 pos = window->DC.CursorPos;
|
|
c_rect rect(pos, pos + size);
|
|
const float icon_slot_width = 35.f;
|
|
const float text_offset = icon_slot_width + 10.f;
|
|
|
|
gui->item_size(rect);
|
|
if (!gui->item_add(rect, id))
|
|
return false;
|
|
|
|
bool user_clicked = rect.Contains(GetMousePos()) && gui->mouse_clicked(0) && gui->is_window_hovered(0);
|
|
const bool hovered = rect.Contains(GetMousePos()) && gui->is_window_hovered(0);
|
|
const bool has_text = buf[0] != '\0';
|
|
const float target_focus = state->active ? 1.f : 0.f;
|
|
const float target_hover = hovered ? 1.f : 0.f;
|
|
|
|
gui->easing(state->hover_alpha, target_hover, 18.f, dynamic_easing);
|
|
gui->easing(state->focus_alpha, target_focus, 22.f, dynamic_easing);
|
|
gui->easing(state->icon_shift, state->active ? -2.5f : (hovered ? -1.f : 0.f), 18.f, dynamic_easing);
|
|
gui->easing(state->text_shift, state->active || has_text ? 4.f : (hovered ? 2.f : 0.f), 20.f, dynamic_easing);
|
|
gui->easing(state->text, state->active || has_text ? clr->white.Value : clr->text.Value, 24.f, dynamic_easing);
|
|
gui->easing(state->icon, state->active ? clr->accent.Value : (hovered ? clr->white.Value : clr->text.Value), 22.f, dynamic_easing);
|
|
gui->easing(state->alpha, has_text ? 0.f : 1.f, 24.f, dynamic_easing);
|
|
|
|
draw->rect_filled(window->DrawList, rect.Min, rect.Max,
|
|
draw->get_clr(clr->widget), s_(9));
|
|
|
|
draw_search_icon(window->DrawList, rect.Min + s_(17.5f + state->icon_shift, 16.f), draw->get_clr(state->icon, 0.72f + state->hover_alpha * 0.16f + state->focus_alpha * 0.12f));
|
|
|
|
const float separator_x = rect.Min.x + s_(icon_slot_width);
|
|
const float separator_width = s_(2.f);
|
|
const c_vec2 separator_min(separator_x - separator_width * 0.5f, rect.Min.y + s_(7));
|
|
const c_vec2 separator_max(separator_x + separator_width * 0.5f, rect.Max.y - s_(7));
|
|
draw->rect_filled(window->DrawList, separator_min, separator_max,
|
|
draw->get_clr(clr->border, 0.78f + state->hover_alpha * 0.10f + state->focus_alpha * 0.10f), s_(999));
|
|
draw->rect_filled(window->DrawList, separator_min + s_(0, 1), separator_max - s_(0, 1),
|
|
draw->get_clr(clr->accent, state->focus_alpha * 0.34f), s_(999));
|
|
|
|
draw->text_clipped(window->DrawList, font->get(inter_medium, 11),
|
|
rect.Min + s_(text_offset + state->text_shift, 0),
|
|
rect.Max - s_(10, 0), draw->get_clr(clr->text, state->alpha * (0.62f + state->hover_alpha * 0.16f + state->focus_alpha * 0.22f)), name.data(), 0, 0, { 0, 0.5f });
|
|
|
|
gui->push_var(style_var_frame_rounding, s_(9));
|
|
gui->push_font(font->get(inter_medium, 11));
|
|
|
|
bool value_changed = text_field_ex(name, "", buf, buf_size, s_(text_offset + state->text_shift), s_(7), draw->get_clr(state->text),
|
|
false, rect, &state->active, 0, 0, 0, user_clicked);
|
|
|
|
gui->pop_font();
|
|
gui->pop_var();
|
|
|
|
return value_changed;
|
|
}
|