diff --git a/src/ui/windows/wallets_dialog.h b/src/ui/windows/wallets_dialog.h index 2a7dff6..c8790da 100644 --- a/src/ui/windows/wallets_dialog.h +++ b/src/ui/windows/wallets_dialog.h @@ -19,6 +19,7 @@ #include "../../app.h" #include "../../config/settings.h" +#include "../../config/version.h" #include "../../util/i18n.h" #include "../../util/platform.h" #include "../../util/text_format.h" @@ -52,15 +53,17 @@ public: if (s_needScan) { scan(); s_needScan = false; } - // ---- #1 Content-sized card (bounded) ----------------------------------------------------- - // Size the card to the wallet count (up to kMaxVisibleRows, then the list scrolls) so a short - // list doesn't float in a sea of empty space. Computed from the KNOWN row count, so it stays a - // fixed-height dialog (no auto-height feedback — that slid content off-screen on a monitor move). - const int kMaxVisibleRows = 8; - const int visRows = std::min(std::max(1, (int)s_rows.size()), kMaxVisibleRows); - const float tableRowH = ImGui::GetFrameHeight() + style.CellPadding.y * 2.0f; - const float tableHdrH = ImGui::GetTextLineHeight() + style.CellPadding.y * 2.0f; - const float tableH = tableHdrH + visRows * tableRowH + 4.0f * dp; + // ---- Content-sized card: a stack of wallet cards, sized to the wallet count (up to + // kMaxVisibleRows, then the list scrolls). Computed from the KNOWN row count so the + // dialog stays fixed-height (no auto-height feedback that slid content off on a move). -- + const int kMaxVisibleRows = 6; + ImFont* nameFont = Type().body1(); + ImFont* metaFont = Type().caption(); + const int visRows = std::min(std::max(1, (int)s_rows.size()), kMaxVisibleRows); + const float cardPadY = Layout::spacingSm(); + 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 capRow = Type().caption()->LegacySize + style.ItemSpacing.y; @@ -73,7 +76,7 @@ public: const float headH = Type().h6()->LegacySize + Layout::spacingXs() // framework h6 title + Type().caption()->LegacySize + Layout::spacingSm(); // intro + gap 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; ov.title = TR("wallets_title"); @@ -88,110 +91,115 @@ public: const std::string active = app->settings() ? app->settings()->getActiveWalletFile() : "wallet.dat"; - // ---- #2 Framed wallet list ------------------------------------------------------------ - const ImGuiTableFlags tflags = ImGuiTableFlags_ScrollY | ImGuiTableFlags_RowBg | - ImGuiTableFlags_BordersInnerH | ImGuiTableFlags_BordersOuter | - ImGuiTableFlags_PadOuterX; - if (ImGui::BeginTable("##wallets", 6, tflags, ImVec2(0, tableH))) { - ImGui::TableSetupColumn(TR("wallets_col_name"), ImGuiTableColumnFlags_WidthStretch); - ImGui::TableSetupColumn(TR("wallets_col_size"), ImGuiTableColumnFlags_WidthFixed, 90.0f * dp); - ImGui::TableSetupColumn(TR("wallets_col_addresses"), ImGuiTableColumnFlags_WidthFixed, 90.0f * dp); - ImGui::TableSetupColumn(TR("wallets_col_balance"), ImGuiTableColumnFlags_WidthFixed, 130.0f * dp); - ImGui::TableSetupColumn(TR("wallets_col_opened"), ImGuiTableColumnFlags_WidthFixed, 120.0f * dp); - ImGui::TableSetupColumn("##action", ImGuiTableColumnFlags_WidthFixed, 100.0f * dp); - ImGui::TableSetupScrollFreeze(0, 1); + // ---- Wallet cards (Material-style rows; no table) ------------------------------------- + const float listW = ImGui::GetContentRegionAvail().x; + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0)); + ImGui::BeginChild("##walletList", ImVec2(listW, listH), false); + ImGui::PopStyleVar(); + ImDrawList* dl = ImGui::GetWindowDrawList(); + const float rowW = ImGui::GetContentRegionAvail().x; + ImFont* icoFont = Type().iconSmall(); + const char* dot = " \xC2\xB7 "; + auto tw = [](ImFont* f, const std::string& s){ return f->CalcTextSizeA(f->LegacySize, FLT_MAX, 0, s.c_str()).x; }; + auto fit = [&](std::string s, ImFont* f, float maxW){ + 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(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). - ImGui::TableNextRow(ImGuiTableRowFlags_Headers); - for (int c = 0; c < 6; ++c) { - ImGui::TableSetColumnIndex(c); - const char* cn = ImGui::TableGetColumnName(c); - if (c == 5 || !cn || !cn[0]) continue; - if (c >= 1 && c <= 3) { - float w = ImGui::CalcTextSize(cn).x; - ImGui::SetCursorPosX(ImGui::GetCursorPosX() + ImGui::GetContentRegionAvail().x - w); - ImGui::TextUnformatted(cn); - } else { - ImGui::TableHeader(cn); - } + for (std::size_t i = 0; i < s_rows.size(); ++i) { + const WalletRow& r = s_rows[i]; + const data::WalletIndexEntry* meta = app->walletIndex().find(r.fileName); + const bool isCurrent = r.inDatadir && r.fileName == active; + + ImVec2 rMin = ImGui::GetCursorScreenPos(); + ImVec2 rMax(rMin.x + rowW, rMin.y + walRowH); + bool hov = ImGui::IsWindowHovered() && ImGui::IsMouseHoveringRect(rMin, rMax); + + // Card background + state + GlassPanelSpec g; g.rounding = 10.0f; g.fillAlpha = hov ? 40 : 26; g.borderAlpha = 45; + 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) { - float w = ImGui::CalcTextSize(s.c_str()).x; - ImGui::SetCursorPosX(ImGui::GetCursorPosX() + ImGui::GetContentRegionAvail().x - w); - if (muted) ImGui::TextDisabled("%s", s.c_str()); else ImGui::TextUnformatted(s.c_str()); - }; - const char* kDash = "\xE2\x80\x94"; + const float padX = Layout::spacingMd(); + const float iconSz = nameFont->LegacySize * 1.15f; + const float midY = (rMin.y + rMax.y) * 0.5f; + float x = rMin.x + padX; + dl->AddText(icoFont, iconSz, ImVec2(x, midY - iconSz * 0.5f), + isCurrent ? Success() : OnSurfaceMedium(), ICON_MD_ACCOUNT_BALANCE_WALLET); + x += iconSz + Layout::spacingSm(); - char nb[32]; - for (std::size_t i = 0; i < s_rows.size(); ++i) { - const WalletRow& r = s_rows[i]; - const data::WalletIndexEntry* meta = app->walletIndex().find(r.fileName); - const bool isCurrent = r.inDatadir && r.fileName == active; + // Action-button slot on the right — reserve room so text truncates before it. + const float btnW = 90.0f * dp, btnH = ImGui::GetFrameHeight(); + const float btnX = rMax.x - padX - btnW; + const float textR = btnX - Layout::spacingMd(); - ImGui::TableNextRow(); - if (isCurrent) ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, WithAlpha(Success(), 26)); - ImGui::PushID(static_cast(i)); - - // Name (+ #4 current badge / #6 external-file info icon) - ImGui::TableNextColumn(); - ImGui::TextUnformatted(r.fileName.c_str()); - if (isCurrent) { - ImGui::SameLine(0, 8.0f * dp); - ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(Success()), "%s", TR("wallets_current")); - } 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(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(); + // Name (+ info icon for external files) + const float topY = rMin.y + cardPadY; + 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)); + dl->AddText(nameFont, nameFont->LegacySize, ImVec2(x, topY), OnSurface(), nm.c_str()); + if (!r.inDatadir) { + ImVec2 ip(x + tw(nameFont, nm) + Layout::spacingSm(), topY + (nameFont->LegacySize - metaFont->LegacySize)); + dl->AddText(icoFont, metaFont->LegacySize, ip, OnSurfaceMedium(), ICON_MD_INFO); + if (ImGui::IsMouseHoveringRect(ip, ImVec2(ip.x + metaFont->LegacySize, ip.y + metaFont->LegacySize))) + Tooltip("%s\n%s", TR("wallets_external_tt"), r.dir.c_str()); } - 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(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())); - // ---- #5 Create a new wallet (primary) ------------------------------------------------- + // ---- Create a new wallet (primary) ---------------------------------------------------- Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), TR("wallets_new_label")); ImGui::SetNextItemWidth(-150.0f * dp); ImGui::InputTextWithHint("##newWalletName", TR("wallets_new_hint"), s_newName, sizeof(s_newName)); @@ -213,7 +221,7 @@ public: 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) { ImGui::PushStyleColor(ImGuiCol_Text, OnSurfaceMedium()); ImGui::TextUnformatted(TR("wallets_add_folder_toggle")); @@ -240,7 +248,7 @@ public: ImGui::Dummy(ImVec2(0, Layout::spacingSm())); 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(); IconButtonStyle rst;