feat(contacts): per-row hover actions + fix add/edit modal HiDPI widths
Two follow-ups from the contacts audit: - Per-row actions: in the Cards/List views, copy/edit/delete icon buttons now appear on the right of a row on hover or selection (the globe badge shows otherwise). The row Selectable uses SetNextItemAllowOverlap so the action InvisibleButtons take click priority; whole-row hover (IsMouseHoveringRect) drives the highlight so it survives hovering an icon; the delete icon turns red while armed (two-click confirm); each has a tooltip. Trailing space is reserved so the text never reflows on hover, and the cursor is restored after the manual action layout. - Add/edit modal HiDPI: the Layout::kDialog* helpers fold dpiScale() (physical px) while raw schema widths are logical, so the schema-path formW/actionW/actionGap/ notesH were unscaled vs their scaled fallbacks. Scale the schema-path values; and since BeginOverlayDialog re-applies dpiScale to cardWidth, divide the already-scaled dialogW back out (it was double-scaled). No visible change at 100%; correct at 150%. Build + ctest + hygiene clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -126,11 +126,16 @@ void RenderContactsTab(App* app)
|
||||
|
||||
const char* title = isEdit ? TR("address_book_edit") : TR("address_book_add");
|
||||
const char* id = isEdit ? "AddressBookEdit" : "AddressBookAdd";
|
||||
float dialogW = std::max(Layout::kDialogMinWidth(), Layout::kDialogDefaultWidth());
|
||||
float formW = addrInput.width > 0 ? addrInput.width : Layout::kDialogFormWidth();
|
||||
float actionW = actionBtn.width > 0 ? actionBtn.width : Layout::kDialogActionWidth();
|
||||
float actionGap = actionBtn.gap > 0 ? actionBtn.gap : Layout::kDialogActionGap();
|
||||
float notesH = notesInput.height > 0 ? notesInput.height : 60.0f;
|
||||
// DPI: the Layout::kDialog* helpers fold dpiScale() (physical px) but the raw schema widths are
|
||||
// logical, so scale the schema-path values to keep both branches consistent physical px inside
|
||||
// the (native-scale) overlay. cardWidth is the exception — BeginOverlayDialog re-applies dpiScale,
|
||||
// so it must be LOGICAL; divide the already-scaled dialogW back out.
|
||||
const float dp = Layout::dpiScale();
|
||||
float dialogW = std::max(Layout::kDialogMinWidth(), Layout::kDialogDefaultWidth()) / dp;
|
||||
float formW = addrInput.width > 0 ? addrInput.width * dp : Layout::kDialogFormWidth();
|
||||
float actionW = actionBtn.width > 0 ? actionBtn.width * dp : Layout::kDialogActionWidth();
|
||||
float actionGap = actionBtn.gap > 0 ? actionBtn.gap * dp : Layout::kDialogActionGap();
|
||||
float notesH = (notesInput.height > 0 ? notesInput.height : 60.0f) * dp;
|
||||
|
||||
material::OverlayDialogSpec ov;
|
||||
ov.title = title; ov.p_open = open;
|
||||
@@ -444,6 +449,7 @@ void RenderContactsTab(App* app)
|
||||
const bool selected = (s_selected_index == static_cast<int>(i));
|
||||
const ImVec2 p = ImGui::GetCursorScreenPos();
|
||||
const float w = ImGui::GetContentRegionAvail().x;
|
||||
ImGui::SetNextItemAllowOverlap(); // let the per-row action buttons overlap the row
|
||||
if (ImGui::Selectable("##it", selected,
|
||||
ImGuiSelectableFlags_SpanAvailWidth | ImGuiSelectableFlags_AllowDoubleClick,
|
||||
ImVec2(0, rowH))) {
|
||||
@@ -451,18 +457,20 @@ void RenderContactsTab(App* app)
|
||||
s_selected_index = static_cast<int>(i);
|
||||
if (ImGui::IsMouseDoubleClicked(0)) openEdit();
|
||||
}
|
||||
const bool hov = ImGui::IsItemHovered();
|
||||
if (hov) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
const bool selHov = ImGui::IsItemHovered();
|
||||
const ImVec2 afterRow = ImGui::GetCursorScreenPos(); // restore this for the next row
|
||||
const ImVec2 mn = p, mx(p.x + w, p.y + rowH);
|
||||
const bool rowHovered = ImGui::IsMouseHoveringRect(mn, mx); // whole-row hover (survives action-icon hover)
|
||||
if (rowHovered && !selected) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
if (asCard) {
|
||||
ImU32 fill = selected ? material::WithAlpha(material::Primary(), 42)
|
||||
: hov ? material::WithAlpha(material::OnSurface(), 26)
|
||||
: material::WithAlpha(material::OnSurface(), 12);
|
||||
ImU32 fill = selected ? material::WithAlpha(material::Primary(), 42)
|
||||
: rowHovered ? material::WithAlpha(material::OnSurface(), 26)
|
||||
: material::WithAlpha(material::OnSurface(), 12);
|
||||
dl->AddRectFilled(mn, mx, fill, round);
|
||||
if (selected) dl->AddRect(mn, mx, material::WithAlpha(material::Primary(), 150), round, 0, 1.6f * dp);
|
||||
} else {
|
||||
if (selected) dl->AddRectFilled(mn, mx, material::WithAlpha(material::Primary(), 34), 0);
|
||||
else if (hov) dl->AddRectFilled(mn, mx, material::WithAlpha(material::OnSurface(), 16), 0);
|
||||
if (selected) dl->AddRectFilled(mn, mx, material::WithAlpha(material::Primary(), 34), 0);
|
||||
else if (rowHovered) dl->AddRectFilled(mn, mx, material::WithAlpha(material::OnSurface(), 16), 0);
|
||||
dl->AddLine(ImVec2(mn.x + pad, mx.y - 0.5f), ImVec2(mx.x - pad, mx.y - 0.5f),
|
||||
material::WithAlpha(material::OnSurface(), 24), 1.0f);
|
||||
}
|
||||
@@ -475,9 +483,10 @@ void RenderContactsTab(App* app)
|
||||
const char* letter = shielded ? "Z" : "T";
|
||||
const ImVec2 ls = lblF->CalcTextSizeA(lblF->LegacySize, FLT_MAX, 0, letter);
|
||||
dl->AddText(lblF, lblF->LegacySize, ImVec2(avC.x - ls.x * 0.5f, avC.y - ls.y * 0.5f), tu, letter);
|
||||
// Label (line 1) + muted address (line 2).
|
||||
// Label (line 1) + muted address (line 2) — reserve trailing room for the actions/globe.
|
||||
const float actHit = icoF->LegacySize + 8.0f * dp; // per-action square hit area
|
||||
const float trailW = 3.0f * actHit + 6.0f * dp;
|
||||
const float tx = mn.x + pad + avR * 2.0f + pad;
|
||||
const float trailW = entry.isGlobal() ? 28.0f * dp : 0.0f;
|
||||
const float textMaxX = mx.x - pad - trailW;
|
||||
const float blockH = lblF->LegacySize + adrF->LegacySize + 3.0f * dp;
|
||||
const float ty = mn.y + rowH * 0.5f - blockH * 0.5f;
|
||||
@@ -487,16 +496,47 @@ void RenderContactsTab(App* app)
|
||||
std::string addr = util::truncateMiddle(entry.address, addrFrontLbl.truncate, addrBackLbl.truncate);
|
||||
dl->AddText(adrF, adrF->LegacySize, ImVec2(tx, ty + lblF->LegacySize + 3.0f * dp),
|
||||
material::OnSurfaceMedium(), addr.c_str());
|
||||
// Trailing globe badge for global contacts.
|
||||
if (entry.isGlobal()) {
|
||||
// Trailing: per-row copy/edit/delete actions on hover/selection; else the globe badge.
|
||||
if (rowHovered || selected) {
|
||||
const char* actGlyph[3] = { ICON_MD_CONTENT_COPY, ICON_MD_EDIT, ICON_MD_DELETE };
|
||||
const char* actTip[3] = { TR("copy_address"), TR("edit"), TR("delete") };
|
||||
float ax = mx.x - pad - actHit; // rightmost action's left edge (delete)
|
||||
for (int a = 2; a >= 0; --a) {
|
||||
ImGui::SetCursorScreenPos(ImVec2(ax, mn.y + rowH * 0.5f - actHit * 0.5f));
|
||||
ImGui::PushID(a + 100);
|
||||
const bool aClk = ImGui::InvisibleButton("##act", ImVec2(actHit, actHit));
|
||||
const bool aHov = ImGui::IsItemHovered();
|
||||
if (aHov) { ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); material::Tooltip("%s", actTip[a]); }
|
||||
if (aClk) {
|
||||
s_selected_index = static_cast<int>(i);
|
||||
if (a == 0) doCopy();
|
||||
else if (a == 1) openEdit();
|
||||
else doDelete();
|
||||
}
|
||||
const bool armedDel = (a == 2 && s_confirm_delete_idx == static_cast<int>(i));
|
||||
const ImU32 acol = armedDel ? material::ReadableError()
|
||||
: aHov ? material::OnSurface()
|
||||
: material::OnSurfaceMedium();
|
||||
const ImVec2 gsz = icoF->CalcTextSizeA(icoF->LegacySize, FLT_MAX, 0, actGlyph[a]);
|
||||
dl->AddText(icoF, icoF->LegacySize,
|
||||
ImVec2(ax + (actHit - gsz.x) * 0.5f, mn.y + rowH * 0.5f - gsz.y * 0.5f),
|
||||
acol, actGlyph[a]);
|
||||
ImGui::PopID();
|
||||
ax -= actHit + 2.0f * dp;
|
||||
}
|
||||
} else if (entry.isGlobal()) {
|
||||
const ImVec2 gs = icoF->CalcTextSizeA(icoF->LegacySize, FLT_MAX, 0, ICON_MD_PUBLIC);
|
||||
dl->AddText(icoF, icoF->LegacySize,
|
||||
ImVec2(mx.x - pad - gs.x, mn.y + rowH * 0.5f - gs.y * 0.5f),
|
||||
material::OnSurfaceMedium(), ICON_MD_PUBLIC);
|
||||
}
|
||||
if (hov) material::Tooltip("%s", entry.address.c_str());
|
||||
if (selHov) material::Tooltip("%s", entry.address.c_str());
|
||||
ImGui::SetCursorScreenPos(afterRow); // undo the action buttons' cursor moves
|
||||
ImGui::PopID();
|
||||
}
|
||||
// The per-row SetCursorScreenPos leaves the cursor at the last row's bottom with no item
|
||||
// submitted there; commit it so ImGui sizes the scroll child (avoids the extend-boundary warning).
|
||||
ImGui::Dummy(ImVec2(0.0f, 0.0f));
|
||||
ImGui::PopStyleVar();
|
||||
ImGui::PopStyleColor(3);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user