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 widget displays incorrect text in some circumstances #295

Closed
bgribble opened this issue Dec 13, 2024 · 6 comments
Closed

Comments

@bgribble
Copy link

Multiline text widget that is used as a combo display and editor (a REPL console, in my app) will display incorrect text (sometimes blank, sometimes garbage characters that look like a wild pointer).

Clicking inside the widget starts the erroneous display; clicking outside the widget gets back to a good display

Repro code below. I am not sure exactly what the triggering conditions are but this is a pretty minimal case that looks a lot like my actual use case.

from imgui_bundle import imgui
from imgui_bundle.python_backends.sdl_backend import SDL2Renderer
from imgui_bundle import im_file_dialog as ifd

import OpenGL.GL as gl  # type: ignore
from sdl2 import *  # type: ignore
import ctypes
import sys


static_text = """
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod 
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim 
veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex 
ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate 
velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint 
occaecat cupidatat non proident, sunt in culpa qui officia deserunt
mollit anim id est laborum.
"""


class AppState:
    text: str = """Hello, World\nLorem ipsum, etc.\netc."""
    text2: str = "Ahh"


app_state = AppState()


def main():
    window, gl_context = impl_pysdl2_init()
    imgui.create_context()
    impl = SDL2Renderer(window)

    running = True
    event = SDL_Event()

    frame_count = 0

    while running:
        while SDL_PollEvent(ctypes.byref(event)) != 0:
            if event.type == SDL_QUIT:
                running = False
                break
            impl.process_event(event)
        impl.process_inputs()

        imgui.new_frame()

        imgui.begin("Multiline text problem", True)
        imgui.text("Should reveal text over time.")
        imgui.text("Click in the multiline text widget to see invisible and erroneous characters")
        imgui.text("Then click outside the widget to see the correct text restored")
        imgui.text("Code taken from demo_widgets.py")

        def callback(cb_data):
            bufpos = int(frame_count / 2)
            cb_data.selection_start = bufpos
            cb_data.selection_end = bufpos
            cb_data.cursor_pos = bufpos
            return 0

        imgui.input_text_multiline(
            "readonly_multiline",
            static_text[:int(frame_count/2)],
            (600, 200),
            imgui.InputTextFlags_.read_only | imgui.InputTextFlags_.callback_always,
            callback=callback
        )


        imgui.end()

        gl.glClearColor(1.0, 1.0, 1.0, 1)
        gl.glClear(gl.GL_COLOR_BUFFER_BIT)

        imgui.render()
        impl.render(imgui.get_draw_data())
        SDL_GL_SwapWindow(window)

        frame_count += 1
        if frame_count > 2*len(static_text):
            frame_count = 0

    impl.shutdown()
    SDL_GL_DeleteContext(gl_context)
    SDL_DestroyWindow(window)
    SDL_Quit()


def impl_pysdl2_init():
    width, height = 1280, 720
    window_name = "minimal ImGui/SDL2 example"

    if SDL_Init(SDL_INIT_EVERYTHING) < 0:
        print(
            "Error: SDL could not initialize! SDL Error: "
            + SDL_GetError().decode("utf-8")
        )
        sys.exit(1)

    SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1)
    SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24)
    SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8)
    SDL_GL_SetAttribute(SDL_GL_ACCELERATED_VISUAL, 1)
    SDL_GL_SetAttribute(SDL_GL_MULTISAMPLEBUFFERS, 1)
    SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, 8)
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, SDL_GL_CONTEXT_FORWARD_COMPATIBLE_FLAG)
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 4)
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 1)
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE)

    SDL_SetHint(SDL_HINT_MAC_CTRL_CLICK_EMULATE_RIGHT_CLICK, b"1")
    SDL_SetHint(SDL_HINT_VIDEO_HIGHDPI_DISABLED, b"1")
    SDL_SetHint(SDL_HINT_VIDEODRIVER, b"wayland,x11")
    SDL_SetHint(SDL_HINT_VIDEO_X11_FORCE_EGL, b"1")

    window = SDL_CreateWindow(
        window_name.encode("utf-8"),
        SDL_WINDOWPOS_CENTERED,
        SDL_WINDOWPOS_CENTERED,
        width,
        height,
        SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE,
    )

    if window is None:
        print(
            "Error: Window could not be created! SDL Error: "
            + SDL_GetError().decode("utf-8")
        )
        sys.exit(1)

    gl_context = SDL_GL_CreateContext(window)
    if gl_context is None:
        print(
            "Error: Cannot create OpenGL Context! SDL Error: "
            + SDL_GetError().decode("utf-8")
        )
        sys.exit(1)

    SDL_GL_MakeCurrent(window, gl_context)
    if SDL_GL_SetSwapInterval(1) < 0:
        print(
            "Warning: Unable to set VSync! SDL Error: " + SDL_GetError().decode("utf-8")
        )
        sys.exit(1)

    return window, gl_context


