diff --git a/src/ui/windows/contacts_tab.cpp b/src/ui/windows/contacts_tab.cpp index 89b8926..90ba1d2 100644 --- a/src/ui/windows/contacts_tab.cpp +++ b/src/ui/windows/contacts_tab.cpp @@ -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(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(vi)]; const auto& entry = book.entries()[i]; ImGui::PushID(static_cast(i)); const bool selected = (s_selected_index == static_cast(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();