2
0
Эх сурвалжийг харах

Clipper: enhanced ImGuiListClipper (#3841)

GamingMinds-DanielC 3 жил өмнө
parent
commit
cd1b5f7883
2 өөрчлөгдсөн 127 нэмэгдсэн , 34 устгасан
  1. 117 34
      imgui.cpp
  2. 10 0
      imgui.h

+ 117 - 34
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;
 }
 

+ 10 - 0
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