if __name__ == "__main__":
    main()
@pthom
Copy link
Owner

pthom commented Dec 14, 2024

Your example code could be simplified to the bare minimum, e.g.

from imgui_bundle import imgui, hello_imgui

static_text = """
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod 
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim 
veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex 
ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate 
velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint 
occaecat cupidatat non proident, sunt in culpa qui officia deserunt
mollit anim id est laborum.
"""

frame_count = 0


def gui():
    global frame_count
    imgui.text("Should reveal text over time.")
    imgui.text("Click in the multiline text widget to see invisible and erroneous characters")
    imgui.text("Then click outside the widget to see the correct text restored")
    imgui.text("Code taken from demo_widgets.py")

    def callback(cb_data):
        bufpos = int(frame_count / 2)
        cb_data.selection_start = bufpos
        cb_data.selection_end = bufpos
        cb_data.cursor_pos = bufpos
        return 0

    imgui.input_text_multiline(
        "readonly_multiline",
        static_text[:int(frame_count/2)],
        (600, 200),
        imgui.InputTextFlags_.read_only | imgui.InputTextFlags_.callback_always,
        callback=callback
    )
    frame_count += 1
    if frame_count > 2*len(static_text):
        frame_count = 0


hello_imgui.run(gui, fps_idle=0)  # where fps_idle=0 will let the app always run at full speed

@pthom
Copy link
Owner

pthom commented Dec 14, 2024

Your example code could be simplified to the bare minimum, ...

I initially wrote "should", but meant "could".
But, first of all, thanks for the repro.

@pthom
Copy link
Owner

pthom commented Dec 14, 2024

And the equivalent c++ program would be:

#include <imgui.h>
#include "misc/cpp/imgui_stdlib.h"
#include <hello_imgui/hello_imgui.h>

std::string static_text = R"(
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim
veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex
ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate
velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint
occaecat cupidatat non proident, sunt in culpa qui officia deserunt
mollit anim id est laborum.
)";

int frame_count = 0;

void gui()
{
    ImGui::Text("Should reveal text over time.");
    ImGui::Text("Click in the multiline text widget to see invisible and erroneous characters");
    ImGui::Text("Then click outside the widget to see the correct text restored");
    ImGui::Text("Code taken from demo_widgets.py");

    auto callback = [](ImGuiInputTextCallbackData* cb_data) -> int
    {
        int bufpos = frame_count / 2;
        cb_data->SelectionStart = bufpos;
        cb_data->SelectionEnd = bufpos;
        cb_data->CursorPos = bufpos;
        return 0;
    };

    std::string subtext = static_text.substr(0, frame_count / 2);
    ImGui::InputTextMultiline(
        "readonly_multiline",
        &subtext,
        ImVec2(600, 200),
        ImGuiInputTextFlags_ReadOnly | ImGuiInputTextFlags_CallbackAlways,
        callback
    );
    frame_count++;
    if (frame_count > 2 * static_text.size())
        frame_count = 0;
}


int main()
{
    HelloImGui::Run(gui);
}

And we observe the same issue with the C++ program, so all in all this related to Dear ImGui itself.

