Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multiline text input horizontal scrolling #383

Open
unpacklo opened this issue Oct 23, 2015 · 10 comments
Open

Multiline text input horizontal scrolling #383

unpacklo opened this issue Oct 23, 2015 · 10 comments

Comments

@unpacklo
Copy link

I was wondering if I was missing something obvious here for setting up a multiline text input with horizontal scrolling:

https://www.youtube.com/watch?v=Ncw78z9yTcA&feature=youtu.be

I know I can set the size manually in the code but that still doesn't help with the case where the text goes off the viewing area. It's rather laborious sometimes to edit some of these longer running lines.

We do have a workaround which is to make the overall window larger so the editing area becomes larger, but seeing as how we have some horizontal scrolling support, seems like that could be added to the multiline text?

@ocornut
Copy link
Owner

ocornut commented Oct 23, 2015

Yes we could, but measuring width is costly and we tend to avoid it this is why the multi-line text input still use single-line still scrolling.

  • Automatic accurate frame to frame width detection would add a non-negligible performance penalty to InputTextMultiline(). OK if you are editing 50 lines, but if you are editing 2000 lines the difference would be problematic.
  • We could otherwise come up with a scheme where you can specify width. And/or width only increasing based on what is visible (so you would only see the full extend of width by moving up/down).
  • Ideally I'd like to rewrite InputText() to carry more state and be more optimal but that's probably not coming soon.

That code is quite complex for various reason so it's not a trivial change but I could look into the middle-ground solution sometimes.

@ocornut
Copy link
Owner

ocornut commented Oct 23, 2015

Untested idea but you could declare a larger child window for scrolling and create the widget inside it using all the width of it.

@unpacklo
Copy link
Author

Hmm... is it not possible to calculate the max line width whenever text is input into the box rather than recomputing every frame?

Or is that what you were referring to in your rewrite that you don' t have time for? :)

@ocornut
Copy link
Owner

ocornut commented Oct 23, 2015

Yes that sort of thing. That would work but for very large text it would have an effect on interaction. However we could add it, I just don't have any enough time to do it all :)

@ocornut
Copy link
Owner

ocornut commented Oct 24, 2015

PS: First thing you can do is call PushItemWidth(-1.0f) to left-align and remove the label so that field.

@unpacklo
Copy link
Author

Sorry for the late response, I just got back to doing a little bit of GUI work and came to revisit this.

I thought about your suggestion of creating a child window and I came up with the following code:

#define MAX(a, b)    (((a) < (b)) ? (b) : (a))
#define INTERNAL     static

struct MultilineScrollState
{
    // Input.
    float scrollRegionX;
    float scrollX;
    ImGuiStorage *storage;

    // Output.
    bool newScrollPositionAvailable;
    float newScrollX;
};

INTERNAL int MultilineScrollCallback(ImGuiTextEditCallbackData *data)
{
    MultilineScrollState *scrollState = (MultilineScrollState *)data->UserData;
    ImGuiID cursorId = ImGui::GetID("cursor");
    int oldCursorIndex = scrollState->storage->GetInt(cursorId, 0);

    if (oldCursorIndex != data->CursorPos)
    {
        int begin = data->CursorPos;

        while ((begin > 0) && (data->Buf[begin - 1] != '\n'))
        {
            --begin;
        }

        float cursorOffset = ImGui::CalcTextSize(data->Buf + begin, data->Buf + data->CursorPos).x;
        float SCROLL_INCREMENT = scrollState->scrollRegionX * 0.25f;

        if (cursorOffset < scrollState->scrollX)
        {
            scrollState->newScrollPositionAvailable = true;
            scrollState->newScrollX = MAX(0.0f, cursorOffset - SCROLL_INCREMENT);
        }
        else if ((cursorOffset - scrollState->scrollRegionX) >= scrollState->scrollX)
        {
            scrollState->newScrollPositionAvailable = true;
            scrollState->newScrollX = cursorOffset - scrollState->scrollRegionX + SCROLL_INCREMENT;
        }
    }

    scrollState->storage->SetInt(cursorId, data->CursorPos);

    return 0;
}

