Browse Source

InputText: Added a line index. Refactored cursor and selection rendering, now simpler, easier to reason about, and faster. (#3237, #952, #1062, #7363)

ocornut 4 days ago
parent
commit
1e52e7b90c
4 changed files with 180 additions and 185 deletions
  1. 2 0
      docs/CHANGELOG.txt
  2. 11 0
      imgui.cpp
  3. 3 1
      imgui_internal.h
  4. 164 184
      imgui_widgets.cpp

+ 2 - 0
docs/CHANGELOG.txt

@@ -66,6 +66,8 @@ Other Changes:
   In theory the buffer size should always account for a zero-terminator, but idioms
   In theory the buffer size should always account for a zero-terminator, but idioms
   such as using InputTextMultiline() with ImGuiInputTextFlags_ReadOnly to display
   such as using InputTextMultiline() with ImGuiInputTextFlags_ReadOnly to display
   a text blob are facilitated by allowing this.
   a text blob are facilitated by allowing this.
+- InputText: refactored internals to simplify and optimizing rendering of selection.
+  Very large selection (e.g. 1 MB) now take less overhead.
 - InputText: revert a change in 1.79 where pressing Down or PageDown on the last line
 - InputText: revert a change in 1.79 where pressing Down or PageDown on the last line
   of a multi-line buffer without a trailing carriage return would keep the cursor
   of a multi-line buffer without a trailing carriage return would keep the cursor
   unmoved. We revert back to move to the end of line in this situation.
   unmoved. We revert back to move to the end of line in this situation.

+ 11 - 0
imgui.cpp

@@ -3074,6 +3074,7 @@ void ImGuiTextBuffer::appendfv(const char* fmt, va_list args)
     va_end(args_copy);
     va_end(args_copy);
 }
 }
 
 
+IM_MSVC_RUNTIME_CHECKS_OFF
 void ImGuiTextIndex::append(const char* base, int old_size, int new_size)
 void ImGuiTextIndex::append(const char* base, int old_size, int new_size)
 {
 {
     IM_ASSERT(old_size >= 0 && new_size >= old_size && new_size >= EndOffset);
     IM_ASSERT(old_size >= 0 && new_size >= old_size && new_size >= EndOffset);
@@ -3087,6 +3088,7 @@ void ImGuiTextIndex::append(const char* base, int old_size, int new_size)
             Offsets.push_back((int)(intptr_t)(p - base));
             Offsets.push_back((int)(intptr_t)(p - base));
     EndOffset = ImMax(EndOffset, new_size);
     EndOffset = ImMax(EndOffset, new_size);
 }
 }
+IM_MSVC_RUNTIME_CHECKS_RESTORE
 
 
 //-----------------------------------------------------------------------------
 //-----------------------------------------------------------------------------
 // [SECTION] ImGuiListClipper
 // [SECTION] ImGuiListClipper
@@ -3414,6 +3416,13 @@ bool ImGuiListClipper::Step()
     return ret;
     return ret;
 }
 }
 
 
+// Generic helper, equivalent to old ImGui::CalcListClipping() but statelesss
+void ImGui::CalcClipRectVisibleItemsY(const ImRect& clip_rect, const ImVec2& pos, float items_height, int* out_visible_start, int* out_visible_end)
+{
+    *out_visible_start = ImMax((int)((clip_rect.Min.y - pos.y) / items_height), 0);
+    *out_visible_end = ImMax((int)ImCeil((clip_rect.Max.y - pos.y) / items_height), *out_visible_start);
+}
+
 //-----------------------------------------------------------------------------
 //-----------------------------------------------------------------------------
 // [SECTION] STYLING
 // [SECTION] STYLING
 //-----------------------------------------------------------------------------
 //-----------------------------------------------------------------------------
@@ -4374,6 +4383,7 @@ void ImGui::Shutdown()
     g.ClipboardHandlerData.clear();
     g.ClipboardHandlerData.clear();
     g.MenusIdSubmittedThisFrame.clear();
     g.MenusIdSubmittedThisFrame.clear();
     g.InputTextState.ClearFreeMemory();
     g.InputTextState.ClearFreeMemory();
+    g.InputTextLineIndex.clear();
     g.InputTextDeactivatedState.ClearFreeMemory();
     g.InputTextDeactivatedState.ClearFreeMemory();
 
 
     g.SettingsWindows.clear();
     g.SettingsWindows.clear();
