From 52c0b1a3406abcbbb9df593b88bb2dd6ffe0290c Mon Sep 17 00:00:00 2001 From: ocornut Date: Thu, 24 Sep 2020 18:08:01 +0200 Subject: [PATCH] ImGuiListClipper: internal rework and tidying up to facilitate supporting frozen rows in tables + stop promoting using constructors parameters. --- imgui.cpp | 114 +++++++++++++++++++++++++++++++--------------- imgui.h | 9 ++-- imgui_demo.cpp | 9 ++-- imgui_widgets.cpp | 3 +- 4 files changed, 91 insertions(+), 44 deletions(-) diff --git a/imgui.cpp b/imgui.cpp index b1a04674..dac97c99 100644 --- a/imgui.cpp +++ b/imgui.cpp @@ -2192,35 +2192,44 @@ static void SetCursorPosYAndSetupForPrevLine(float pos_y, float line_height) columns->LineMinY = window->DC.CursorPos.y; // Setting this so that cell Y position are set properly } -// Use case A: Begin() called from constructor with items_height<0, then called again from Sync() in StepNo 1 +ImGuiListClipper::ImGuiListClipper(int items_count, float items_height) +{ + DisplayStart = DisplayEnd = 0; + ItemsCount = -1; + StepNo = 0; + ItemsHeight = StartPosY = 0.0f; + Begin(items_count, items_height); +} + +ImGuiListClipper::~ImGuiListClipper() +{ + IM_ASSERT(ItemsCount == -1 && "Forgot to call End(), or to Step() until false?"); +} + +// Use case A: Begin() called from constructor with items_height<0, then called again from Step() 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) +void ImGuiListClipper::Begin(int items_count, float items_height) { ImGuiContext& g = *GImGui; ImGuiWindow* window = g.CurrentWindow; StartPosY = window->DC.CursorPos.y; ItemsHeight = items_height; - ItemsCount = count; + ItemsCount = items_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) - SetCursorPosYAndSetupForPrevLine(StartPosY + DisplayStart * ItemsHeight, ItemsHeight); // advance cursor - StepNo = 2; - } + DisplayStart = -1; + DisplayEnd = 0; } void ImGuiListClipper::End() { - if (ItemsCount < 0) + if (ItemsCount < 0) // Already ended return; + // In theory here we should assert that ImGui::GetCursorPosY() == StartPosY + DisplayEnd * ItemsHeight, but it feels saner to just seek at the end and not assert/crash the user. - if (ItemsCount < INT_MAX) - SetCursorPosYAndSetupForPrevLine(StartPosY + ItemsCount * ItemsHeight, ItemsHeight); // advance cursor + if (ItemsCount < INT_MAX && DisplayStart >= 0) + SetCursorPosYAndSetupForPrevLine(StartPosY + ItemsCount * ItemsHeight, ItemsHeight); ItemsCount = -1; StepNo = 3; } @@ -2230,38 +2239,70 @@ bool ImGuiListClipper::Step() ImGuiContext& g = *GImGui; ImGuiWindow* window = g.CurrentWindow; - if (ItemsCount == 0 || window->SkipItems) + // Reached end of list + if (DisplayEnd >= ItemsCount || window->SkipItems) { - ItemsCount = -1; + End(); 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. + + // 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) { - DisplayStart = 0; - DisplayEnd = 1; StartPosY = window->DC.CursorPos.y; - StepNo = 1; - return true; + if (ItemsHeight <= 0.0f) + { + // Submit the first item so we can measure its height (generally it is 0..1) + DisplayStart = 0; + DisplayEnd = 1; + StepNo = 1; + return true; + } + + // Already has item height (given by user in Begin): skip to calculating step + DisplayStart = DisplayEnd; + StepNo = 2; } - 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. + + // Step 1: the clipper infer height from first element + if (StepNo == 1) { - if (ItemsCount == 1) { ItemsCount = -1; return false; } - float items_height = window->DC.CursorPos.y - StartPosY; - IM_ASSERT(items_height > 0.0f); // If this triggers, it means Item 0 hasn't moved the cursor vertically - Begin(ItemsCount - 1, items_height); - DisplayStart++; - DisplayEnd++; - StepNo = 3; - return true; + IM_ASSERT(ItemsHeight <= 0.0f); + ItemsHeight = window->DC.CursorPos.y - StartPosY; + IM_ASSERT(ItemsHeight > 0.0f && "Unable to calculate item height! First item hasn't moved the cursor vertically!"); + StepNo = 2; } - if (StepNo == 2) // Step 2: empty 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 2: calculate the actual range of elements to display, and position the cursor before the first element + if (StepNo == 2) { - IM_ASSERT(DisplayStart >= 0 && DisplayEnd >= 0); + IM_ASSERT(ItemsHeight > 0.0f); + + int already_submitted = DisplayEnd; + ImGui::CalcListClipping(ItemsCount - already_submitted, ItemsHeight, &DisplayStart, &DisplayEnd); + DisplayStart += already_submitted; + DisplayEnd += already_submitted; + + // Seek cursor + if (DisplayStart > already_submitted) + SetCursorPosYAndSetupForPrevLine(StartPosY + DisplayStart * ItemsHeight, ItemsHeight); + 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(); + + // 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. + if (StepNo == 3) + { + // Seek cursor + if (ItemsCount < INT_MAX) + SetCursorPosYAndSetupForPrevLine(StartPosY + ItemsCount * ItemsHeight, ItemsHeight); // advance cursor + ItemsCount = -1; + return false; + } + + IM_ASSERT(0); return false; } @@ -8062,7 +8103,7 @@ ImVec2 ImGui::FindBestWindowPosForPopupEx(const ImVec2& ref_pos, const ImVec2& s // For tooltip we prefer avoiding the cursor at all cost even if it means that part of the tooltip won't be visible. if (policy == ImGuiPopupPositionPolicy_Tooltip) - return ref_pos + ImVec2(2, 2); + return ref_pos + ImVec2(2, 2); // Otherwise try to keep within display ImVec2 pos = ref_pos; @@ -10439,7 +10480,8 @@ void ImGui::ShowMetricsWindow(bool* p_open) NodeDrawCmdShowMeshAndBoundingBox(window, draw_list, pcmd, elem_offset, true, false); // Display individual triangles/vertices. Hover on to get the corresponding triangle highlighted. - ImGuiListClipper clipper(pcmd->ElemCount / 3); // Manually coarse clip our print out of individual vertices to save CPU, only items that may be visible. + ImGuiListClipper clipper; + clipper.Begin(pcmd->ElemCount / 3); // Manually coarse clip our print out of individual vertices to save CPU, only items that may be visible. while (clipper.Step()) for (int prim = clipper.DisplayStart, idx_i = elem_offset + clipper.DisplayStart * 3; prim < clipper.DisplayEnd; prim++) { diff --git a/imgui.h b/imgui.h index 8d0ca495..67516adc 100644 --- a/imgui.h +++ b/imgui.h @@ -1900,7 +1900,8 @@ struct ImGuiStorage // - 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 { - int DisplayStart, DisplayEnd; + int DisplayStart; + int DisplayEnd; int ItemsCount; // [Internal] @@ -1911,12 +1912,12 @@ struct ImGuiListClipper // 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). // items_height: Use -1.0f to be calculated automatically on first step. Otherwise pass in the distance between your items, typically GetTextLineHeightWithSpacing() or GetFrameHeightWithSpacing(). // 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. + ImGuiListClipper(int items_count = -1, float items_height = -1.0f); + ~ImGuiListClipper(); - 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. IMGUI_API void End(); // Automatically called on the last call of Step() that returns false. + IMGUI_API bool Step(); // Call until it returns false. The DisplayStart/DisplayEnd fields will be set and you can process/draw those items. }; // Helpers macros to generate 32-bit encoded colors diff --git a/imgui_demo.cpp b/imgui_demo.cpp index 749d1160..96c0034e 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -3340,7 +3340,8 @@ static void ShowDemoWindowColumns() ImGui::BeginChild("##ScrollingRegion", child_size, false, ImGuiWindowFlags_HorizontalScrollbar); ImGui::Columns(10); int ITEMS_COUNT = 2000; - ImGuiListClipper clipper(ITEMS_COUNT); // Also demonstrate using the clipper for large list + ImGuiListClipper clipper; // Also demonstrate using the clipper for large list + clipper.Begin(ITEMS_COUNT); while (clipper.Step()) { for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) @@ -4330,7 +4331,8 @@ struct ExampleAppConsole // To use the clipper we can replace your standard loop: // for (int i = 0; i < Items.Size; i++) // With: - // ImGuiListClipper clipper(Items.Size); + // ImGuiListClipper clipper; + // clipper.Begin(Items.Size); // while (clipper.Step()) // for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) // - That your items are evenly spaced (same height) @@ -4897,7 +4899,8 @@ static void ShowExampleAppLongText(bool* p_open) { // Multiple calls to Text(), manually coarsely clipped - demonstrate how to use the ImGuiListClipper helper. ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0)); - ImGuiListClipper clipper(lines); + ImGuiListClipper clipper; + clipper.Begin(lines); while (clipper.Step()) for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) ImGui::Text("%i The quick brown fox jumps over the lazy dog", i); diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index fd802737..c5b44f80 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -6154,7 +6154,8 @@ 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. ImGuiContext& g = *GImGui; bool value_changed = false; - ImGuiListClipper clipper(items_count, GetTextLineHeightWithSpacing()); // We know exactly our line height here so we pass it as a minor optimization, but generally you don't need to. + ImGuiListClipper clipper; + clipper.Begin(items_count, GetTextLineHeightWithSpacing()); // We know exactly our line height here so we pass it as a minor optimization, but generally you don't need to. while (clipper.Step()) for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) {