Clipper: enhanced ImGuiListClipper (#3841)

This commit is contained in:
GamingMinds-DanielC 2021-10-11 15:57:29 +02:00 committed by ocornut
parent b409df34db
commit cd1b5f7883
2 changed files with 128 additions and 35 deletions

147
imgui.cpp
View File

@ -2288,6 +2288,42 @@ void ImGui::CalcListClipping(int items_count, float items_height, int* out_items
*out_items_display_end = end; *out_items_display_end = end;
} }
static int SortAndFuseRanges(int* range_start, int* range_end, int range_count)
{
// Helper to order ranges and fuse them together if possible.
// First sort both rangeStart and rangeEnd by rangeStart. Since this helper will just sort 2 or 3 entries, a bubble sort will do fine.
for (int sort_end = range_count - 1; sort_end > 0; --sort_end)
{
for (int i = 0; i < sort_end; ++i)
{
if (range_start[i] > range_start[i + 1])
{
ImSwap(range_start[i], range_start[i + 1]);
ImSwap(range_end[i], range_end[i + 1]);
}
}
}
// Now fuse ranges together as much as possible.
for (int i = 1; i < range_count;)
{
if (range_end[i - 1] >= range_start[i])
{
range_end[i - 1] = ImMax(range_end[i - 1], range_end[i]);
range_count--;
for (int j = i; j < range_count; ++j)
{
range_start[j] = range_start[j + 1];
range_end[j] = range_end[j + 1];
}
}
else
i++;
}
return range_count;
}
static void SetCursorPosYAndSetupForPrevLine(float pos_y, float line_height) static void SetCursorPosYAndSetupForPrevLine(float pos_y, float line_height)
{ {
// Set cursor position and a few other things so that SetScrollHereY() and Columns() can work when seeking cursor. // Set cursor position and a few other things so that SetScrollHereY() and Columns() can work when seeking cursor.
@ -2354,7 +2390,27 @@ void ImGuiListClipper::End()
if (ItemsCount < INT_MAX && DisplayStart >= 0) if (ItemsCount < INT_MAX && DisplayStart >= 0)
SetCursorPosYAndSetupForPrevLine(StartPosY + (ItemsCount - ItemsFrozen) * ItemsHeight, ItemsHeight); SetCursorPosYAndSetupForPrevLine(StartPosY + (ItemsCount - ItemsFrozen) * ItemsHeight, ItemsHeight);
ItemsCount = -1; ItemsCount = -1;
StepNo = 3; StepNo = RangeCount;
}
void ImGuiListClipper::ForceDisplayRange(int item_start, int item_end)
{
if (DisplayStart < 0 && RangeCount + YRangeCount < 1) // Only allowed after Begin() and if there has not been a specified range yet.
{
RangeStart[RangeCount] = item_start;
RangeEnd[RangeCount] = item_end;
RangeCount++;
}
}
void ImGuiListClipper::ForceDisplayYRange(float y_min, float y_max)
{
if (DisplayStart < 0 && RangeCount + YRangeCount < 1) // Only allowed after Begin() and if there has not been a specified range yet.
{
YRangeMin[YRangeCount] = y_min;
YRangeMax[YRangeCount] = y_max;
YRangeCount++;
}
} }
bool ImGuiListClipper::Step() bool ImGuiListClipper::Step()
@ -2373,6 +2429,8 @@ bool ImGuiListClipper::Step()
return false; return false;
} }
bool calc_clipping = false;
// Step 0: Let you process the first element (regardless of it being visible or not, so we can measure the element height) // Step 0: Let you process the first element (regardless of it being visible or not, so we can measure the element height)
if (StepNo == 0) if (StepNo == 0)
{ {
@ -2389,22 +2447,24 @@ bool ImGuiListClipper::Step()
StartPosY = window->DC.CursorPos.y; StartPosY = window->DC.CursorPos.y;
if (ItemsHeight <= 0.0f) if (ItemsHeight <= 0.0f)
{ {
// Submit the first item so we can measure its height (generally it is 0..1) // Submit the first item (or range) so we can measure its height (generally it is 0..1)
DisplayStart = ItemsFrozen; RangeStart[RangeCount] = ItemsFrozen;
DisplayEnd = ItemsFrozen + 1; RangeEnd[RangeCount] = ItemsFrozen + 1;
if (++RangeCount > 1)
RangeCount = SortAndFuseRanges(RangeStart, RangeEnd, RangeCount);
DisplayStart = ImMax(RangeStart[0], ItemsFrozen);
DisplayEnd = ImMin(RangeEnd[0], ItemsCount);
StepNo = 1; StepNo = 1;
return true; return true;
} }
// Already has item height (given by user in Begin): skip to calculating step calc_clipping = true; // If on the first step with known item height, calculate clipping.
DisplayStart = DisplayEnd;
StepNo = 2;
} }
// Step 1: the clipper infer height from first element // Step 1: Let the clipper infer height from first range
if (StepNo == 1) if (ItemsHeight <= 0.0f)
{ {
IM_ASSERT(ItemsHeight <= 0.0f); IM_ASSERT(StepNo == 1);
if (table) if (table)
{ {
const float pos_y1 = table->RowPosY1; // Using this instead of StartPosY to handle clipper straddling the frozen row const float pos_y1 = table->RowPosY1; // Using this instead of StartPosY to handle clipper straddling the frozen row
@ -2414,49 +2474,72 @@ bool ImGuiListClipper::Step()
} }
else else
{ {
ItemsHeight = window->DC.CursorPos.y - StartPosY; ItemsHeight = (window->DC.CursorPos.y - StartPosY) / (float)(DisplayEnd - DisplayStart);
} }
IM_ASSERT(ItemsHeight > 0.0f && "Unable to calculate item height! First item hasn't moved the cursor vertically!"); IM_ASSERT(ItemsHeight > 0.0f && "Unable to calculate item height! First item hasn't moved the cursor vertically!");
StepNo = 2;
calc_clipping = true; // If item height had to be calculated, calculate clipping afterwards.
} }
// Reached end of list // Step 0 or 1: Calculate the actual range of visible elements.
if (DisplayEnd >= ItemsCount) if (calc_clipping)
{
End();
return false;
}
// Step 2: calculate the actual range of elements to display, and position the cursor before the first element
if (StepNo == 2)
{ {
IM_ASSERT(ItemsHeight > 0.0f); IM_ASSERT(ItemsHeight > 0.0f);
int already_submitted = DisplayEnd; int already_submitted = DisplayEnd;
ImGui::CalcListClipping(ItemsCount - already_submitted, ItemsHeight, &DisplayStart, &DisplayEnd); ImGui::CalcListClipping(ItemsCount - already_submitted, ItemsHeight, &RangeStart[RangeCount], &RangeEnd[RangeCount]);
DisplayStart += already_submitted;
DisplayEnd += already_submitted; // Only add another range if it hasn't been handled by the initial range.
if (RangeStart[RangeCount] < RangeEnd[RangeCount])
{
RangeStart[RangeCount] += already_submitted;
RangeEnd[RangeCount] += already_submitted;
RangeCount++;
}
// Convert specified y ranges to item index ranges.
for (int i = 0; i < YRangeCount; ++i)
{
int start = already_submitted + (int)((YRangeMin[i] - window->DC.CursorPos.y) / ItemsHeight);
int end = already_submitted + (int)((YRangeMax[i] - window->DC.CursorPos.y) / ItemsHeight) + 1;
start = ImMax(start, already_submitted);
end = ImMin(end, ItemsCount);
if (start < end)
{
RangeStart[RangeCount] = start;
RangeEnd[RangeCount] = end;
RangeCount++;
}
}
// Try to sort and fuse only if there is more than 1 range remaining.
if (RangeCount > StepNo + 1)
RangeCount = StepNo + SortAndFuseRanges(&RangeStart[StepNo], &RangeEnd[StepNo], RangeCount - StepNo);
}
// Step 0+ (if item height is given in advance) or 1+: Display the next range in line.
if (StepNo < RangeCount)
{
int already_submitted = DisplayEnd;
DisplayStart = ImMax(RangeStart[StepNo], already_submitted);
DisplayEnd = ImMin(RangeEnd[StepNo], ItemsCount);
// Seek cursor // Seek cursor
if (DisplayStart > already_submitted) if (DisplayStart > already_submitted)
SetCursorPosYAndSetupForPrevLine(StartPosY + (DisplayStart - ItemsFrozen) * ItemsHeight, ItemsHeight); SetCursorPosYAndSetupForPrevLine(StartPosY + (DisplayStart - ItemsFrozen) * ItemsHeight, ItemsHeight);
StepNo = 3; StepNo++;
return true; return true;
} }
// Step 3: the clipper validate that we have reached the expected Y position (corresponding to element DisplayEnd), // After the last step: Let 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. // Advance the cursor to the end of the list and then returns 'false' to end the loop.
if (StepNo == 3)
{
// Seek cursor
if (ItemsCount < INT_MAX) if (ItemsCount < INT_MAX)
SetCursorPosYAndSetupForPrevLine(StartPosY + (ItemsCount - ItemsFrozen) * ItemsHeight, ItemsHeight); // advance cursor SetCursorPosYAndSetupForPrevLine(StartPosY + (ItemsCount - ItemsFrozen) * ItemsHeight, ItemsHeight); // advance cursor
ItemsCount = -1; ItemsCount = -1;
return false;
}
IM_ASSERT(0);
return false; return false;
} }