@@ -4489,6 +4499,7 @@ void ImGui::GcCompactTransientMiscBuffers()
     ImGuiContext& g = *GImGui;
     ImGuiContext& g = *GImGui;
     g.ItemFlagsStack.clear();
     g.ItemFlagsStack.clear();
     g.GroupStack.clear();
     g.GroupStack.clear();
+    g.InputTextLineIndex.clear();
     g.MultiSelectTempDataStacked = 0;
     g.MultiSelectTempDataStacked = 0;
     g.MultiSelectTempData.clear_destruct();
     g.MultiSelectTempData.clear_destruct();
     TableGcCompactSettings();
     TableGcCompactSettings();

+ 3 - 1
imgui_internal.h

@@ -809,7 +809,7 @@ struct ImGuiTextIndex
 
 
     void            clear()                                 { Offsets.clear(); EndOffset = 0; }
     void            clear()                                 { Offsets.clear(); EndOffset = 0; }
     int             size()                                  { return Offsets.Size; }
     int             size()                                  { return Offsets.Size; }
-    const char*     get_line_begin(const char* base, int n) { return base + Offsets[n]; }
+    const char*     get_line_begin(const char* base, int n) { return base + (Offsets.Size != 0 ? Offsets[n] : 0); }
     const char*     get_line_end(const char* base, int n)   { return base + (n + 1 < Offsets.Size ? (Offsets[n + 1] - 1) : EndOffset); }
     const char*     get_line_end(const char* base, int n)   { return base + (n + 1 < Offsets.Size ? (Offsets[n + 1] - 1) : EndOffset); }
     void            append(const char* base, int old_size, int new_size);
     void            append(const char* base, int old_size, int new_size);
 };
 };
@@ -2430,6 +2430,7 @@ struct ImGuiContext
 
 
     // Widget state
     // Widget state
     ImGuiInputTextState     InputTextState;
     ImGuiInputTextState     InputTextState;
+    ImGuiTextIndex          InputTextLineIndex;                 // Temporary storage
     ImGuiInputTextDeactivatedState InputTextDeactivatedState;
     ImGuiInputTextDeactivatedState InputTextDeactivatedState;
     ImFontBaked             InputTextPasswordFontBackupBaked;
     ImFontBaked             InputTextPasswordFontBackupBaked;
     ImFontFlags             InputTextPasswordFontBackupFlags;
     ImFontFlags             InputTextPasswordFontBackupFlags;
@@ -3258,6 +3259,7 @@ namespace ImGui
     IMGUI_API float         CalcWrapWidthForPos(const ImVec2& pos, float wrap_pos_x);
     IMGUI_API float         CalcWrapWidthForPos(const ImVec2& pos, float wrap_pos_x);
     IMGUI_API void          PushMultiItemsWidths(int components, float width_full);
     IMGUI_API void          PushMultiItemsWidths(int components, float width_full);
     IMGUI_API void          ShrinkWidths(ImGuiShrinkWidthItem* items, int count, float width_excess, float width_min);
     IMGUI_API void          ShrinkWidths(ImGuiShrinkWidthItem* items, int count, float width_excess, float width_min);
+    IMGUI_API void          CalcClipRectVisibleItemsY(const ImRect& clip_rect, const ImVec2& pos, float items_height, int* out_visible_start, int* out_visible_end);
 
 
     // Parameter stacks (shared)
     // Parameter stacks (shared)
     IMGUI_API const ImGuiStyleVarInfo* GetStyleVarInfo(ImGuiStyleVar idx);
     IMGUI_API const ImGuiStyleVarInfo* GetStyleVarInfo(ImGuiStyleVar idx);

+ 164 - 184
imgui_widgets.cpp

@@ -135,7 +135,6 @@ static const ImU64          IM_U64_MAX = (2ULL * 9223372036854775807LL + 1);
 
 
 // For InputTextEx()
 // For InputTextEx()
 static bool     InputTextFilterCharacter(ImGuiContext* ctx, unsigned int* p_char, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data, bool input_source_is_clipboard = false);
 static bool     InputTextFilterCharacter(ImGuiContext* ctx, unsigned int* p_char, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data, bool input_source_is_clipboard = false);
