ocornut / imgui

Dear ImGui: Bloat-free Graphical User interface for C++ with minimal dependencies
MIT License
61.02k stars 10.28k forks source link

Scrollable Child Window + Draw List Transformation/Clipping #3355

Open michaelquigley opened 4 years ago

michaelquigley commented 4 years ago

Version: 1.76 Back-ends: imgui_impl_glfw + imgui_impl_opengl3 Compiler: MSVC 19 Operating System: Windows 10

I haven't found a concise example of how best to deal with this situation. I think this might be a useful general use case to document here as an issue.

If I have a child window, and I want to do custom drawing on it with ImDrawList calls, what is the best way to transform my coordinates into the space of the scrolled region?

It sounds like there is a 16-bit limitation on draw list size for some rendering back ends and configurations, so I'd like to be smart about clipping. I've noticed the term "coarse clipping" mentioned in a few examples and issues... I'm naively assuming this means having some sort of basic logic along the lines of:

if (either end of my line is inside the visible area) {
   draw_list->AddLine(...);
}

If that's not what we mean by coarse clipping, what do we mean?

Is there anything else that should be done to make this sort of drawing on a scrolled child window more efficient?

I'd like to be able to use a child window as a "viewport" showing a region of a larger drawing.

My un-transformed draw list example looks like this:

ImGui::BeginChild("child_2", {0, 0}, false, ImGuiWindowFlags_HorizontalScrollbar);
ImGui::TextUnformatted("child_2");
ImGui::GetWindowDrawList()->AddLine({0, 0}, {500, 500}, 0xFFFFFFFF);
ImGui::SetCursorPos({1500, 1500});
ImGui::TextUnformatted("hello");
ImGui::EndChild();

The line drawn by AddLine does not scroll with the position of the scrollbars on the child window:

scroll

michaelquigley commented 4 years ago

Interesting where the {0, 0} point falls in the above screenshot. I ended up having to change the starting Y value to -375 to get closer to the upper left corner:

minus375

ImGui::BeginChild("child_2", {0, 0}, false, ImGuiWindowFlags_HorizontalScrollbar);
ImGui::TextUnformatted("child_2");
ImGui::GetWindowDrawList()->AddLine({0, -375}, {500, 500}, 0xFFFFFFFF);
ImGui::SetCursorPos({1500, 1500});
ImGui::TextUnformatted("hello");
ImGui::EndChild();
ocornut commented 4 years ago

what is the best way to transform my coordinates into the space of the scrolled region?

ImDrawList positions are always in absolute coordinates. You can use GetWindowPos() (window upper right corner) or GetCursorScreenPos() (current layout position) as a reference point and that later one will handle scrolling.

From there you can apply any other transformation you like (scaling, offseting your points etc.).

It sounds like there is a 16-bit limitation on draw list size for some rendering back ends and configurations, so I'd like to be smart about clipping.

That's right, however note that it means you can draw 32768 lines without worrying, so that coarse clipping is more for performances reason. (or ~10922 lines if using <1.77 and thickness >1.0f) and you might not need to care.

I'm naively assuming this means having some sort of basic logic along the lines of:

Correct.

michaelquigley commented 4 years ago

That's what I was looking for. Here is an animated example of a fuller implementation:

panels

