|
|
|
@ -3097,12 +3097,6 @@ static ImVec2 InputTextCalcTextSizeW(const ImWchar* text_begin, const ImWchar* t
|
|
|
|
|
if (c == '\r')
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
if (*s == 1)
|
|
|
|
|
{
|
|
|
|
|
s += 9;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const float char_width = font->GetCharAdvance((ImWchar)c) * scale;
|
|
|
|
|
line_width += char_width;
|
|
|
|
|
}
|
|
|
|
@ -7509,3 +7503,838 @@ void ImGui::Columns(int columns_count, const char* id, bool border)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//-------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
// Custom part
|
|
|
|
|
|
|
|
|
|
int ColoredImTextStrFromUtf8(ImWchar* buf, int buf_size, const char* in_text, const char* in_text_end, const char** in_remaining = NULL)
|
|
|
|
|
{
|
|
|
|
|
ImWchar* buf_out = buf;
|
|
|
|
|
ImWchar* buf_end = buf + buf_size;
|
|
|
|
|
while (buf_out < buf_end - 1 && (!in_text_end || in_text < in_text_end) && *in_text)
|
|
|
|
|
{
|
|
|
|
|
unsigned int c;
|
|
|
|
|
if (*in_text == 1)
|
|
|
|
|
{
|
|
|
|
|
in_text += 9;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
in_text += ImTextCharFromUtf8(&c, in_text, in_text_end);
|
|
|
|
|
if (c == 0)
|
|
|
|
|
break;
|
|
|
|
|
if (c < 0x10000) // FIXME: Losing characters that don't fit in 2 bytes
|
|
|
|
|
* buf_out++ = (ImWchar)c;
|
|
|
|
|
}
|
|
|
|
|
*buf_out = 0;
|
|
|
|
|
if (in_remaining)
|
|
|
|
|
* in_remaining = in_text;
|
|
|
|
|
return (int)(buf_out - buf);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static ImVec2 ColoredInputTextCalcTextSizeW(const ImWchar* text_begin, const ImWchar* text_end, const ImWchar** remaining = NULL, ImVec2* out_offset = NULL, bool stop_on_new_line = false)
|
|
|
|
|
{
|
|
|
|
|
ImGuiContext& g = *GImGui;
|
|
|
|
|
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 ImWchar* s = text_begin;
|
|
|
|
|
while (s < text_end)
|
|
|
|
|
{
|
|
|
|
|
if (*s == 1)
|
|
|
|
|
{
|
|
|
|
|
s += 9;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
unsigned int c = (unsigned int)(*s++);
|
|
|
|
|
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 = font->GetCharAdvance((ImWchar)c) * 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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Same code as InputTextEx but with colored functions
|
|
|
|
|
bool ImGui::ColoredInputTextEx(const char* label, const char* hint, char* buf, int buf_size, const ImVec2& size_arg, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* callback_user_data)
|
|
|
|
|
{
|
|
|
|
|
ImGuiWindow* window = GetCurrentWindow();
|
|
|
|
|
if (window->SkipItems)
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
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!
|
|
|
|
|
|
|
|
|
|
if (is_multiline) // Open group before calling GetID() because groups tracks id created within their scope,
|
|
|
|
|
BeginGroup();
|
|
|
|
|
const ImGuiID id = window->GetID(label);
|
|
|
|
|
const ImVec2 label_size = CalcTextSize(label, NULL, true);
|
|
|
|
|
ImVec2 size = CalcItemSize(size_arg, CalcItemWidth(), (is_multiline ? GetTextLineHeight() * 8.0f : label_size.y) + style.FramePadding.y * 2.0f); // Arbitrary default of 8 lines high for multi-line
|
|
|
|
|
const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + size);
|
|
|
|
|
const ImRect total_bb(frame_bb.Min, frame_bb.Max + ImVec2(label_size.x > 0.0f ? (style.ItemInnerSpacing.x + label_size.x) : 0.0f, 0.0f));
|
|
|
|
|
|
|
|
|
|
ImGuiWindow* draw_window = window;
|
|
|
|
|
if (is_multiline)
|
|
|
|
|
{
|
|
|
|
|
if (!ItemAdd(total_bb, id, &frame_bb))
|
|
|
|
|
{
|
|
|
|
|
ItemSize(total_bb, style.FramePadding.y);
|
|
|
|
|
EndGroup();
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
if (!BeginChildFrame(id, frame_bb.GetSize()))
|
|
|
|
|
{
|
|
|
|
|
EndChildFrame();
|
|
|
|
|
EndGroup();
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
draw_window = GetCurrentWindow();
|
|
|
|
|
draw_window->DC.NavLayerActiveMaskNext |= draw_window->DC.NavLayerCurrentMask; // This is to ensure that EndChild() will display a navigation highlight
|
|
|
|
|
size.x -= draw_window->ScrollbarSizes.x;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
ItemSize(total_bb, style.FramePadding.y);
|
|
|
|
|
if (!ItemAdd(total_bb, id, &frame_bb))
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
const bool hovered = ItemHoverable(frame_bb, id);
|
|
|
|
|
if (hovered)
|
|
|
|
|
g.MouseCursor = ImGuiMouseCursor_TextInput;
|
|
|
|
|
|
|
|
|
|
// NB: we are only allowed to access 'edit_state' if we are the active widget.
|
|
|
|
|
ImGuiInputTextState* state = NULL;
|
|
|
|
|
if (g.InputTextState.ID == id)
|
|
|
|
|
state = &g.InputTextState;
|
|
|
|
|
|
|
|
|
|
const bool focus_requested = FocusableItemRegister(window, id);
|
|
|
|
|
const bool focus_requested_by_code = focus_requested && (g.FocusRequestCurrWindow == window && g.FocusRequestCurrCounterAll == window->DC.FocusCounterAll);
|
|
|
|
|
const bool focus_requested_by_tab = focus_requested && !focus_requested_by_code;
|
|
|
|
|
|
|
|
|
|
const bool user_clicked = hovered && io.MouseClicked[0];
|
|
|
|
|
const bool user_nav_input_start = (g.ActiveId != id) && ((g.NavInputId == id) || (g.NavActivateId == id && g.NavInputSource == ImGuiInputSource_NavKeyboard));
|
|
|
|
|
const bool user_scroll_finish = is_multiline && state != NULL && g.ActiveId == 0 && g.ActiveIdPreviousFrame == GetScrollbarID(draw_window, ImGuiAxis_Y);
|
|
|
|
|
const bool user_scroll_active = is_multiline && state != NULL && g.ActiveId == GetScrollbarID(draw_window, ImGuiAxis_Y);
|
|
|
|
|
|
|
|
|
|
bool clear_active_id = false;
|
|
|
|
|
bool select_all = (g.ActiveId != id) && ((flags & ImGuiInputTextFlags_AutoSelectAll) != 0 || user_nav_input_start) && (!is_multiline);
|
|
|
|
|
|
|
|
|
|
const bool init_make_active = (focus_requested || user_clicked || user_scroll_finish || user_nav_input_start);
|
|
|
|
|
const bool init_state = (init_make_active || user_scroll_active);
|
|
|
|
|
if (init_state && g.ActiveId != id)
|
|
|
|
|
{
|
|
|
|
|
// Access state even if we don't own it yet.
|
|
|
|
|
state = &g.InputTextState;
|
|
|
|
|
state->CursorAnimReset();
|
|
|
|
|
|
|
|
|
|
// Take a copy of the initial buffer value (both in original UTF-8 format and converted to wchar)
|
|
|
|
|
// From the moment we focused we are ignoring the content of 'buf' (unless we are in read-only mode)
|
|
|
|
|
const int buf_len = (int)strlen(buf);
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
// Start edition
|
|
|
|
|
const char* buf_end = NULL;
|
|
|
|
|
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 = ColoredImTextStrFromUtf8(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.
|
|
|
|
|
|
|
|
|
|
// Preserve cursor position and undo/redo stack if we come back to same widget
|
|
|
|
|
// FIXME: For non-readonly widgets we might be able to require that TextAIsValid && TextA == buf ? (untested) and discard undo stack if user buffer has changed.
|
|
|
|
|
const bool recycle_state = (state->ID == id);
|
|
|
|
|
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->ID = id;
|
|
|
|
|
state->ScrollX = 0.0f;
|
|
|
|
|
stb_textedit_initialize_state(&state->Stb, !is_multiline);
|
|
|
|
|
if (!is_multiline && focus_requested_by_code)
|
|
|
|
|
select_all = true;
|
|
|
|
|
}
|
|
|
|
|
if (flags & ImGuiInputTextFlags_AlwaysInsertMode)
|
|
|
|
|
state->Stb.insert_mode = 1;
|
|
|
|
|
if (!is_multiline && (focus_requested_by_tab || (user_clicked && io.KeyCtrl)))
|
|
|
|
|
select_all = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (g.ActiveId != id && init_make_active)
|
|
|
|
|
{
|
|
|
|
|
IM_ASSERT(state && state->ID == id);
|
|
|
|
|
SetActiveID(id, window);
|
|
|
|
|
SetFocusID(id, window);
|
|
|
|
|
FocusWindow(window);
|
|
|
|
|
IM_ASSERT(ImGuiNavInput_COUNT < 32);
|
|
|
|
|
g.ActiveIdBlockNavInputFlags = (1 << ImGuiNavInput_Cancel);
|
|
|
|
|
if (flags & (ImGuiInputTextFlags_CallbackCompletion | ImGuiInputTextFlags_AllowTabInput)) // Disable keyboard tabbing out as we will use the \t character.
|
|
|
|
|
g.ActiveIdBlockNavInputFlags |= (1 << ImGuiNavInput_KeyTab_);
|
|
|
|
|
if (!is_multiline && !(flags & ImGuiInputTextFlags_CallbackHistory))
|
|
|
|
|
g.ActiveIdAllowNavDirFlags = ((1 << ImGuiDir_Up) | (1 << ImGuiDir_Down));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
const bool render_cursor = (g.ActiveId == id) || (state && user_scroll_active);
|
|
|
|
|
bool render_selection = state && state->HasSelection() && (RENDER_SELECTION_WHEN_INACTIVE || render_cursor);
|
|
|
|
|
bool value_changed = false;
|
|
|
|
|
bool enter_pressed = 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 = ColoredImTextStrFromUtf8(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);
|
|
|
|
|
|
|
|
|
|
// Password pushes a temporary font with only a fallback glyph
|
|
|
|
|
if (is_password && !is_displaying_hint)
|
|
|
|
|
{
|
|
|
|
|
const ImFontGlyph* glyph = g.Font->FindGlyph('*');
|
|
|
|
|
ImFont* password_font = &g.InputTextPasswordFont;
|
|
|
|
|
password_font->FontSize = g.Font->FontSize;
|
|
|
|
|
password_font->Scale = g.Font->Scale;
|
|
|
|
|
password_font->DisplayOffset = g.Font->DisplayOffset;
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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->BufCapacityA = buf_size;
|
|
|
|
|
state->UserFlags = flags;
|
|
|
|
|
state->UserCallback = callback;
|
|
|
|
|
state->UserCallbackData = callback_user_data;
|
|
|
|
|
|
|
|
|
|
// 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];
|
|
|
|
|
g.WantTextInputNextFrame = 1;
|
|
|
|
|
|
|
|
|
|
// Edit in progress
|
|
|
|
|
const float mouse_x = (io.MousePos.x - frame_bb.Min.x - style.FramePadding.x) + state->ScrollX;
|
|
|
|
|
const float mouse_y = (is_multiline ? (io.MousePos.y - draw_window->DC.CursorPos.y - style.FramePadding.y) : (g.FontSize * 0.5f));
|
|
|
|
|
|
|
|
|
|
const bool is_osx = io.ConfigMacOSXBehaviors;
|
|
|
|
|
if (select_all || (hovered && !is_osx && io.MouseDoubleClicked[0]))
|
|
|
|
|
{
|
|
|
|
|
state->SelectAll();
|
|
|
|
|
state->SelectedAllMouseLock = true;
|
|
|
|
|
}
|
|
|
|
|
else if (hovered && is_osx && io.MouseDoubleClicked[0])
|
|
|
|
|
{
|
|
|
|
|
// Double-click select a word only, OS X style (by simulating keystrokes)
|
|
|
|
|
state->OnKeyPressed(STB_TEXTEDIT_K_WORDLEFT);
|
|
|
|
|
state->OnKeyPressed(STB_TEXTEDIT_K_WORDRIGHT | STB_TEXTEDIT_K_SHIFT);
|
|
|
|
|
}
|
|
|
|
|
else if (io.MouseClicked[0] && !state->SelectedAllMouseLock)
|
|
|
|
|
{
|
|
|
|
|
if (hovered)
|
|
|
|
|
{
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
// It is ill-defined whether the back-end needs to send a \t character when pressing the TAB keys.
|
|
|
|
|
// Win32 and GLFW naturally do it but not SDL.
|
|
|
|
|
const bool ignore_char_inputs = (io.KeyCtrl && !io.KeyAlt) || (is_osx && io.KeySuper);
|
|
|
|
|
if ((flags & ImGuiInputTextFlags_AllowTabInput) && IsKeyPressedMap(ImGuiKey_Tab) && !ignore_char_inputs && !io.KeyShift && !is_readonly)
|
|
|
|
|
if (!io.InputQueueCharacters.contains('\t'))
|
|
|
|
|
{
|
|
|
|
|
unsigned int c = '\t'; // Insert TAB
|
|
|
|
|
if (InputTextFilterCharacter(&c, flags, callback, callback_user_data))
|
|
|
|
|
state->OnKeyPressed((int)c);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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.
|
|
|
|
|
if (io.InputQueueCharacters.Size > 0)
|
|
|
|
|
{
|
|
|
|
|
if (!ignore_char_inputs && !is_readonly && !user_nav_input_start)
|
|
|
|
|
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' && io.KeyShift)
|
|
|
|
|
continue;
|
|
|
|
|
if (InputTextFilterCharacter(&c, flags, callback, callback_user_data))
|
|
|
|
|
state->OnKeyPressed((int)c);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Consume characters
|
|
|
|
|
io.InputQueueCharacters.resize(0);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Process other shortcuts/key-presses
|
|
|
|
|
bool cancel_edit = false;
|
|
|
|
|
if (g.ActiveId == id && !g.ActiveIdIsJustActivated && !clear_active_id)
|
|
|
|
|
{
|
|
|
|
|
IM_ASSERT(state != NULL);
|
|
|
|
|
const int k_mask = (io.KeyShift ? STB_TEXTEDIT_K_SHIFT : 0);
|
|
|
|
|
const bool is_osx = io.ConfigMacOSXBehaviors;
|
|
|
|
|
const bool is_shortcut_key = (is_osx ? (io.KeySuper && !io.KeyCtrl) : (io.KeyCtrl && !io.KeySuper)) && !io.KeyAlt && !io.KeyShift; // OS X style: Shortcuts using Cmd/Super instead of Ctrl
|
|
|
|
|
const bool is_osx_shift_shortcut = is_osx && io.KeySuper && io.KeyShift && !io.KeyCtrl && !io.KeyAlt;
|
|
|
|
|
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.KeySuper && !io.KeyCtrl && !io.KeyAlt; // OS X style: Line/Text Start and End using Cmd+Arrows instead of Home/End
|
|
|
|
|
const bool is_ctrl_key_only = io.KeyCtrl && !io.KeyShift && !io.KeyAlt && !io.KeySuper;
|
|
|
|
|
const bool is_shift_key_only = io.KeyShift && !io.KeyCtrl && !io.KeyAlt && !io.KeySuper;
|
|
|
|
|
|
|
|
|
|
const bool is_cut = ((is_shortcut_key && IsKeyPressedMap(ImGuiKey_X)) || (is_shift_key_only && IsKeyPressedMap(ImGuiKey_Delete))) && !is_readonly && !is_password && (!is_multiline || state->HasSelection());
|
|
|
|
|
const bool is_copy = ((is_shortcut_key && IsKeyPressedMap(ImGuiKey_C)) || (is_ctrl_key_only && IsKeyPressedMap(ImGuiKey_Insert))) && !is_password && (!is_multiline || state->HasSelection());
|
|
|
|
|
const bool is_paste = ((is_shortcut_key && IsKeyPressedMap(ImGuiKey_V)) || (is_shift_key_only && IsKeyPressedMap(ImGuiKey_Insert))) && !is_readonly;
|
|
|
|
|
const bool is_undo = ((is_shortcut_key && IsKeyPressedMap(ImGuiKey_Z)) && !is_readonly && is_undoable);
|
|
|
|
|
const bool is_redo = ((is_shortcut_key && IsKeyPressedMap(ImGuiKey_Y)) || (is_osx_shift_shortcut && IsKeyPressedMap(ImGuiKey_Z))) && !is_readonly && is_undoable;
|
|
|
|
|
|
|
|
|
|
if (IsKeyPressedMap(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 (IsKeyPressedMap(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 (IsKeyPressedMap(ImGuiKey_UpArrow) && is_multiline) { if (io.KeyCtrl) SetWindowScrollY(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 (IsKeyPressedMap(ImGuiKey_DownArrow) && is_multiline) { if (io.KeyCtrl) SetWindowScrollY(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 (IsKeyPressedMap(ImGuiKey_Home)) { state->OnKeyPressed(io.KeyCtrl ? STB_TEXTEDIT_K_TEXTSTART | k_mask : STB_TEXTEDIT_K_LINESTART | k_mask); }
|
|
|
|
|
else if (IsKeyPressedMap(ImGuiKey_End)) { state->OnKeyPressed(io.KeyCtrl ? STB_TEXTEDIT_K_TEXTEND | k_mask : STB_TEXTEDIT_K_LINEEND | k_mask); }
|
|
|
|
|
else if (IsKeyPressedMap(ImGuiKey_Delete) && !is_readonly) { state->OnKeyPressed(STB_TEXTEDIT_K_DELETE | k_mask); }
|
|
|
|
|
else if (IsKeyPressedMap(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.KeySuper && !io.KeyAlt && !io.KeyCtrl)
|
|
|
|
|
state->OnKeyPressed(STB_TEXTEDIT_K_LINESTART | STB_TEXTEDIT_K_SHIFT);
|
|
|
|
|
}
|
|
|
|
|
state->OnKeyPressed(STB_TEXTEDIT_K_BACKSPACE | k_mask);
|
|
|
|
|
}
|
|
|
|
|
else if (IsKeyPressedMap(ImGuiKey_Enter))
|
|
|
|
|
{
|
|
|
|
|
bool ctrl_enter_for_new_line = (flags & ImGuiInputTextFlags_CtrlEnterForNewLine) != 0;
|
|
|
|
|
if (!is_multiline || (ctrl_enter_for_new_line && !io.KeyCtrl) || (!ctrl_enter_for_new_line && io.KeyCtrl))
|
|
|
|
|
{
|
|
|
|
|
enter_pressed = clear_active_id = true;
|
|
|
|
|
}
|
|
|
|
|
else if (!is_readonly)
|
|
|
|
|
{
|
|
|
|
|
unsigned int c = '\n'; // Insert new line
|
|
|
|
|
if (InputTextFilterCharacter(&c, flags, callback, callback_user_data))
|
|
|
|
|
state->OnKeyPressed((int)c);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else if (IsKeyPressedMap(ImGuiKey_Escape))
|
|
|
|
|
{
|
|
|
|
|
clear_active_id = cancel_edit = true;
|
|
|
|
|
}
|
|
|
|
|
else if (is_undo || is_redo)
|
|
|
|
|
{
|
|
|
|
|
state->OnKeyPressed(is_undo ? STB_TEXTEDIT_K_UNDO : STB_TEXTEDIT_K_REDO);
|
|
|
|
|
state->ClearSelection();
|
|
|
|
|
}
|
|
|
|
|
else if (is_shortcut_key && IsKeyPressedMap(ImGuiKey_A))
|
|
|
|
|
{
|
|
|
|
|
state->SelectAll();
|
|
|
|
|
state->CursorFollow = true;
|
|
|
|
|
}
|
|
|
|
|
else if (is_cut || is_copy)
|
|
|
|
|
{
|
|
|
|
|
// Cut, Copy
|
|
|
|
|
if (io.SetClipboardTextFn)
|
|
|
|
|
{
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
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);
|
|
|
|
|
ImWchar* clipboard_filtered = (ImWchar*)IM_ALLOC((clipboard_len + 1) * sizeof(ImWchar));
|
|
|
|
|
int clipboard_filtered_len = 0;
|
|
|
|
|
for (const char* s = clipboard; *s; )
|
|
|
|
|
{
|
|
|
|
|
unsigned int c;
|
|
|
|
|
s += ImTextCharFromUtf8(&c, s, NULL);
|
|
|
|
|
if (c == 0)
|
|
|
|
|
break;
|
|
|
|
|
if (c >= 0x10000 || !InputTextFilterCharacter(&c, flags, callback, callback_user_data))
|
|
|
|
|
continue;
|
|
|
|
|
clipboard_filtered[clipboard_filtered_len++] = (ImWchar)c;
|
|
|
|
|
}
|
|
|
|
|
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.
|
|
|
|
|
if (g.ActiveId == id)
|
|
|
|
|
{
|
|
|
|
|
IM_ASSERT(state != NULL);
|
|
|
|
|
const char* apply_new_text = NULL;
|
|
|
|
|
int apply_new_text_length = 0;
|
|
|
|
|
if (cancel_edit)
|
|
|
|
|
{
|
|
|
|
|
// Restore initial value. Only return true if restoring to the initial value changes the current buffer contents.
|
|
|
|
|
if (!is_readonly && strcmp(buf, state->InitialTextA.Data) != 0)
|
|
|
|
|
{
|
|
|
|
|
apply_new_text = state->InitialTextA.Data;
|
|
|
|
|
apply_new_text_length = 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. Also this allows the user to use InputText() with ImGuiInputTextFlags_EnterReturnsTrue without maintaining any user-side storage.
|
|
|
|
|
bool apply_edit_back_to_user_buffer = !cancel_edit || (enter_pressed && (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.
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// User callback
|
|
|
|
|
if ((flags & (ImGuiInputTextFlags_CallbackCompletion | ImGuiInputTextFlags_CallbackHistory | 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_COUNT;
|
|
|
|
|
if ((flags & ImGuiInputTextFlags_CallbackCompletion) != 0 && IsKeyPressedMap(ImGuiKey_Tab))
|
|
|
|
|
{
|
|
|
|
|
event_flag = ImGuiInputTextFlags_CallbackCompletion;
|
|
|
|
|
event_key = ImGuiKey_Tab;
|
|
|
|
|
}
|
|
|
|
|
else if ((flags & ImGuiInputTextFlags_CallbackHistory) != 0 && IsKeyPressedMap(ImGuiKey_UpArrow))
|
|
|
|
|
{
|
|
|
|
|
event_flag = ImGuiInputTextFlags_CallbackHistory;
|
|
|
|
|
event_key = ImGuiKey_UpArrow;
|
|
|
|
|
}
|
|
|
|
|
else if ((flags & ImGuiInputTextFlags_CallbackHistory) != 0 && IsKeyPressedMap(ImGuiKey_DownArrow))
|
|
|
|
|
{
|
|
|
|
|
event_flag = ImGuiInputTextFlags_CallbackHistory;
|
|
|
|
|
event_key = ImGuiKey_DownArrow;
|
|
|
|
|
}
|
|
|
|
|
else if (flags & ImGuiInputTextFlags_CallbackAlways)
|
|
|
|
|
event_flag = ImGuiInputTextFlags_CallbackAlways;
|
|
|
|
|
|
|
|
|
|
if (event_flag)
|
|
|
|
|
{
|
|
|
|
|
ImGuiInputTextCallbackData callback_data;
|
|
|
|
|
memset(&callback_data, 0, sizeof(ImGuiInputTextCallbackData));
|
|
|
|
|
callback_data.EventFlag = event_flag;
|
|
|
|
|
callback_data.Flags = flags;
|
|
|
|
|
callback_data.UserData = callback_user_data;
|
|
|
|
|
|
|
|
|
|
callback_data.EventKey = event_key;
|
|
|
|
|
callback_data.Buf = state->TextA.Data;
|
|
|
|
|
callback_data.BufTextLen = state->CurLenA;
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
// Call user code
|
|
|
|
|
callback(&callback_data);
|
|
|
|
|
|
|
|
|
|
// Read back what user may have modified
|
|
|
|
|
IM_ASSERT(callback_data.Buf == state->TextA.Data); // Invalid to modify those fields
|
|
|
|
|
IM_ASSERT(callback_data.BufSize == state->BufCapacityA);
|
|
|
|
|
IM_ASSERT(callback_data.Flags == flags);
|
|
|
|
|
if (callback_data.CursorPos != utf8_cursor_pos) { state->Stb.cursor = ImTextCountCharsFromUtf8(callback_data.Buf, callback_data.Buf + callback_data.CursorPos); state->CursorFollow = true; }
|
|
|
|
|
if (callback_data.SelectionStart != utf8_selection_start) { state->Stb.select_start = ImTextCountCharsFromUtf8(callback_data.Buf, callback_data.Buf + callback_data.SelectionStart); }
|
|
|
|
|
if (callback_data.SelectionEnd != utf8_selection_end) { state->Stb.select_end = ImTextCountCharsFromUtf8(callback_data.Buf, callback_data.Buf + callback_data.SelectionEnd); }
|
|
|
|
|
if (callback_data.BufDirty)
|
|
|
|
|
{
|
|
|
|
|
IM_ASSERT(callback_data.BufTextLen == (int)strlen(callback_data.Buf)); // You need to maintain BufTextLen if you change the text!
|
|
|
|
|
if (callback_data.BufTextLen > backup_current_text_length && is_resizable)
|
|
|
|
|
state->TextW.resize(state->TextW.Size + (callback_data.BufTextLen - backup_current_text_length));
|
|
|
|
|
state->CurLenW = ColoredImTextStrFromUtf8(state->TextW.Data, state->TextW.Size, callback_data.Buf, NULL);
|
|
|
|
|
state->CurLenA = callback_data.BufTextLen; // Assume correct length and valid UTF-8 from user, saves us an extra strlen()
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Copy result to user buffer
|
|
|
|
|
if (apply_new_text)
|
|
|
|
|
{
|
|
|
|
|
IM_ASSERT(apply_new_text_length >= 0);
|
|
|
|
|
if (backup_current_text_length != apply_new_text_length && is_resizable)
|
|
|
|
|
{
|
|
|
|
|
ImGuiInputTextCallbackData callback_data;
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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));
|
|
|
|
|
value_changed = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Clear temporary user storage
|
|
|
|
|
state->UserFlags = 0;
|
|
|
|
|
state->UserCallback = NULL;
|
|
|
|
|
state->UserCallbackData = NULL;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Release active ID at the end of the function (so e.g. pressing Return still does a final application of the value)
|
|
|
|
|
if (clear_active_id && g.ActiveId == id)
|
|
|
|
|
ClearActiveID();
|
|
|
|
|
|
|
|
|
|
// Render frame
|
|
|
|
|
if (!is_multiline)
|
|
|
|
|
{
|
|
|
|
|
RenderNavHighlight(frame_bb, id);
|
|
|
|
|
RenderFrame(frame_bb.Min, frame_bb.Max, GetColorU32(ImGuiCol_FrameBg), true, style.FrameRounding);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const ImVec4 clip_rect(frame_bb.Min.x, frame_bb.Min.y, frame_bb.Min.x + size.x, frame_bb.Min.y + size.y); // Not using frame_bb.Max because we have adjusted size
|
|
|
|
|
ImVec2 draw_pos = is_multiline ? draw_window->DC.CursorPos : frame_bb.Min + style.FramePadding;
|
|
|
|
|
ImVec2 text_size(0.0f, 0.0f);
|
|
|
|
|
|
|
|
|
|
// 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.
|
|
|
|
|
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
|
|
|
|
|
if (is_displaying_hint)
|
|
|
|
|
{
|
|
|
|
|
buf_display = hint;
|
|
|
|
|
buf_display_end = hint + strlen(hint);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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 ImWchar* text_begin = state->TextW.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 };
|
|
|
|
|
int searches_result_line_no[2] = { -1000, -1000 };
|
|
|
|
|
int searches_remaining = 0;
|
|
|
|
|
if (render_cursor)
|
|
|
|
|
{
|
|
|
|
|
searches_input_ptr[0] = text_begin + state->Stb.cursor;
|
|
|
|
|
searches_result_line_no[0] = -1;
|
|
|
|
|
searches_remaining++;
|
|
|
|
|
}
|
|
|
|
|
if (render_selection)
|
|
|
|
|
{
|
|
|
|
|
searches_input_ptr[1] = text_begin + ImMin(state->Stb.select_start, state->Stb.select_end);
|
|
|
|
|
searches_result_line_no[1] = -1;
|
|
|
|
|
searches_remaining++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Iterate all lines to find our line numbers
|
|
|
|
|
// 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-bits
|
|
|
|
|
for (const ImWchar* 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; }
|
|
|
|
|
}
|
|
|
|
|
line_count++;
|
|
|
|
|
if (searches_result_line_no[0] == -1)
|
|
|
|
|
searches_result_line_no[0] = line_count;
|
|
|
|
|
if (searches_result_line_no[1] == -1)
|
|
|
|
|
searches_result_line_no[1] = line_count;
|
|
|
|
|
|
|
|
|
|
// Calculate 2d position by finding the beginning of the line and measuring distance
|
|
|
|
|
cursor_offset.x = ColoredInputTextCalcTextSizeW(ImStrbolW(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 = ColoredInputTextCalcTextSizeW(ImStrbolW(searches_input_ptr[1], text_begin), searches_input_ptr[1]).x;
|
|
|
|
|
select_start_offset.y = searches_result_line_no[1] * 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(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 = size.x * 0.25f;
|
|
|
|
|
if (cursor_offset.x < state->ScrollX)
|
|
|
|
|
state->ScrollX = (float)(int)ImMax(0.0f, cursor_offset.x - scroll_increment_x);
|
|
|
|
|
else if (cursor_offset.x - size.x >= state->ScrollX)
|
|
|
|
|
state->ScrollX = (float)(int)(cursor_offset.x - size.x + scroll_increment_x);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
state->ScrollX = 0.0f;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Vertical scroll
|
|
|
|
|
if (is_multiline)
|
|
|
|
|
{
|
|
|
|
|
float scroll_y = draw_window->Scroll.y;
|
|
|
|
|
if (cursor_offset.y - g.FontSize < scroll_y)
|
|
|
|
|
scroll_y = ImMax(0.0f, cursor_offset.y - g.FontSize);
|
|
|
|
|
else if (cursor_offset.y - size.y >= scroll_y)
|
|
|
|
|
scroll_y = cursor_offset.y - size.y;
|
|
|
|
|
draw_window->DC.CursorPos.y += (draw_window->Scroll.y - scroll_y); // Manipulate cursor pos immediately avoid a frame of lag
|
|
|
|
|
draw_window->Scroll.y = scroll_y;
|
|
|
|
|
draw_pos.y = draw_window->DC.CursorPos.y;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
state->CursorFollow = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Draw selection
|
|
|
|
|
const ImVec2 draw_scroll = ImVec2(state->ScrollX, 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);
|
|
|
|
|
|
|
|
|
|
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; )
|
|
|
|
|
{
|
|
|
|
|
if (rect_pos.y > clip_rect.w + g.FontSize)
|
|
|
|
|
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-bits
|
|
|
|
|
//p = p ? p + 1 : text_selected_end;
|
|
|
|
|
while (p < text_selected_end)
|
|
|
|
|
if (*p++ == '\n')
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
ImVec2 rect_size = ColoredInputTextCalcTextSizeW(p, text_selected_end, &p, NULL, true);
|
|
|
|
|
if (rect_size.x <= 0.0f) rect_size.x = (float)(int)(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);
|
|
|
|
|
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.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.
|
|
|
|
|
if (is_multiline || (buf_display_end - buf_display) < buf_display_max_length)
|
|
|
|
|
{
|
|
|
|
|
ImU32 col = GetColorU32(is_displaying_hint ? ImGuiCol_TextDisabled : ImGuiCol_Text);
|
|
|
|
|
draw_window->DrawList->AddText(g.Font, g.FontSize, draw_pos - draw_scroll, col, buf_display, buf_display_end, 0.0f, is_multiline ? NULL : &clip_rect);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Draw blinking cursor
|
|
|
|
|
if (render_cursor)
|
|
|
|
|
{
|
|
|
|
|
state->CursorAnim += io.DeltaTime;
|
|
|
|
|
bool cursor_is_visible = (!g.IO.ConfigInputTextCursorBlink) || (state->CursorAnim <= 0.0f) || ImFmod(state->CursorAnim, 1.20f) <= 0.80f;
|
|
|
|
|
ImVec2 cursor_screen_pos = draw_pos + cursor_offset - draw_scroll;
|
|
|
|
|
ImRect cursor_screen_rect(cursor_screen_pos.x, cursor_screen_pos.y - g.FontSize + 0.5f, cursor_screen_pos.x + 1.0f, cursor_screen_pos.y - 1.5f);
|
|
|
|
|
if (cursor_is_visible && cursor_screen_rect.Overlaps(clip_rect))
|
|
|
|
|
draw_window->DrawList->AddLine(cursor_screen_rect.Min, cursor_screen_rect.GetBL(), GetColorU32(ImGuiCol_Text));
|
|
|
|
|
|
|
|
|
|
// Notify OS of text input position for advanced IME (-1 x offset so that Windows IME can cover our cursor. Bit of an extra nicety.)
|
|
|
|
|
if (!is_readonly)
|
|
|
|
|
g.PlatformImePos = ImVec2(cursor_screen_pos.x - 1.0f, cursor_screen_pos.y - g.FontSize);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
// Render text only (no selection, no cursor)
|
|
|
|
|
if (is_multiline)
|
|
|
|
|
text_size = ImVec2(size.x, InputTextCalcTextLenAndLineCount(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);
|
|
|
|
|
draw_window->DrawList->AddText(g.Font, g.FontSize, draw_pos, col, buf_display, buf_display_end, 0.0f, is_multiline ? NULL : &clip_rect);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (is_multiline)
|
|
|
|
|
{
|
|
|
|
|
Dummy(text_size + ImVec2(0.0f, g.FontSize)); // Always add room to scroll an extra line
|
|
|
|
|
EndChildFrame();
|
|
|
|
|
EndGroup();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (is_password && !is_displaying_hint)
|
|
|
|
|
PopFont();
|
|
|
|
|
|
|
|
|
|
// Log as text
|
|
|
|
|
if (g.LogEnabled && !(is_password && !is_displaying_hint))
|
|
|
|
|
LogRenderedText(&draw_pos, buf_display, buf_display_end);
|
|
|
|
|
|
|
|
|
|
if (label_size.x > 0)
|
|
|
|
|
RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label);
|
|
|
|
|
|
|
|
|
|
if (value_changed && !(flags & ImGuiInputTextFlags_NoMarkEdited))
|
|
|
|
|
MarkItemEdited(id);
|
|
|
|
|
|
|
|
|
|
IMGUI_TEST_ENGINE_ITEM_INFO(id, label, window->DC.ItemFlags);
|
|
|
|
|
if ((flags & ImGuiInputTextFlags_EnterReturnsTrue) != 0)
|
|
|
|
|
return enter_pressed;
|
|
|
|
|
else
|
|
|
|
|
return value_changed;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool ImGui::ColoredInputTextMultiline(const char* label, char* buf, size_t buf_size, const ImVec2& size, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data)
|
|
|
|
|
{
|
|
|
|
|
return ColoredInputTextEx(label, NULL, buf, (int)buf_size, size, flags | ImGuiInputTextFlags_Multiline, callback, user_data);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|