-static int      InputTextCalcTextLenAndLineCount(ImGuiContext* ctx, const char* text_begin, const char** out_text_end, float wrap_width);
 static ImVec2   InputTextCalcTextSize(ImGuiContext* ctx, const char* text_begin, const char* text_end_display, const char* text_end, const char** out_remaining = NULL, ImVec2* out_offset = NULL, ImDrawTextFlags flags = 0);
 static ImVec2   InputTextCalcTextSize(ImGuiContext* ctx, const char* text_begin, const char* text_end_display, const char* text_end, const char** out_remaining = NULL, ImVec2* out_offset = NULL, ImDrawTextFlags flags = 0);
 
 
 //-------------------------------------------------------------------------
 //-------------------------------------------------------------------------
@@ -3938,46 +3937,6 @@ bool ImGui::InputTextWithHint(const char* label, const char* hint, char* buf, si
     return InputTextEx(label, hint, buf, (int)buf_size, ImVec2(0, 0), flags, callback, user_data);
     return InputTextEx(label, hint, buf, (int)buf_size, ImVec2(0, 0), flags, callback, user_data);
 }
 }
 
 
-// This is only used in the path where the multiline widget is inactive.
-static int InputTextCalcTextLenAndLineCount(ImGuiContext* ctx, const char* text_begin, const char** out_text_end, float wrap_width)
-{
-    int line_count = 0;
-    const char* s = text_begin;
-    if (wrap_width == 0.0f)
-    {
-        while (true)
-        {
-            const char* s_eol = strchr(s, '\n');
-            line_count++;
-            if (s_eol == NULL)
-            {
-                s = s + ImStrlen(s);
-                break;
-            }
-            s = s_eol + 1;
-        }
-    }
-    else
-    {
-        // FIXME-WORDWRAP, FIXME-OPT: This is very suboptimal.
-        // We basically want both text_end and text_size, they could more optimally be emitted from a RenderText call that uses word-wrapping.
-        ImGuiContext& g = *ctx;
-        ImFont* font = g.Font;
-        const char* text_end = text_begin + strlen(text_begin);
-        while (s < text_end)
-        {
-            s = ImFontCalcWordWrapPositionEx(font, g.FontSize, s, text_end, wrap_width, ImDrawTextFlags_WrapKeepBlanks);
-            s = (*s == '\n') ? s + 1 : s;
-            line_count++;
-        }
-        if (text_end > text_begin && text_end[-1] == '\n')
-            line_count++;
-        IM_ASSERT(s == text_end);
-    }
-    *out_text_end = s;
-    return line_count;
-}
-
 static ImVec2 InputTextCalcTextSize(ImGuiContext* ctx, const char* text_begin, const char* text_end_display, const char* text_end, const char** out_remaining, ImVec2* out_offset, ImDrawTextFlags flags)
 static ImVec2 InputTextCalcTextSize(ImGuiContext* ctx, const char* text_begin, const char* text_end_display, const char* text_end, const char** out_remaining, ImVec2* out_offset, ImDrawTextFlags flags)
 {
 {
     ImGuiContext& g = *ctx;
     ImGuiContext& g = *ctx;
@@ -4555,6 +4514,83 @@ void ImGui::InputTextDeactivateHook(ImGuiID id)
     }
     }
 }
 }
 
 
