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

Enhanced ImGuiListClipper for drag&drop operations #3841

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 117 additions & 34 deletions imgui.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2213,6 +2213,42 @@ void ImGui::CalcListClipping(int items_count, float items_height, int* out_items
*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)
{
// Set cursor position and a few other things so that SetScrollHereY() and Columns() can work when seeking cursor.
Expand Down Expand Up @@ -2279,7 +2315,27 @@ void ImGuiListClipper::End()
if (ItemsCount < INT_MAX && DisplayStart >= 0)
SetCursorPosYAndSetupForPrevLine(StartPosY + (ItemsCount - ItemsFrozen) * ItemsHeight, ItemsHeight);
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()
Expand All @@ -2298,6 +2354,8 @@ bool ImGuiListClipper::Step()
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)
if (StepNo == 0)
{
Expand All @@ -2314,22 +2372,24 @@ bool ImGuiListClipper::Step()
StartPosY = window->DC.CursorPos.y;
if (ItemsHeight <= 0.0f)
{
// Submit the first item so we can measure its height (generally it is 0..1)
DisplayStart = ItemsFrozen;
DisplayEnd = ItemsFrozen + 1;
// Submit the first item (or range) so we can measure its height (generally it is 0..1)
RangeStart[RangeCount] = ItemsFrozen;
RangeEnd[RangeCount] = ItemsFrozen + 1;
if (++RangeCount > 1)
RangeCount = SortAndFuseRanges(RangeStart, RangeEnd, RangeCount);
DisplayStart = ImMax(RangeStart[0], ItemsFrozen);
DisplayEnd = ImMin(RangeEnd[0], ItemsCount);
StepNo = 1;
return true;
}

// Already has item height (given by user in Begin): skip to calculating step
DisplayStart = DisplayEnd;
StepNo = 2;
calc_clipping = true; // If on the first step with known item height, calculate clipping.
}

// Step 1: the clipper infer height from first element
if (StepNo == 1)
// Step 1: Let the clipper infer height from first range
if (ItemsHeight <= 0.0f)
{
IM_ASSERT(ItemsHeight <= 0.0f);
IM_ASSERT(StepNo == 1);
if (table)
{
const float pos_y1 = table->RowPosY1; // Using this instead of StartPosY to handle clipper straddling the frozen row
Expand All @@ -2339,49 +2399,72 @@ bool ImGuiListClipper::Step()
}
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!");
StepNo = 2;
}

// Reached end of list
if (DisplayEnd >= ItemsCount)
{
End();
return false;
calc_clipping = true; // If item height had to be calculated, calculate clipping afterwards.
}

// Step 2: calculate the actual range of elements to display, and position the cursor before the first element
if (StepNo == 2)
// Step 0 or 1: Calculate the actual range of visible elements.
if (calc_clipping)
{
IM_ASSERT(ItemsHeight > 0.0f);

int already_submitted = DisplayEnd;
ImGui::CalcListClipping(ItemsCount - already_submitted, ItemsHeight, &DisplayStart, &DisplayEnd);
DisplayStart += already_submitted;
DisplayEnd += already_submitted;
ImGui::CalcListClipping(ItemsCount - already_submitted, ItemsHeight, &RangeStart[RangeCount], &RangeEnd[RangeCount]);

// 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
if (DisplayStart > already_submitted)
SetCursorPosYAndSetupForPrevLine(StartPosY + (DisplayStart - ItemsFrozen) * ItemsHeight, ItemsHeight);

StepNo = 3;
StepNo++;
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.
if (StepNo == 3)
{
// Seek cursor
if (ItemsCount < INT_MAX)
SetCursorPosYAndSetupForPrevLine(StartPosY + (ItemsCount - ItemsFrozen) * ItemsHeight, ItemsHeight); // advance cursor
ItemsCount = -1;
return false;
}
if (ItemsCount < INT_MAX)
SetCursorPosYAndSetupForPrevLine(StartPosY + (ItemsCount - ItemsFrozen) * ItemsHeight, ItemsHeight); // advance cursor
ItemsCount = -1;

IM_ASSERT(0);
return false;
}

Expand Down
10 changes: 10 additions & 0 deletions imgui.h
Original file line number Diff line number Diff line change
Expand Up @@ -2143,6 +2143,7 @@ struct ImGuiStorage
// Usage:
// ImGuiListClipper clipper;
// 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())
// for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++)
// ImGui::Text("line number %d", i);
Expand All @@ -2159,6 +2160,12 @@ struct ImGuiListClipper

// [Internal]
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 ItemsFrozen;
float ItemsHeight;
Expand All @@ -2171,6 +2178,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().
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 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.

#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
Expand Down