I have two side-by-size child windows, one with scrollbars and one without. Vertically scrolling child_2 will vertically scroll child_1 in unison. The intention is that child_1 will contain a "legend" for lanes of information shown in child_2. Implemented a rough and simple splitter (stolen from https://github.com/ocornut/imgui/issues/125#issuecomment-135775009).

The full example looks like this:

static float w = 200.0f;
static float scroll_y = 0.0f;

ImGui::BeginChild("child_1", {w, 0.0f}, true, ImGuiWindowFlags_NoScrollbar);
auto draw_list = ImGui::GetWindowDrawList();
auto cursor = ImGui::GetCursorScreenPos();
auto legend = ImGui::GetWindowSize();
ImGui::TextUnformatted("child_1");
for(auto i = 1; i < 14; i++) {
    draw_list->AddLine(cursor + ImVec2{5, i * 50.0f}, cursor + ImVec2{legend.x - 10, i * 50.0f}, 0xFFFFFFFF);
}
ImGui::SetCursorPos({0, 1500});
ImGui::SetScrollY(scroll_y);
ImGui::EndChild();

ImGui::SameLine();
cursor = ImGui::GetCursorPos();
ImGui::InvisibleButton("v_splitter", {8, ImGui::GetWindowSize().y - 10});
if(ImGui::IsItemActive()) {
    w += ImGui::GetIO().MouseDelta.x;
}
auto button = ImGui::GetItemRectSize();
draw_list->AddRect(cursor, cursor + button, 0xFFFFFFFF);

ImGui::SameLine();
ImGui::BeginChild("child_2", {0, 0}, false, ImGuiWindowFlags_HorizontalScrollbar);
draw_list = ImGui::GetWindowDrawList();
cursor = ImGui::GetCursorScreenPos();
ImGui::TextUnformatted("child_2");
for(auto i = 1; i < 14; i++) {
    draw_list->AddLine(cursor + ImVec2{5, i * 50.0f}, cursor + ImVec2{1495, i * 50.0f}, 0xFFFFFFFF);
}
draw_list->AddLine(cursor + ImVec2{50, 50}, cursor + ImVec2{1000, 1000}, 0xFFFFFFFF);
ImGui::SetCursorPos({1500, 1500});
scroll_y = ImGui::GetScrollY();
ImGui::EndChild();

Also using an operator on ImVec2 like this:

static inline ImVec2 operator+(const ImVec2& lhs, const ImVec2& rhs) { return ImVec2(lhs.x + rhs.x, lhs.y + rhs.y); }

Is ImGui::SetCursorPos the best approach for communicating the final size of the child window's content region to ImGui?

I tried to update child_1's Y scroll position after I grab it in child_2, but the ImGui::BeginChild/ImGui::EndChild for child_1 placed after child_2, did not seem to work. Known issue? The implementation above is off by 1 frame, but that should be totally livable.

I will also look into 32-bit draw lists (my backend is glfw+opengl3), as I suspect I'll end up needing more than 16-bits eventually.

ocornut commented 4 years ago

Is ImGui::SetCursorPos the best approach for communicating the final size of the child window's content region to ImGui?

Yes.

I tried to update child_1's Y scroll position after I grab it in child_2, but the ImGui::BeginChild/ImGui::EndChild for child_1 placed after child_2, did not seem to work. Known issue? The implementation above is off by 1 frame, but that should be totally livable.

The effect SetScrollXXX function are deferred until next frame is you are already Note that there is a SetNextWindowScroll() function in imgui_internal.h you may be able to use. I think it could be moved to imgui.h.

Note that you could technically also just handling scrolling/panning yourself and ignore the provided scrollbars.

michaelquigley commented 4 years ago

The effect SetScrollXXX function are deferred until next frame is you are already Note that there is a SetNextWindowScroll() function in imgui_internal.h you may be able to use. I think it could be moved to imgui.h.

Ahh... well, then I'm off by 2 frames. With child_2 driving the positioning, and child_1 being to the left of child_2 and rendered first, I'm not seeing how I can not have child_1's Y position off by 1 frame, unless another BeginChild/EndChild after child_2 works (it didn't work at all for me).

In other words:

ImGui::BeginChild("child_1");
ImGui::EndChild();

ImGui::BeginChild("child_2)";
ImGui::EndChild();

ImGui::BeginChild("child_1");
ImGui::SetScrollY(...);
ImGui::EndChild();

...did not work for me at all. child_1 did not scroll in unison with child_2 (it did not scroll at all).

What if I draw child_2 first using SetCursorPosition in the parent, and then draw child_1? I would have to do my own child sizing, but that might let me draw child_1 after child_2?

Note that you could technically also just handling scrolling/panning yourself and ignore the provided scrollbars.

I've taken that approach in other interfaces that I've built using ImGui... but those designs didn't need scrollbars at all. This design wants scrollbars on that second child, and I feel like I would end up having to re-create a bunch of scrollbar sizing and position code that the child implementation already provides? I'll try mocking that up too, for the learning if nothing else.

Thank you for the assistance!

ocornut commented 2 years ago

Is ImGui::SetCursorPos the best approach for communicating the final size of the child window's content region to ImGui?

Hi! This is Omar pretending to a be a bot! The quoted message (posted 2020/07/17) suggested an incorrect usage pattern which we had to obsolete in 1.89. An item (even a Dummy()) needs to be submitted to extend parent window/cell boundaries. See #5548 for details.

epajarre commented 9 months ago

(Sorry for hijacking this thread, but I think this is sort of related)

I am doing my scrollable area by first creating a Top level window with forced scrollbars, and then adding a child window with the drawing area size I want. I later add an InvisibleButton but I am not really doing anything interactive yet.

I got the above working, rendering and scrolling around works fine. Now for optimization reasons and for example implementing a zoom which would keep the focus area visible, I would like to know what part of my child window is actually visible through the scrollable view?

Is there a direct API for this info, or should I (can I?) just calculate it from the child window size + scroll information?

epajarre commented 9 months ago

Replying to my own question, I have found that using GetContentRegionAvail() for area size and GetCursorScreenPos()+GetScrollX/Y() for area corner, seems to give me sufficiently accurate information for my use.

Zooming in and out works well enough after I implemented it so that the zoom slider is used for scrollbar positioning immediately but it is delayed by one frame before it is used in actual rendering. This causes scrollbar change which happens in next frame to be synchronized with rendering. (Noticed the scrollbar behaviour from Omar's comment)