+static int* ImLowerBound(int* in_begin, int* in_end, int v)
+{
+    int* in_p = in_begin;
+    for (size_t count = (size_t)(in_end - in_p); count > 0; )
+    {
+        size_t count2 = count >> 1;
+        int* mid = in_p + count2;
+        if (*mid < v)
+        {
+            in_p = ++mid;
+            count -= count2 + 1;
+        }
+        else
+        {
+            count = count2;
+        }
+    }
+    return in_p;
+}
+
+// FIXME-WORDWRAP: Bundle some of this into ImGuiTextIndex and/or extract as a different tool?
+// 'max_output_buffer_size' happens to be a meaningful optimization to avoid writing the full line_index when not necessarily needed (e.g. very large buffer, scrolled up, inactive)
+static int InputTextLineIndexBuild(ImGuiInputTextFlags flags, ImGuiTextIndex* line_index, const char* buf, const char* buf_end, float wrap_width, int max_output_buffer_size)
+{
+    ImGuiContext& g = *GImGui;
+    int size = 0;
+    if (flags & ImGuiInputTextFlags_WordWrap)
+    {
+        for (const char* s = buf; s < buf_end; )
+        {
+            if (size++ <= max_output_buffer_size)
+                line_index->Offsets.push_back((int)(s - buf));
+            s = ImFontCalcWordWrapPositionEx(g.Font, g.FontSize, s, buf_end, wrap_width, ImDrawTextFlags_WrapKeepBlanks);
+            s = (*s == '\n') ? s + 1 : s;
+        }
+    }
+    else
+    {
+        for (const char* s = buf; s < buf_end; )
+        {
+            if (size++ <= max_output_buffer_size)
+                line_index->Offsets.push_back((int)(s - buf));
+            s = (const char*)ImMemchr(s, '\n', buf_end - s);
+            s = s ? s + 1 : buf_end;
+        }
+    }
+    if (size == 0)
+    {
+        line_index->Offsets.push_back(0);
+        size++;
+    }
+    if (buf_end > buf && buf_end[-1] == '\n' && size <= max_output_buffer_size)
+    {
+        line_index->Offsets.push_back((int)(buf_end - buf));
+        size++;
+    }
+    return size;
+}
+
+static ImVec2 InputTextLineIndexGetPosOffset(ImGuiContext& g, ImGuiInputTextState* state, ImGuiTextIndex* line_index, const char* buf, const char* buf_end, int cursor_n)
+{
+    const char* cursor_ptr = buf + cursor_n;
+    int* it_begin = line_index->Offsets.begin();
+    int* it_end = line_index->Offsets.end();
+    const int* it = ImLowerBound(it_begin, it_end, cursor_n);
+    if (it > it_begin)
+        if (it == it_end || *it != cursor_n || (cursor_ptr[-1] != '\n' && cursor_ptr[-1] != 0 && state != NULL && state->LastMoveDirectionLR == ImGuiDir_Right))
+            it--;
+
+    const int line_no = (it == it_begin) ? 0 : line_index->Offsets.index_from_ptr(it);
+    const char* line_start = line_index->get_line_begin(buf, line_no);
+    ImVec2 offset;
+    offset.x = InputTextCalcTextSize(&g, line_start, cursor_ptr, buf_end, NULL, NULL, ImDrawTextFlags_WrapKeepBlanks).x;
+    offset.y = (line_no + 1) * g.FontSize;
+    return offset;
+}
+
 // Edit a string of text
 // Edit a string of text
 // - buf_size account for the zero-terminator, so a buf_size of 6 can hold "Hello" but not "Hello!".
 // - buf_size account for the zero-terminator, so a buf_size of 6 can hold "Hello" but not "Hello!".
 //   This is so we can easily call InputText() on static arrays using ARRAYSIZE() and to match
 //   This is so we can easily call InputText() on static arrays using ARRAYSIZE() and to match
@@ -5327,15 +5363,40 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_
         buf_display = hint;
         buf_display = hint;
         buf_display_end = hint + ImStrlen(hint);
         buf_display_end = hint + ImStrlen(hint);
     }
     }
+    else
+    {
+        if (render_cursor || render_selection || g.ActiveId == id)
+            buf_display_end = buf_display + state->TextLen; //-V595
+        else
+            buf_display_end = buf_display + ImStrlen(buf_display); // FIXME-OPT: For multi-line path this would optimally be folded into the InputTextLineIndex build below.
+    }
+
+    // Calculate visibility
+    int line_visible_n0 = 0, line_visible_n1 = 1;
+    if (is_multiline)
+        CalcClipRectVisibleItemsY(clip_rect, draw_pos, g.FontSize, &line_visible_n0, &line_visible_n1);
+
+    // Build line index for easy data access (makes code below simpler and faster)
+    ImGuiTextIndex* line_index = &g.InputTextLineIndex;
+    line_index->Offsets.resize(0);
+    line_index->EndOffset = (int)(buf_display_end - buf_display);
+    int line_count = 1;
+    if (is_multiline)
+        line_count = InputTextLineIndexBuild(flags, line_index, buf_display, buf_display_end, wrap_width, (render_cursor && state && state->CursorFollow) ? INT_MAX : line_visible_n1 + 1);
+    line_visible_n1 = ImMin(line_visible_n1, line_count);
+
+    // Store text height (we don't need width)
+    text_size = ImVec2(inner_size.x, line_count * g.FontSize);
+    //GetForegroundDrawList()->AddRect(draw_pos + ImVec2(0, line_visible_n0 * g.FontSize), draw_pos + ImVec2(frame_size.x, line_visible_n1 * g.FontSize), IM_COL32(255, 0, 0, 255));
+
+    // Calculate blinking cursor position
+    const ImVec2 cursor_offset = render_cursor && state ? InputTextLineIndexGetPosOffset(g, state, line_index, buf_display, buf_display_end, state->Stb->cursor) : ImVec2(0.0f, 0.0f);
+    ImVec2 draw_scroll;
 
 
     // Render text. We currently only render selection when the widget is active or while scrolling.
     // Render text. We currently only render selection when the widget is active or while scrolling.
