fix(balance): disambiguate address drag — edge to reorder, centre to transfer

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 <noreply@anthropic.com>
This commit is contained in:
2026-06-10 20:53:54 -05:00
parent b6567ee196
commit 4ee830c5dd
2 changed files with 58 additions and 33 deletions

View File

@@ -139,6 +139,9 @@ void RenderSharedAddressList(App* app, float listH, float availW,
static float s_dragStartY = 0.0f; // mouse Y at drag start static float s_dragStartY = 0.0f; // mouse Y at drag start
static bool s_dragActive = false; // drag distance threshold passed static bool s_dragActive = false; // drag distance threshold passed
static int s_dropTargetIdx = -1; // row hovered during drag 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 // Copy feedback
static int s_copiedRow = -1; static int s_copiedRow = -1;
@@ -314,38 +317,45 @@ void RenderSharedAddressList(App* app, float listH, float availW,
s_copiedRow = s_dragIdx; s_copiedRow = s_dragIdx;
s_copiedTime = (float)ImGui::GetTime(); s_copiedTime = (float)ImGui::GetTime();
} }
// Drop // Drop. Released over a row's CENTER (transfer mode) and that row has funds -> transfer;
if (s_dragActive && s_dropTargetIdx >= 0 && s_dropTargetIdx != s_dragIdx) { // released over an EDGE, in a gap, or on a zero-balance row -> reorder.
const auto& srcRow = rows[s_dragIdx]; if (s_dragActive) {
const auto& dstRow = rows[s_dropTargetIdx]; bool didTransfer = false;
// Transfer: if dropped on another row and drag was active if (s_dropMode == 0 && s_dropTargetIdx >= 0 && s_dropTargetIdx != s_dragIdx &&
if (srcRow.info->balance > 1e-9) { s_dragIdx < (int)rows.size()) {
AddressTransferDialog::TransferInfo ti; const auto& srcRow = rows[s_dragIdx];
ti.fromAddr = srcRow.info->address; const auto& dstRow = rows[s_dropTargetIdx];
ti.toAddr = dstRow.info->address; if (srcRow.info->balance > 1e-9) {
ti.fromBalance = srcRow.info->balance; AddressTransferDialog::TransferInfo ti;
ti.toBalance = dstRow.info->balance; ti.fromAddr = srcRow.info->address;
ti.fromIsZ = srcRow.isZ; ti.toAddr = dstRow.info->address;
ti.toIsZ = dstRow.isZ; ti.fromBalance = srcRow.info->balance;
AddressTransferDialog::show(app, ti); 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) { if (!didTransfer) {
// Reorder: dropped in gap — compute insert position from mouseY // Reorder — compute insert position from mouseY.
int insertIdx = 0; int insertIdx = 0;
for (int i = 0; i < (int)rows.size(); ++i) { for (int i = 0; i < (int)rows.size(); ++i) {
if (mousePos.y > rowY[i] + rowH * 0.5f) insertIdx = i + 1; if (mousePos.y > rowY[i] + rowH * 0.5f) insertIdx = i + 1;
} }
if (insertIdx != s_dragIdx && insertIdx != s_dragIdx + 1) { if (insertIdx != s_dragIdx && insertIdx != s_dragIdx + 1) {
int targetIdx = (insertIdx > s_dragIdx) ? insertIdx - 1 : insertIdx; int targetIdx = (insertIdx > s_dragIdx) ? insertIdx - 1 : insertIdx;
if (targetIdx >= 0 && targetIdx < (int)rows.size()) { if (targetIdx >= 0 && targetIdx < (int)rows.size() && s_dragIdx < (int)rows.size()) {
app->swapAddressOrder(rows[s_dragIdx].info->address, app->swapAddressOrder(rows[s_dragIdx].info->address,
rows[targetIdx].info->address); rows[targetIdx].info->address);
}
} }
} }
} }
s_dragIdx = -1; s_dragIdx = -1;
s_dragActive = false; s_dragActive = false;
s_dropTargetIdx = -1; s_dropTargetIdx = -1;
s_dropMode = 0;
} }
// ---- PASS 2: Render rows ---- // ---- 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), ImVec2(rowEnd.x + 2*dp, rowEnd.y + 2*dp),
IM_COL32(0, 0, 0, 30), 6.0f * dp); IM_COL32(0, 0, 0, 30), 6.0f * dp);
dl->AddRectFilled(rowPos, rowEnd, IM_COL32(30, 30, 40, 120), 4.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 // Tooltip following cursor — reflect the gesture: centre of a row = transfer to it,
if (s_dropTargetIdx >= 0 && s_dropTargetIdx < (int)rows.size()) { // 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]; const auto& target = rows[s_dropTargetIdx];
ImGui::SetTooltip("%s\n%s\n\n%s %s", ImGui::SetTooltip("%s\n%s\n\n%s %s",
truncateAddress(addr.address, 32).c_str(), truncateAddress(addr.address, 32).c_str(),
@@ -380,9 +391,10 @@ void RenderSharedAddressList(App* app, float listH, float availW,
TR("transfer_to"), TR("transfer_to"),
truncateAddress(target.info->address, 32).c_str()); truncateAddress(target.info->address, 32).c_str());
} else { } else {
ImGui::SetTooltip("%s\n%s", ImGui::SetTooltip("%s\n%s\n\n%s",
truncateAddress(addr.address, 32).c_str(), 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 // Hover effects
bool hovered = !isDragged && material::IsRectHovered(rowPos, rowEnd); 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)) { if (s_dragActive && s_dragIdx != row_idx && material::IsRectHovered(rowPos, rowEnd)) {
s_dropTargetIdx = row_idx; s_dropTargetIdx = row_idx;
ImU32 dropCol = WithAlpha(Primary(), 40); const float relY = rowH > 0.0f ? (mousePos.y - rowPos.y) / rowH : 0.5f;
dl->AddRectFilled(rowPos, rowEnd, dropCol, 4.0f * dp); s_dropMode = (relY < 0.30f || relY > 0.70f) ? 1 : 0;
dl->AddRect(rowPos, rowEnd, WithAlpha(Primary(), 140), 4.0f * dp, 0, 2.0f * dp); 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) { if (hovered && selected_row != row_idx && !s_dragActive) {

View File

@@ -484,6 +484,7 @@ void I18n::loadBuiltinEnglish()
strings_["no_icons_found"] = "No icons match your search."; strings_["no_icons_found"] = "No icons match your search.";
strings_["transfer_funds"] = "Transfer Funds"; strings_["transfer_funds"] = "Transfer Funds";
strings_["transfer_to"] = "Transfer to:"; 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_["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_["shielding_notice"] = "Note: This will shield funds from a transparent (T) address to a private (Z) address.";
strings_["result_preview"] = "Result Preview"; strings_["result_preview"] = "Result Preview";