perf(overview): clip the address list to the visible rows

The Overview address list built all N rows on every frame (icons,
labels, balances, buttons, an InvisibleButton and a context-menu
check per address) and allocated a vector of N row positions, however
few rows fit on screen; the edge mask then walked all their vertices.

An ImGuiListClipper now submits only the rows that intersect the
list's clip rect, plus:
- one guard row on each side, so dividers and anti-aliased edges at
  the clip boundary are drawn as before;
- the row being dragged and the row whose context menu is open, so
  drag-to-reorder and the menu keep working when they scroll away;
- the rows keyboard navigation targets (kept by the clipper itself).

Before each row the cursor is put back exactly where the unclipped
loop left it, so the density-scaled spacing, which depends on the
cursor position, and every row's geometry are unchanged. Row tops are
computed from the index, so the per-frame vector is gone, and the
scroll extent is the same. The row model (sort, filter) is still
rebuilt over all addresses each frame.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-10 09:50:03 -05:00
co-authored by Claude Opus 5.5
parent bb973a1007
commit c2c2a8325b
4 changed files with 100 additions and 11 deletions
+37 -11
View File
@@ -142,6 +142,7 @@ void RenderSharedAddressList(App* app, float listH, float availW,
// Copy feedback
static int s_copiedRow = -1;
static float s_copiedTime = 0.0f;
static int s_ctxRow = -1; // row whose context popup is open: always submitted (clipper must not drop it)
// ---- Build and filter address rows ----
std::vector<AddressListInput> rowInputs;
@@ -296,11 +297,11 @@ void RenderSharedAddressList(App* app, float listH, float availW,
ImU32 rowSelectCol = S.resolveColor("var(--row-select)", IM_COL32(255, 255, 255, 20));
ImU32 dividerCol = S.resolveColor("var(--sidebar-divider)", IM_COL32(255, 255, 255, 15));
// Compute Y positions for all rows (pass 1)
std::vector<float> rowY(rows.size());
float cursorStartY = ImGui::GetCursorScreenPos().y;
for (int i = 0; i < (int)rows.size(); ++i)
rowY[i] = cursorStartY + i * rowH;
// Row i's top is a pure function of its index (fixed pitch rowH): no per-frame table.
const float cursorStartY = ImGui::GetCursorScreenPos().y;
const float rowLineX = ImGui::GetCursorScreenPos().x; // every row's x (line start)
const float rowItemSpacingY = ImGui::GetStyle().ItemSpacing.y;
auto rowTop = [cursorStartY, rowH](int i) { return cursorStartY + i * rowH; };
// Drag logic — detect drag start, compute drag position
ImVec2 mousePos = ImGui::GetMousePos();
@@ -344,7 +345,7 @@ void RenderSharedAddressList(App* app, float listH, float availW,
// Reorder — compute insert position from mouseY.
int insertIdx = 0;
for (int i = 0; i < (int)rows.size(); ++i) {
if (mousePos.y > rowY[i] + rowH * 0.5f) insertIdx = i + 1;
if (mousePos.y > rowTop(i) + rowH * 0.5f) insertIdx = i + 1;
}
if (insertIdx != s_dragIdx && insertIdx != s_dragIdx + 1 &&
s_dragIdx < (int)rows.size()) {
@@ -369,14 +370,35 @@ void RenderSharedAddressList(App* app, float listH, float availW,
s_dropMode = 0;
}
// ---- PASS 2: Render rows ----
for (int row_idx = 0; row_idx < (int)rows.size(); row_idx++) {
// ---- PASS 2: Render rows (visible rows only) ----
// ImGuiListClipper submits only rows that intersect the child's clip rect (and, itself, the rows of an
// ImGui nav request / the nav-focused row, so keyboard nav keeps working). Always submitted on top:
// - one guard row on each side of the visible window (a row draws its divider on the next row's top);
// - the row being dragged (its elevation + tooltip follow the mouse; its drag threshold is tested in
// its own body) and the row whose context popup is open (the popup exists only while submitted).
const int rowCount = (int)rows.size();
ImGuiListClipper rowClipper;
rowClipper.Begin(rowCount, rowH);
{
const AddressRowWindow guard = ComputeAddressRowWindow(dl->GetClipRectMin().y, dl->GetClipRectMax().y,
cursorStartY, rowH, rowCount);
rowClipper.IncludeItemsByIndex(guard.begin, guard.end);
}
if (s_dragIdx >= 0 && s_dragIdx < rowCount) rowClipper.IncludeItemByIndex(s_dragIdx);
if (s_ctxRow >= rowCount) s_ctxRow = -1;
if (s_ctxRow >= 0) rowClipper.IncludeItemByIndex(s_ctxRow);
while (rowClipper.Step()) {
for (int row_idx = rowClipper.DisplayStart; row_idx < rowClipper.DisplayEnd; row_idx++) {
// Replay the cursor the unclipped loop had here: the Layout::spacing*() tokens in this body read
// GetContentRegionAvail().y (densityScale), so every row keeps exactly its old geometry.
ImGui::SetCursorScreenPos(ImVec2(rowLineX,
AddressRowEntryCursorY(row_idx, cursorStartY, rowH, rowItemSpacingY)));
const auto& row = rows[row_idx];
const auto& addr = *row.info;
bool isDragged = (s_dragIdx == row_idx && s_dragActive);
// For dragged row, offset Y to follow mouse
float drawY = rowY[row_idx];
float drawY = rowTop(row_idx);
if (isDragged) {
drawY = mousePos.y - s_dragOffsetY;
}
@@ -648,7 +670,7 @@ void RenderSharedAddressList(App* app, float listH, float availW,
// Advance cursor for interaction area
ImGui::PushID(row_idx);
ImGui::SetCursorScreenPos(ImVec2(rowPos.x, rowY[row_idx]));
ImGui::SetCursorScreenPos(ImVec2(rowPos.x, rowTop(row_idx)));
ImGui::InvisibleButton("##addr", ImVec2(innerW, rowH));
if (ImGui::IsItemHovered() && !s_dragActive) {
if (row.isZ) material::Tooltip("%s", addr.address.c_str());
@@ -658,6 +680,7 @@ void RenderSharedAddressList(App* app, float listH, float availW,
// Context menu
const auto& acrTheme = GetCurrentAcrylicTheme();
if (effects::ImGuiAcrylic::BeginAcrylicContextItem("AddressContext", 0, acrTheme.menu)) {
s_ctxRow = row_idx;
if (ImGui::MenuItem(TR("copy_address"))) {
ImGui::SetClipboardText(addr.address.c_str());
s_copiedRow = row_idx;
@@ -729,18 +752,21 @@ void RenderSharedAddressList(App* app, float listH, float availW,
ImGui::EndMenu();
}
effects::ImGuiAcrylic::EndAcrylicPopup();
} else if (s_ctxRow == row_idx) {
s_ctxRow = -1; // its popup closed
}
ImGui::PopID();
// Separator between rows
if (row_idx < (int)rows.size() - 1 && selected_row != row_idx) {
float sepY = rowY[row_idx] + rowH;
float sepY = rowTop(row_idx) + rowH;
dl->AddLine(
ImVec2(rowPos.x + rowPadLeft + rowIconSz * 2.0f, sepY),
ImVec2(rowPos.x + innerW - Layout::spacingLg(), sepY),
dividerCol);
}
}
} // while (rowClipper.Step())
// Advance cursor past all rows
ImGui::SetCursorScreenPos(ImVec2(ImGui::GetCursorScreenPos().x,