-    // FIXME: This is one of the messiest piece of the whole codebase.
+    const ImU32 text_col = GetColorU32(is_displaying_hint ? ImGuiCol_TextDisabled : ImGuiCol_Text);
     if (render_cursor || render_selection)
     if (render_cursor || render_selection)
     {
     {
-        IM_ASSERT(state != NULL);
-        if (!is_displaying_hint)
-            buf_display_end = buf_display + state->TextLen;
-
         // Render text (with cursor and selection)
         // Render text (with cursor and selection)
         // This is going to be messy. We need to:
         // This is going to be messy. We need to:
         // - Display the text (this alone can be more easily clipped)
         // - Display the text (this alone can be more easily clipped)
@@ -5343,85 +5404,8 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_
         // - Measure text height (for scrollbar)
         // - 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)
         // 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.
         // FIXME: This should occur on buf_display but we'd need to maintain cursor/select_start/select_end for UTF-8.
-        const char* text_begin = buf_display;
-        const char* text_end = text_begin + state->TextLen;
-        ImVec2 cursor_offset;
-        float select_start_offset_y = 0.0f; // Offset of beginning of non-wrapped line for selection.
-
-        {
-            // Find lines numbers straddling cursor and selection min position
-            int cursor_line_no = render_cursor ? -1 : -1000;
-            int selmin_line_no = render_selection ? -1 : -1000;
-            const char* cursor_ptr = render_cursor ? text_begin + state->Stb->cursor : NULL;
-            const char* selmin_ptr = render_selection ? text_begin + ImMin(state->Stb->select_start, state->Stb->select_end) : NULL;
-            const char* cursor_line_start = NULL;
-            const char* selmin_line_start = NULL;
-            bool cursor_straddle_word_wrap = false;
-
-            // Count lines and find line number for cursor and selection ends
-            // FIXME: Switch to zero-based index to reduce confusion.
-            int line_count = 1;
-            if (is_multiline)
-            {
-                if (!is_wordwrap)
-                {
-                    for (const char* s = text_begin; (s = (const char*)ImMemchr(s, '\n', (size_t)(text_end - s))) != NULL; s++)
-                    {
-                        if (cursor_line_no == -1 && s >= cursor_ptr) { cursor_line_no = line_count; }
-                        if (selmin_line_no == -1 && s >= selmin_ptr) { selmin_line_no = line_count; }
-                        line_count++;
-                    }
-                }
-                else
-                {
-                    bool is_start_of_non_wrapped_line = true;
-                    int line_count_for_non_wrapped_line = 1;
-                    for (const char* s = text_begin; s < text_end; s = (*s == '\n') ? s + 1 : s)
-                    {
-                        const char* s_eol = ImFontCalcWordWrapPositionEx(g.Font, g.FontSize, s, text_end, wrap_width, ImDrawTextFlags_WrapKeepBlanks);
-                        const char* s_prev = s;
-                        s = s_eol;
-                        if (cursor_line_no == -1 && s >= cursor_ptr) { cursor_line_start = s_prev; cursor_line_no = line_count; }
-                        if (selmin_line_no == -1 && s >= selmin_ptr) { selmin_line_start = s_prev; selmin_line_no = line_count_for_non_wrapped_line; }
-                        if (s == cursor_ptr && *cursor_ptr != '\n' && *cursor_ptr != 0)
-                            cursor_straddle_word_wrap = true;
-                        is_start_of_non_wrapped_line = (*s == '\n');
-                        line_count++;
-                        if (is_start_of_non_wrapped_line)
-                            line_count_for_non_wrapped_line = line_count;
-                    }
-                }
-                //IMGUI_DEBUG_LOG("%d\n", selmin_line_no);
-            }
-            if (cursor_line_no == -1)
-                cursor_line_no = line_count;
-            if (cursor_line_start == NULL)
-                cursor_line_start = ImStrbol(cursor_ptr, text_begin);
-            if (selmin_line_no == -1)
-                selmin_line_no = line_count;
-            if (selmin_line_start == NULL)
-                selmin_line_start = ImStrbol(cursor_ptr, text_begin);
-
-            // Calculate 2d position by finding the beginning of the line and measuring distance
-            if (render_cursor)
-            {
-                cursor_offset.x = InputTextCalcTextSize(&g, cursor_line_start, cursor_ptr, text_end, NULL, NULL, ImDrawTextFlags_WrapKeepBlanks).x;
-                cursor_offset.y = cursor_line_no * g.FontSize;
-                if (is_multiline && cursor_straddle_word_wrap && state->LastMoveDirectionLR == ImGuiDir_Left)
-                    cursor_offset = ImVec2(0.0f, cursor_offset.y + g.FontSize);
-            }
-            if (selmin_line_no >= 0)
-                select_start_offset_y = selmin_line_no * g.FontSize;
-
-            // Store text height (note that we haven't calculated text width at all, see GitHub issues #383, #1224)
-            if (is_multiline)
-            {
-                if (is_wordwrap && text_end > text_begin && text_end[-1] != '\n')
-                    line_count--;
-                text_size = ImVec2(inner_size.x, line_count * g.FontSize);
-            }
-            state->LineCount = line_count;
-        }
+        IM_ASSERT(state != NULL);
+        state->LineCount = line_count;
 
 
         // Scroll
         // Scroll
         float new_scroll_y = scroll_y;
         float new_scroll_y = scroll_y;
@@ -5447,7 +5431,7 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_
             {
             {
                 // Test if cursor is vertically visible
                 // Test if cursor is vertically visible
                 if (cursor_offset.y - g.FontSize < scroll_y)
                 if (cursor_offset.y - g.FontSize < scroll_y)
-                    new_scroll_y  = ImMax(0.0f, cursor_offset.y - g.FontSize);
+                    new_scroll_y = ImMax(0.0f, cursor_offset.y - g.FontSize);
                 else if (cursor_offset.y - (inner_size.y - style.FramePadding.y * 2.0f) >= scroll_y)
                 else if (cursor_offset.y - (inner_size.y - style.FramePadding.y * 2.0f) >= scroll_y)
                     new_scroll_y = cursor_offset.y - inner_size.y + style.FramePadding.y * 2.0f;
                     new_scroll_y = cursor_offset.y - inner_size.y + style.FramePadding.y * 2.0f;
             }
             }
@@ -5466,57 +5450,60 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_
             scroll_y = ImClamp(new_scroll_y, 0.0f, scroll_max_y);
             scroll_y = ImClamp(new_scroll_y, 0.0f, scroll_max_y);
             draw_pos.y += (draw_window->Scroll.y - scroll_y);   // Manipulate cursor pos immediately avoid a frame of lag
             draw_pos.y += (draw_window->Scroll.y - scroll_y);   // Manipulate cursor pos immediately avoid a frame of lag
             draw_window->Scroll.y = scroll_y;
             draw_window->Scroll.y = scroll_y;
+            CalcClipRectVisibleItemsY(clip_rect, draw_pos, g.FontSize, &line_visible_n0, &line_visible_n1);
+            line_visible_n1 = ImMin(line_visible_n1, line_count);
         }
         }
 
 
         // Draw selection
         // Draw selection
