perf(contacts): clip off-screen rows in the Cards and List views

The contacts Cards/List row loop submitted every contact every frame.
It now runs through an ImGuiListClipper, so only the on-screen rows are
built; the fixed step (row height plus the item spacing pushed above)
matches the per-row cursor exactly, so the scroll extent, fade, hover
actions, selection and keyboard navigation are unchanged. Table view is
left unclipped (its rows can wrap).

Verified on DX11 (Windows) at normal and large window sizes and at
font scale 1.5, in both Cards and List views.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-10-14 16:55:07 -05:00
co-authored by Claude Opus 4.8
parent 3f85953452
commit 4d95ef68f6
+10 -2
View File
@@ -1310,8 +1310,15 @@ void RenderContactsTab(App* app)
const float lblSz = lblF->LegacySize * sc; // scaled label + address text
const float adrSz = adrF->LegacySize * sc;
bool rowDeleteRequested = false; // per-row delete is deferred until after the loop
for (size_t vi = 0; vi < visibleRows.size(); ++vi) {
size_t i = visibleRows[vi];
// C183(c): clip the Cards/List rows. Fixed step = the Selectable height (rowH) plus the
// ItemSpacing.y pushed above (8*dp card mode, 0 list mode); each row restores the cursor to
// `afterRow`, so the step is exact and the scroll extent is unchanged. (Table mode is NOT
// clipped -- its rows can wrap.)
ImGuiListClipper rowClipper;
rowClipper.Begin(static_cast<int>(visibleRows.size()), rowH + (asCard ? 8.0f * dp : 0.0f));
while (rowClipper.Step()) {
for (int vi = rowClipper.DisplayStart; vi < rowClipper.DisplayEnd; ++vi) {
size_t i = visibleRows[static_cast<size_t>(vi)];
const auto& entry = book.entries()[i];
ImGui::PushID(static_cast<int>(i));
const bool selected = (s_selected_index == static_cast<int>(i));
@@ -1424,6 +1431,7 @@ void RenderContactsTab(App* app)
ImGui::SetCursorScreenPos(afterRow); // undo the action buttons' cursor moves
ImGui::PopID();
}
} // while (rowClipper.Step())
// Run the deferred per-row delete now that the loop over book.entries() has finished
// (doDelete's two-stage confirm arms on the first click and removes on the second).
if (rowDeleteRequested) doDelete();