INTERNAL bool ImGuiInputTextMultiline(const char* label, char* buf, size_t buf_size, float height, ImGuiInputTextFlags flags = 0)
{
    float scrollbarSize = ImGui::GetStyle().ScrollbarSize;
    float labelWidth = ImGui::CalcTextSize(label).x + scrollbarSize;
    float SCROLL_WIDTH = 2000.0f; // Very large scrolling width to allow for very long lines.
    MultilineScrollState scrollState = {};

    // Set up child region for horizontal scrolling of the text box.
    ImGui::BeginChild(label, ImVec2(-labelWidth, height), false, ImGuiWindowFlags_HorizontalScrollbar);
    scrollState.scrollRegionX = MAX(0.0f, ImGui::GetWindowWidth() - scrollbarSize);
    scrollState.scrollX = ImGui::GetScrollX();
    scrollState.storage = ImGui::GetStateStorage();
    bool changed = ImGui::InputTextMultiline(label, buf, buf_size, ImVec2(SCROLL_WIDTH, MAX(0.0f, height - scrollbarSize)),
                                             flags | ImGuiInputTextFlags_CallbackAlways, MultilineScrollCallback, &scrollState);

    if (scrollState.newScrollPositionAvailable)
    {
        ImGui::SetScrollX(scrollState.newScrollX);
    }

    ImGui::EndChild();
    ImGui::SameLine();
    ImGui::Text(label);

    return changed;
}

It's a little bit of a hack (probably doing some bad things here) but seems to work for me. Problems are that the vertical scrollbar isn't really accessible and the horizontal scrollbar is sized according to a very large constant set ahead of time, but these are small issues for us compared to just having the horizontal scrolling available.

Here's what it looks like in action:

https://youtu.be/MTijYhbJG-w

-Dale Kim

@ocornut
Copy link
Owner

ocornut commented Jul 14, 2017

Reviving this topic because #1224 raised it. Thanks Dale and sorry I haven't reacted on your idea yet.

As posted in the other thread here is another temporary solution, which works and is simple but is more costly (calculate width every frame). If you only edit small buffers it'd be another good-enough workaround until the feature is implemented.

inputtext_multiline_calc_width.diff

diff --git a/imgui.cpp b/imgui.cpp
index e3c6f1a..baa116f 100644
--- a/imgui.cpp
+++ b/imgui.cpp
@@ -7704,7 +7704,7 @@ bool ImGui::InputTextEx(const char* label, char* buf, int buf_size, const ImVec2
     ImGuiWindow* draw_window = window;
     if (is_multiline)
     {
-        if (!BeginChildFrame(id, frame_bb.GetSize()))
+        if (!BeginChildFrame(id, frame_bb.GetSize(), ImGuiWindowFlags_HorizontalScrollbar))
         {
             EndChildFrame();
             EndGroup();
@@ -8133,7 +8133,7 @@ bool ImGui::InputTextEx(const char* label, char* buf, int buf_size, const ImVec2
 
             // 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);
+                text_size = InputTextCalcTextSizeW(edit_state.Text.Data, edit_state.Text.Data + edit_state.CurLenW);
         }
 
         // Scroll
@@ -8221,7 +8221,10 @@ bool ImGui::InputTextEx(const char* label, char* buf, int buf_size, const ImVec2
         // Render text only
         const char* buf_end = NULL;
         if (is_multiline)
-            text_size = ImVec2(size.x, InputTextCalcTextLenAndLineCount(buf_display, &buf_end) * g.FontSize); // We don't need width
+        {
+            buf_end = buf_display + strlen(buf_display);
+            text_size = g.Font->CalcTextSizeA(g.FontSize, FLT_MAX, 0.0f, buf_display, buf_end);
+        }
         draw_window->DrawList->AddText(g.Font, g.FontSize, render_pos, GetColorU32(ImGuiCol_Text), buf_display, buf_end, 0.0f, is_multiline ? NULL : &clip_rect);
     }

@andrewmcdonald-oxb
Copy link

Just a note to say I encountered this problem today, so I'll be happy to see the eventual rewrite to support this. I recognise it might be low on your priority list though.

jerbmega added a commit to TeamREPENTOGON/imgui that referenced this issue Mar 10, 2024
Based on ocornut#383. This is NOT good
for performance but should work well enough for our case. We're not modifying
giant files- our buffer is locked to 1024 characters and probably won't
expand much further beyond that.
jerbmega added a commit to TeamREPENTOGON/imgui that referenced this issue Mar 16, 2024
Based on ocornut#383. This is NOT good
for performance but should work well enough for our case. We're not modifying
giant files- our buffer is locked to 1024 characters and probably won't
expand much further beyond that.
@tekglitch
Copy link

tekglitch commented Apr 27, 2024

Was this ever solved @ocornut @unpacklo ? A fairly simple hack is to wrap the text input widget in a child frame and make a function to find the longest line in the buffer. Something like this:
EDIT: **This has the added benefit of allowing you to move the scrollbar away from the Editbox! By padding the side or bottom of the edit box(or by restricting it's size) you can push the container to move far enough to place the slider wherever you want. In a vertical orientation(swap everything to the y axis) and insert '\n' every X(WordWrap length) bytes in the buffer and you end up with auto text wrapping. **

// We need to find the longest line denoted by '\n' in the buffer
size_t FindLongestLength(const char* str) {
    size_t maxLength = 0;
    size_t currentLength = 0;
    for (const char* p = str; *p != '\0'; ++p) {
        if (*p == '\n') {
            maxLength = std::max(maxLength, currentLength);
            currentLength = 0; // Reset length for new line
        } else {
            ++currentLength;
        }
    }
    // Check the length of the last line (in case there's no '\n' at the end)
    maxLength = std::max(maxLength, currentLength);
    return maxLength;
}

Now you would Create a child frame of static size, with your edit inside it:

    ImGui::Begin("Editor");
    
    //We want to hide the container(ChildFrame())'s background so styles still work on the edit box.
    ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.0f, 0.0f, 0.0f, 0.0f)); 
    ImGui::PushStyleColor(ImGuiCol_ScrollbarBg, ImVec4(0.0f, 0.0f, 0.0f, 0.0f)); 
    
    // Create a static sized ChildFrame to use as a scrollable container(wrapper) for the EditBox.
    ImGui::BeginChildFrame(ImGui::GetID("##Edit_Window_Container"), ImVec2(800, 600), ImGuiWindowFlags_AlwaysVerticalScrollbar | ImGuiWindowFlags_AlwaysHorizontalScrollbar); // I like to just leave the scrollbars always visible to avoid odd resizes once a char extends past the frame bounds.

    static char buffer[50000] = "Some really long string.\nThe next line in the text input.\nMore stuff....";
    
    // You will also want to add padding here. This really only needs to be calculated if the edit is edited or first created.
    size_t inputWidth = FindLongestLength(buffer); 
    
    //Now set the Text edit background color and make it visible.
    ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.1f, 0.1f, 0.3f, 1.0f)); 
    ImGui::PushStyleColor(ImGuiCol_ScrollbarBg, ImVec4(0.1f, 0.1f, 0.3f, 1.0f));
    
    ImGui::InputTextMultiline("##Editor_", buffer, IM_ARRAYSIZE(buffer), ImVec2(inputWidth, -1.0f), ImGuiInputTextFlags_None);
    
    ImGui::EndChildFrame();
    ImGui::PopStyleColor(4);
    ImGui::End();

