1
0
mirror of https://github.com/ocornut/imgui.git synced 2024-11-28 09:30:56 +01:00

Clipper: bunch of rework. (#3841, #1725)

- Focused/NavId now always included in display range.
- Any number of steps (while preserving zero-alloc policy).
- Non contiguous ranges for nav processing
- Moved new fields internally (+ moved StepNo away from sight so it doesn't get missused).
- Generally tweaks/refactors.
This commit is contained in:
ocornut 2021-10-11 17:19:20 +02:00
parent cd1b5f7883
commit 93cccd27f6
4 changed files with 197 additions and 150 deletions

View File

@ -41,7 +41,12 @@ Other Changes:
- Added IsMouseTripleClicked() function. Tracking multi-click count in IO structure. (#3229) [@kudaba] - Added IsMouseTripleClicked() function. Tracking multi-click count in IO structure. (#3229) [@kudaba]
- Modals: fixed issue hovering popups inside a child inside a modal. (#4676, #4527) - Modals: fixed issue hovering popups inside a child inside a modal. (#4676, #4527)
- Fixed IsWindowFocused()/IsWindowHovered() issues with childs inside popups. (#4676) - Fixed IsWindowFocused()/IsWindowHovered() issues with childs inside popups. (#4676)
- Clipper: Fixed content height declaration slightly mismatching the one of when not using a clipper. - Clipper: currently focused item is automatically included in clipper range.
Fixes issue where e.g. drag and dropping an item and scrolling ensure the item source location is
still submitted. (#3841, #1725) [@GamingMinds-DanielC, @ocornut]
- Clipper: rework so gamepad/keyboard navigation doesn't create spikes in number of items requested
by the clipper to display. (#3841)
- Clipper: Fixed content height declaration slightly mismatching the value of when not using a clipper.
(an additional ItemSpacing.y was declared, affecting scrollbar range). (an additional ItemSpacing.y was declared, affecting scrollbar range).
- Nav: pressing PageUp/PageDown/Home/End when in Menu layer automatically moves back to Main layer. - Nav: pressing PageUp/PageDown/Home/End when in Menu layer automatically moves back to Main layer.
- Nav: fixed resizing window from borders setting navigation to Menu layer. - Nav: fixed resizing window from borders setting navigation to Menu layer.

247
imgui.cpp
View File

@ -2248,6 +2248,8 @@ static bool GetSkipItemForListClipping()
// Helper to calculate coarse clipping of large list of evenly sized items. // Helper to calculate coarse clipping of large list of evenly sized items.
// NB: Prefer using the ImGuiListClipper higher-level helper if you can! Read comments and instructions there on how those use this sort of pattern. // NB: Prefer using the ImGuiListClipper higher-level helper if you can! Read comments and instructions there on how those use this sort of pattern.
// NB: 'items_count' is only used to clamp the result, if you don't know your count you can use INT_MAX // NB: 'items_count' is only used to clamp the result, if you don't know your count you can use INT_MAX
// FIXME: This legacy API is not ideal because it assume we will return a single contiguous rectangle.
// Prefer using ImGuiListClipper which returns disconnected ranges.
void ImGui::CalcListClipping(int items_count, float items_height, int* out_items_display_start, int* out_items_display_end) void ImGui::CalcListClipping(int items_count, float items_height, int* out_items_display_start, int* out_items_display_end)
{ {
ImGuiContext& g = *GImGui; ImGuiContext& g = *GImGui;
@ -2266,15 +2268,16 @@ void ImGui::CalcListClipping(int items_count, float items_height, int* out_items
} }
// We create the union of the ClipRect and the scoring rect which at worst should be 1 page away from ClipRect // We create the union of the ClipRect and the scoring rect which at worst should be 1 page away from ClipRect
ImRect unclipped_rect = window->ClipRect; // We don't include g.NavId's rectangle in there (unless g.NavJustMovedToId is set) because the rectangle enlargement can get costly.
ImRect rect = window->ClipRect;
if (g.NavMoveScoringItems) if (g.NavMoveScoringItems)
unclipped_rect.Add(g.NavScoringRect); rect.Add(g.NavScoringNoClipRect);
if (g.NavJustMovedToId && window->NavLastIds[0] == g.NavJustMovedToId) if (g.NavJustMovedToId && window->NavLastIds[0] == g.NavJustMovedToId)
unclipped_rect.Add(WindowRectRelToAbs(window, window->NavRectRel[0])); // Could store and use NavJustMovedToRectRel rect.Add(WindowRectRelToAbs(window, window->NavRectRel[0])); // Could store and use NavJustMovedToRectRel
const ImVec2 pos = window->DC.CursorPos; const ImVec2 pos = window->DC.CursorPos;
int start = (int)((unclipped_rect.Min.y - pos.y) / items_height); int start = (int)((rect.Min.y - pos.y) / items_height);
int end = (int)((unclipped_rect.Max.y - pos.y) / items_height); int end = (int)((rect.Max.y - pos.y) / items_height);
// When performing a navigation request, ensure we have one item extra in the direction we are moving to // When performing a navigation request, ensure we have one item extra in the direction we are moving to
if (g.NavMoveScoringItems && g.NavMoveClipDir == ImGuiDir_Up) if (g.NavMoveScoringItems && g.NavMoveClipDir == ImGuiDir_Up)
@ -2288,47 +2291,35 @@ 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) static void ImGuiListClipper_SortAndFuseRanges(ImVector<ImGuiListClipperRange>& ranges, int offset = 0)
{ {
// Helper to order ranges and fuse them together if possible. if (ranges.Size - offset <= 1)
// First sort both rangeStart and rangeEnd by rangeStart. Since this helper will just sort 2 or 3 entries, a bubble sort will do fine. return;
for (int sort_end = range_count - 1; sort_end > 0; --sort_end)
{ // Helper to order ranges and fuse them together if possible (bubble sort is fine as we are only sorting 2-3 entries)
for (int i = 0; i < sort_end; ++i) for (int sort_end = ranges.Size - offset - 1; sort_end > 0; --sort_end)
{ for (int i = offset; i < sort_end + offset; ++i)
if (range_start[i] > range_start[i + 1]) if (ranges[i].Min > ranges[i + 1].Min)
{ ImSwap(ranges[i], ranges[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. // Now fuse ranges together as much as possible.
for (int i = 1; i < range_count;) for (int i = 1 + offset; i < ranges.Size; i++)
{ {
if (range_end[i - 1] >= range_start[i]) IM_ASSERT(!ranges[i].PosToIndexConvert && !ranges[i - 1].PosToIndexConvert);
{ if (ranges[i - 1].Max < ranges[i].Min)
range_end[i - 1] = ImMax(range_end[i - 1], range_end[i]); continue;
range_count--; ranges[i - 1].Min = ImMin(ranges[i - 1].Min, ranges[i].Min);
for (int j = i; j < range_count; ++j) ranges[i - 1].Max = ImMax(ranges[i - 1].Max, ranges[i].Max);
{ ranges.erase(ranges.Data + i);
range_start[j] = range_start[j + 1]; i--;
range_end[j] = range_end[j + 1];
} }
} }
else
i++;
}
return range_count; static void ImGuiListClipper_SeekCursorAndSetupPrevLine(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.
// FIXME: It is problematic that we have to do that here, because custom/equivalent end-user code would stumble on the same issue. // FIXME: It is problematic that we have to do that here, because custom/equivalent end-user code would stumble on the same issue.
// The clipper should probably have a 4th step to display the last item in a regular manner. // The clipper should probably have a final step to display the last item in a regular manner, maybe with an opt-out flag for data sets which may have costly seek?
ImGuiContext& g = *GImGui; ImGuiContext& g = *GImGui;
ImGuiWindow* window = g.CurrentWindow; ImGuiWindow* window = g.CurrentWindow;
float off_y = pos_y - window->DC.CursorPos.y; float off_y = pos_y - window->DC.CursorPos.y;
@ -2349,6 +2340,14 @@ static void SetCursorPosYAndSetupForPrevLine(float pos_y, float line_height)
} }
} }
static void ImGuiListClipper_SeekCursorForItem(ImGuiListClipper* clipper, int item_n)
{
// StartPosY starts from ItemsFrozen hence the subtraction
ImGuiListClipperData* data = (ImGuiListClipperData*)clipper->TempData;
float pos_y = clipper->StartPosY + (item_n - data->ItemsFrozen) * clipper->ItemsHeight;
ImGuiListClipper_SeekCursorAndSetupPrevLine(pos_y, clipper->ItemsHeight);
}
ImGuiListClipper::ImGuiListClipper() ImGuiListClipper::ImGuiListClipper()
{ {
memset(this, 0, sizeof(*this)); memset(this, 0, sizeof(*this));
@ -2375,48 +2374,60 @@ void ImGuiListClipper::Begin(int items_count, float items_height)
StartPosY = window->DC.CursorPos.y; StartPosY = window->DC.CursorPos.y;
ItemsHeight = items_height; ItemsHeight = items_height;
ItemsCount = items_count; ItemsCount = items_count;
ItemsFrozen = 0;
StepNo = 0;
DisplayStart = -1; DisplayStart = -1;
DisplayEnd = 0; DisplayEnd = 0;
// Acquire temporary buffer
if (++g.ClipperTempDataStacked > g.ClipperTempData.Size)
g.ClipperTempData.resize(g.ClipperTempDataStacked, ImGuiListClipperData());
ImGuiListClipperData* data = &g.ClipperTempData[g.ClipperTempDataStacked - 1];
data->Reset(this);
TempData = data;
} }
void ImGuiListClipper::End() void ImGuiListClipper::End()
{ {
ImGuiContext& g = *GImGui;
if (ItemsCount < 0) // Already ended if (ItemsCount < 0) // Already ended
return; 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. // In theory here we should assert that we are already at the right position, but it seems saner to just seek at the end and not assert/crash the user.
ImGuiListClipperData* data = (ImGuiListClipperData*)TempData;
if (ItemsCount < INT_MAX && DisplayStart >= 0) if (ItemsCount < INT_MAX && DisplayStart >= 0)
SetCursorPosYAndSetupForPrevLine(StartPosY + (ItemsCount - ItemsFrozen) * ItemsHeight, ItemsHeight); ImGuiListClipper_SeekCursorForItem(this, ItemsCount);
ItemsCount = -1; ItemsCount = -1;
StepNo = RangeCount; data->StepNo = data->Ranges.Size;
// Restore temporary buffer and fix back pointers which may be invalidated when nesting
IM_ASSERT(g.ClipperTempDataStacked > 0);
data = (--g.ClipperTempDataStacked > 0) ? &g.ClipperTempData[g.ClipperTempDataStacked - 1] : NULL;
if (data)
data->ListClipper->TempData = data;
} }
void ImGuiListClipper::ForceDisplayRange(int item_start, int item_end) void ImGuiListClipper::ForceDisplayRangeByIndices(int item_min, int item_max)
{ {
if (DisplayStart < 0 && RangeCount + YRangeCount < 1) // Only allowed after Begin() and if there has not been a specified range yet. ImGuiListClipperData* data = (ImGuiListClipperData*)TempData;
{ IM_ASSERT(DisplayStart < 0); // Only allowed after Begin() and if there has not been a specified range yet.
RangeStart[RangeCount] = item_start; IM_ASSERT(item_min <= item_max);
RangeEnd[RangeCount] = item_end; if (item_min < item_max)
RangeCount++; data->Ranges.push_back(ImGuiListClipperRange::FromIndices(item_min, item_max));
}
} }
void ImGuiListClipper::ForceDisplayYRange(float y_min, float y_max) void ImGuiListClipper::ForceDisplayRangeByPositions(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. ImGuiListClipperData* data = (ImGuiListClipperData*)TempData;
{ IM_ASSERT(DisplayStart < 0); // Only allowed after Begin() and if there has not been a specified range yet.
YRangeMin[YRangeCount] = y_min; IM_ASSERT(y_min <= y_max);
YRangeMax[YRangeCount] = y_max; if (y_min < y_max)
YRangeCount++; data->Ranges.push_back(ImGuiListClipperRange::FromPositions(y_min, y_max, 0, 0));
}
} }
bool ImGuiListClipper::Step() bool ImGuiListClipper::Step()
{ {
ImGuiContext& g = *GImGui; ImGuiContext& g = *GImGui;
ImGuiWindow* window = g.CurrentWindow; ImGuiWindow* window = g.CurrentWindow;
ImGuiListClipperData* data = (ImGuiListClipperData*)TempData;
ImGuiTable* table = g.CurrentTable; ImGuiTable* table = g.CurrentTable;
if (table && table->IsInsideRow) if (table && table->IsInsideRow)
@ -2429,46 +2440,41 @@ 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)
if (StepNo == 0)
{
// While we are in frozen row state, keep displaying items one by one, unclipped // While we are in frozen row state, keep displaying items one by one, unclipped
// FIXME: Could be stored as a table-agnostic state. // FIXME: Could be stored as a table-agnostic state.
if (table != NULL && !table->IsUnfrozenRows) if (data->StepNo == 0 && table != NULL && !table->IsUnfrozenRows)
{ {
DisplayStart = ItemsFrozen; DisplayStart = data->ItemsFrozen;
DisplayEnd = ItemsFrozen + 1; DisplayEnd = data->ItemsFrozen + 1;
ItemsFrozen++; data->ItemsFrozen++;
return true; return true;
} }
// Step 0: Let you process the first element (regardless of it being visible or not, so we can measure the element height)
bool calc_clipping = false;
if (data->StepNo == 0)
{
StartPosY = window->DC.CursorPos.y; StartPosY = window->DC.CursorPos.y;
if (ItemsHeight <= 0.0f) if (ItemsHeight <= 0.0f)
{ {
// Submit the first item (or range) so we can measure its height (generally it is 0..1) // Submit the first item (or range) so we can measure its height (generally the first range is 0..1)
RangeStart[RangeCount] = ItemsFrozen; data->Ranges.push_front(ImGuiListClipperRange::FromIndices(data->ItemsFrozen, data->ItemsFrozen + 1));
RangeEnd[RangeCount] = ItemsFrozen + 1; DisplayStart = ImMax(data->Ranges[0].Min, data->ItemsFrozen);
if (++RangeCount > 1) DisplayEnd = ImMin(data->Ranges[0].Max, ItemsCount);
RangeCount = SortAndFuseRanges(RangeStart, RangeEnd, RangeCount); data->StepNo = 1;
DisplayStart = ImMax(RangeStart[0], ItemsFrozen);
DisplayEnd = ImMin(RangeEnd[0], ItemsCount);
StepNo = 1;
return true; return true;
} }
calc_clipping = true; // If on the first step with known item height, calculate clipping. calc_clipping = true; // If on the first step with known item height, calculate clipping.
} }
// Step 1: Let the clipper infer height from first range // Step 1: Let the clipper infer height from first range
if (ItemsHeight <= 0.0f) if (ItemsHeight <= 0.0f)
{ {
IM_ASSERT(StepNo == 1); IM_ASSERT(data->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 RowPosY1 instead of StartPosY to handle clipper straddling the frozen row
const float pos_y2 = table->RowPosY2; // Using this instead of CursorPos.y to take account of tallest cell. const float pos_y2 = table->RowPosY2; // Using RowPosY2 instead of CursorPos.y to take account of tallest cell.
ItemsHeight = pos_y2 - pos_y1; ItemsHeight = pos_y2 - pos_y1;
window->DC.CursorPos.y = pos_y2; window->DC.CursorPos.y = pos_y2;
} }
@ -2477,67 +2483,64 @@ bool ImGuiListClipper::Step()
ItemsHeight = (window->DC.CursorPos.y - StartPosY) / (float)(DisplayEnd - DisplayStart); 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!");
calc_clipping = true; // If item height had to be calculated, calculate clipping afterwards. calc_clipping = true; // If item height had to be calculated, calculate clipping afterwards.
} }
// Step 0 or 1: Calculate the actual range of visible elements. // Step 0 or 1: Calculate the actual ranges of visible elements.
const int already_submitted = DisplayEnd;
if (calc_clipping) if (calc_clipping)
{ {
IM_ASSERT(ItemsHeight > 0.0f); if (g.LogEnabled)
int already_submitted = DisplayEnd;
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; // If logging is active, do not perform any clipping
RangeEnd[RangeCount] += already_submitted; data->Ranges.push_back(ImGuiListClipperRange::FromIndices(0, ItemsCount));
RangeCount++; }
else
{
// Add range selected to be included for navigation
if (g.NavMoveScoringItems)
data->Ranges.push_back(ImGuiListClipperRange::FromPositions(g.NavScoringNoClipRect.Min.y, g.NavScoringNoClipRect.Max.y, 0, 0));
// Add focused/active item
ImRect nav_rect_abs = ImGui::WindowRectRelToAbs(window, window->NavRectRel[0]);
if (g.NavId != 0 && window->NavLastIds[0] == g.NavId)
data->Ranges.push_back(ImGuiListClipperRange::FromPositions(nav_rect_abs.Min.y, nav_rect_abs.Max.y, 0, 0));
// Add visible range
const int off_min = (g.NavMoveScoringItems && g.NavMoveClipDir == ImGuiDir_Up) ? -1 : 0;
const int off_max = (g.NavMoveScoringItems && g.NavMoveClipDir == ImGuiDir_Down) ? 1 : 0;
data->Ranges.push_back(ImGuiListClipperRange::FromPositions(window->ClipRect.Min.y, window->ClipRect.Max.y, off_min, off_max));
} }
// Convert specified y ranges to item index ranges. // Convert position ranges to item index ranges
for (int i = 0; i < YRangeCount; ++i) // - Very important: when a starting position is after our maximum item, we set Min to (ItemsCount - 1). This allows us to handle most forms of wrapping.
// - Due to how Selectable extra padding they tend to be "unaligned" with exact unit in the item list,
// which with the flooring/ceiling tend to lead to 2 items instead of one being submitted.
for (int i = 0; i < data->Ranges.Size; i++)
if (data->Ranges[i].PosToIndexConvert)
{ {
int start = already_submitted + (int)((YRangeMin[i] - window->DC.CursorPos.y) / ItemsHeight); data->Ranges[i].Min = ImClamp(already_submitted + (int)ImFloor((data->Ranges[i].Min - window->DC.CursorPos.y) / ItemsHeight) + data->Ranges[i].PosToIndexOffsetMin, already_submitted, ItemsCount - 1);
int end = already_submitted + (int)((YRangeMax[i] - window->DC.CursorPos.y) / ItemsHeight) + 1; data->Ranges[i].Max = ImClamp(already_submitted + (int)ImCeil((data->Ranges[i].Max - window->DC.CursorPos.y) / ItemsHeight) + 0 + data->Ranges[i].PosToIndexOffsetMax, data->Ranges[i].Min + 1, ItemsCount);
data->Ranges[i].PosToIndexConvert = false;
start = ImMax(start, already_submitted);
end = ImMin(end, ItemsCount);
if (start < end)
{
RangeStart[RangeCount] = start;
RangeEnd[RangeCount] = end;
RangeCount++;
} }
} ImGuiListClipper_SortAndFuseRanges(data->Ranges, data->StepNo);
// 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. // Step 0+ (if item height is given in advance) or 1+: Display the next range in line.
if (StepNo < RangeCount) if (data->StepNo < data->Ranges.Size)
{ {
int already_submitted = DisplayEnd; DisplayStart = ImMax(data->Ranges[data->StepNo].Min, already_submitted);
DisplayStart = ImMax(RangeStart[StepNo], already_submitted); DisplayEnd = ImMin(data->Ranges[data->StepNo].Max, ItemsCount);
DisplayEnd = ImMin(RangeEnd[StepNo], ItemsCount); if (DisplayStart > already_submitted) //-V1051
ImGuiListClipper_SeekCursorForItem(this, DisplayStart);
// Seek cursor data->StepNo++;
if (DisplayStart > already_submitted)
SetCursorPosYAndSetupForPrevLine(StartPosY + (DisplayStart - ItemsFrozen) * ItemsHeight, ItemsHeight);
StepNo++;
return true; return true;
} }
// After the last step: Let 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 (ItemsCount < INT_MAX) if (ItemsCount < INT_MAX)
SetCursorPosYAndSetupForPrevLine(StartPosY + (ItemsCount - ItemsFrozen) * ItemsHeight, ItemsHeight); // advance cursor ImGuiListClipper_SeekCursorForItem(this, ItemsCount);
ItemsCount = -1; ItemsCount = -1;
return false; return false;
@ -4336,6 +4339,8 @@ void ImGui::Shutdown(ImGuiContext* context)
g.CurrentTabBarStack.clear(); g.CurrentTabBarStack.clear();
g.ShrinkWidthBuffer.clear(); g.ShrinkWidthBuffer.clear();
g.ClipperTempData.clear_destruct();
g.Tables.Clear(); g.Tables.Clear();
g.TablesTempData.clear_destruct(); g.TablesTempData.clear_destruct();
g.DrawChannelsTempMergeBuffer.clear(); g.DrawChannelsTempMergeBuffer.clear();
@ -9605,6 +9610,7 @@ void ImGui::NavUpdateCreateMoveRequest()
if (!IsActiveIdUsingNavDir(ImGuiDir_Down) && (IsNavInputTest(ImGuiNavInput_DpadDown, read_mode) || IsNavInputTest(ImGuiNavInput_KeyDown_, read_mode))) { g.NavMoveDir = ImGuiDir_Down; } if (!IsActiveIdUsingNavDir(ImGuiDir_Down) && (IsNavInputTest(ImGuiNavInput_DpadDown, read_mode) || IsNavInputTest(ImGuiNavInput_KeyDown_, read_mode))) { g.NavMoveDir = ImGuiDir_Down; }
} }
g.NavMoveClipDir = g.NavMoveDir; g.NavMoveClipDir = g.NavMoveDir;
g.NavScoringNoClipRect = ImRect(+FLT_MAX, +FLT_MAX, -FLT_MAX, -FLT_MAX);
} }
// Update PageUp/PageDown/Home/End scroll // Update PageUp/PageDown/Home/End scroll
@ -9613,6 +9619,11 @@ void ImGui::NavUpdateCreateMoveRequest()
float scoring_rect_offset_y = 0.0f; float scoring_rect_offset_y = 0.0f;
if (window && g.NavMoveDir == ImGuiDir_None && nav_keyboard_active) if (window && g.NavMoveDir == ImGuiDir_None && nav_keyboard_active)
scoring_rect_offset_y = NavUpdatePageUpPageDown(); scoring_rect_offset_y = NavUpdatePageUpPageDown();
if (scoring_rect_offset_y != 0.0f)
{
g.NavScoringNoClipRect = window->InnerRect;
g.NavScoringNoClipRect.TranslateY(scoring_rect_offset_y);
}
// [DEBUG] Always send a request // [DEBUG] Always send a request
#if IMGUI_DEBUG_NAV_SCORING #if IMGUI_DEBUG_NAV_SCORING
@ -9666,8 +9677,10 @@ void ImGui::NavUpdateCreateMoveRequest()
scoring_rect.Max.x = scoring_rect.Min.x; scoring_rect.Max.x = scoring_rect.Min.x;
IM_ASSERT(!scoring_rect.IsInverted()); // Ensure if we have a finite, non-inverted bounding box here will allows us to remove extraneous ImFabs() calls in NavScoreItem(). IM_ASSERT(!scoring_rect.IsInverted()); // Ensure if we have a finite, non-inverted bounding box here will allows us to remove extraneous ImFabs() calls in NavScoreItem().
//GetForegroundDrawList()->AddRect(scoring_rect.Min, scoring_rect.Max, IM_COL32(255,200,0,255)); // [DEBUG] //GetForegroundDrawList()->AddRect(scoring_rect.Min, scoring_rect.Max, IM_COL32(255,200,0,255)); // [DEBUG]
//if (!g.NavScoringNoClipRect.IsInverted()) { GetForegroundDrawList()->AddRect(g.NavScoringNoClipRect.Min, g.NavScoringNoClipRect.Max, IM_COL32(255, 200, 0, 255)); } // [DEBUG]
} }
g.NavScoringRect = scoring_rect; g.NavScoringRect = scoring_rect;
g.NavScoringNoClipRect.Add(scoring_rect);
} }
// Apply result from previous frame navigation directional move request. Always called from NavUpdate() // Apply result from previous frame navigation directional move request. Always called from NavUpdate()

47
imgui.h
View File

@ -2172,53 +2172,46 @@ struct ImGuiStorage
}; };
// Helper: Manually clip large list of items. // Helper: Manually clip large list of items.
// If you are submitting lots of evenly spaced items and you have a random access to the list, you can perform coarse // If you have lots evenly spaced items and you have a random access to the list, you can perform coarse
// clipping based on visibility to save yourself from processing those items at all. // clipping based on visibility to only submit items that are in view.
// The clipper calculates the range of visible items and advance the cursor to compensate for the non-visible items we have skipped. // The clipper calculates the range of visible items and advance the cursor to compensate for the non-visible items we have skipped.
// (Dear ImGui already clip items based on their bounds but it needs to measure text size to do so, whereas manual coarse clipping before submission makes this cost and your own data fetching/submission cost almost null) // (Dear ImGui already clip items based on their bounds but: it needs to first layout the item to do so, and generally
// fetching/submitting your own data incurs additional cost. Coarse clipping using ImGuiListClipper allows you to easily
// scale using lists with tens of thousands of items without a problem)
// 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);
// Generally what happens is: // Generally what happens is:
// - Clipper lets you process the first element (DisplayStart = 0, DisplayEnd = 1) regardless of it being visible or not. // - Clipper lets you process the first element (DisplayStart = 0, DisplayEnd = 1) regardless of it being visible or not.
// - User code submit one element. // - User code submit that one element.
// - Clipper can measure the height of the first element // - Clipper can measure the height of the first element
// - Clipper calculate the actual range of elements to display based on the current clipping rectangle, position the cursor before the first visible element. // - Clipper calculate the actual range of elements to display based on the current clipping rectangle, position the cursor before the first visible element.
// - User code submit visible elements. // - User code submit visible elements.
// - The clipper also handles various subtleties related to keyboard/gamepad navigation, wrapping etc.
struct ImGuiListClipper struct ImGuiListClipper
{ {
int DisplayStart; int DisplayStart; // First item to display, updated by each call to Step()
int DisplayEnd; int DisplayEnd; // End of items to display (exclusive)
int ItemsCount; // [Internal] Number of items
// [Internal] float ItemsHeight; // [Internal] Height of item after a first step and item submission can calculate it
int ItemsCount; float StartPosY; // [Internal] Cursor position at the time of Begin() or after table frozen rows are all processed
int RangeStart[4]; // 1 for the user, rest for internal use void* TempData; // [Internal] Internal data
int RangeEnd[4];
int RangeCount;
int YRangeMin[1];
int YRangeMax[1];
int YRangeCount;
int StepNo;
int ItemsFrozen;
float ItemsHeight;
float StartPosY;
IMGUI_API ImGuiListClipper();
IMGUI_API ~ImGuiListClipper();
// items_count: 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_count: 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(). // 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 ImGuiListClipper();
IMGUI_API ~ImGuiListClipper();
IMGUI_API void Begin(int items_count, float items_height = -1.0f);
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.
// Call ForceDisplayRangeXXX functions before first call to Step() if you need a range of items to be displayed regardless of visibility.
IMGUI_API void ForceDisplayRangeByIndices(int item_min, int item_max); // item_max is exclusive e.g. use (42, 42+1) to make item 42 always visible BUT due to alignment/padding of certain items it is likely that an extra item may be included on either end of the display range.
IMGUI_API void ForceDisplayRangeByPositions(float y_min, float y_max); // Absolute coordinates
#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS #ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
inline ImGuiListClipper(int items_count, float items_height = -1.0f) { memset(this, 0, sizeof(*this)); ItemsCount = -1; Begin(items_count, items_height); } // [removed in 1.79] inline ImGuiListClipper(int items_count, float items_height = -1.0f) { memset(this, 0, sizeof(*this)); ItemsCount = -1; Begin(items_count, items_height); } // [removed in 1.79]
#endif #endif

View File

@ -18,6 +18,7 @@ Index of this file:
// [SECTION] Generic helpers // [SECTION] Generic helpers
// [SECTION] ImDrawList support // [SECTION] ImDrawList support
// [SECTION] Widgets support: flags, enums, data structures // [SECTION] Widgets support: flags, enums, data structures
// [SECTION] Clipper support
// [SECTION] Navigation support // [SECTION] Navigation support
// [SECTION] Columns support // [SECTION] Columns support
// [SECTION] Multi-select support // [SECTION] Multi-select support
@ -1162,6 +1163,34 @@ struct ImGuiPtrOrIndex
ImGuiPtrOrIndex(int index) { Ptr = NULL; Index = index; } ImGuiPtrOrIndex(int index) { Ptr = NULL; Index = index; }
}; };
//-----------------------------------------------------------------------------
// [SECTION] Clipper support
//-----------------------------------------------------------------------------
struct ImGuiListClipperRange
{
int Min;
int Max;
bool PosToIndexConvert; // Begin/End are absolute position (will be converted to indices later)
ImS8 PosToIndexOffsetMin; // Add to Min after converting to indices
ImS8 PosToIndexOffsetMax; // Add to Min after converting to indices
static ImGuiListClipperRange FromIndices(int min, int max) { ImGuiListClipperRange r = { min, max, false, 0, 0 }; return r; }
static ImGuiListClipperRange FromPositions(float y1, float y2, int off_min, int off_max) { ImGuiListClipperRange r = { (int)y1, (int)y2, true, (ImS8)off_min, (ImS8)off_max }; return r; }
};
// Temporary clipper data, buffers shared/reused between instances
struct ImGuiListClipperData
{
ImGuiListClipper* ListClipper;
int StepNo;
int ItemsFrozen;
ImVector<ImGuiListClipperRange> Ranges;
ImGuiListClipperData() { memset(this, 0, sizeof(*this)); }
void Reset(ImGuiListClipper* clipper) { ListClipper = clipper; StepNo = ItemsFrozen = 0; Ranges.resize(0); }
};
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
// [SECTION] Navigation support // [SECTION] Navigation support
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
@ -1583,6 +1612,7 @@ struct ImGuiContext
ImGuiDir NavMoveDirForDebug; ImGuiDir NavMoveDirForDebug;
ImGuiDir NavMoveClipDir; // FIXME-NAV: Describe the purpose of this better. Might want to rename? ImGuiDir NavMoveClipDir; // FIXME-NAV: Describe the purpose of this better. Might want to rename?
ImRect NavScoringRect; // Rectangle used for scoring, in screen space. Based of window->NavRectRel[], modified for directional navigation scoring. ImRect NavScoringRect; // Rectangle used for scoring, in screen space. Based of window->NavRectRel[], modified for directional navigation scoring.
ImRect NavScoringNoClipRect; // Some nav operations (such as PageUp/PageDown) enforce a region which clipper will attempt to always keep submitted
int NavScoringDebugCount; // Metrics for debugging int NavScoringDebugCount; // Metrics for debugging
int NavTabbingInputableRemaining; // >0 when counting items for tabbing int NavTabbingInputableRemaining; // >0 when counting items for tabbing
ImGuiNavItemData NavMoveResultLocal; // Best move request candidate within NavWindow ImGuiNavItemData NavMoveResultLocal; // Best move request candidate within NavWindow
@ -1627,6 +1657,10 @@ struct ImGuiContext
ImVector<unsigned char> DragDropPayloadBufHeap; // We don't expose the ImVector<> directly, ImGuiPayload only holds pointer+size ImVector<unsigned char> DragDropPayloadBufHeap; // We don't expose the ImVector<> directly, ImGuiPayload only holds pointer+size
unsigned char DragDropPayloadBufLocal[16]; // Local buffer for small payloads unsigned char DragDropPayloadBufLocal[16]; // Local buffer for small payloads
// Clipper
int ClipperTempDataStacked;
ImVector<ImGuiListClipperData> ClipperTempData;
// Table // Table
ImGuiTable* CurrentTable; ImGuiTable* CurrentTable;
int TablesTempDataStacked; // Temporary table data size (because we leave previous instances undestructed, we generally don't use TablesTempData.Size) int TablesTempDataStacked; // Temporary table data size (because we leave previous instances undestructed, we generally don't use TablesTempData.Size)
@ -1814,6 +1848,8 @@ struct ImGuiContext
DragDropHoldJustPressedId = 0; DragDropHoldJustPressedId = 0;
memset(DragDropPayloadBufLocal, 0, sizeof(DragDropPayloadBufLocal)); memset(DragDropPayloadBufLocal, 0, sizeof(DragDropPayloadBufLocal));
ClipperTempDataStacked = 0;
CurrentTable = NULL; CurrentTable = NULL;
TablesTempDataStacked = 0; TablesTempDataStacked = 0;
CurrentTabBar = NULL; CurrentTabBar = NULL;