ImGuiListClipper new version, detect height automatically, fix compatibility with SetScrollPosHere (#662)

This commit is contained in:
ocornut 2016-05-16 19:22:51 +02:00
parent fa5ae60bce
commit 7a28f5bb81
3 changed files with 156 additions and 74 deletions

155
imgui.cpp
View File

@ -1607,6 +1607,88 @@ float ImGuiSimpleColumns::CalcExtraSpace(float avail_w)
return ImMax(0.0f, avail_w - Width); return ImMax(0.0f, avail_w - Width);
} }
//-----------------------------------------------------------------------------
// ImGuiListClipper
//-----------------------------------------------------------------------------
static void SetCursorPosYAndSetupDummyPrevLine(float pos_y, float line_height)
{
// Setting those fields so that SetScrollHere() can properly function after the end of our clipper usage.
// If we end up needing more accurate data (to e.g. use SameLine) we may as well make the clipper have a fourth step to let user process and display the last item in their list.
ImGui::SetCursorPosY(pos_y);
ImGuiWindow* window = ImGui::GetCurrentWindow();
window->DC.CursorPosPrevLine.y = window->DC.CursorPos.y - line_height;
window->DC.PrevLineHeight = (line_height - GImGui->Style.ItemSpacing.y);
}
// Use case A: Begin() called from constructor with items_height<0, then called again from Sync() in StepNo 1
// Use case B: Begin() called from constructor with items_height>0
// FIXME-LEGACY: Ideally we should remove the Begin/End functions but they are part of the legacy API we still support. This is why some of the code in Step() calling Begin() and reassign some fields, spaghetti style.
void ImGuiListClipper::Begin(int count, float items_height)
{
StartPosY = ImGui::GetCursorPosY();
ItemsHeight = items_height;
ItemsCount = count;
StepNo = 0;
DisplayEnd = DisplayStart = -1;
if (ItemsHeight > 0.0f)
{
ImGui::CalcListClipping(ItemsCount, ItemsHeight, &DisplayStart, &DisplayEnd); // calculate how many to clip/display
if (DisplayStart > 0)
SetCursorPosYAndSetupDummyPrevLine(StartPosY + DisplayStart * ItemsHeight, ItemsHeight); // advance cursor
StepNo = 2;
}
}
void ImGuiListClipper::End()
{
if (ItemsCount < 0)
return;
float cur_y = ImGui::GetCursorPosY(); (void)cur_y;
float expected_display_end_y = StartPosY + DisplayEnd * ItemsHeight;
IM_ASSERT(fabsf(cur_y - expected_display_end_y) < 1.0f); // if this triggers, it probably means your items have varying height (in which case you can't use this helper) or the explicit height you have passed was incorrect.
if (ItemsCount < INT_MAX)
SetCursorPosYAndSetupDummyPrevLine(StartPosY + ItemsCount * ItemsHeight, ItemsHeight); // advance cursor
ItemsCount = -1;
StepNo = 3;
}
bool ImGuiListClipper::Step()
{
if (ItemsCount == 0 || ImGui::GetCurrentWindowRead()->SkipItems)
{
ItemsCount = -1;
return false;
}
if (StepNo == 0) // Step 0: the clipper let you process the first element, regardless of it being visible or not, so we can measure the element height.
{
DisplayStart = 0;
DisplayEnd = 1;
StartPosY = ImGui::GetCursorPosY();
StepNo = 1;
return true;
}
if (StepNo == 1) // Step 1: the clipper infer height from first element, calculate the actual range of elements to display, and position the cursor before the first element.
{
if (ItemsCount == 1) { ItemsCount = -1; return false; }
float items_height = ImGui::GetCursorPosY() - StartPosY;
IM_ASSERT(items_height > 0.0f); // If this triggers, it means Item 0 hasn't moved the cursor vertically
ImGui::SetCursorPosY(StartPosY); // Rewind cursor so we can Begin() again, this time with a known height.
Begin(ItemsCount, items_height);
StepNo = 3;
return true;
}
if (StepNo == 2) // Step 2: dummy step only required if an explicit items_height was passed to constructor or Begin() and user still call Step(). Does nothing and switch to Step 3.
{
IM_ASSERT(DisplayStart >= 0 && DisplayEnd >= 0);
StepNo = 3;
return true;
}
if (StepNo == 3) // Step 3: the clipper validate that we have reached the expected Y position (corresponding to element DisplayEnd), advance the cursor to the end of the list and then returns 'false' to end the loop.
End();
return false;
}
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
// ImGuiWindow // ImGuiWindow
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
@ -2878,18 +2960,8 @@ ImVec2 ImGui::CalcTextSize(const char* text, const char* text_end, bool hide_tex
} }
// Helper to calculate coarse clipping of large list of evenly sized items. // Helper to calculate coarse clipping of large list of evenly sized items.
// NB: Prefer using the ImGuiListClipper higher-level helper if you can! // NB: Prefer using the ImGuiListClipper higher-level helper if you can! Read comments and instructions there on how those use this sort of pattern.
// NB: 'items_count' is only used to clamp the result, if you don't know your count you can use INT_MAX // NB: 'items_count' is only used to clamp the result, if you don't know your count you can use INT_MAX
// If you are displaying thousands of items and you have a random access to the list, you can perform clipping yourself to save on CPU.
// {
// float item_height = ImGui::GetTextLineHeightWithSpacing();
// int display_start, display_end;
// ImGui::CalcListClipping(count, item_height, &display_start, &display_end); // calculate how many to clip/display
// ImGui::SetCursorPosY(ImGui::GetCursorPosY() + (display_start) * item_height); // advance cursor
// for (int i = display_start; i < display_end; i++) // display only visible items
// // TODO: display visible item
// ImGui::SetCursorPosY(ImGui::GetCursorPosY() + (count - display_end) * item_height); // advance cursor
// }
void ImGui::CalcListClipping(int items_count, float items_height, int* out_items_display_start, int* out_items_display_end) void ImGui::CalcListClipping(int items_count, float items_height, int* out_items_display_start, int* out_items_display_end)
{ {
ImGuiContext& g = *GImGui; ImGuiContext& g = *GImGui;
@ -2901,6 +2973,11 @@ void ImGui::CalcListClipping(int items_count, float items_height, int* out_items
*out_items_display_end = items_count; *out_items_display_end = items_count;
return; return;
} }
if (window->SkipItems)
{
*out_items_display_start = *out_items_display_end = 0;
return;
}
const ImVec2 pos = window->DC.CursorPos; const ImVec2 pos = window->DC.CursorPos;
int start = (int)((window->ClipRect.Min.y - pos.y) / items_height); int start = (int)((window->ClipRect.Min.y - pos.y) / items_height);
@ -8492,22 +8569,22 @@ bool ImGui::ListBox(const char* label, int* current_item, bool (*items_getter)(v
// Assume all items have even height (= 1 line of text). If you need items of different or variable sizes you can create a custom version of ListBox() in your code without using the clipper. // Assume all items have even height (= 1 line of text). If you need items of different or variable sizes you can create a custom version of ListBox() in your code without using the clipper.
bool value_changed = false; bool value_changed = false;
ImGuiListClipper clipper(items_count, ImGui::GetTextLineHeightWithSpacing()); ImGuiListClipper clipper(items_count, ImGui::GetTextLineHeightWithSpacing());
for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) while (clipper.Step())
{ for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++)
const bool item_selected = (i == *current_item);
const char* item_text;
if (!items_getter(data, i, &item_text))
item_text = "*Unknown item*";
ImGui::PushID(i);
if (ImGui::Selectable(item_text, item_selected))
{ {
*current_item = i; const bool item_selected = (i == *current_item);
value_changed = true; const char* item_text;
if (!items_getter(data, i, &item_text))
item_text = "*Unknown item*";
ImGui::PushID(i);
if (ImGui::Selectable(item_text, item_selected))
{
*current_item = i;
value_changed = true;
}
ImGui::PopID();
} }
ImGui::PopID();
}
clipper.End();
ImGui::ListBoxFooter(); ImGui::ListBoxFooter();
return value_changed; return value_changed;
} }
@ -9520,22 +9597,22 @@ void ImGui::ShowMetricsWindow(bool* p_open)
} }
if (!pcmd_node_open) if (!pcmd_node_open)
continue; continue;
ImGuiListClipper clipper(pcmd->ElemCount/3, ImGui::GetTextLineHeight()*3 + ImGui::GetStyle().ItemSpacing.y); // Manually coarse clip our print out of individual vertices to save CPU, only items that may be visible. ImGuiListClipper clipper(pcmd->ElemCount/3); // Manually coarse clip our print out of individual vertices to save CPU, only items that may be visible.
for (int prim = clipper.DisplayStart, vtx_i = elem_offset + clipper.DisplayStart*3; prim < clipper.DisplayEnd; prim++) while (clipper.Step())
{ for (int prim = clipper.DisplayStart, vtx_i = elem_offset + clipper.DisplayStart*3; prim < clipper.DisplayEnd; prim++)
char buf[300], *buf_p = buf;
ImVec2 triangles_pos[3];
for (int n = 0; n < 3; n++, vtx_i++)
{ {
ImDrawVert& v = draw_list->VtxBuffer[idx_buffer ? idx_buffer[vtx_i] : vtx_i]; char buf[300], *buf_p = buf;
triangles_pos[n] = v.pos; ImVec2 triangles_pos[3];
buf_p += sprintf(buf_p, "%s %04d { pos = (%8.2f,%8.2f), uv = (%.6f,%.6f), col = %08X }\n", (n == 0) ? "vtx" : " ", vtx_i, v.pos.x, v.pos.y, v.uv.x, v.uv.y, v.col); for (int n = 0; n < 3; n++, vtx_i++)
{
ImDrawVert& v = draw_list->VtxBuffer[idx_buffer ? idx_buffer[vtx_i] : vtx_i];
triangles_pos[n] = v.pos;
buf_p += sprintf(buf_p, "%s %04d { pos = (%8.2f,%8.2f), uv = (%.6f,%.6f), col = %08X }\n", (n == 0) ? "vtx" : " ", vtx_i, v.pos.x, v.pos.y, v.uv.x, v.uv.y, v.col);
}
ImGui::Selectable(buf, false);
if (ImGui::IsItemHovered())
overlay_draw_list->AddPolyline(triangles_pos, 3, IM_COL32(255,255,0,255), true, 1.0f, false); // Add triangle without AA, more readable for large-thin triangle
} }
ImGui::Selectable(buf, false);
if (ImGui::IsItemHovered())
overlay_draw_list->AddPolyline(triangles_pos, 3, IM_COL32(255,255,0,255), true, 1.0f, false); // Add triangle without AA, more readable for large-thin triangle
}
clipper.End();
ImGui::TreePop(); ImGui::TreePop();
} }
overlay_draw_list->PopClipRect(); overlay_draw_list->PopClipRect();

