diff --git a/src/ui/windows/balance_address_list.cpp b/src/ui/windows/balance_address_list.cpp index 937baec..07d047b 100644 --- a/src/ui/windows/balance_address_list.cpp +++ b/src/ui/windows/balance_address_list.cpp @@ -2,6 +2,7 @@ #include #include +#include #include namespace dragonx { @@ -106,5 +107,26 @@ std::string FormatAddressUsdValue(double balance, double priceUsd) return buffer; } +AddressRowWindow ComputeAddressRowWindow(float clipMinY, float clipMaxY, float startY, float rowH, int rowCount) +{ + AddressRowWindow w; + if (rowCount <= 0) return w; + w.end = rowCount; // fail open (every row) on degenerate input + if (!(rowH > 0.0f)) return w; + const double first = std::floor(((double)clipMinY - startY) / rowH) - 1.0; + const double last = std::ceil(((double)clipMaxY - startY) / rowH) + 1.0; // exclusive + if (!std::isfinite(first) || !std::isfinite(last)) return w; + w.begin = (int)std::clamp(first, 0.0, (double)rowCount); + w.end = (int)std::clamp(last, (double)w.begin, (double)rowCount); + return w; +} + +float AddressRowEntryCursorY(int i, float startY, float rowH, float itemSpacingY) +{ + if (i <= 0) return startY; + const float prevTop = startY + (i - 1) * rowH; // the row loop's row-top expression + return (float)(int)(prevTop + rowH + itemSpacingY); // ImGui::ItemSize: IM_TRUNC(line_y1 + line_height + ItemSpacing.y) +} + } // namespace ui } // namespace dragonx diff --git a/src/ui/windows/balance_address_list.h b/src/ui/windows/balance_address_list.h index 4234e96..034a492 100644 --- a/src/ui/windows/balance_address_list.h +++ b/src/ui/windows/balance_address_list.h @@ -63,5 +63,18 @@ AddressRowLayout ComputeAddressRowLayout(float rowX, float spacingXs); std::string FormatAddressUsdValue(double balance, double priceUsd); +// ---- Overview address-list clipping (fixed row pitch rowH from startY) ---- +// [begin, end) = the rows intersecting [clipMinY, clipMaxY) plus ONE guard row on each side (a row draws its +// divider / AA fringe just past its own slot), clamped to [0, rowCount). Non-finite input -> every row. +struct AddressRowWindow { + int begin = 0; + int end = 0; +}; +AddressRowWindow ComputeAddressRowWindow(float clipMinY, float clipMaxY, float startY, float rowH, int rowCount); +// Cursor y the unclipped row loop had when row i's body ran: row 0 = startY; row i > 0 = where ImGui::ItemSize +// (imgui 1.92.6) leaves it after row i-1's InvisibleButton(h = rowH) placed at startY + (i-1)*rowH, i.e. +// IM_TRUNC(top + rowH + ItemSpacing.y). Replaying it keeps the cursor-dependent Layout::spacing*() exact. +float AddressRowEntryCursorY(int i, float startY, float rowH, float itemSpacingY); + } // namespace ui } // namespace dragonx diff --git a/src/ui/windows/balance_components.cpp b/src/ui/windows/balance_components.cpp index 9bc115d..7ce407d 100644 --- a/src/ui/windows/balance_components.cpp +++ b/src/ui/windows/balance_components.cpp @@ -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 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 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, diff --git a/tests/test_phase4.cpp b/tests/test_phase4.cpp index 3dfb1c1..54981de 100644 --- a/tests/test_phase4.cpp +++ b/tests/test_phase4.cpp @@ -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::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();