fix(contacts): edit-dialog layout polish + per-row edit + hover address

Address five reported issues:

- Address field is now full width with a centered Paste button beneath it
  (was a narrow field with Paste crammed alongside).
- The pinned "Show in every wallet" checkbox no longer clips at the column
  bottom — the Notes fill reserves a clear margin for it.
- Image mode: more spacing between the preview circle and the Choose/Remove
  row so the button isn't crowding the avatar.
- Contact list: hovering a row now un-collapses the address to its full form
  inline (clipped to the text column) instead of popping a tooltip.
- Per-row copy/edit/delete icons fire on the first click on an unselected row:
  the action lambdas validate s_selected_index freshly (via selValid())
  instead of the frame-top has_selection bool, which was stale in the same
  frame the icon set the selection.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-15 08:53:05 -05:00
parent 9bc2a0b62a
commit a338fac208

View File

@@ -187,9 +187,12 @@ void RenderContactsTab(App* app)
bool has_selection = s_selected_index >= 0 && s_selected_index < static_cast<int>(book.size());
// Shared delete/copy actions (used by both the toolbar buttons and keyboard shortcuts).
// Shared delete/copy/edit actions (used by the toolbar, keyboard shortcuts, and per-row icons).
// Each validates s_selected_index FRESHLY rather than the frame-top has_selection bool: a per-row
// icon sets the selection then invokes the action in the SAME frame, so the cached bool is stale.
auto selValid = [&]() { return s_selected_index >= 0 && s_selected_index < static_cast<int>(book.size()); };
auto doDelete = [&]() {
if (!has_selection) return;
if (!selValid()) return;
if (s_confirm_delete_idx == s_selected_index) {
book.removeEntry(s_selected_index);
s_selected_index = -1;
@@ -201,12 +204,12 @@ void RenderContactsTab(App* app)
}
};
auto doCopy = [&]() {
if (!has_selection) return;
if (!selValid()) return;
ImGui::SetClipboardText(book.entries()[s_selected_index].address.c_str());
Notifications::instance().info(TR("address_copied"));
};
auto openEdit = [&]() {
if (!has_selection) return;
if (!selValid()) return;
loadEditFields(book.entries()[s_selected_index]);
s_show_edit_dialog = true;
s_focus_edit_field = true;
@@ -327,16 +330,14 @@ void RenderContactsTab(App* app)
ImGui::Spacing();
// Address + inline Paste (available for both add and edit — handy when correcting an address).
// Address (full width) with a centered Paste button beneath it.
material::LabeledInput(TR("address_label"), isEdit ? "##EditAddress" : "##AddAddress",
s_edit_address, sizeof(s_edit_address), -1.0f);
{
float pasteW = btnFont->CalcTextSizeA(btnFont->LegacySize, FLT_MAX, 0, TR("paste")).x
+ ImGui::GetStyle().FramePadding.x * 2.0f + 12.0f * dp;
float addrW = ImGui::GetContentRegionAvail().x - pasteW - ImGui::GetStyle().ItemSpacing.x;
material::LabeledInput(TR("address_label"), isEdit ? "##EditAddress" : "##AddAddress",
s_edit_address, sizeof(s_edit_address), addrW);
ImGui::SameLine(0, ImGui::GetStyle().ItemSpacing.x);
// Bottom-align the button with the input box (skip the label line above the input).
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + ImGui::GetTextLineHeightWithSpacing());
+ ImGui::GetStyle().FramePadding.x * 2.0f + 24.0f * dp;
ImGui::SetCursorPosX(ImGui::GetCursorPosX() +
std::max(0.0f, (ImGui::GetContentRegionAvail().x - pasteW) * 0.5f));
if (material::TactileButton(TR("paste"), ImVec2(pasteW, 0), btnFont)) {
const char* clipboard = ImGui::GetClipboardText();
if (clipboard) copyEditField(s_edit_address, sizeof(s_edit_address), clipboard);
@@ -346,8 +347,10 @@ void RenderContactsTab(App* app)
ImGui::Spacing();
// Notes grows to fill the column above the Global checkbox, which is pinned to the bottom.
// Reserve the checkbox frame + the item spacing before it + a clear bottom margin so its
// descenders ("y"/"g") never clip against the child's edge.
{
float globalReserve = ImGui::GetFrameHeight() + Layout::spacingSm();
float globalReserve = ImGui::GetFrameHeight() + Layout::spacingLg() * 2.0f;
float labelLine = ImGui::GetTextLineHeightWithSpacing();
float notesFill = std::max(60.0f * dp,
ImGui::GetContentRegionAvail().y - globalReserve - labelLine);
@@ -514,8 +517,8 @@ void RenderContactsTab(App* app)
ImVec2 a = ImGui::GetContentRegionAvail();
ImVec2 origin = ImGui::GetCursorScreenPos();
float btnH = 40.0f * dp;
float blockH = prR * 2.0f + Layout::spacingSm()
+ (hasImg ? capF->LegacySize + Layout::spacingSm() : 0.0f) + btnH;
float blockH = prR * 2.0f + Layout::spacingLg()
+ (hasImg ? capF->LegacySize + Layout::spacingMd() : 0.0f) + btnH;
float topPad = std::max(Layout::spacingSm(), (a.y - blockH) * 0.32f);
float cx = origin.x + a.x * 0.5f;
float cy = origin.y + topPad + prR;
@@ -547,13 +550,13 @@ void RenderContactsTab(App* app)
mdl->AddText(gf, gsz, ImVec2(cc.x - gs.x*0.5f, cc.y - gs.y*0.5f),
material::OnSurfaceDisabled(), ICON_MD_ADD_PHOTO_ALTERNATE);
}
float rowY = cy + prR + Layout::spacingSm();
float rowY = cy + prR + Layout::spacingLg(); // clear gap between the preview and the row
if (hasImg) {
std::string fname = std::filesystem::path(s_edit_avatar.substr(4)).filename().string();
std::string fit = fitText(fname, capF, a.x - 8.0f * dp);
ImVec2 fs = capF->CalcTextSizeA(capF->LegacySize, FLT_MAX, 0, fit.c_str());
mdl->AddText(capF, capF->LegacySize, ImVec2(cx - fs.x*0.5f, rowY), material::OnSurfaceMedium(), fit.c_str());
rowY += capF->LegacySize + Layout::spacingSm();
rowY += capF->LegacySize + Layout::spacingMd();
}
// Buttons row, centered.
ImGui::SetCursorScreenPos(ImVec2(origin.x, rowY));
@@ -915,9 +918,15 @@ void RenderContactsTab(App* app)
dl->PushClipRect(ImVec2(tx, mn.y), ImVec2(textMaxX, mx.y), true);
dl->AddText(lblF, lblF->LegacySize, ImVec2(tx, ty), material::OnSurface(), entry.label.c_str());
dl->PopClipRect();
std::string addr = util::truncateMiddle(entry.address, addrFrontLbl.truncate, addrBackLbl.truncate);
// Un-collapse to the full address on hover (clipped to the text column so it never
// runs under the trailing actions); middle-truncated otherwise.
std::string addr = rowHovered
? entry.address
: util::truncateMiddle(entry.address, addrFrontLbl.truncate, addrBackLbl.truncate);
dl->PushClipRect(ImVec2(tx, mn.y), ImVec2(textMaxX, mx.y), true);
dl->AddText(adrF, adrF->LegacySize, ImVec2(tx, ty + lblF->LegacySize + 3.0f * dp),
material::OnSurfaceMedium(), addr.c_str());
dl->PopClipRect();
// Trailing: the globe badge stays pinned far-right (global contacts); per-row copy/edit/
// delete actions appear to its LEFT on hover/selection.
float rightX = mx.x - pad;
@@ -955,7 +964,6 @@ void RenderContactsTab(App* app)
ax -= actHit + 2.0f * dp;
}
}
if (selHov) material::Tooltip("%s", entry.address.c_str());
ImGui::SetCursorScreenPos(afterRow); // undo the action buttons' cursor moves
ImGui::PopID();
}