44
imgui.h
View File

@ -1049,36 +1049,32 @@ struct ImColor
}; };
// Helper: Manually clip large list of items. // Helper: Manually clip large list of items.
// If you are displaying thousands of even spaced items and you have a random access to the list, you can perform clipping yourself to save on CPU. // If you are displaying thousands of evenly spaced items and you have a random access to the list, you can perform clipping yourself to save on CPU.
// The clipper calculates the range of visible items and advance the cursor to compensate for the non-visible items we have skipped.
// Usage: // Usage:
// ImGuiListClipper clipper(count, ImGui::GetTextLineHeightWithSpacing()); // ImGuiListClipper clipper(1000); // we have 1000 elements, evenly spaced.
// for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) // display only visible items // while (clipper.Step())
// ImGui::Text("line number %d", i); // for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++)
// clipper.End(); // ImGui::Text("line number %d", i);
// NB: 'count' is only used to clamp the result, if you don't know your count you can use INT_MAX // - Step 0: the clipper let you process the first element, regardless of it being visible or not, so we can measure the element height.
// - Step 1: the clipper infer height from first element, calculate the actual range of elements to display, and position the cursor before the first element.
// - Step 2: dummy step only required if an explicit items_height was passed to constructor or Begin() and user still call Step(). Does nothing and switch to Step 3.
// - Step 3: the clipper validate that we have reached the expected Y position (corresponding to element DisplayEnd), advance the cursor to the end of the list and then returns 'false' to end the loop.
struct ImGuiListClipper struct ImGuiListClipper
{ {
float StartPosY;
float ItemsHeight; float ItemsHeight;
int ItemsCount, DisplayStart, DisplayEnd; int ItemsCount, StepNo, DisplayStart, DisplayEnd;
ImGuiListClipper() { ItemsHeight = 0.0f; ItemsCount = DisplayStart = DisplayEnd = -1; } // items_count: Use -1 to ignore (you can call Begin later). Use INT_MAX if you don't know how many items you have (in which case the cursor won't be advanced in the final step).
ImGuiListClipper(int count, float height) { ItemsCount = -1; Begin(count, height); } // items_height: Use -1.0f to be calculated automatically on first step. Otherwise pass in the distance between your items, typically GetTextLineHeightWithSpacing() or GetItemsLineHeightWithSpacing().
~ImGuiListClipper() { IM_ASSERT(ItemsCount == -1); } // user forgot to call End() // If you don't specify an items_height, you NEED to call Step(). If you specify items_height you may call the old Begin()/End() api directly, but prefer calling Step().
ImGuiListClipper(int items_count = -1, float items_height = -1.0f) { Begin(items_count, items_height); } // NB: Begin() initialize every fields (as we allow user to call Begin/End multiple times on a same instance if they want).
~ImGuiListClipper() { IM_ASSERT(ItemsCount == -1); } // Assert if user forgot to call End() or Step() until false.
void Begin(int count, float height) // items_height: generally pass GetTextLineHeightWithSpacing() or GetItemsLineHeightWithSpacing() IMGUI_API bool Step(); // Call until it returns false. The DisplayStart/DisplayEnd fields will be set and you can process/draw those items.
{ IMGUI_API void Begin(int items_count, float items_height = -1.0f); // Automatically called by constructor if you passed 'items_count' or by Step() in Step 1.
IM_ASSERT(ItemsCount == -1); IMGUI_API void End(); // Automatically called on the last call of Step() that returns false.
ItemsCount = count;
ItemsHeight = height;
ImGui::CalcListClipping(ItemsCount, ItemsHeight, &DisplayStart, &DisplayEnd); // calculate how many to clip/display
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + DisplayStart * ItemsHeight); // advance cursor
}
void End()
{
IM_ASSERT(ItemsCount >= 0);
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + (ItemsCount - DisplayEnd) * ItemsHeight); // advance cursor
ItemsCount = -1;
}
}; };
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------

