diff --git a/imgui.cpp b/imgui.cpp index 8f9a4f0c..d77c8ff3 100644 --- a/imgui.cpp +++ b/imgui.cpp @@ -2288,6 +2288,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. @@ -2354,7 +2390,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() @@ -2373,6 +2429,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) { @@ -2389,22 +2447,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 @@ -2414,49 +2474,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; } diff --git a/imgui.h b/imgui.h index a95c814f..132375e6 100644 --- a/imgui.h +++ b/imgui.h @@ -2179,6 +2179,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); @@ -2195,6 +2196,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; @@ -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(). 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