-        const ImVec2 draw_scroll = ImVec2(state->Scroll.x, 0.0f);
+        draw_scroll.x = state->Scroll.x;
         if (render_selection)
         if (render_selection)
         {
         {
-            const char* text_selected_begin = text_begin + ImMin(state->Stb->select_start, state->Stb->select_end);
-            const char* text_selected_end = text_begin + ImMax(state->Stb->select_start, state->Stb->select_end);
+            const ImU32 bg_color = GetColorU32(ImGuiCol_TextSelectedBg, render_cursor ? 1.0f : 0.6f); // FIXME: current code flow mandate that render_cursor is always true here, we are leaving the transparent one for tests.
+            const float bg_offy_up = is_multiline ? 0.0f : -1.0f;    // FIXME: those offsets should be part of the style? they don't play so well with multi-line selection.
+            const float bg_offy_dn = is_multiline ? 0.0f : 2.0f;
+            const float bg_eol_width = IM_TRUNC(g.FontBaked->GetCharAdvance((ImWchar)' ') * 0.50f); // So we can see selected empty lines
 
 
-            ImU32 bg_color = GetColorU32(ImGuiCol_TextSelectedBg, render_cursor ? 1.0f : 0.6f); // FIXME: current code flow mandate that render_cursor is always true here, we are leaving the transparent one for tests.
-            float bg_offy_up = is_multiline ? 0.0f : -1.0f;    // FIXME: those offsets should be part of the style? they don't play so well with multi-line selection.
-            float bg_offy_dn = is_multiline ? 0.0f : 2.0f;
-            float bg_min_width = IM_TRUNC(g.FontBaked->GetCharAdvance((ImWchar)' ') * 0.50f); // So we can see selected empty lines
-            ImVec2 rect_pos = draw_pos - draw_scroll;
-            rect_pos.y += select_start_offset_y;
-            for (const char* p = ImStrbol(text_selected_begin, text_begin); p < text_selected_end; rect_pos.y += g.FontSize)
+            const char* text_selected_begin = buf_display + ImMin(state->Stb->select_start, state->Stb->select_end);
+            const char* text_selected_end = buf_display + ImMax(state->Stb->select_start, state->Stb->select_end);
+            for (int line_n = line_visible_n0; line_n < line_visible_n1; line_n++)
             {
             {
-                if (rect_pos.y > clip_rect.Max.y + g.FontSize)
-                    break;
-                const char* p_eol = is_wordwrap ? ImFontCalcWordWrapPositionEx(g.Font, g.FontSize, p, text_end, wrap_width, ImDrawTextFlags_WrapKeepBlanks) : (const char*)ImMemchr((void*)p, '\n', text_selected_end - p);
-                if (p_eol == NULL)
-                    p_eol = text_selected_end;
-                const char* p_next = is_wordwrap ? (*p_eol == '\n' ? p_eol + 1 : p_eol) : (p_eol + 1);
-                if (rect_pos.y >= clip_rect.Min.y)
-                {
-                    const char* line_selected_begin = (text_selected_begin > p) ? text_selected_begin : p;
-                    const char* line_selected_end = (text_selected_end < p_eol) ? text_selected_end : p_eol;
-                    if ((*p_eol == '\n' && text_selected_begin <= p_eol) || (text_selected_begin < p_eol))
-                    {
-                        ImVec2 rect_offset = CalcTextSize(p, line_selected_begin);
-                        ImVec2 rect_size = CalcTextSize(line_selected_begin, line_selected_end);
-                        rect_size.x = ImMax(rect_size.x, bg_min_width); // So we can see selected empty lines
-                        ImRect rect(rect_pos + ImVec2(rect_offset.x, bg_offy_up - g.FontSize), rect_pos + ImVec2(rect_offset.x + rect_size.x, bg_offy_dn));
-                        rect.ClipWith(clip_rect);
-                        if (rect.Overlaps(clip_rect))
-                            draw_window->DrawList->AddRectFilled(rect.Min, rect.Max, bg_color);
-                    }
-                }
-                p = p_next;
+                const char* p = line_index->get_line_begin(buf_display, line_n);
+                const char* p_eol = line_index->get_line_end(buf_display, line_n);
+                const bool p_eol_is_wrap = (p_eol < buf_display_end && *p_eol != '\n');
+                if (p_eol_is_wrap)
+                    p_eol++;
+                const char* line_selected_begin = (text_selected_begin > p) ? text_selected_begin : p;
+                const char* line_selected_end = (text_selected_end < p_eol) ? text_selected_end : p_eol;
+
+                float rect_width = 0.0f;
+                if (line_selected_begin < line_selected_end)
+                    rect_width += CalcTextSize(line_selected_begin, line_selected_end).x;
+                if (text_selected_begin <= p_eol && text_selected_end > p_eol && !p_eol_is_wrap)
+                    rect_width += bg_eol_width; // So we can see selected empty lines
+                if (rect_width == 0.0f)
+                    continue;
+
+                ImRect rect;
+                rect.Min.x = draw_pos.x - draw_scroll.x + CalcTextSize(p, line_selected_begin).x;
+                rect.Min.y = draw_pos.y - draw_scroll.y + line_n * g.FontSize;
+                rect.Max.x = rect.Min.x + rect_width;
+                rect.Max.y = rect.Min.y + bg_offy_dn + g.FontSize;
+                rect.Min.y -= bg_offy_up;
+                rect.ClipWith(clip_rect);
+                draw_window->DrawList->AddRectFilled(rect.Min, rect.Max, bg_color);
             }
             }
         }
         }
 
 