View File

@ -2014,24 +2014,33 @@ struct ExampleAppConsole
ImGui::PopStyleVar(); ImGui::PopStyleVar();
ImGui::Separator(); ImGui::Separator();
// Display every line as a separate entry so we can change their color or add custom widgets. If you only want raw text you can use ImGui::TextUnformatted(log.begin(), log.end());
// NB- if you have thousands of entries this approach may be too inefficient. You can seek and display only the lines that are visible - CalcListClipping() is a helper to compute this information.
// If your items are of variable size you may want to implement code similar to what CalcListClipping() does. Or split your data into fixed height items to allow random-seeking into your list.
ImGui::BeginChild("ScrollingRegion", ImVec2(0,-ImGui::GetItemsLineHeightWithSpacing()), false, ImGuiWindowFlags_HorizontalScrollbar); ImGui::BeginChild("ScrollingRegion", ImVec2(0,-ImGui::GetItemsLineHeightWithSpacing()), false, ImGuiWindowFlags_HorizontalScrollbar);
if (ImGui::BeginPopupContextWindow()) if (ImGui::BeginPopupContextWindow())
{ {
if (ImGui::Selectable("Clear")) ClearLog(); if (ImGui::Selectable("Clear")) ClearLog();
ImGui::EndPopup(); ImGui::EndPopup();
} }
// Display every line as a separate entry so we can change their color or add custom widgets. If you only want raw text you can use ImGui::TextUnformatted(log.begin(), log.end());
// NB- if you have thousands of entries this approach may be too inefficient and may require user-side clipping to only process visible items.
// You can seek and display only the lines that are visible using the ImGuiListClipper helper, if your elements are evenly spaced and you have cheap random access to the elements.
// To use the clipper we could replace the 'for (int i = 0; i < Items.Size; i++)' loop with:
// ImGuiListClipper clipper(Items.Size);
// while (clipper.Step())
// for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++)
// However take note that you can not use this code as is if a filter is active because it breaks the 'cheap random-access' property. We would need random-access on the post-filtered list.
// A typical application wanting coarse clipping and filtering may want to pre-compute an array of indices that passed the filtering test, recomputing this array when user changes the filter,
// and appending newly elements as they are inserted. This is left as a task to the user until we can manage to improve this example code!
// If your items are of variable size you may want to implement code similar to what ImGuiListClipper does. Or split your data into fixed height items to allow random-seeking into your list.
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4,1)); // Tighten spacing ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4,1)); // Tighten spacing
for (int i = 0; i < Items.Size; i++) for (int i = 0; i < Items.Size; i++)
{ {
const char* item = Items[i]; const char* item = Items[i];
if (!filter.PassFilter(item)) if (!filter.PassFilter(item))
continue; continue;
ImVec4 col = ImColor(255,255,255); // A better implementation may store a type per-item. For the sample let's just parse the text. ImVec4 col = ImVec4(1.0f,1.0f,1.0f,1.0f); // A better implementation may store a type per-item. For the sample let's just parse the text.
if (strstr(item, "[error]")) col = ImColor(255,100,100); if (strstr(item, "[error]")) col = ImColor(1.0f,0.4f,0.4f,1.0f);
else if (strncmp(item, "# ", 2) == 0) col = ImColor(255,200,150); else if (strncmp(item, "# ", 2) == 0) col = ImColor(1.0f,0.78f,0.58f,1.0f);
ImGui::PushStyleColor(ImGuiCol_Text, col); ImGui::PushStyleColor(ImGuiCol_Text, col);
ImGui::TextUnformatted(item); ImGui::TextUnformatted(item);
ImGui::PopStyleColor(); ImGui::PopStyleColor();
@ -2441,10 +2450,10 @@ static void ShowExampleAppLongText(bool* p_open)
{ {
// Multiple calls to Text(), manually coarsely clipped - demonstrate how to use the ImGuiListClipper helper. // Multiple calls to Text(), manually coarsely clipped - demonstrate how to use the ImGuiListClipper helper.
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0,0)); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0,0));
ImGuiListClipper clipper(lines, ImGui::GetTextLineHeightWithSpacing()); // Here we changed spacing is zero anyway so we could use GetTextLineHeight(), but _WithSpacing() is typically more correct ImGuiListClipper clipper(lines);
for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) while (clipper.Step())
ImGui::Text("%i The quick brown fox jumps over the lazy dog\n", i); for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++)
clipper.End(); ImGui::Text("%i The quick brown fox jumps over the lazy dog", i);
ImGui::PopStyleVar(); ImGui::PopStyleVar();
break; break;
} }
@ -2452,7 +2461,7 @@ static void ShowExampleAppLongText(bool* p_open)
// Multiple calls to Text(), not clipped (slow) // Multiple calls to Text(), not clipped (slow)
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0,0)); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0,0));
for (int i = 0; i < lines; i++) for (int i = 0; i < lines; i++)
ImGui::Text("%i The quick brown fox jumps over the lazy dog\n", i); ImGui::Text("%i The quick brown fox jumps over the lazy dog", i);
ImGui::PopStyleVar(); ImGui::PopStyleVar();
break; break;
} }