From 4ee830c5ddb6fa78fd986fdd5efd9997870d38db Mon Sep 17 00:00:00 2001 From: DanS Date: Wed, 10 Jun 2026 20:53:54 -0500 Subject: [PATCH] =?UTF-8?q?fix(balance):=20disambiguate=20address=20drag?= =?UTF-8?q?=20=E2=80=94=20edge=20to=20reorder,=20centre=20to=20transfer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The address list supported two drag gestures that collided: dragging a row onto another transferred funds, dragging into a gap reordered. Since rows are contiguous, a reorder-drag was almost always over another row, so it triggered a fund transfer instead of reordering. Disambiguate by WHERE on the target row the drag is released (user's suggestion): the top/bottom ~30% edge bands = reorder (an insertion line is shown), the centre = transfer (the row highlights). A zero-balance row or an off-row drop always reorders. Tooltip and i18n hint updated to match. Co-Authored-By: Claude Opus 4.8 --- src/ui/windows/balance_components.cpp | 90 +++++++++++++++++---------- src/util/i18n.cpp | 1 + 2 files changed, 58 insertions(+), 33 deletions(-) diff --git a/src/ui/windows/balance_components.cpp b/src/ui/windows/balance_components.cpp index 4a51b5c..3456ea9 100644 --- a/src/ui/windows/balance_components.cpp +++ b/src/ui/windows/balance_components.cpp @@ -139,6 +139,9 @@ void RenderSharedAddressList(App* app, float listH, float availW, static float s_dragStartY = 0.0f; // mouse Y at drag start static bool s_dragActive = false; // drag distance threshold passed static int s_dropTargetIdx = -1; // row hovered during drag + static int s_dropMode = 0; // 0 = transfer (released over a row's CENTER), 1 = reorder + // (released over a row's top/bottom EDGE). Disambiguates the + // two drag gestures by WHERE on the target row you release. // Copy feedback static int s_copiedRow = -1; @@ -314,38 +317,45 @@ void RenderSharedAddressList(App* app, float listH, float availW, s_copiedRow = s_dragIdx; s_copiedTime = (float)ImGui::GetTime(); } - // Drop - if (s_dragActive && s_dropTargetIdx >= 0 && s_dropTargetIdx != s_dragIdx) { - const auto& srcRow = rows[s_dragIdx]; - const auto& dstRow = rows[s_dropTargetIdx]; - // Transfer: if dropped on another row and drag was active - if (srcRow.info->balance > 1e-9) { - AddressTransferDialog::TransferInfo ti; - ti.fromAddr = srcRow.info->address; - ti.toAddr = dstRow.info->address; - ti.fromBalance = srcRow.info->balance; - ti.toBalance = dstRow.info->balance; - ti.fromIsZ = srcRow.isZ; - ti.toIsZ = dstRow.isZ; - AddressTransferDialog::show(app, ti); + // Drop. Released over a row's CENTER (transfer mode) and that row has funds -> transfer; + // released over an EDGE, in a gap, or on a zero-balance row -> reorder. + if (s_dragActive) { + bool didTransfer = false; + if (s_dropMode == 0 && s_dropTargetIdx >= 0 && s_dropTargetIdx != s_dragIdx && + s_dragIdx < (int)rows.size()) { + const auto& srcRow = rows[s_dragIdx]; + const auto& dstRow = rows[s_dropTargetIdx]; + if (srcRow.info->balance > 1e-9) { + AddressTransferDialog::TransferInfo ti; + ti.fromAddr = srcRow.info->address; + ti.toAddr = dstRow.info->address; + ti.fromBalance = srcRow.info->balance; + ti.toBalance = dstRow.info->balance; + ti.fromIsZ = srcRow.isZ; + ti.toIsZ = dstRow.isZ; + AddressTransferDialog::show(app, ti); + didTransfer = true; + } } - } else if (s_dragActive && s_dropTargetIdx < 0) { - // Reorder: dropped in gap — 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 (insertIdx != s_dragIdx && insertIdx != s_dragIdx + 1) { - int targetIdx = (insertIdx > s_dragIdx) ? insertIdx - 1 : insertIdx; - if (targetIdx >= 0 && targetIdx < (int)rows.size()) { - app->swapAddressOrder(rows[s_dragIdx].info->address, - rows[targetIdx].info->address); + if (!didTransfer) { + // 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 (insertIdx != s_dragIdx && insertIdx != s_dragIdx + 1) { + int targetIdx = (insertIdx > s_dragIdx) ? insertIdx - 1 : insertIdx; + if (targetIdx >= 0 && targetIdx < (int)rows.size() && s_dragIdx < (int)rows.size()) { + app->swapAddressOrder(rows[s_dragIdx].info->address, + rows[targetIdx].info->address); + } } } } s_dragIdx = -1; s_dragActive = false; s_dropTargetIdx = -1; + s_dropMode = 0; } // ---- PASS 2: Render rows ---- @@ -371,8 +381,9 @@ void RenderSharedAddressList(App* app, float listH, float availW, ImVec2(rowEnd.x + 2*dp, rowEnd.y + 2*dp), IM_COL32(0, 0, 0, 30), 6.0f * dp); dl->AddRectFilled(rowPos, rowEnd, IM_COL32(30, 30, 40, 120), 4.0f * dp); - // Tooltip following cursor — show transfer intent if over a target row - if (s_dropTargetIdx >= 0 && s_dropTargetIdx < (int)rows.size()) { + // Tooltip following cursor — reflect the gesture: centre of a row = transfer to it, + // edge of a row = reorder (move here). + if (s_dropTargetIdx >= 0 && s_dropTargetIdx < (int)rows.size() && s_dropMode == 0) { const auto& target = rows[s_dropTargetIdx]; ImGui::SetTooltip("%s\n%s\n\n%s %s", truncateAddress(addr.address, 32).c_str(), @@ -380,9 +391,10 @@ void RenderSharedAddressList(App* app, float listH, float availW, TR("transfer_to"), truncateAddress(target.info->address, 32).c_str()); } else { - ImGui::SetTooltip("%s\n%s", + ImGui::SetTooltip("%s\n%s\n\n%s", truncateAddress(addr.address, 32).c_str(), - row.isZ ? TR("shielded") : TR("transparent")); + row.isZ ? TR("shielded") : TR("transparent"), + TR("address_reorder_hint")); } } @@ -407,12 +419,24 @@ void RenderSharedAddressList(App* app, float listH, float availW, // Hover effects bool hovered = !isDragged && material::IsRectHovered(rowPos, rowEnd); - // Drop target highlight when dragging another row over this one + // Drop target feedback when dragging another row over this one. The cursor's vertical + // position WITHIN the row picks the gesture: top/bottom edge = reorder (show an insertion + // line), centre = transfer (highlight the whole row). if (s_dragActive && s_dragIdx != row_idx && material::IsRectHovered(rowPos, rowEnd)) { s_dropTargetIdx = row_idx; - ImU32 dropCol = WithAlpha(Primary(), 40); - dl->AddRectFilled(rowPos, rowEnd, dropCol, 4.0f * dp); - dl->AddRect(rowPos, rowEnd, WithAlpha(Primary(), 140), 4.0f * dp, 0, 2.0f * dp); + const float relY = rowH > 0.0f ? (mousePos.y - rowPos.y) / rowH : 0.5f; + s_dropMode = (relY < 0.30f || relY > 0.70f) ? 1 : 0; + if (s_dropMode == 0) { + // Transfer: highlight the whole target row. + dl->AddRectFilled(rowPos, rowEnd, WithAlpha(Primary(), 40), 4.0f * dp); + dl->AddRect(rowPos, rowEnd, WithAlpha(Primary(), 140), 4.0f * dp, 0, 2.0f * dp); + } else { + // Reorder: draw an insertion line at the near (top or bottom) edge. + const float lineY = (relY < 0.5f) ? rowPos.y : rowEnd.y; + dl->AddLine(ImVec2(rowPos.x, lineY), ImVec2(rowEnd.x, lineY), + WithAlpha(Primary(), 230), 2.5f * dp); + dl->AddCircleFilled(ImVec2(rowPos.x, lineY), 3.0f * dp, WithAlpha(Primary(), 230)); + } } if (hovered && selected_row != row_idx && !s_dragActive) { diff --git a/src/util/i18n.cpp b/src/util/i18n.cpp index d4fc79f..7585887 100644 --- a/src/util/i18n.cpp +++ b/src/util/i18n.cpp @@ -484,6 +484,7 @@ void I18n::loadBuiltinEnglish() strings_["no_icons_found"] = "No icons match your search."; strings_["transfer_funds"] = "Transfer Funds"; strings_["transfer_to"] = "Transfer to:"; + strings_["address_reorder_hint"] = "Drop on a row's edge to reorder, or its centre to transfer"; strings_["deshielding_warning"] = "Warning: This will de-shield funds from a private (Z) address to a transparent (T) address."; strings_["shielding_notice"] = "Note: This will shield funds from a transparent (T) address to a private (Z) address."; strings_["result_preview"] = "Result Preview";