+        // Render text
         // We test for 'buf_display_max_length' as a way to avoid some pathological cases (e.g. single-line 1 MB string) which would make ImDrawList crash.
         // 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.
         // FIXME-OPT: Multiline could submit a smaller amount of contents to AddText() since we already iterated through it.
-        if (is_multiline || (buf_display_end - buf_display) < buf_display_max_length)
-        {
-            ImU32 col = GetColorU32(is_displaying_hint ? ImGuiCol_TextDisabled : ImGuiCol_Text);
-            if (col & IM_COL32_A_MASK)
-                g.Font->RenderText(draw_window->DrawList, g.FontSize, draw_pos - draw_scroll, col, clip_rect.AsVec4(), buf_display, buf_display_end, wrap_width, ImDrawTextFlags_WrapKeepBlanks);
-            //draw_window->DrawList->AddText(g.Font, g.FontSize, draw_pos - draw_scroll, col, buf_display, buf_display_end, wrap_width, is_multiline ? NULL : &clip_rect.AsVec4());
-        }
+        if ((is_multiline || (buf_display_end - buf_display) < buf_display_max_length) && (text_col & IM_COL32_A_MASK) && (line_visible_n0 < line_visible_n1))
+            g.Font->RenderText(draw_window->DrawList, g.FontSize,
+                draw_pos - draw_scroll + ImVec2(0.0f, line_visible_n0 * g.FontSize),
+                text_col, clip_rect.AsVec4(),
+                line_index->get_line_begin(buf_display, line_visible_n0),
+                line_index->get_line_end(buf_display, line_visible_n1 - 1),
+                wrap_width, ImDrawTextFlags_WrapKeepBlanks);
 
 
         // Draw blinking cursor
         // Draw blinking cursor
         if (render_cursor)
         if (render_cursor)