However, let's have a deep look at those two lines:

    std::string subtext = static_text.substr(0, frame_count / 2);
    ImGui::InputTextMultiline(
        "readonly_multiline",
        &subtext,
        ImVec2(600, 200),
        ImGuiInputTextFlags_ReadOnly | ImGuiInputTextFlags_CallbackAlways,
        callback
    );

On each frame, you are creating a new string, and asking to display a widget to edit this string.
As a matter of fact, it is likely and very acceptable that a text editor widget has an internal cache.
By changing the string you give it every frame you invalidate the cache each frame, and this flashing you see is a consequence.

So I have a question about your use case: are trying to use the text editor as a "scrollable long text renderer"?
If yes, then text_wrapped + begin/end_child may do the trick.

@bgribble
Copy link
Author

By changing the string you give it every frame you invalidate the cache each frame, and this flashing you see is a consequence.

As an explanation for the behavior, that seems somewhat likely, except that the "flashing" is persistent between renders and changes modes based on unrelated activity.

It's still a bug, though... I am passing a string to input_text_multiline and it's displaying something different. In the Python bindings, there's no alternative to passing a "new string" every time.

So I have a question about your use case: are trying to use the text editor as a "scrollable long text renderer"?
If yes, then text_wrapped + begin/end_child may do the trick.

It's being used as an interactive REPL console, so it's both a "scrollable long text renderer" and an interactive editable. I have it as ReadOnly because I want to control the key bindings and manage things like history and selection myself, rather than allowing Imgui to do it. I will give it a try with text_wrapped as you suggest.

@pthom
Copy link
Owner

pthom commented Dec 17, 2024

It's still a bug, though... I am passing a string to input_text_multiline and it's displaying something different. In the Python bindings, there's no alternative to passing a "new string" every time.

The C/C++ version below does not create a string at each frame, and reuses the same char* address (aka "buffer" below).
It still has the same issue.

So the issue is not due to having a string whose address that changes at every frame, but it is due to the fact that this widget displays an allegedly user-editable string but which is changed at every frame.

All in all, this is due to the way Dear ImGui handles text edition. Would this be considered as a bug in ImGui? I'm not sure (TBH, I doubt it would be). You may want to post this as an issue in the imgui repo, though.

Repro code:

#include <imgui.h>
#include "misc/cpp/imgui_stdlib.h"
#include <hello_imgui/hello_imgui.h>

std::string static_text = R"(
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim
veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex
ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate
velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint
occaecat cupidatat non proident, sunt in culpa qui officia deserunt
mollit anim id est laborum.
)";


char buffer[2048];

int frame_count = 0;

void gui()
{
    ImGui::Text("Should reveal text over time.");
    ImGui::Text("Click in the multiline text widget to see invisible and erroneous characters");
    ImGui::Text("Then click outside the widget to see the correct text restored");
    ImGui::Text("Code taken from demo_widgets.py");

    auto callback = [](ImGuiInputTextCallbackData* cb_data) -> int
    {
        int bufpos = frame_count / 2;
        cb_data->SelectionStart = bufpos;
        cb_data->SelectionEnd = bufpos;
        cb_data->CursorPos = bufpos;
        return 0;
    };

    memset(buffer, 0, sizeof(buffer));
    strncpy(buffer, static_text.c_str(), frame_count / 2);

    ImGui::InputTextMultiline(
        "readonly_multiline",
        buffer,
        sizeof(buffer),
        ImVec2(600, 200),
        ImGuiInputTextFlags_ReadOnly | ImGuiInputTextFlags_CallbackAlways,
        callback
    );
    frame_count++;
    if (frame_count > 2 * static_text.size())
        frame_count = 0;
}


int main()
{
    HelloImGui::Run(gui);
}

@pthom
Copy link
Owner

pthom commented Dec 17, 2024

Would this be considered as a bug in ImGui? I'm not sure (TBH, I doubt it would be). You may want to post this as an issue in the imgui repo, though.

On a second examination, since you are passing the flag ImGuiInputTextFlags_ReadOnly, it might be seen as a workable issue.

Video demo of the issue

v.mp4

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants