feat(contacts): switchable Cards / List / Table view for the address list
Add a persisted view toggle so the contacts address list can be rendered three ways, moving it away from a bare data-grid toward a Material look: - Cards: tactile rounded cards with a circular Z/T type-avatar, label over a muted truncated address, trailing globe badge, primary-tint + outline selection, hover fill, and a centred Material empty state. - List: borderless two-line rows (same avatar + label/address) with hover tint and a thin per-row divider — denser than cards. - Table: the previous 3-column table, material-ized (grid borders dropped, row backgrounds + interactive sort kept). The toggle is an icon SegmentedControl (view-agenda / view-list / table-rows) right-aligned on the toolbar; the choice persists via a new contacts_view_mode_ setting (0 cards / 1 list / 2 table, mirroring portfolio_style_). Cards/List sort by label; Table keeps its sortable headers. All geometry is dpiScale()-aware; the globe badge is drawn in the icon font in every mode. Default is Cards. Build + ctest + hygiene clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -174,6 +174,8 @@ bool Settings::load(const std::string& path)
|
|||||||
}
|
}
|
||||||
loadScalar(j, "portfolio_style", portfolio_style_);
|
loadScalar(j, "portfolio_style", portfolio_style_);
|
||||||
if (portfolio_style_ < 0 || portfolio_style_ > 2) portfolio_style_ = 0;
|
if (portfolio_style_ < 0 || portfolio_style_ > 2) portfolio_style_ = 0;
|
||||||
|
loadScalar(j, "contacts_view_mode", contacts_view_mode_);
|
||||||
|
if (contacts_view_mode_ < 0 || contacts_view_mode_ > 2) contacts_view_mode_ = 0;
|
||||||
loadScalar(j, "scanline_enabled", scanline_enabled_);
|
loadScalar(j, "scanline_enabled", scanline_enabled_);
|
||||||
loadScalar(j, "console_line_accents", console_line_accents_);
|
loadScalar(j, "console_line_accents", console_line_accents_);
|
||||||
loadScalar(j, "console_text_color", console_text_color_);
|
loadScalar(j, "console_text_color", console_text_color_);
|
||||||
@@ -428,6 +430,7 @@ bool Settings::save(const std::string& path)
|
|||||||
j["window_opacity"] = window_opacity_;
|
j["window_opacity"] = window_opacity_;
|
||||||
j["balance_layout"] = balance_layout_; // saved as string ID
|
j["balance_layout"] = balance_layout_; // saved as string ID
|
||||||
j["portfolio_style"] = portfolio_style_;
|
j["portfolio_style"] = portfolio_style_;
|
||||||
|
j["contacts_view_mode"] = contacts_view_mode_;
|
||||||
j["scanline_enabled"] = scanline_enabled_;
|
j["scanline_enabled"] = scanline_enabled_;
|
||||||
j["console_line_accents"] = console_line_accents_;
|
j["console_line_accents"] = console_line_accents_;
|
||||||
j["console_text_color"] = console_text_color_;
|
j["console_text_color"] = console_text_color_;
|
||||||
|
|||||||
@@ -184,6 +184,9 @@ public:
|
|||||||
// Left/Right arrows on the Market tab (like the Overview layouts).
|
// Left/Right arrows on the Market tab (like the Overview layouts).
|
||||||
int getPortfolioStyle() const { return portfolio_style_; }
|
int getPortfolioStyle() const { return portfolio_style_; }
|
||||||
void setPortfolioStyle(int v) { portfolio_style_ = (v < 0 || v > 2) ? 0 : v; }
|
void setPortfolioStyle(int v) { portfolio_style_ = (v < 0 || v > 2) ? 0 : v; }
|
||||||
|
// Contacts tab address-list view: 0 = cards, 1 = list, 2 = table.
|
||||||
|
int getContactsViewMode() const { return contacts_view_mode_; }
|
||||||
|
void setContactsViewMode(int v) { contacts_view_mode_ = (v < 0 || v > 2) ? 0 : v; }
|
||||||
|
|
||||||
// Console scanline effect
|
// Console scanline effect
|
||||||
bool getScanlineEnabled() const { return scanline_enabled_; }
|
bool getScanlineEnabled() const { return scanline_enabled_; }
|
||||||
@@ -490,6 +493,7 @@ private:
|
|||||||
#endif
|
#endif
|
||||||
std::string balance_layout_ = "classic";
|
std::string balance_layout_ = "classic";
|
||||||
int portfolio_style_ = 0; // Market portfolio row style (0 single / 1 two-line / 2 hero)
|
int portfolio_style_ = 0; // Market portfolio row style (0 single / 1 two-line / 2 hero)
|
||||||
|
int contacts_view_mode_ = 0; // Contacts address-list view (0 cards / 1 list / 2 table)
|
||||||
bool scanline_enabled_ = true;
|
bool scanline_enabled_ = true;
|
||||||
bool console_line_accents_ = true; // left color accent bars in console output
|
bool console_line_accents_ = true; // left color accent bars in console output
|
||||||
bool console_text_color_ = true; // per-channel text coloring in console output
|
bool console_text_color_ = true; // per-channel text coloring in console output
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
#include "contacts_tab.h"
|
#include "contacts_tab.h"
|
||||||
#include "../../app.h"
|
#include "../../app.h"
|
||||||
|
#include "../../config/settings.h"
|
||||||
#include "../../data/address_book.h"
|
#include "../../data/address_book.h"
|
||||||
#include "../../util/i18n.h"
|
#include "../../util/i18n.h"
|
||||||
#include "../../util/text_format.h"
|
#include "../../util/text_format.h"
|
||||||
@@ -275,6 +276,26 @@ void RenderContactsTab(App* app)
|
|||||||
|
|
||||||
if (!has_selection) ImGui::EndDisabled();
|
if (!has_selection) ImGui::EndDisabled();
|
||||||
|
|
||||||
|
// Address-list view toggle (Cards / List / Table) — right-aligned on the toolbar row; persisted.
|
||||||
|
int viewMode = app->settings() ? app->settings()->getContactsViewMode() : 0;
|
||||||
|
{
|
||||||
|
const char* segIcons[3] = { ICON_MD_VIEW_AGENDA, ICON_MD_VIEW_LIST, ICON_MD_TABLE_ROWS };
|
||||||
|
const float segH = ImGui::GetFrameHeight();
|
||||||
|
const float segW = 44.0f * dp * 3.0f;
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + std::max(0.0f, ImGui::GetContentRegionAvail().x - segW));
|
||||||
|
ImVec2 segOrigin = ImGui::GetCursorScreenPos();
|
||||||
|
int segClk = material::SegmentedControl(ImGui::GetWindowDrawList(), segOrigin, segW, segH,
|
||||||
|
segIcons, 3, viewMode, material::Type().iconSmall(),
|
||||||
|
"##contactsView", dp);
|
||||||
|
if (segClk >= 0 && app->settings()) {
|
||||||
|
app->settings()->setContactsViewMode(segClk);
|
||||||
|
app->settings()->save();
|
||||||
|
viewMode = segClk;
|
||||||
|
}
|
||||||
|
ImGui::Dummy(ImVec2(segW, segH));
|
||||||
|
}
|
||||||
|
|
||||||
// Search / filter
|
// Search / filter
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x);
|
ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x);
|
||||||
@@ -299,32 +320,50 @@ void RenderContactsTab(App* app)
|
|||||||
visibleRows.push_back(i);
|
visibleRows.push_back(i);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Address list — size the table to fill the tab, leaving room for the count footer.
|
// Address list — fill the tab, leaving room for the count footer.
|
||||||
float footerH = ImGui::GetTextLineHeightWithSpacing() + ImGui::GetStyle().ItemSpacing.y;
|
const float footerH = ImGui::GetTextLineHeightWithSpacing() + ImGui::GetStyle().ItemSpacing.y;
|
||||||
float tableH = ImGui::GetContentRegionAvail().y - footerH;
|
float listH = ImGui::GetContentRegionAvail().y - footerH;
|
||||||
if (tableH < 120.0f * dp) tableH = 120.0f * dp;
|
if (listH < 120.0f * dp) listH = 120.0f * dp;
|
||||||
if (ImGui::BeginTable("AddressBookTable", 3,
|
|
||||||
ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Sortable |
|
|
||||||
ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY,
|
|
||||||
ImVec2(0, tableH)))
|
|
||||||
{
|
|
||||||
// All columns stretch to share the tab width (address widest, notes a small share) so long labels
|
|
||||||
// no longer clip mid-word and the notes column no longer reserves a fixed empty block on the right.
|
|
||||||
// Stretch weights are relative, so no dpiScale() is needed here.
|
|
||||||
ImGui::TableSetupColumn(TR("label"), ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_DefaultSort, 1.5f);
|
|
||||||
ImGui::TableSetupColumn(TR("address_label"), ImGuiTableColumnFlags_WidthStretch, 2.6f);
|
|
||||||
ImGui::TableSetupColumn(TR("notes"), ImGuiTableColumnFlags_WidthStretch, 1.0f);
|
|
||||||
ImGui::TableSetupScrollFreeze(0, 1);
|
|
||||||
ImGui::TableHeadersRow();
|
|
||||||
|
|
||||||
// Sort the visible view by the active column (indices stay storage indices).
|
const bool lightTheme = material::IsLightTheme();
|
||||||
if (ImGuiTableSortSpecs* specs = ImGui::TableGetSortSpecs()) {
|
auto typeColor = [&](bool shielded) -> ImU32 {
|
||||||
if (specs->SpecsCount > 0) {
|
ImVec4 c = shielded ? (lightTheme ? ImVec4(0.10f,0.55f,0.38f,1.0f) : ImVec4(0.35f,0.80f,0.60f,1.0f))
|
||||||
const ImGuiTableColumnSortSpecs& sc = specs->Specs[0];
|
: (lightTheme ? ImVec4(0.72f,0.48f,0.05f,1.0f) : ImVec4(0.95f,0.72f,0.30f,1.0f));
|
||||||
bool asc = sc.SortDirection != ImGuiSortDirection_Descending;
|
return ImGui::ColorConvertFloat4ToU32(c);
|
||||||
const auto& entries = book.entries();
|
};
|
||||||
std::stable_sort(visibleRows.begin(), visibleRows.end(),
|
// Centered Material empty state (no contacts yet / no search match).
|
||||||
[&](size_t a, size_t b) {
|
auto emptyState = [&](const char* iconGlyph, const char* msgKey) {
|
||||||
|
const char* msg = TR(msgKey);
|
||||||
|
ImFont* icoF = material::Type().iconLarge();
|
||||||
|
ImFont* txtF = material::Type().body2();
|
||||||
|
const float availW = ImGui::GetContentRegionAvail().x;
|
||||||
|
ImVec2 is = icoF->CalcTextSizeA(icoF->LegacySize, FLT_MAX, 0, iconGlyph);
|
||||||
|
ImVec2 ts = txtF->CalcTextSizeA(txtF->LegacySize, FLT_MAX, 0, msg);
|
||||||
|
ImGui::SetCursorPos(ImVec2((availW - is.x) * 0.5f, ImGui::GetCursorPosY() + listH * 0.32f));
|
||||||
|
ImGui::PushFont(icoF);
|
||||||
|
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(material::OnSurfaceDisabled()), "%s", iconGlyph);
|
||||||
|
ImGui::PopFont();
|
||||||
|
ImGui::SetCursorPosX((availW - ts.x) * 0.5f);
|
||||||
|
material::Type().textColored(material::TypeStyle::Body2, material::OnSurfaceMedium(), msg);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (viewMode == 2) {
|
||||||
|
// ── TABLE mode (material-ized): no outer/grid borders, row backgrounds, interactive sort. ──
|
||||||
|
if (ImGui::BeginTable("AddressBookTable", 3,
|
||||||
|
ImGuiTableFlags_RowBg | ImGuiTableFlags_Sortable |
|
||||||
|
ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY, ImVec2(0, listH)))
|
||||||
|
{
|
||||||
|
ImGui::TableSetupColumn(TR("label"), ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_DefaultSort, 1.5f);
|
||||||
|
ImGui::TableSetupColumn(TR("address_label"), ImGuiTableColumnFlags_WidthStretch, 2.6f);
|
||||||
|
ImGui::TableSetupColumn(TR("notes"), ImGuiTableColumnFlags_WidthStretch, 1.0f);
|
||||||
|
ImGui::TableSetupScrollFreeze(0, 1);
|
||||||
|
ImGui::TableHeadersRow();
|
||||||
|
if (ImGuiTableSortSpecs* specs = ImGui::TableGetSortSpecs()) {
|
||||||
|
if (specs->SpecsCount > 0) {
|
||||||
|
const ImGuiTableColumnSortSpecs& sc = specs->Specs[0];
|
||||||
|
bool asc = sc.SortDirection != ImGuiSortDirection_Descending;
|
||||||
|
const auto& entries = book.entries();
|
||||||
|
std::stable_sort(visibleRows.begin(), visibleRows.end(), [&](size_t a, size_t b) {
|
||||||
const std::string* ka; const std::string* kb;
|
const std::string* ka; const std::string* kb;
|
||||||
switch (sc.ColumnIndex) {
|
switch (sc.ColumnIndex) {
|
||||||
case 1: ka = &entries[a].address; kb = &entries[b].address; break;
|
case 1: ka = &entries[a].address; kb = &entries[b].address; break;
|
||||||
@@ -334,74 +373,135 @@ void RenderContactsTab(App* app)
|
|||||||
int cmp = toLower(*ka).compare(toLower(*kb));
|
int cmp = toLower(*ka).compare(toLower(*kb));
|
||||||
return asc ? (cmp < 0) : (cmp > 0);
|
return asc ? (cmp < 0) : (cmp > 0);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (book.empty()) {
|
|
||||||
ImGui::TableNextRow();
|
|
||||||
ImGui::TableNextColumn();
|
|
||||||
ImGui::TextDisabled("%s", TR("address_book_empty"));
|
|
||||||
} else if (visibleRows.empty()) {
|
|
||||||
ImGui::TableNextRow();
|
|
||||||
ImGui::TableNextColumn();
|
|
||||||
ImGui::TextDisabled("%s", TR("contacts_search_no_match"));
|
|
||||||
} else {
|
|
||||||
for (size_t vi = 0; vi < visibleRows.size(); ++vi) {
|
for (size_t vi = 0; vi < visibleRows.size(); ++vi) {
|
||||||
size_t i = visibleRows[vi];
|
size_t i = visibleRows[vi];
|
||||||
const auto& entry = book.entries()[i];
|
const auto& entry = book.entries()[i];
|
||||||
|
|
||||||
ImGui::TableNextRow();
|
ImGui::TableNextRow();
|
||||||
ImGui::PushID(static_cast<int>(i));
|
ImGui::PushID(static_cast<int>(i));
|
||||||
|
|
||||||
ImGui::TableNextColumn();
|
ImGui::TableNextColumn();
|
||||||
bool is_selected = (s_selected_index == static_cast<int>(i));
|
bool is_selected = (s_selected_index == static_cast<int>(i));
|
||||||
if (ImGui::Selectable(entry.label.c_str(), is_selected,
|
if (ImGui::Selectable(entry.label.c_str(), is_selected,
|
||||||
ImGuiSelectableFlags_SpanAllColumns)) {
|
ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowDoubleClick)) {
|
||||||
if (s_selected_index != static_cast<int>(i)) s_confirm_delete_idx = -1;
|
if (s_selected_index != static_cast<int>(i)) s_confirm_delete_idx = -1;
|
||||||
s_selected_index = static_cast<int>(i);
|
s_selected_index = static_cast<int>(i);
|
||||||
|
if (ImGui::IsMouseDoubleClicked(0)) openEdit();
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(0)) {
|
|
||||||
s_selected_index = static_cast<int>(i);
|
|
||||||
openEdit();
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::TableNextColumn();
|
ImGui::TableNextColumn();
|
||||||
// Z/T type badge + address in normal (legible) text — not muted.
|
|
||||||
// Darker/more-saturated variants on light skins so the badge doesn't wash out.
|
|
||||||
bool shielded = isShieldedAddr(entry.address);
|
bool shielded = isShieldedAddr(entry.address);
|
||||||
bool lightTheme = material::IsLightTheme();
|
ImGui::PushFont(material::Type().subtitle2());
|
||||||
ImVec4 zCol = lightTheme ? ImVec4(0.10f, 0.55f, 0.38f, 1.0f) : ImVec4(0.35f, 0.80f, 0.60f, 1.0f);
|
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(typeColor(shielded)), "%s", shielded ? "Z" : "T");
|
||||||
ImVec4 tCol = lightTheme ? ImVec4(0.72f, 0.48f, 0.05f, 1.0f) : ImVec4(0.95f, 0.72f, 0.30f, 1.0f);
|
ImGui::PopFont();
|
||||||
ImGui::TextColored(shielded ? zCol : tCol, "%s", shielded ? "Z" : "T");
|
|
||||||
ImGui::SameLine(0.0f, 6.0f * dp);
|
ImGui::SameLine(0.0f, 6.0f * dp);
|
||||||
std::string addr_display = entry.address;
|
std::string addr_display = util::truncateMiddle(entry.address, addrFrontLbl.truncate, addrBackLbl.truncate);
|
||||||
int addrTruncLen = (addrTable.columns.count("address") && addrTable.columns.at("address").truncate > 0) ? addrTable.columns.at("address").truncate : 40;
|
|
||||||
if (addr_display.length() > static_cast<size_t>(addrTruncLen)) {
|
|
||||||
addr_display = util::truncateMiddle(addr_display, addrFrontLbl.truncate, addrBackLbl.truncate);
|
|
||||||
}
|
|
||||||
ImGui::TextUnformatted(addr_display.c_str());
|
ImGui::TextUnformatted(addr_display.c_str());
|
||||||
if (ImGui::IsItemHovered()) {
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", entry.address.c_str());
|
||||||
material::Tooltip("%s", entry.address.c_str());
|
|
||||||
}
|
|
||||||
// Globe badge marks a global contact (shown in every wallet). Must be drawn in the icon
|
|
||||||
// font — the text font has no Material glyphs, so it rendered as tofu ("?") before.
|
|
||||||
if (entry.isGlobal()) {
|
if (entry.isGlobal()) {
|
||||||
ImGui::SameLine(0.0f, 8.0f * dp);
|
ImGui::SameLine(0.0f, 8.0f * dp);
|
||||||
ImGui::PushFont(material::Type().iconSmall());
|
ImGui::PushFont(material::Type().iconSmall());
|
||||||
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(material::OnSurfaceMedium()),
|
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(material::OnSurfaceMedium()), "%s", ICON_MD_PUBLIC);
|
||||||
"%s", ICON_MD_PUBLIC);
|
|
||||||
ImGui::PopFont();
|
ImGui::PopFont();
|
||||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("contact_global_badge_tt"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("contact_global_badge_tt"));
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::TableNextColumn();
|
ImGui::TableNextColumn();
|
||||||
ImGui::TextUnformatted(entry.notes.c_str());
|
ImGui::TextUnformatted(entry.notes.c_str());
|
||||||
|
|
||||||
ImGui::PopID();
|
ImGui::PopID();
|
||||||
}
|
}
|
||||||
|
ImGui::EndTable();
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
ImGui::EndTable();
|
// ── CARDS (0) / LIST (1) mode — tactile Material items, no grid lines. ──
|
||||||
|
const bool asCard = (viewMode == 0);
|
||||||
|
// Cards/list have no interactive sort header — sort by label ascending.
|
||||||
|
{
|
||||||
|
const auto& entries = book.entries();
|
||||||
|
std::stable_sort(visibleRows.begin(), visibleRows.end(), [&](size_t a, size_t b) {
|
||||||
|
return toLower(entries[a].label).compare(toLower(entries[b].label)) < 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0,0,0,0));
|
||||||
|
ImGui::BeginChild("##contactList", ImVec2(0, listH), false, ImGuiWindowFlags_NoBackground);
|
||||||
|
if (visibleRows.empty()) {
|
||||||
|
emptyState(ICON_MD_CONTACTS, book.empty() ? "address_book_empty" : "contacts_search_no_match");
|
||||||
|
} else {
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0,0,0,0));
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(0,0,0,0));
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImVec4(0,0,0,0));
|
||||||
|
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, asCard ? 8.0f * dp : 0.0f));
|
||||||
|
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||||
|
ImFont* lblF = material::Type().subtitle2();
|
||||||
|
ImFont* adrF = material::Type().caption();
|
||||||
|
ImFont* icoF = material::Type().iconSmall();
|
||||||
|
const float pad = 12.0f * dp;
|
||||||
|
const float avR = 15.0f * dp;
|
||||||
|
const float rowH = avR * 2.0f + (asCard ? 16.0f * dp : 12.0f * dp);
|
||||||
|
const float round = 10.0f * dp;
|
||||||
|
for (size_t vi = 0; vi < visibleRows.size(); ++vi) {
|
||||||
|
size_t i = visibleRows[vi];
|
||||||
|
const auto& entry = book.entries()[i];
|
||||||
|
ImGui::PushID(static_cast<int>(i));
|
||||||
|
const bool selected = (s_selected_index == static_cast<int>(i));
|
||||||
|
const ImVec2 p = ImGui::GetCursorScreenPos();
|
||||||
|
const float w = ImGui::GetContentRegionAvail().x;
|
||||||
|
if (ImGui::Selectable("##it", selected,
|
||||||
|
ImGuiSelectableFlags_SpanAvailWidth | ImGuiSelectableFlags_AllowDoubleClick,
|
||||||
|
ImVec2(0, rowH))) {
|
||||||
|
if (!selected) s_confirm_delete_idx = -1;
|
||||||
|
s_selected_index = static_cast<int>(i);
|
||||||
|
if (ImGui::IsMouseDoubleClicked(0)) openEdit();
|
||||||
|
}
|
||||||
|
const bool hov = ImGui::IsItemHovered();
|
||||||
|
if (hov) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
|
const ImVec2 mn = p, mx(p.x + w, p.y + rowH);
|
||||||
|
if (asCard) {
|
||||||
|
ImU32 fill = selected ? material::WithAlpha(material::Primary(), 42)
|
||||||
|
: hov ? 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);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
// Circular type avatar with the Z/T letter.
|
||||||
|
const bool shielded = isShieldedAddr(entry.address);
|
||||||
|
const ImU32 tu = typeColor(shielded);
|
||||||
|
const ImVec2 avC(mn.x + pad + avR, mn.y + rowH * 0.5f);
|
||||||
|
dl->AddCircleFilled(avC, avR, material::WithAlpha(tu, lightTheme ? 45 : 60));
|
||||||
|
dl->AddCircle(avC, avR, material::WithAlpha(tu, 190), 0, 1.4f * dp);
|
||||||
|
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).
|
||||||
|
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;
|
||||||
|
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);
|
||||||
|
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()) {
|
||||||
|
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());
|
||||||
|
ImGui::PopID();
|
||||||
|
}
|
||||||
|
ImGui::PopStyleVar();
|
||||||
|
ImGui::PopStyleColor(3);
|
||||||
|
}
|
||||||
|
ImGui::EndChild();
|
||||||
|
ImGui::PopStyleColor();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Status line — singular form for exactly one contact ("1 address saved", not "1 addresses saved").
|
// Status line — singular form for exactly one contact ("1 address saved", not "1 addresses saved").
|
||||||
|
|||||||
Reference in New Issue
Block a user