10
imgui.h
View File

@ -2179,6 +2179,7 @@ struct ImGuiStorage
// Usage: // Usage:
// ImGuiListClipper clipper; // ImGuiListClipper clipper;
// clipper.Begin(1000); // We have 1000 elements, evenly spaced. // clipper.Begin(1000); // We have 1000 elements, evenly spaced.
// clipper.ForceDisplay(42); // Optional, force element with given index to be displayed (use f.e. if you need to update a tooltip for a drag&drop source)
// while (clipper.Step()) // while (clipper.Step())
// for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) // for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++)
// ImGui::Text("line number %d", i); // ImGui::Text("line number %d", i);
@ -2195,6 +2196,12 @@ struct ImGuiListClipper
// [Internal] // [Internal]
int ItemsCount; int ItemsCount;
int RangeStart[4]; // 1 for the user, rest for internal use
int RangeEnd[4];
int RangeCount;
int YRangeMin[1];
int YRangeMax[1];
int YRangeCount;
int StepNo; int StepNo;
int ItemsFrozen; int ItemsFrozen;
float ItemsHeight; float ItemsHeight;
@ -2207,6 +2214,9 @@ struct ImGuiListClipper
// items_height: Use -1.0f to be calculated automatically on first step. Otherwise pass in the distance between your items, typically GetTextLineHeightWithSpacing() or GetFrameHeightWithSpacing(). // items_height: Use -1.0f to be calculated automatically on first step. Otherwise pass in the distance between your items, typically GetTextLineHeightWithSpacing() or GetFrameHeightWithSpacing().
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. 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.
IMGUI_API void End(); // Automatically called on the last call of Step() that returns false. IMGUI_API void End(); // Automatically called on the last call of Step() that returns false.
IMGUI_API void ForceDisplayRange(int item_start, int item_end); // Optionally call before the first call to Step() if you need a range of items to be displayed regardless of visibility.
inline void ForceDisplay(int item_start, int item_count = 1) { ForceDisplayRange(item_start, item_start + item_count); } // Like ForceDisplayRange, but with a number instead of an end index.
IMGUI_API void ForceDisplayYRange(float y_min, float y_max); // Like ForceDisplayRange, but with y coordinates instead of item indices.
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 bool Step(); // Call until it returns false. The DisplayStart/DisplayEnd fields will be set and you can process/draw those items.
#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS #ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS