Repository: AidanSun05/ImGuiTextSelect
Pull request #8: Add wrapped text support
Author: login: twixuss
Created: 2025-04-18T14:20:54Z | Merged: 2025-04-21T04:58:15Z

![output](https://github.com/user-attachments/assets/89390b6c-96cf-4d70-bd55-bda5b232cb5c)

Wrapping is disabled by default, can be enabled by providing a bool flag in constructor.

It's a bit slow though. 20k line text renders at ~30 fps and when selecting it drops to ~13.


Diff:
diff --git a/textselect.cpp b/textselect.cpp
index 8b04ef2..3d7a324 100755
--- a/textselect.cpp
+++ b/textselect.cpp
@@ -140,6 +140,39 @@ TextSelect::Selection TextSelect::getSelection() const {
     return { startX, startY, endX, endY };
 }
 
+//
+// Taken from imgui_draw.cpp
+//
+// Trim trailing space and find beginning of next line
+static inline const char* CalcWordWrapNextLineStartA(const char* text, const char* text_end)
+{
+    while (text < text_end && ImCharIsBlankA(*text))
+        text++;
+    if (*text == '\n')
+        text++;
+    return text;
+}
+
+// Split `text` that does not fit in `wrapWidth` into multiple lines.
+static ImVector<std::string_view> wrapText(std::string_view text, float wrapWidth, ImFont *font) {
+    ImVector<std::string_view> result;
+    const char *textEnd = text.data() + text.size();
+    const char *wrappedLineStart = text.data();
+    const char *wrappedLineEnd = text.data();
+    while (wrappedLineEnd != textEnd) {
+        wrappedLineStart = wrappedLineEnd;
+        wrappedLineEnd = font->CalcWordWrapPositionA(1, wrappedLineStart, textEnd, wrapWidth);
+
+        if (wrappedLineEnd - wrappedLineStart != 0) {
+            result.push_back({ wrappedLineStart, static_cast<std::size_t>(wrappedLineEnd - wrappedLineStart) });
+        }
+
+        wrappedLineEnd = CalcWordWrapNextLineStartA(wrappedLineEnd, textEnd);
+    }
+
+    return result;
+}
+
 void TextSelect::handleMouseDown(const ImVec2& cursorPosStart) {
     std::size_t numLines = getNumLines();
 
@@ -150,11 +183,60 @@ void TextSelect::handleMouseDown(const ImVec2& cursorPosStart) {
     const float textHeight = ImGui::GetTextLineHeightWithSpacing();
     ImVec2 mousePos = ImGui::GetMousePos() - cursorPosStart;
 
-    // Get Y position of mouse cursor, in terms of line number (clamped to the valid range)
-    std::size_t y = static_cast<std::size_t>(std::min(std::max(std::floor(mousePos.y / textHeight), 0.0f), static_cast<float>(numLines - 1)));
+    std::string_view currentLine;
+    std::size_t x, y;
+    if (enableWordWrap) {
+        ImGuiWindow *window = ImGui::GetCurrentWindow();
+
+        const float wrapWidth = ImGui::CalcWrapWidthForPos(window->DC.CursorPos, 0);
+
+        // Find the index of the line under the cursor and its size.
+        float accumulatedHeight = 0;
+        ImVec2 currentLineSize = {};
+        y = numLines - 1;
+        for (std::size_t i = 0; i < numLines - 1; ++i) {
+            std::string_view line = getLineAtIdx(i);
+            currentLineSize = ImGui::CalcTextSize(line.data(), line.data() + line.size(), false, wrapWidth);
+
+            if (mousePos.y < accumulatedHeight + currentLineSize.y) {
+                y = i;
+                break;
+            }
+
+            accumulatedHeight += currentLineSize.y + ImGui::GetCurrentContext()->Style.ItemSpacing.y;
+        }
+        if (y >= numLines) {
+            return;
+        }
+
+        currentLine = getLineAtIdx(y);
+
+        ImFont *font = ImGui::GetCurrentContext()->Font;
+
+        auto subLines = wrapText(currentLine, wrapWidth, font);
+
+        if (subLines.size() == 0) {
+            x = 0;
+        } else {
+            // Calculate index of the sub-line in the current line.
+            std::size_t localWrappedY = static_cast<std::size_t>(std::clamp(std::floor((mousePos.y - accumulatedHeight) / textHeight), 0.0f, static_cast<float>(subLines.size() - 1)));
+
+            auto currentSubLine = subLines[localWrappedY];
+
+            x = utf8::distance(currentLine.data(), currentSubLine.data()) + getCharIndex(currentSubLine, mousePos.x);
+        }
+    } else {
+        // Get Y position of mouse cursor, in terms of line number (clamped to the valid range)
+        y = static_cast<std::size_t>(std::clamp(std::floor(mousePos.y / textHeight), 0.0f, static_cast<float>(numLines - 1)));
+        if (y >= numLines) {
+            return;
+        }
+
+        currentLine = getLineAtIdx(y);
+        x = getCharIndex(currentLine, mousePos.x);
+    }
+
 
-    std::string_view currentLine = getLineAtIdx(y);
-    std::size_t x = getCharIndex(currentLine, mousePos.x);
 
     // Get mouse click count and determine action
     if (int mouseClicks = ImGui::GetMouseClickedCount(ImGuiMouseButton_Left); mouseClicks > 0) {
@@ -247,6 +329,16 @@ void TextSelect::handleScrolling() const {
     }
 }
 
+static void drawSelectionRect(const ImVec2& cursorPosStart, float minX, float minY, float maxX, float maxY) {
+    // Get rectangle corner points offset from the cursor's start position in the window
+    ImVec2 rectMin = cursorPosStart + ImVec2{ minX, minY };
+    ImVec2 rectMax = cursorPosStart + ImVec2{ maxX, maxY };
+
+    // Draw the rectangle
+    ImU32 color = ImGui::GetColorU32(ImGuiCol_TextSelectedBg);
+    ImGui::GetWindowDrawList()->AddRectFilled(rectMin, rectMax, color);
+}
+
 void TextSelect::drawSelection(const ImVec2& cursorPosStart) const {
     if (!hasSelection()) {
         return;
@@ -260,6 +352,86 @@ void TextSelect::drawSelection(const ImVec2& cursorPosStart) const {
         return;
     }
 
+    if (enableWordWrap) {
+        ImGuiContext *context = ImGui::GetCurrentContext();
+        ImGuiWindow *window = ImGui::GetCurrentWindow();
+        ImFont *font = context->Font;
+        const float wrapWidth = ImGui::CalcWrapWidthForPos(window->DC.CursorPos, 0);
+        const float newlineWidth = ImGui::CalcTextSize(" ").x;
+
+        // Calculate height of region before selection.
+        float accumulatedHeight = 0;
+        for (std::size_t i = 0; i < startY; ++i) {
+            std::string_view line = getLineAtIdx(i);
+            accumulatedHeight += ImGui::CalcTextSize(line.data(), line.data() + line.size(), false, wrapWidth).y + context->Style.ItemSpacing.y;
+        }
+
+        const float textHeight = context->FontSize;
+        const float itemSpacing = context->Style.ItemSpacing.y;
+
+        // Render wrapped lines.
+        for (std::size_t i = startY; i <= endY; ++i) {
+            std::string_view line = getLineAtIdx(i);
+            const char *lineEnd = line.data() + line.size();
+
+            auto subLines = wrapText(line, wrapWidth, font);
+
+            if (subLines.size() == 0) {
+                // If this line is empty just draw one quad on the left side.
+
+                float minY = accumulatedHeight;
+                accumulatedHeight += textHeight + itemSpacing;
+                float maxY = accumulatedHeight;
+
+                float minX = 0;
+                float maxX = newlineWidth;
+
+                drawSelectionRect(cursorPosStart, minX, minY, maxX, maxY);
+            }
+
+            for (std::size_t j = 0; j < subLines.size(); ++j) {
+                auto subLine = subLines[j];
+                const char *subLineStart = subLine.data();
+                const char *subLineEnd = subLine.data() + subLine.size();
+
+                // Indices of sub-line bounds relative to the start of the whole line.
+                std::size_t subLineStartX = utf8::distance(line.data(), subLineStart);
+                std::size_t subLineEndX   = utf8::distance(line.data(), subLineEnd);
+
+                float minY = accumulatedHeight;
+                accumulatedHeight += textHeight;
+                // Item spacing is not applied between sub-lines
+                if (subLineEnd == lineEnd) {
+                    // We are rendering last sub-line.
+                    accumulatedHeight += itemSpacing;
+                }
+                float maxY = accumulatedHeight;
+                
+                // Skip unselected sub-lines.
+                if (i == startY && startX >= subLineEndX) {
+                    // Sub-line before selection
+                    continue;
+                }
+                if (i == endY && endX < subLineStartX) {
+                    // Sub-line after selection
+                    break;
+                }
+
+                // The first and last rectangles should only extend to the selection boundaries
+                // The middle rectangles (if any) enclose the entire line + some extra width for the newline.
+                bool isStartSubLine = i == startY && subLineStartX <= startX && startX <= subLineEndX;
+                bool isEndSubLine = i == endY && subLineStartX <= endX && endX <= subLineEndX;
+
+                float minX = isStartSubLine ? substringSizeX(subLine, 0, startX - std::min(subLineStartX, startX)) : 0;
+                float maxX = isEndSubLine ? substringSizeX(subLine, 0, endX - std::min(subLineStartX, endX)) : substringSizeX(subLine, 0) + newlineWidth;
+
+                drawSelectionRect(cursorPosStart, minX, minY, maxX, maxY);
+            }
+        }
+
+        return;
+    }
+
     // Add a rectangle to the draw list for each line contained in the selection
     for (std::size_t i = startY; i <= endY; i++) {
         std::string_view line = getLineAtIdx(i);
@@ -278,13 +450,7 @@ void TextSelect::drawSelection(const ImVec2& cursorPosStart) const {
         float minY = static_cast<float>(i) * textHeight;
         float maxY = static_cast<float>(i + 1) * textHeight;
 
-        // Get rectangle corner points offset from the cursor's start position in the window
-        ImVec2 rectMin = cursorPosStart + ImVec2{ minX, minY };
-        ImVec2 rectMax = cursorPosStart + ImVec2{ maxX, maxY };
-
-        // Draw the rectangle
-        ImU32 color = ImGui::GetColorU32(ImGuiCol_TextSelectedBg);
-        ImGui::GetWindowDrawList()->AddRectFilled(rectMin, rectMax, color);
+        drawSelectionRect(cursorPosStart, minX, minY, maxX, maxY);
     }
 }
 
diff --git a/textselect.hpp b/textselect.hpp
index 7932604..1d59e23 100755
--- a/textselect.hpp
+++ b/textselect.hpp
@@ -47,6 +47,10 @@ class TextSelect {
     // Indicates whether selection should be updated. This is needed for distinguishing mouse drags that are
     // initiated by clicking the text, or different element.
     bool shouldHandleMouseDown = false;
+    
+    // Indicates whether text selection with word wrapping should be enabled.
+    bool enableWordWrap;
+
 
     // Gets the user selection. Start and end are guaranteed to be in order.
     Selection getSelection() const;
@@ -65,7 +69,10 @@ class TextSelect {
     // getLineAtIdx: Function taking a std::size_t (line number) and returning the string in that line
     // getNumLines: Function returning a std::size_t (total number of lines of text)
     template <class T, class U>
-    TextSelect(const T& getLineAtIdx, const U& getNumLines) : getLineAtIdx(getLineAtIdx), getNumLines(getNumLines) {}
+    TextSelect(const T& getLineAtIdx, const U& getNumLines, bool enableWordWrap = false) :
+        getLineAtIdx(getLineAtIdx),
+        getNumLines(getNumLines),
+        enableWordWrap(enableWordWrap) {}
 
     // Checks if there is an active selection in the text.
     bool hasSelection() const {


Reviews:
user: login: AidanSun05
body: I left some minor suggestions related to code style which could benefit readability.
state: CHANGES_REQUESTED
author_association: OWNER
submitted_at: 2025-04-20T05:12:17Z
user: login: AidanSun05
body: Looks great overall. I'll go ahead and merge this now.

One thing that stood out is that selection and rendering now have two separate code paths for wrapped and non-wrapped text. In the future, it might be a good idea for us to consider a small refactor where selection and rendering operate on unified accessor functions which serve as an abstraction over wrapped/non-wrapped text. For example, given the screenshot below:

![Screenshot 2025-04-20 215306](https://github.com/user-attachments/assets/4d45a702-5dfa-4b09-8c7f-5a15cb070cce)

The physical lines (as they appear in memory) are:

```cpp
{
    "Line 1",
    "Line 2",
    "Line 3",
    "A longer line",
    "Text selection in Dear ImGui",
    "UTF-8 characters Ë ⑤ 三【 】┌──┐"
}
```

While the virtual lines (as they appear wrapped on screen) are:

```cpp
{
    "Line 1",
    "Line 2",
    "Line 3",
    "A longer line",
    "Text selection in ", // (note the trailing space)
    "Dear ImGui",
    "UTF-8 characters Ë ",
    "⑤ 三【 】┌──┐"
}
```

Introducing accessor functions like `getVirtualLineAtIdx` and `getVirtualNumLines` could allow us to unify access to physical and virtual lines by abstracting the difference when word wrapping is enabled, while they would behave the same as their current counterparts if word wrapping is disabled. This allows `handleMouseDown` and `drawSelection` to operate purely on virtual lines, which reduces duplication and improves separation of concerns.

One tricky aspect would be handling triple-click and copying, which would require us to join multiple virtual lines together without newlines.

Let me know if you're willing to open a follow-up PR for this refactoring or if you want to discuss further. Thanks for your contributions!
state: APPROVED
author_association: OWNER
submitted_at: 2025-04-21T04:58:09Z

Review comments:
diff_hunk: @@ -150,11 +183,53 @@ void TextSelect::handleMouseDown(const ImVec2& cursorPosStart) {
     const float textHeight = ImGui::GetTextLineHeightWithSpacing();
     ImVec2 mousePos = ImGui::GetMousePos() - cursorPosStart;
 
-    // Get Y position of mouse cursor, in terms of line number (clamped to the valid range)
-    std::size_t y = static_cast<std::size_t>(std::min(std::max(std::floor(mousePos.y / textHeight), 0.0f), static_cast<float>(numLines - 1)));
+    std::string_view currentLine;
+    std::size_t x, y;
+    if (enableWordWrap) {
+        ImGuiWindow *window = ImGui::GetCurrentWindow();
+
+        const float wrapWidth = ImGui::CalcWrapWidthForPos(window->DC.CursorPos, 0);
+
+        // Find the index of the line under the cursor and its size.
+        float accumulatedHeight = 0;
+        ImVec2 currentLineSize = {};
+        y = numLines - 1;
+        for (std::size_t i = 0; i < numLines - 1; ++i) {
+            std::string_view line = getLineAtIdx(i);
+            currentLineSize = ImGui::CalcTextSize(line.data(), line.data() + line.size(), false, wrapWidth);
+
+            if (mousePos.y < accumulatedHeight + currentLineSize.y) {
+                y = i;
+                break;
+            }
+
+            accumulatedHeight += currentLineSize.y + ImGui::GetCurrentContext()->Style.ItemSpacing.y;
+        }
+        if (y >= numLines)
+            return;
+
+        currentLine = getLineAtIdx(y);
+
+        // Calculate index of the sub-line in the current line.
+        std::size_t localWrappedY = static_cast<std::size_t>(std::min(std::max(std::floor((mousePos.y - accumulatedHeight) / textHeight), 0.0f), std::round(currentLineSize.y / textHeight) - 1));
path: textselect.cpp
user: login: AidanSun05
body: The library uses C++17 so we can use `std::clamp()` over the `std::min(std::max(...))` pattern to improve readability.

```suggestion
        std::size_t localWrappedY = static_cast<std::size_t>(std::clamp(std::floor((mousePos.y - accumulatedHeight) / textHeight), 0.0f, std::round(currentLineSize.y / textHeight) - 1));
```
created_at: 2025-04-20T04:53:57Z
author_association: OWNER
diff_hunk: @@ -150,11 +183,53 @@ void TextSelect::handleMouseDown(const ImVec2& cursorPosStart) {
     const float textHeight = ImGui::GetTextLineHeightWithSpacing();
     ImVec2 mousePos = ImGui::GetMousePos() - cursorPosStart;
 
-    // Get Y position of mouse cursor, in terms of line number (clamped to the valid range)
-    std::size_t y = static_cast<std::size_t>(std::min(std::max(std::floor(mousePos.y / textHeight), 0.0f), static_cast<float>(numLines - 1)));
+    std::string_view currentLine;
+    std::size_t x, y;
+    if (enableWordWrap) {
+        ImGuiWindow *window = ImGui::GetCurrentWindow();
+
+        const float wrapWidth = ImGui::CalcWrapWidthForPos(window->DC.CursorPos, 0);
+
+        // Find the index of the line under the cursor and its size.
+        float accumulatedHeight = 0;
+        ImVec2 currentLineSize = {};
+        y = numLines - 1;
+        for (std::size_t i = 0; i < numLines - 1; ++i) {
+            std::string_view line = getLineAtIdx(i);
+            currentLineSize = ImGui::CalcTextSize(line.data(), line.data() + line.size(), false, wrapWidth);
+
+            if (mousePos.y < accumulatedHeight + currentLineSize.y) {
+                y = i;
+                break;
+            }
+
+            accumulatedHeight += currentLineSize.y + ImGui::GetCurrentContext()->Style.ItemSpacing.y;
+        }
+        if (y >= numLines)
+            return;
+
+        currentLine = getLineAtIdx(y);
+
+        // Calculate index of the sub-line in the current line.
+        std::size_t localWrappedY = static_cast<std::size_t>(std::min(std::max(std::floor((mousePos.y - accumulatedHeight) / textHeight), 0.0f), std::round(currentLineSize.y / textHeight) - 1));
+
+        ImFont *font = ImGui::GetCurrentContext()->Font;
+
+        auto subLines = wrapText(currentLine, wrapWidth, font);
+        auto currentSubLine = subLines[localWrappedY];
+
+        x = currentSubLine.data() - currentLine.data() + getCharIndex(currentSubLine, mousePos.x);
+    } else {
+        // Get Y position of mouse cursor, in terms of line number (clamped to the valid range)
+        y = static_cast<std::size_t>(std::min(std::max(std::floor(mousePos.y / textHeight), 0.0f), static_cast<float>(numLines - 1)));
path: textselect.cpp
user: login: AidanSun05
body: This is the same as above:

```suggestion
        y = static_cast<std::size_t>(std::clamp(std::floor(mousePos.y / textHeight), 0.0f, static_cast<float>(numLines - 1)));
```
created_at: 2025-04-20T04:56:42Z
author_association: OWNER
diff_hunk: @@ -150,11 +183,53 @@ void TextSelect::handleMouseDown(const ImVec2& cursorPosStart) {
     const float textHeight = ImGui::GetTextLineHeightWithSpacing();
     ImVec2 mousePos = ImGui::GetMousePos() - cursorPosStart;
 
-    // Get Y position of mouse cursor, in terms of line number (clamped to the valid range)
-    std::size_t y = static_cast<std::size_t>(std::min(std::max(std::floor(mousePos.y / textHeight), 0.0f), static_cast<float>(numLines - 1)));
+    std::string_view currentLine;
+    std::size_t x, y;
+    if (enableWordWrap) {
+        ImGuiWindow *window = ImGui::GetCurrentWindow();
+
+        const float wrapWidth = ImGui::CalcWrapWidthForPos(window->DC.CursorPos, 0);
+
+        // Find the index of the line under the cursor and its size.
+        float accumulatedHeight = 0;
+        ImVec2 currentLineSize = {};
+        y = numLines - 1;
+        for (std::size_t i = 0; i < numLines - 1; ++i) {
+            std::string_view line = getLineAtIdx(i);
+            currentLineSize = ImGui::CalcTextSize(line.data(), line.data() + line.size(), false, wrapWidth);
+
+            if (mousePos.y < accumulatedHeight + currentLineSize.y) {
+                y = i;
+                break;
+            }
+
+            accumulatedHeight += currentLineSize.y + ImGui::GetCurrentContext()->Style.ItemSpacing.y;
+        }
+        if (y >= numLines)
+            return;
path: textselect.cpp
user: login: AidanSun05
body: Please ensure the bodies of single-line `if` statements are surrounded by braces.

```suggestion
        if (y >= numLines) {
            return;
        }
```
created_at: 2025-04-20T04:57:47Z
author_association: OWNER
diff_hunk: @@ -150,11 +183,53 @@ void TextSelect::handleMouseDown(const ImVec2& cursorPosStart) {
     const float textHeight = ImGui::GetTextLineHeightWithSpacing();
     ImVec2 mousePos = ImGui::GetMousePos() - cursorPosStart;
 
-    // Get Y position of mouse cursor, in terms of line number (clamped to the valid range)
-    std::size_t y = static_cast<std::size_t>(std::min(std::max(std::floor(mousePos.y / textHeight), 0.0f), static_cast<float>(numLines - 1)));
+    std::string_view currentLine;
+    std::size_t x, y;
+    if (enableWordWrap) {
+        ImGuiWindow *window = ImGui::GetCurrentWindow();
+
+        const float wrapWidth = ImGui::CalcWrapWidthForPos(window->DC.CursorPos, 0);
+
+        // Find the index of the line under the cursor and its size.
+        float accumulatedHeight = 0;
+        ImVec2 currentLineSize = {};
+        y = numLines - 1;
+        for (std::size_t i = 0; i < numLines - 1; ++i) {
+            std::string_view line = getLineAtIdx(i);
+            currentLineSize = ImGui::CalcTextSize(line.data(), line.data() + line.size(), false, wrapWidth);
+
+            if (mousePos.y < accumulatedHeight + currentLineSize.y) {
+                y = i;
+                break;
+            }
+
+            accumulatedHeight += currentLineSize.y + ImGui::GetCurrentContext()->Style.ItemSpacing.y;
+        }
+        if (y >= numLines)
+            return;
+
+        currentLine = getLineAtIdx(y);
+
+        // Calculate index of the sub-line in the current line.
+        std::size_t localWrappedY = static_cast<std::size_t>(std::min(std::max(std::floor((mousePos.y - accumulatedHeight) / textHeight), 0.0f), std::round(currentLineSize.y / textHeight) - 1));
+
+        ImFont *font = ImGui::GetCurrentContext()->Font;
+
+        auto subLines = wrapText(currentLine, wrapWidth, font);
+        auto currentSubLine = subLines[localWrappedY];
+
+        x = currentSubLine.data() - currentLine.data() + getCharIndex(currentSubLine, mousePos.x);
+    } else {
+        // Get Y position of mouse cursor, in terms of line number (clamped to the valid range)
+        y = static_cast<std::size_t>(std::min(std::max(std::floor(mousePos.y / textHeight), 0.0f), static_cast<float>(numLines - 1)));
+        if (y >= numLines)
+            return;
path: textselect.cpp
user: login: AidanSun05
body: This is another place where we can add braces.

```suggestion
        if (y >= numLines) {
            return;
        }
```
created_at: 2025-04-20T05:05:35Z
author_association: OWNER
diff_hunk: @@ -47,6 +47,8 @@ class TextSelect {
     // Indicates whether selection should be updated. This is needed for distinguishing mouse drags that are
     // initiated by clicking the text, or different element.
     bool shouldHandleMouseDown = false;
+    
+    bool enableWordWrap = true;
path: textselect.hpp
user: login: AidanSun05
body: We set this field in the constructor initializer list so we won't need a dedicated initializer here. Also, a brief comment about what this field does would be nice for consistency.

```suggestion
    // Indicates whether text selection with word wrapping should be enabled.
    bool enableWordWrap;

```
created_at: 2025-04-20T05:07:16Z
author_association: OWNER
diff_hunk: @@ -140,6 +140,39 @@ TextSelect::Selection TextSelect::getSelection() const {
     return { startX, startY, endX, endY };
 }
 
+//
+// Taken from imgui_draw.cpp
+//
+// Trim trailing space and find beginning of next line
+static inline const char* CalcWordWrapNextLineStartA(const char* text, const char* text_end)
+{
+    while (text < text_end && ImCharIsBlankA(*text))
+        text++;
+    if (*text == '\n')
+        text++;
+    return text;
+}
+
+// Split `text` that does not fit in `wrapWidth` into multiple lines.
+static ImVector<std::string_view> wrapText(std::string_view text, float wrapWidth, ImFont *font) {
+    ImVector<std::string_view> result;
+    const char *textEnd = text.data() + text.size();
+    const char *wrappedLineStart = text.data();
+    const char *wrappedLineEnd = text.data();
+    while (wrappedLineEnd != textEnd) {
+        wrappedLineStart = wrappedLineEnd;
+        wrappedLineEnd = font->CalcWordWrapPositionA(1, wrappedLineStart, textEnd, wrapWidth);
+
+        if (wrappedLineEnd - wrappedLineStart != 0) {
+            result.push_back(std::string_view(wrappedLineStart, wrappedLineEnd));
path: textselect.cpp
user: login: AidanSun05
body: We can't use the (start, end) constructor since it was introduced in C++20 and this library uses C++17. Also, using brace-initialization and an explicit `std::size_t` cast matches the rest of the code.

```suggestion
            result.push_back({ wrappedLineStart, static_cast<std::size_t>(wrappedLineEnd - wrappedLineStart) });
```
created_at: 2025-04-20T05:10:53Z
author_association: OWNER

PR comments:
user: login: AidanSun05
created_at: 2025-04-20T05:12:24Z
body: Thanks for implementing this large feature! It's coming along well, but sometimes drawing the rectangle with wrapped text and UTF-8 characters behaves unexpectedly even though the text copies correctly. This recording below shows triple-click and dragging behavior:

![recording](https://github.com/user-attachments/assets/d3cc5921-26fc-4e61-b8bf-f2726de851c1)

I see a lot of direct manipulation of string indices through pointer arithmetic which would be a likely cause for this issue, since in UTF-8 one byte is not necessarily one character. The preferred way to accomplish this would be to use the functions provided by utfcpp.

Also, thanks for leaving a comment about the performance of this feature. I think the slowness with a lot of text is acceptable for this library, and part of it is caused by Dear ImGui getting pretty slow when it tries to render it all - this is what `ImGuiListClipper` tries to solve, but it's designed for constant-height text so it doesn't work with word wrapping.
author_association: OWNER
user: login: twixuss
created_at: 2025-04-21T09:08:18Z
body: Thanks for review.
author_association: CONTRIBUTOR