@@ -5544,26 +5531,19 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_
     }
     }
     else
     else
     {
     {
-        // Render text only (no selection, no cursor)
-        if (is_multiline)
-            text_size = ImVec2(inner_size.x, InputTextCalcTextLenAndLineCount(&g, buf_display, &buf_display_end, wrap_width) * g.FontSize); // We don't need width
-        else if (!is_displaying_hint && g.ActiveId == id)
-            buf_display_end = buf_display + state->TextLen;
-        else if (!is_displaying_hint)
-            buf_display_end = buf_display + ImStrlen(buf_display);
-
-        if (is_multiline || (buf_display_end - buf_display) < buf_display_max_length)
-        {
-            // Find render position for right alignment
-            if (flags & ImGuiInputTextFlags_ElideLeft)
-                draw_pos.x = ImMin(draw_pos.x, frame_bb.Max.x - CalcTextSize(buf_display, NULL).x - style.FramePadding.x);
+        // Find render position for right alignment (single-line only)
+        if (flags & ImGuiInputTextFlags_ElideLeft)
+            draw_pos.x = ImMin(draw_pos.x, frame_bb.Max.x - CalcTextSize(buf_display, NULL).x - style.FramePadding.x);
 
 
-            const ImVec2 draw_scroll = /*state ? ImVec2(state->Scroll.x, 0.0f) :*/ ImVec2(0.0f, 0.0f); // Preserve scroll when inactive?
-            ImU32 col = GetColorU32(is_displaying_hint ? ImGuiCol_TextDisabled : ImGuiCol_Text);
-            if (col & IM_COL32_A_MASK)
-                g.Font->RenderText(draw_window->DrawList, g.FontSize, draw_pos - draw_scroll, col, clip_rect.AsVec4(), buf_display, buf_display_end, wrap_width, ImDrawTextFlags_WrapKeepBlanks);
-            //draw_window->DrawList->AddText(g.Font, g.FontSize, draw_pos - draw_scroll, col, buf_display, buf_display_end, wrap_width, is_multiline ? NULL : &clip_rect.AsVec4());
-        }
+        // Render text only (no selection, no cursor)
+        //draw_scroll.x = state->Scroll.x; // Preserve scroll when inactive?
+        if ((is_multiline || (buf_display_end - buf_display) < buf_display_max_length) && (text_col & IM_COL32_A_MASK) && (line_visible_n0 < line_visible_n1))
+            g.Font->RenderText(draw_window->DrawList, g.FontSize,
+                draw_pos - draw_scroll + ImVec2(0.0f, line_visible_n0 * g.FontSize),
+                text_col, clip_rect.AsVec4(),
+                line_index->get_line_begin(buf_display, line_visible_n0),
+                line_index->get_line_end(buf_display, line_visible_n1 - 1),
+                wrap_width, ImDrawTextFlags_WrapKeepBlanks);
     }
     }
 
 
     if (is_password && !is_displaying_hint)
     if (is_password && !is_displaying_hint)