Obviously you can handle the frame colors many different ways, such as just using the container frame colors, and making the input text edit background opaque instead. What this will do is always resize the edit box's width to fit the longest line inside it. the container (ChildFrame) will always stay the same size, and will allow horizontal scrolling.

For a rough idea on how to add auto text wrapping you could do something like this(This code is a rough example and may not function as-is):

// Pass in out string, the max length(TextWrap value), and your output buffer. and this will more or less insert a '\n' every time a line exceeds the TextWrap value
void wordWrap(const char* str, size_t maxLineLength, char* wrappedBuffer) {
    size_t length = strlen(str);
    size_t numNewlines = length / maxLineLength + 1; // Estimate number of newlines
    size_t newLength = length + numNewlines; // Maximum possible length of the wrapped string

    size_t lineLength = 0;
    size_t index = 0;
    for (size_t i = 0; i < length; ++i) {
        char currentChar = str[i];
        wrappedBuffer[index++] = currentChar;

        if (currentChar == '\n') {
            lineLength = 0; // Reset line length for new line
        } else {
            ++lineLength;
            if (lineLength >= maxLineLength) {
                wrappedBuffer[index++] = '\n'; // Insert newline character for word wrapping
                lineLength = 0; // Reset line length for new line
            }
        }
    }
    wrappedBuffer[index] = '\0'; // Null-terminate the wrapped string
}

The ImGui code is all the same, except you modify the Height value rather than the width. (unless you want the size completely static) Obviously the pitfall of the last method is, your data from the editbox will contain '\n' at WordWrap interval. There are several ways to solve this. Either by stripping '\n' every WordWrap interval, or having a vector to track the position(location) of each '\n so they can easily be removed without running a search loop.

@ocornut
Copy link
Owner

ocornut commented Apr 27, 2024

A fairly simple hack is to wrap the text input widget in a child frame and make a function to find the longest line in the buffer

That’s prohibitively too slow. We ought to support eg 1 MB text buffer without flinching. I am waiting for a rewrite of InputText to solve this along with other issues.

I know you mean well but your solutions are inadequate for the purpose of dear imgui.

edit
Many issues we have been struggling width comes from doing transforms and manipulating multiple buffers, so the last thing we would do today is add another one on top of that.
In addition, your maxlinelength thing assume monospace characters, the real measurement loop would be slower. We technically already fast-skip through carriage return to calculate line count.

ocornut added a commit that referenced this issue Aug 23, 2024
… expose it in a way that's agnostic of our use of a child window (#7913, #383)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants