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
+28
View File
@@ -6452,6 +6452,33 @@ void testScrollEdgeFade()
}
}
void testAddressRowWindow()
{
using namespace dragonx::ui;
auto w = ComputeAddressRowWindow(100.0f, 300.0f, 100.0f, 50.0f, 10);
EXPECT_EQ(w.begin, 0); EXPECT_EQ(w.end, 5); // rows 0-3 visible + guard 4 (its top is the clip edge)
w = ComputeAddressRowWindow(175.0f, 260.0f, 100.0f, 50.0f, 10);
EXPECT_EQ(w.begin, 0); EXPECT_EQ(w.end, 5); // rows 1-3 visible + guards 0 and 4
w = ComputeAddressRowWindow(420.0f, 520.0f, 100.0f, 50.0f, 20);
EXPECT_EQ(w.begin, 5); EXPECT_EQ(w.end, 10); // rows 6-8 visible + guards 5 and 9
w = ComputeAddressRowWindow(90.0f, 200.0f, 100.0f, 50.0f, 10);
EXPECT_EQ(w.begin, 0); EXPECT_EQ(w.end, 3); // list starts below the clip top
w = ComputeAddressRowWindow(100.0f, 300.0f, 100.0f, 50.0f, 3);
EXPECT_EQ(w.begin, 0); EXPECT_EQ(w.end, 3); // clamped to the row count
w = ComputeAddressRowWindow(900.0f, 1000.0f, 100.0f, 50.0f, 5);
EXPECT_EQ(w.begin, 5); EXPECT_EQ(w.end, 5); // nothing visible: empty (IncludeItemsByIndex no-op)
w = ComputeAddressRowWindow(100.0f, 300.0f, 100.0f, 50.0f, 0);
EXPECT_EQ(w.begin, 0); EXPECT_EQ(w.end, 0);
w = ComputeAddressRowWindow(std::numeric_limits<float>::quiet_NaN(), 300.0f, 100.0f, 50.0f, 7);
EXPECT_EQ(w.begin, 0); EXPECT_EQ(w.end, 7); // non-finite: fail open (every row)
w = ComputeAddressRowWindow(100.0f, 300.0f, 100.0f, 0.0f, 4);
EXPECT_EQ(w.begin, 0); EXPECT_EQ(w.end, 4); // degenerate pitch: every row
// Row entry cursor = where ImGui::ItemSize left it after the previous row's InvisibleButton.
EXPECT_NEAR(AddressRowEntryCursorY(0, 100.25f, 60.5f, 4.0f), 100.25, 0.0);
EXPECT_NEAR(AddressRowEntryCursorY(1, 100.25f, 60.5f, 4.0f), 164.0, 0.0); // trunc(100.25+60.5+4)
EXPECT_NEAR(AddressRowEntryCursorY(3, 100.25f, 60.5f, 4.0f), 285.0, 0.0); // trunc(221.25+60.5+4)
}
void testMiningBenchmarkModel()
{
dragonx::ui::ThreadBenchmark benchmark;
@@ -14783,6 +14810,7 @@ int main()
testMiningEarningsCache();
testMiningEarningsMidnightRollover();
testScrollEdgeFade();
testAddressRowWindow();
testBalanceAddressListModel();
testExplorerBlockCache();
testTransactionHistoryCache();