feat(wallets): card-based wallet list — cleaner Material look, no table
Replace the wallet table with a stack of Material-style cards inside a scrolling child. Each wallet is a rounded card: a leading wallet icon, the filename as the primary line, and a muted metadata subtitle (size · N addresses · balance DRGX · last opened). The action sits on the right — Open / Import buttons, or a green "Active" chip for the current wallet. The current wallet's card gets a green tint, border, and icon; external files show an info icon + tooltip; cards highlight on hover. The dialog stays content-sized + fixed-height (computed from the wallet count, up to 6 before it scrolls), so the monitor-drag stability is unaffected. Adversarially reviewed (two-lens workflow + synthesis): the reviewers' DPI unit-mixing and broken-scroll findings were false positives (LegacySize is the DPI-scaled atlas size, and BeginChild takes physical px while the framework's cardHeight takes logical) — the only real issue was byte-wise truncation that could split a multibyte UTF-8 char, now fixed to pop whole code points. Verified via the headless sweep at 100%, 150%, and light skin. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -19,6 +19,7 @@
|
|||||||
|
|
||||||
#include "../../app.h"
|
#include "../../app.h"
|
||||||
#include "../../config/settings.h"
|
#include "../../config/settings.h"
|
||||||
|
#include "../../config/version.h"
|
||||||
#include "../../util/i18n.h"
|
#include "../../util/i18n.h"
|
||||||
#include "../../util/platform.h"
|
#include "../../util/platform.h"
|
||||||
#include "../../util/text_format.h"
|
#include "../../util/text_format.h"
|
||||||
@@ -52,15 +53,17 @@ public:
|
|||||||
|
|
||||||
if (s_needScan) { scan(); s_needScan = false; }
|
if (s_needScan) { scan(); s_needScan = false; }
|
||||||
|
|
||||||
// ---- #1 Content-sized card (bounded) -----------------------------------------------------
|
// ---- Content-sized card: a stack of wallet cards, sized to the wallet count (up to
|
||||||
// Size the card to the wallet count (up to kMaxVisibleRows, then the list scrolls) so a short
|
// kMaxVisibleRows, then the list scrolls). Computed from the KNOWN row count so the
|
||||||
// list doesn't float in a sea of empty space. Computed from the KNOWN row count, so it stays a
|
// dialog stays fixed-height (no auto-height feedback that slid content off on a move). --
|
||||||
// fixed-height dialog (no auto-height feedback — that slid content off-screen on a monitor move).
|
const int kMaxVisibleRows = 6;
|
||||||
const int kMaxVisibleRows = 8;
|
ImFont* nameFont = Type().body1();
|
||||||
const int visRows = std::min(std::max(1, (int)s_rows.size()), kMaxVisibleRows);
|
ImFont* metaFont = Type().caption();
|
||||||
const float tableRowH = ImGui::GetFrameHeight() + style.CellPadding.y * 2.0f;
|
const int visRows = std::min(std::max(1, (int)s_rows.size()), kMaxVisibleRows);
|
||||||
const float tableHdrH = ImGui::GetTextLineHeight() + style.CellPadding.y * 2.0f;
|
const float cardPadY = Layout::spacingSm();
|
||||||
const float tableH = tableHdrH + visRows * tableRowH + 4.0f * dp;
|
const float walRowH = cardPadY * 2.0f + nameFont->LegacySize + Layout::spacingXs() + metaFont->LegacySize;
|
||||||
|
const float cardGap = Layout::spacingSm();
|
||||||
|
const float listH = visRows * walRowH + std::max(0, visRows - 1) * cardGap + 4.0f * dp;
|
||||||
|
|
||||||
const float ctrlRow = ImGui::GetFrameHeightWithSpacing();
|
const float ctrlRow = ImGui::GetFrameHeightWithSpacing();
|
||||||
const float capRow = Type().caption()->LegacySize + style.ItemSpacing.y;
|
const float capRow = Type().caption()->LegacySize + style.ItemSpacing.y;
|
||||||
@@ -73,7 +76,7 @@ public:
|
|||||||
const float headH = Type().h6()->LegacySize + Layout::spacingXs() // framework h6 title
|
const float headH = Type().h6()->LegacySize + Layout::spacingXs() // framework h6 title
|
||||||
+ Type().caption()->LegacySize + Layout::spacingSm(); // intro + gap
|
+ Type().caption()->LegacySize + Layout::spacingSm(); // intro + gap
|
||||||
const float padV = 48.0f; // content-child padding (top+bottom) + margin
|
const float padV = 48.0f; // content-child padding (top+bottom) + margin
|
||||||
const float cardH = (headH + tableH + belowH + padV) / dp;
|
const float cardH = (headH + listH + belowH + padV) / dp;
|
||||||
|
|
||||||
OverlayDialogSpec ov;
|
OverlayDialogSpec ov;
|
||||||
ov.title = TR("wallets_title");
|
ov.title = TR("wallets_title");
|
||||||
@@ -88,110 +91,115 @@ public:
|
|||||||
|
|
||||||
const std::string active = app->settings() ? app->settings()->getActiveWalletFile() : "wallet.dat";
|
const std::string active = app->settings() ? app->settings()->getActiveWalletFile() : "wallet.dat";
|
||||||
|
|
||||||
// ---- #2 Framed wallet list ------------------------------------------------------------
|
// ---- Wallet cards (Material-style rows; no table) -------------------------------------
|
||||||
const ImGuiTableFlags tflags = ImGuiTableFlags_ScrollY | ImGuiTableFlags_RowBg |
|
const float listW = ImGui::GetContentRegionAvail().x;
|
||||||
ImGuiTableFlags_BordersInnerH | ImGuiTableFlags_BordersOuter |
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
|
||||||
ImGuiTableFlags_PadOuterX;
|
ImGui::BeginChild("##walletList", ImVec2(listW, listH), false);
|
||||||
if (ImGui::BeginTable("##wallets", 6, tflags, ImVec2(0, tableH))) {
|
ImGui::PopStyleVar();
|
||||||
ImGui::TableSetupColumn(TR("wallets_col_name"), ImGuiTableColumnFlags_WidthStretch);
|
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||||
ImGui::TableSetupColumn(TR("wallets_col_size"), ImGuiTableColumnFlags_WidthFixed, 90.0f * dp);
|
const float rowW = ImGui::GetContentRegionAvail().x;
|
||||||
ImGui::TableSetupColumn(TR("wallets_col_addresses"), ImGuiTableColumnFlags_WidthFixed, 90.0f * dp);
|
ImFont* icoFont = Type().iconSmall();
|
||||||
ImGui::TableSetupColumn(TR("wallets_col_balance"), ImGuiTableColumnFlags_WidthFixed, 130.0f * dp);
|
const char* dot = " \xC2\xB7 ";
|
||||||
ImGui::TableSetupColumn(TR("wallets_col_opened"), ImGuiTableColumnFlags_WidthFixed, 120.0f * dp);
|
auto tw = [](ImFont* f, const std::string& s){ return f->CalcTextSizeA(f->LegacySize, FLT_MAX, 0, s.c_str()).x; };
|
||||||
ImGui::TableSetupColumn("##action", ImGuiTableColumnFlags_WidthFixed, 100.0f * dp);
|
auto fit = [&](std::string s, ImFont* f, float maxW){
|
||||||
ImGui::TableSetupScrollFreeze(0, 1);
|
bool t=false;
|
||||||
|
while (s.size()>1 && tw(f,s)>maxW){
|
||||||
|
// Pop a whole UTF-8 code point (never split a multibyte char — external .dat names
|
||||||
|
// and some locale metadata are non-ASCII).
|
||||||
|
while (s.size()>1 && (static_cast<unsigned char>(s.back()) & 0xC0) == 0x80) s.pop_back();
|
||||||
|
if (s.size()>1) s.pop_back();
|
||||||
|
t=true;
|
||||||
|
}
|
||||||
|
if (t) s += "\xE2\x80\xA6";
|
||||||
|
return s;
|
||||||
|
};
|
||||||
|
|
||||||
// ---- #3 Right-aligned headers for the numeric columns (Size / Addresses / Balance).
|
for (std::size_t i = 0; i < s_rows.size(); ++i) {
|
||||||
ImGui::TableNextRow(ImGuiTableRowFlags_Headers);
|
const WalletRow& r = s_rows[i];
|
||||||
for (int c = 0; c < 6; ++c) {
|
const data::WalletIndexEntry* meta = app->walletIndex().find(r.fileName);
|
||||||
ImGui::TableSetColumnIndex(c);
|
const bool isCurrent = r.inDatadir && r.fileName == active;
|
||||||
const char* cn = ImGui::TableGetColumnName(c);
|
|
||||||
if (c == 5 || !cn || !cn[0]) continue;
|
ImVec2 rMin = ImGui::GetCursorScreenPos();
|
||||||
if (c >= 1 && c <= 3) {
|
ImVec2 rMax(rMin.x + rowW, rMin.y + walRowH);
|
||||||
float w = ImGui::CalcTextSize(cn).x;
|
bool hov = ImGui::IsWindowHovered() && ImGui::IsMouseHoveringRect(rMin, rMax);
|
||||||
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + ImGui::GetContentRegionAvail().x - w);
|
|
||||||
ImGui::TextUnformatted(cn);
|
// Card background + state
|
||||||
} else {
|
GlassPanelSpec g; g.rounding = 10.0f; g.fillAlpha = hov ? 40 : 26; g.borderAlpha = 45;
|
||||||
ImGui::TableHeader(cn);
|
DrawGlassPanel(dl, rMin, rMax, g);
|
||||||
}
|
if (isCurrent) {
|
||||||
|
dl->AddRectFilled(rMin, rMax, WithAlpha(Success(), 16), 10.0f);
|
||||||
|
dl->AddRect(rMin, rMax, WithAlpha(Success(), 130), 10.0f, 0, 1.6f * dp);
|
||||||
|
} else if (hov) {
|
||||||
|
dl->AddRect(rMin, rMax, WithAlpha(OnSurface(), 70), 10.0f, 0, 1.0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto rcell = [&](const std::string& s, bool muted) {
|
const float padX = Layout::spacingMd();
|
||||||
float w = ImGui::CalcTextSize(s.c_str()).x;
|
const float iconSz = nameFont->LegacySize * 1.15f;
|
||||||
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + ImGui::GetContentRegionAvail().x - w);
|
const float midY = (rMin.y + rMax.y) * 0.5f;
|
||||||
if (muted) ImGui::TextDisabled("%s", s.c_str()); else ImGui::TextUnformatted(s.c_str());
|
float x = rMin.x + padX;
|
||||||
};
|
dl->AddText(icoFont, iconSz, ImVec2(x, midY - iconSz * 0.5f),
|
||||||
const char* kDash = "\xE2\x80\x94";
|
isCurrent ? Success() : OnSurfaceMedium(), ICON_MD_ACCOUNT_BALANCE_WALLET);
|
||||||
|
x += iconSz + Layout::spacingSm();
|
||||||
|
|
||||||
char nb[32];
|
// Action-button slot on the right — reserve room so text truncates before it.
|
||||||
for (std::size_t i = 0; i < s_rows.size(); ++i) {
|
const float btnW = 90.0f * dp, btnH = ImGui::GetFrameHeight();
|
||||||
const WalletRow& r = s_rows[i];
|
const float btnX = rMax.x - padX - btnW;
|
||||||
const data::WalletIndexEntry* meta = app->walletIndex().find(r.fileName);
|
const float textR = btnX - Layout::spacingMd();
|
||||||
const bool isCurrent = r.inDatadir && r.fileName == active;
|
|
||||||
|
|
||||||
ImGui::TableNextRow();
|
// Name (+ info icon for external files)
|
||||||
if (isCurrent) ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, WithAlpha(Success(), 26));
|
const float topY = rMin.y + cardPadY;
|
||||||
ImGui::PushID(static_cast<int>(i));
|
float extra = (!r.inDatadir) ? (metaFont->LegacySize + Layout::spacingSm()) : 0.0f;
|
||||||
|
std::string nm = fit(r.fileName, nameFont, std::max(24.0f * dp, textR - x - extra));
|
||||||
// Name (+ #4 current badge / #6 external-file info icon)
|
dl->AddText(nameFont, nameFont->LegacySize, ImVec2(x, topY), OnSurface(), nm.c_str());
|
||||||
ImGui::TableNextColumn();
|
if (!r.inDatadir) {
|
||||||
ImGui::TextUnformatted(r.fileName.c_str());
|
ImVec2 ip(x + tw(nameFont, nm) + Layout::spacingSm(), topY + (nameFont->LegacySize - metaFont->LegacySize));
|
||||||
if (isCurrent) {
|
dl->AddText(icoFont, metaFont->LegacySize, ip, OnSurfaceMedium(), ICON_MD_INFO);
|
||||||
ImGui::SameLine(0, 8.0f * dp);
|
if (ImGui::IsMouseHoveringRect(ip, ImVec2(ip.x + metaFont->LegacySize, ip.y + metaFont->LegacySize)))
|
||||||
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(Success()), "%s", TR("wallets_current"));
|
Tooltip("%s\n%s", TR("wallets_external_tt"), r.dir.c_str());
|
||||||
} else if (!r.inDatadir) {
|
|
||||||
ImGui::SameLine(0, 8.0f * dp);
|
|
||||||
ImGui::PushFont(Type().iconSmall());
|
|
||||||
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium()), "%s", ICON_MD_INFO);
|
|
||||||
ImGui::PopFont();
|
|
||||||
if (ImGui::IsItemHovered()) Tooltip("%s\n%s", TR("wallets_external_tt"), r.dir.c_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::TableNextColumn();
|
|
||||||
rcell(util::Platform::formatFileSize(static_cast<uint64_t>(r.sizeBytes)), false);
|
|
||||||
|
|
||||||
ImGui::TableNextColumn();
|
|
||||||
if (meta && meta->cachedAddressCount >= 0) { snprintf(nb, sizeof(nb), "%lld", (long long)meta->cachedAddressCount); rcell(nb, false); }
|
|
||||||
else rcell(kDash, true);
|
|
||||||
|
|
||||||
ImGui::TableNextColumn();
|
|
||||||
if (meta && meta->cachedBalance >= 0.0) { snprintf(nb, sizeof(nb), "%.4f", meta->cachedBalance); rcell(nb, false); }
|
|
||||||
else rcell(kDash, true);
|
|
||||||
|
|
||||||
ImGui::TableNextColumn();
|
|
||||||
if (meta && meta->lastOpenedEpoch > 0)
|
|
||||||
ImGui::TextUnformatted(util::formatTimeAgo(meta->lastOpenedEpoch).c_str());
|
|
||||||
else ImGui::TextDisabled("%s", TR("wallets_never"));
|
|
||||||
|
|
||||||
// ---- #4 Action column (Active as a chip; Open/Import as buttons) ---------------
|
|
||||||
ImGui::TableNextColumn();
|
|
||||||
if (isCurrent) {
|
|
||||||
const char* al = TR("wallets_active");
|
|
||||||
ImVec2 ts = ImGui::CalcTextSize(al);
|
|
||||||
ImVec2 p = ImGui::GetCursorScreenPos();
|
|
||||||
float cx = 8.0f * dp, cy = 3.0f * dp;
|
|
||||||
ImDrawList* cdl = ImGui::GetWindowDrawList();
|
|
||||||
cdl->AddRectFilled(p, ImVec2(p.x + ts.x + cx * 2, p.y + ts.y + cy * 2), WithAlpha(Success(), 34), 4.0f * dp);
|
|
||||||
cdl->AddText(ImVec2(p.x + cx, p.y + cy), Success(), al);
|
|
||||||
ImGui::Dummy(ImVec2(ts.x + cx * 2, ts.y + cy * 2));
|
|
||||||
} else if (r.inDatadir) {
|
|
||||||
if (StyledButton(TR("wallets_open"), ImVec2(90.0f * dp, 0))) {
|
|
||||||
app->switchToWallet(r.fileName);
|
|
||||||
s_open = false;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (StyledButton(TR("wallets_import"), ImVec2(90.0f * dp, 0)))
|
|
||||||
importAndOpen(app, r);
|
|
||||||
if (ImGui::IsItemHovered()) Tooltip("%s", TR("wallets_import_tt"));
|
|
||||||
}
|
|
||||||
ImGui::PopID();
|
|
||||||
}
|
}
|
||||||
ImGui::EndTable();
|
|
||||||
|
// Metadata line (size · N addresses · balance DRGX · last opened)
|
||||||
|
std::string ms = util::Platform::formatFileSize((uint64_t)r.sizeBytes);
|
||||||
|
char b[64];
|
||||||
|
if (meta && meta->cachedAddressCount >= 0) { snprintf(b, sizeof(b), "%s%lld %s", dot, (long long)meta->cachedAddressCount, TR("wallets_col_addresses")); ms += b; }
|
||||||
|
if (meta && meta->cachedBalance >= 0.0) { snprintf(b, sizeof(b), "%s%.4f %s", dot, meta->cachedBalance, DRAGONX_TICKER); ms += b; }
|
||||||
|
ms += dot;
|
||||||
|
ms += (meta && meta->lastOpenedEpoch > 0) ? util::formatTimeAgo(meta->lastOpenedEpoch) : std::string(TR("wallets_never"));
|
||||||
|
ms = fit(ms, metaFont, std::max(24.0f * dp, textR - x));
|
||||||
|
dl->AddText(metaFont, metaFont->LegacySize, ImVec2(x, topY + nameFont->LegacySize + Layout::spacingXs()),
|
||||||
|
OnSurfaceMedium(), ms.c_str());
|
||||||
|
|
||||||
|
// Action: Active chip (current) / Open / Import
|
||||||
|
ImGui::PushID(static_cast<int>(i));
|
||||||
|
if (isCurrent) {
|
||||||
|
const char* al = TR("wallets_active");
|
||||||
|
float aw = tw(metaFont, al), cw = aw + 16.0f * dp, ch = metaFont->LegacySize + 6.0f * dp;
|
||||||
|
ImVec2 cp(rMax.x - padX - cw, midY - ch * 0.5f);
|
||||||
|
dl->AddRectFilled(cp, ImVec2(cp.x + cw, cp.y + ch), WithAlpha(Success(), 34), 5.0f * dp);
|
||||||
|
dl->AddText(metaFont, metaFont->LegacySize, ImVec2(cp.x + 8.0f * dp, cp.y + 3.0f * dp), Success(), al);
|
||||||
|
} else if (r.inDatadir) {
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(btnX, midY - btnH * 0.5f));
|
||||||
|
if (StyledButton(TR("wallets_open"), ImVec2(btnW, btnH))) {
|
||||||
|
app->switchToWallet(r.fileName);
|
||||||
|
s_open = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(btnX, midY - btnH * 0.5f));
|
||||||
|
if (StyledButton(TR("wallets_import"), ImVec2(btnW, btnH)))
|
||||||
|
importAndOpen(app, r);
|
||||||
|
if (ImGui::IsItemHovered()) Tooltip("%s", TR("wallets_import_tt"));
|
||||||
|
}
|
||||||
|
ImGui::PopID();
|
||||||
|
|
||||||
|
// Advance to the next card.
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(rMin.x, rMax.y + cardGap));
|
||||||
|
ImGui::Dummy(ImVec2(rowW, 0));
|
||||||
}
|
}
|
||||||
|
ImGui::EndChild();
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||||
|
|
||||||
// ---- #5 Create a new wallet (primary) -------------------------------------------------
|
// ---- Create a new wallet (primary) ----------------------------------------------------
|
||||||
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), TR("wallets_new_label"));
|
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), TR("wallets_new_label"));
|
||||||
ImGui::SetNextItemWidth(-150.0f * dp);
|
ImGui::SetNextItemWidth(-150.0f * dp);
|
||||||
ImGui::InputTextWithHint("##newWalletName", TR("wallets_new_hint"), s_newName, sizeof(s_newName));
|
ImGui::InputTextWithHint("##newWalletName", TR("wallets_new_hint"), s_newName, sizeof(s_newName));
|
||||||
@@ -213,7 +221,7 @@ public:
|
|||||||
|
|
||||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||||
|
|
||||||
// ---- #5 Add-folder tucked behind a toggle so it doesn't compete with Create -----------
|
// ---- Add-folder tucked behind a toggle so it doesn't compete with Create --------------
|
||||||
if (!s_showAddFolder) {
|
if (!s_showAddFolder) {
|
||||||
ImGui::PushStyleColor(ImGuiCol_Text, OnSurfaceMedium());
|
ImGui::PushStyleColor(ImGuiCol_Text, OnSurfaceMedium());
|
||||||
ImGui::TextUnformatted(TR("wallets_add_folder_toggle"));
|
ImGui::TextUnformatted(TR("wallets_add_folder_toggle"));
|
||||||
@@ -240,7 +248,7 @@ public:
|
|||||||
|
|
||||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
// ---- #6 Refresh: icon-only, de-emphasized next to the text actions --------------------
|
// ---- Refresh: icon-only, de-emphasized next to the text actions -----------------------
|
||||||
{
|
{
|
||||||
float bh = ImGui::GetFrameHeight();
|
float bh = ImGui::GetFrameHeight();
|
||||||
IconButtonStyle rst;
|
IconButtonStyle rst;
|
||||||
|
|||||||
Reference in New Issue
Block a user