diff --git a/res/fonts/NotoSansCJK-Subset.ttf b/res/fonts/NotoSansCJK-Subset.ttf index 0e78496..0ff306e 100644 Binary files a/res/fonts/NotoSansCJK-Subset.ttf and b/res/fonts/NotoSansCJK-Subset.ttf differ diff --git a/res/lang/de.json b/res/lang/de.json index 943cc73..2b1feb6 100644 --- a/res/lang/de.json +++ b/res/lang/de.json @@ -839,6 +839,10 @@ "portfolio_spark_min": "Minute", "portfolio_spark_month": "Monat", "portfolio_spark_week": "Woche", + "portfolio_style_compact": "Kompakte Zeilen", + "portfolio_style_detailed": "Detaillierte Zeilen", + "portfolio_style_featured": "Hervorgehobene Zeilen", + "portfolio_style_label": "Portfolio-Stil", "portfolio_untitled": "Ohne Titel", "price_chart": "Preisdiagramm", "privacy_great": "Großartige Privatsphäre!", @@ -1341,6 +1345,7 @@ "wallets_new_hint": "Name (z. B. Ersparnisse)", "wallets_new_label": "Eine neue Wallet erstellen:", "wallets_open": "Öffnen", + "wallets_reveal": "Ordner anzeigen", "wallets_title": "Wallets", "warning": "Warnung", "warning_upper": "WARNUNG!", diff --git a/res/lang/es.json b/res/lang/es.json index c070664..fe49dfe 100644 --- a/res/lang/es.json +++ b/res/lang/es.json @@ -839,6 +839,10 @@ "portfolio_spark_min": "Minuto", "portfolio_spark_month": "Mes", "portfolio_spark_week": "Semana", + "portfolio_style_compact": "Filas compactas", + "portfolio_style_detailed": "Filas detalladas", + "portfolio_style_featured": "Filas destacadas", + "portfolio_style_label": "Estilo de cartera", "portfolio_untitled": "Sin título", "price_chart": "Gráfico de Precios", "privacy_great": "¡Excelente privacidad!", @@ -1341,6 +1345,7 @@ "wallets_new_hint": "Nombre (p. ej. ahorros)", "wallets_new_label": "Crear una nueva cartera:", "wallets_open": "Abrir", + "wallets_reveal": "Mostrar carpeta", "wallets_title": "Carteras", "warning": "Advertencia", "warning_upper": "¡ADVERTENCIA!", diff --git a/res/lang/fr.json b/res/lang/fr.json index fe66f28..6ac7aed 100644 --- a/res/lang/fr.json +++ b/res/lang/fr.json @@ -839,6 +839,10 @@ "portfolio_spark_min": "Minute", "portfolio_spark_month": "Mois", "portfolio_spark_week": "Semaine", + "portfolio_style_compact": "Lignes compactes", + "portfolio_style_detailed": "Lignes détaillées", + "portfolio_style_featured": "Lignes en vedette", + "portfolio_style_label": "Style du portefeuille", "portfolio_untitled": "Sans titre", "price_chart": "Graphique des prix", "privacy_great": "Excellente confidentialité !", @@ -1341,6 +1345,7 @@ "wallets_new_hint": "Nom (par ex. épargne)", "wallets_new_label": "Créer un nouveau portefeuille :", "wallets_open": "Ouvrir", + "wallets_reveal": "Afficher le dossier", "wallets_title": "Portefeuilles", "warning": "Attention", "warning_upper": "ATTENTION !", diff --git a/res/lang/ja.json b/res/lang/ja.json index abd9c7f..1e4c18a 100644 --- a/res/lang/ja.json +++ b/res/lang/ja.json @@ -839,6 +839,10 @@ "portfolio_spark_min": "分", "portfolio_spark_month": "月", "portfolio_spark_week": "週", + "portfolio_style_compact": "コンパクト行", + "portfolio_style_detailed": "詳細行", + "portfolio_style_featured": "注目行", + "portfolio_style_label": "ポートフォリオスタイル", "portfolio_untitled": "無題", "price_chart": "価格チャート", "privacy_great": "優れたプライバシーです!", @@ -1341,6 +1345,7 @@ "wallets_new_hint": "名前(例:savings)", "wallets_new_label": "新しいウォレットを作成:", "wallets_open": "開く", + "wallets_reveal": "フォルダを開く", "wallets_title": "ウォレット", "warning": "警告", "warning_upper": "警告!", diff --git a/res/lang/ko.json b/res/lang/ko.json index e958b63..63825b7 100644 --- a/res/lang/ko.json +++ b/res/lang/ko.json @@ -839,6 +839,10 @@ "portfolio_spark_min": "분", "portfolio_spark_month": "월", "portfolio_spark_week": "주", + "portfolio_style_compact": "간결한 행", + "portfolio_style_detailed": "상세 행", + "portfolio_style_featured": "강조 행", + "portfolio_style_label": "포트폴리오 스타일", "portfolio_untitled": "제목 없음", "price_chart": "가격 차트", "privacy_great": "프라이버시가 우수합니다!", @@ -1341,6 +1345,7 @@ "wallets_new_hint": "이름(예: savings)", "wallets_new_label": "새 지갑 만들기:", "wallets_open": "열기", + "wallets_reveal": "폴더 열기", "wallets_title": "지갑", "warning": "경고", "warning_upper": "경고!", diff --git a/res/lang/pt.json b/res/lang/pt.json index 11806f3..7d95b2a 100644 --- a/res/lang/pt.json +++ b/res/lang/pt.json @@ -839,6 +839,10 @@ "portfolio_spark_min": "Minuto", "portfolio_spark_month": "Mês", "portfolio_spark_week": "Semana", + "portfolio_style_compact": "Linhas compactas", + "portfolio_style_detailed": "Linhas detalhadas", + "portfolio_style_featured": "Linhas em destaque", + "portfolio_style_label": "Estilo do portfólio", "portfolio_untitled": "Sem título", "price_chart": "Gráfico de Preços", "privacy_great": "Ótima privacidade!", @@ -1341,6 +1345,7 @@ "wallets_new_hint": "Nome (ex.: poupança)", "wallets_new_label": "Criar uma nova carteira:", "wallets_open": "Abrir", + "wallets_reveal": "Mostrar pasta", "wallets_title": "Carteiras", "warning": "Aviso", "warning_upper": "AVISO!", diff --git a/res/lang/ru.json b/res/lang/ru.json index 1d004fc..e8a0cae 100644 --- a/res/lang/ru.json +++ b/res/lang/ru.json @@ -839,6 +839,10 @@ "portfolio_spark_min": "Минута", "portfolio_spark_month": "Месяц", "portfolio_spark_week": "Неделя", + "portfolio_style_compact": "Компактные строки", + "portfolio_style_detailed": "Подробные строки", + "portfolio_style_featured": "Избранные строки", + "portfolio_style_label": "Стиль портфеля", "portfolio_untitled": "Без названия", "price_chart": "График цен", "privacy_great": "Отличная конфиденциальность!", @@ -1341,6 +1345,7 @@ "wallets_new_hint": "Имя (например, savings)", "wallets_new_label": "Создать новый кошелёк:", "wallets_open": "Открыть", + "wallets_reveal": "Открыть папку", "wallets_title": "Кошельки", "warning": "Предупреждение", "warning_upper": "ПРЕДУПРЕЖДЕНИЕ!", diff --git a/res/lang/zh.json b/res/lang/zh.json index 9e7042e..f24a0d8 100644 --- a/res/lang/zh.json +++ b/res/lang/zh.json @@ -839,6 +839,10 @@ "portfolio_spark_min": "分钟", "portfolio_spark_month": "月", "portfolio_spark_week": "周", + "portfolio_style_compact": "紧凑行", + "portfolio_style_detailed": "详细行", + "portfolio_style_featured": "特色行", + "portfolio_style_label": "投资组合样式", "portfolio_untitled": "未命名", "price_chart": "价格图表", "privacy_great": "隐私性极佳!", @@ -1341,6 +1345,7 @@ "wallets_new_hint": "名称(例如 savings)", "wallets_new_label": "创建新钱包:", "wallets_open": "打开", + "wallets_reveal": "打开文件夹", "wallets_title": "钱包", "warning": "警告", "warning_upper": "警告!", diff --git a/src/app.h b/src/app.h index d3c8c02..b74f72c 100644 --- a/src/app.h +++ b/src/app.h @@ -833,6 +833,7 @@ private: std::vector addresses, z_addresses, t_addresses; std::vector transactions; double market_price_usd=0; + double market_change_24h=0; } sweep_state_snapshot_; bool sidebar_user_toggled_ = false; // user manually toggled — suppress auto-collapse float sidebar_width_anim_ = 0.0f; // animated width (0 = uninitialized) diff --git a/src/app_sweep.cpp b/src/app_sweep.cpp index 697ef50..1fdbf24 100644 --- a/src/app_sweep.cpp +++ b/src/app_sweep.cpp @@ -13,6 +13,7 @@ #include "app.h" +#include "config/settings.h" #include "ui/schema/skin_manager.h" #include "ui/notifications.h" #include "ui/sidebar.h" @@ -23,6 +24,7 @@ #include +#include #include #include @@ -31,6 +33,13 @@ namespace dragonx { namespace { namespace fs = std::filesystem; +// The real portfolio, snapshotted while demo groups are shown during a full sweep. Settings are +// forward-declared in app.h, so this can't live in the app.h SweepStateSnapshot struct. +std::vector g_pfSnapshot; +int g_pfStyleSnapshot = 0; +bool g_pfSnapshotValid = false; +std::vector g_marketHistorySnapshot; // real price history, restored at sweep end + // Filesystem-safe one-word tab name. const char* sweepPageName(ui::NavPage page) { @@ -93,6 +102,7 @@ void App::installDemoWalletData() s.addresses = state_.addresses; s.z_addresses = state_.z_addresses; s.t_addresses = state_.t_addresses; s.transactions = state_.transactions; s.market_price_usd = state_.market.price_usd; + s.market_change_24h = state_.market.change_24h; s.valid = true; applyHealthyDemoState(); @@ -102,6 +112,18 @@ void App::installDemoWalletData() state_.privateBalance = 12.50000000; state_.transparentBalance = 3.25000000; state_.totalBalance = 15.75000000; state_.unconfirmedBalance = 0.50000000; state_.market.price_usd = 0.01336200; + state_.market.change_24h = 5.24000000; + // Wavy, gently-rising price history so the row sparklines render (restored at sweep end). + g_marketHistorySnapshot = state_.market.price_history; + { + std::vector hist; + for (int i = 0; i < 48; i++) { + double t = static_cast(i); + hist.push_back(0.01250 + 0.0000180 * t + + 0.00060 * std::sin(t * 0.55) + 0.00025 * std::sin(t * 1.7)); + } + state_.market.price_history = hist; + } auto zaddr = [](const char* a, double bal, const char* label) { AddressInfo i; i.address = a; i.balance = bal; i.type = "shielded"; i.label = label; return i; @@ -135,6 +157,32 @@ void App::installDemoWalletData() 1751290000, 0, "zs1demosecondaryshieldedaddressforuisweep0000000000000000000000000", "pending"), }; + // Demo portfolio groups so the Market tab's row styles render with real-looking data. Snapshot + // the user's real portfolio first; setPortfolio* only mutate memory (save() is never called + // here), and clearDemoWalletData() restores them at sweep end. + if (settings_) { + g_pfSnapshot = settings_->getPortfolioEntries(); + g_pfStyleSnapshot = settings_->getPortfolioStyle(); + g_pfSnapshotValid = true; + auto grp = [](const char* label, const char* icon, uint32_t color, const char* addr, + bool drgx, bool value, bool ch, bool spark) { + config::Settings::PortfolioEntry e; + e.label = label; e.icon = icon; e.color = color; e.outlineOpacity = 30; + e.addresses = { addr }; e.priceBasis = 0; + e.showDrgx = drgx; e.showValue = value; e.show24h = ch; e.showSparkline = spark; + return e; + }; + settings_->setPortfolioEntries({ + grp("Savings", "savings", 0xFFFF9D4Fu, + "zs1demoprimaryshieldedaddressforuisweep000000000000000000000000000", true, true, true, true), + grp("Mining rewards", "pickaxe", 0xFF3DB0FFu, + "t1DemoTransparentAddressForUiSweep00000", true, true, true, true), + grp("Cold storage", "diamond", 0xFFFF7BB0u, + "zs1demosecondaryshieldedaddressforuisweep0000000000000000000000000", true, true, false, false), + }); + settings_->setPortfolioStyle(0); + } + seedChatDemoData(); } @@ -168,7 +216,18 @@ void App::clearDemoWalletData() state_.addresses = s.addresses; state_.z_addresses = s.z_addresses; state_.t_addresses = s.t_addresses; state_.transactions = s.transactions; state_.market.price_usd = s.market_price_usd; + state_.market.change_24h = s.market_change_24h; + state_.market.price_history = g_marketHistorySnapshot; + g_marketHistorySnapshot.clear(); s = SweepStateSnapshot{}; // invalidate + + // Restore the user's real portfolio (demo groups were in-memory only). + if (g_pfSnapshotValid && settings_) { + settings_->setPortfolioEntries(g_pfSnapshot); + settings_->setPortfolioStyle(g_pfStyleSnapshot); + } + g_pfSnapshot.clear(); + g_pfSnapshotValid = false; } // ── Catalog ────────────────────────────────────────────────────────────────────────────────── @@ -326,6 +385,17 @@ void App::buildSweepCatalog() add("popup-send-confirm", ui::NavPage::Send, [](App& a) { ui::SweepShowSendConfirm(&a, true); }, [](App& a) { ui::SweepShowSendConfirm(&a, false); }); + + // Market portfolio row styles — capture all three so the redesign is reviewable per skin. + add("market-rows-compact", ui::NavPage::Market, + [](App& a) { if (a.settings_) a.settings_->setPortfolioStyle(0); }, + [](App& a) { if (a.settings_) a.settings_->setPortfolioStyle(0); }); + add("market-rows-detailed", ui::NavPage::Market, + [](App& a) { if (a.settings_) a.settings_->setPortfolioStyle(1); }, + [](App& a) { if (a.settings_) a.settings_->setPortfolioStyle(0); }); + add("market-rows-featured", ui::NavPage::Market, + [](App& a) { if (a.settings_) a.settings_->setPortfolioStyle(2); }, + [](App& a) { if (a.settings_) a.settings_->setPortfolioStyle(0); }); } // ── State machine ─────────────────────────────────────────────────────────────────────────── diff --git a/src/config/settings.cpp b/src/config/settings.cpp index 8e4fd7c..0f1fc4f 100644 --- a/src/config/settings.cpp +++ b/src/config/settings.cpp @@ -172,6 +172,8 @@ bool Settings::load(const std::string& path) if (idx >= 0 && idx < 9) balance_layout_ = legacyIds[idx]; } } + loadScalar(j, "portfolio_style", portfolio_style_); + if (portfolio_style_ < 0 || portfolio_style_ > 2) portfolio_style_ = 0; loadScalar(j, "scanline_enabled", scanline_enabled_); if (j.contains("hidden_addresses") && j["hidden_addresses"].is_array()) { hidden_addresses_.clear(); @@ -344,6 +346,7 @@ bool Settings::load(const std::string& path) entry.showSparkline = e["show_sparkline"].get(); if (e.contains("sparkline_interval") && e["sparkline_interval"].is_number_integer()) entry.sparklineInterval = e["sparkline_interval"].get(); + entry.scope = e.value("scope", ""); if (e.contains("grid_col") && e["grid_col"].is_number_integer()) entry.gridCol = e["grid_col"].get(); if (e.contains("grid_row") && e["grid_row"].is_number_integer()) @@ -418,6 +421,7 @@ bool Settings::save(const std::string& path) j["ui_opacity"] = ui_opacity_; j["window_opacity"] = window_opacity_; j["balance_layout"] = balance_layout_; // saved as string ID + j["portfolio_style"] = portfolio_style_; j["scanline_enabled"] = scanline_enabled_; j["hidden_addresses"] = json::array(); for (const auto& addr : hidden_addresses_) @@ -520,6 +524,7 @@ bool Settings::save(const std::string& path) entry["show_24h"] = e.show24h; entry["show_sparkline"] = e.showSparkline; entry["sparkline_interval"] = e.sparklineInterval; + entry["scope"] = e.scope; entry["grid_col"] = e.gridCol; entry["grid_row"] = e.gridRow; entry["grid_w"] = e.gridW; diff --git a/src/config/settings.h b/src/config/settings.h index 6c6a8c1..96ba965 100644 --- a/src/config/settings.h +++ b/src/config/settings.h @@ -92,12 +92,16 @@ public: bool show24h = false; // show the 24h % change (live-market bases only) bool showSparkline = false; // show a price-trend sparkline (live-market bases only) int sparklineInterval = 0; // 0=minute 1=hour 2=day 3=week 4=month (resample of price history) - // Dashboard grid placement on the Market portfolio. col/row in fine ~32px square cells - // (-1 = auto-place), w/h in cells (span). Set when the user drags/resizes a card. + // Per-wallet visibility: "" (shown in every wallet — legacy/global) or a wallet-identity + // hash (shown only when that wallet is active). New entries are tagged with the current + // wallet so a portfolio built for wallet A doesn't clutter wallet B. + std::string scope; + // Legacy dashboard-grid placement (deprecated by the row layout; kept for back-compat so an + // older build's saved positions aren't dropped, but no longer used by the renderer). int gridCol = -1; int gridRow = -1; - int gridW = 8; // default group width (cells); minimum is 8 - int gridH = 3; // default group height (cells); minimum is 2, card design adapts + int gridW = 8; + int gridH = 3; }; // Theme @@ -176,6 +180,11 @@ public: std::string getBalanceLayout() const { return balance_layout_; } void setBalanceLayout(const std::string& v) { balance_layout_ = v; } + // Market-tab portfolio row style: 0 = single-line, 1 = two-line, 2 = value-hero. Cycled with + // Left/Right arrows on the Market tab (like the Overview layouts). + int getPortfolioStyle() const { return portfolio_style_; } + void setPortfolioStyle(int v) { portfolio_style_ = (v < 0 || v > 2) ? 0 : v; } + // Console scanline effect bool getScanlineEnabled() const { return scanline_enabled_; } void setScanlineEnabled(bool v) { scanline_enabled_ = v; } @@ -467,6 +476,7 @@ private: float window_opacity_ = 1.0f; // Mac/Linux: default fully opaque #endif std::string balance_layout_ = "classic"; + int portfolio_style_ = 0; // Market portfolio row style (0 single / 1 two-line / 2 hero) bool scanline_enabled_ = true; std::set hidden_addresses_; std::set favorite_addresses_; diff --git a/src/ui/windows/market_tab.cpp b/src/ui/windows/market_tab.cpp index 71cd270..589d221 100644 --- a/src/ui/windows/market_tab.cpp +++ b/src/ui/windows/market_tab.cpp @@ -18,6 +18,7 @@ #include "../material/colors.h" #include "../material/typography.h" #include "../material/project_icons.h" +#include "../notifications.h" #include "../effects/imgui_acrylic.h" #include "../effects/scroll_fade_shader.h" #include "../effects/low_spec.h" @@ -175,18 +176,6 @@ static void pfDrawSparklineFilled(ImDrawList* dl, ImVec2 mn, ImVec2 mx, dl->AddPolyline(pts.data(), n, WithAlpha(lineCol, 220), ImDrawFlags_None, 1.4f); } -// Dashboard-grid state for the portfolio cards (drag to move, corner-drag to resize). -struct PfCell { int col, row, w, h; }; -struct PfGridDrag { - int drag = -1; // card index being dragged (-1 = none) - int resize = -1; // card index being resized - bool moved = false; // drag passed the click threshold this gesture - ImVec2 press; // mouse pos at grab - ImVec2 grab; // mouse offset within the card at grab - PfCell old = {0,0,1,1}; // dragged card's cell at grab (for swap-on-drop) - PfCell target = {0,0,1,1};// live drop target cell -}; -static PfGridDrag s_grid; // Build the right-hand amount strings for a group card / preview from its price-data config. struct PfDisplay { @@ -326,14 +315,18 @@ static void pfPersist(config::Settings* settings, const std::vectorsettings(); if (s_pfEdit.label[0] == '\0') return; auto entries = settings->getPortfolioEntries(); bool existing = (s_pfEdit.sel >= 0 && s_pfEdit.sel < (int)entries.size()); config::Settings::PortfolioEntry base; if (existing) { base = entries[s_pfEdit.sel]; if (pfWorkingMatches(base)) return; } auto e = pfBuildWorking(base); + if (!existing) e.scope = app->activeWalletIdentityHash(); if (existing) entries[s_pfEdit.sel] = e; else { entries.push_back(e); s_pfEdit.sel = (int)entries.size() - 1; } pfPersist(settings, entries); @@ -830,6 +823,7 @@ static void RenderPortfolioEditor(App* app) auto es = settings->getPortfolioEntries(); config::Settings::PortfolioEntry ne; ne.label = TR("portfolio_untitled"); + ne.scope = app->activeWalletIdentityHash(); // new group defaults to this wallet es.push_back(ne); pfPersist(settings, es); s_pfEdit.sel = (int)es.size() - 1; @@ -956,7 +950,7 @@ static void RenderPortfolioEditor(App* app) PortfolioBeginEdit(app, s_pfEdit.sel); // reload the working set from the stored entry ImGui::SameLine(0, sp); ImGui::BeginDisabled(s_pfEdit.label[0] == '\0'); - if (material::TactileButton(TR("portfolio_save"), ImVec2(bw, addH))) pfCommitIfNeeded(settings); + if (material::TactileButton(TR("portfolio_save"), ImVec2(bw, addH))) pfCommitIfNeeded(app); ImGui::EndDisabled(); ImGui::EndDisabled(); } @@ -967,7 +961,7 @@ static void RenderPortfolioEditor(App* app) { float bh = 40.0f * dp, bw = 120.0f * dp; material::RightAlignX(bw); - if (material::TactileButton(TR("portfolio_close"), ImVec2(bw, bh))) { pfCommitIfNeeded(settings); s_pfEdit.open = false; } + if (material::TactileButton(TR("portfolio_close"), ImVec2(bw, bh))) { pfCommitIfNeeded(app); s_pfEdit.open = false; } } material::EndOverlayDialog(); @@ -975,157 +969,95 @@ static void RenderPortfolioEditor(App* app) bool PortfolioEditorActive() { return s_pfEdit.open; } -// Resolve each portfolio group's grid cell: honor a stored placement when it fits, else auto-place -// row-major into the first free cells. Pure + deterministic (no ImGui). One PfCell per entry. -static std::vector pfComputeGridLayout( - const std::vector& entries, int cols, int minW, int minH) -{ - int n = (int)entries.size(); - std::vector layout((size_t)std::max(0, n)); - std::vector> occ; - auto ensure = [&](int rEnd) { while ((int)occ.size() < rEnd) occ.emplace_back(cols, 0); }; - auto fits = [&](int c, int r, int w, int h) -> bool { - if (c < 0 || r < 0 || c + w > cols) return false; - ensure(r + h); - for (int rr = r; rr < r + h; rr++) for (int cc = c; cc < c + w; cc++) if (occ[rr][cc]) return false; - return true; - }; - auto mark = [&](int c, int r, int w, int h) { ensure(r + h); - for (int rr = r; rr < r + h; rr++) for (int cc = c; cc < c + w; cc++) occ[rr][cc] = 1; }; - std::vector placed((size_t)std::max(0, n), 0); - for (int i = 0; i < n; i++) { - const auto& e = entries[i]; - int w = std::max(minW, std::min(e.gridW, cols)), h = std::max(minH, e.gridH); - if (e.gridCol >= 0 && e.gridRow >= 0 && fits(e.gridCol, e.gridRow, w, h)) { - layout[i] = {e.gridCol, e.gridRow, w, h}; mark(e.gridCol, e.gridRow, w, h); placed[i] = 1; - } - } - for (int i = 0; i < n; i++) { - if (placed[i]) continue; - const auto& e = entries[i]; - int w = std::max(minW, std::min(e.gridW, cols)), h = std::max(minH, e.gridH); - int fc = 0, fr = 0; bool found = false; - for (int r = 0; !found && r < 4096; r++) - for (int c = 0; c + w <= cols; c++) - if (fits(c, r, w, h)) { fc = c; fr = r; found = true; break; } - layout[i] = {fc, fr, w, h}; mark(fc, fr, w, h); - } - return layout; -} -// Shared drawing context for a portfolio group card (bound once per frame in RenderMarketTab). -struct MktCardCtx { - ImDrawList* dl; - const WalletState* state; - const MarketInfo* market; - float mktDp; - float pfInset; - float pfCardPad; - ImFont* sub1; - ImFont* capFont; -}; - -// Draw one portfolio group card (name/icon, value + %chg, secondary DRGX, sparkline body) within -// [gcMin,gcMax] (inset for gaps). hov = hover/active highlight. Pure drawing into cx.dl. -static void mktDrawCard(const MktCardCtx& cx, ImVec2 gcMin, ImVec2 gcMax, - const config::Settings::PortfolioEntry& e, bool hov) +// ---- Portfolio ROW layouts (replace the drag/resize grid) -------------------------------------- +// Draw one group as a full-width row card in [rowMin,rowMax]; style 0=single-line, 1=two-line, +// 2=value-hero. Keeps every field (icon, accent, label, DRGX, value, 24h %, sparkline) — hidden +// fields per the entry's config simply don't render. Pure drawing into dl. +static void pfDrawRow(ImDrawList* dl, ImVec2 rowMin, ImVec2 rowMax, + const config::Settings::PortfolioEntry& e, const WalletState& state, + const MarketInfo& market, int style, float dp, + ImFont* sub1, ImFont* body2, ImFont* capFont, bool hov) { - ImDrawList* dl = cx.dl; - const auto& state = *cx.state; - const auto& market = *cx.market; - float mktDp = cx.mktDp, pfInset = cx.pfInset; - ImFont* sub1 = cx.sub1, *capFont = cx.capFont; - // The grid is gapless, so inset the card within its cell block for separation. - gcMin.x += pfInset; gcMin.y += pfInset; gcMax.x -= pfInset; gcMax.y -= pfInset; double bal = data::SumPortfolioBalance(e.addresses, state.addresses); ImU32 accent = e.color ? (ImU32)e.color : 0; - float pad = cx.pfCardPad; - GlassPanelSpec gcGlass; gcGlass.rounding = 10.0f; - gcGlass.fillAlpha = hov ? 34 : 22; gcGlass.borderAlpha = 40; - DrawGlassPanel(dl, gcMin, gcMax, gcGlass); - // Accented groups get a colored outline around the whole card (configurable opacity); - // others get a subtle hover ring. + float stripW = (style == 2 ? 5.0f : 3.0f) * dp; + + GlassPanelSpec g; g.rounding = 10.0f; g.fillAlpha = hov ? 34 : 22; g.borderAlpha = 40; + DrawGlassPanel(dl, rowMin, rowMax, g); if (accent) { + dl->AddRectFilled(rowMin, ImVec2(rowMin.x + stripW, rowMax.y), accent, 10.0f, ImDrawFlags_RoundCornersLeft); int oa = (int)(std::max(0, std::min(100, e.outlineOpacity)) * 2.55f + 0.5f); - dl->AddRect(gcMin, gcMax, WithAlpha(accent, oa), 10.0f, 0, hov ? 2.5f : 2.0f); - } else if (hov) dl->AddRect(gcMin, gcMax, WithAlpha(OnSurface(), 70), 10.0f, 0, 1.0f); + dl->AddRect(rowMin, rowMax, WithAlpha(accent, oa), 10.0f, 0, hov ? 2.0f : 1.5f); + } else if (hov) { + dl->AddRect(rowMin, rowMax, WithAlpha(OnSurface(), 70), 10.0f, 0, 1.0f); + } - auto fit = [&](std::string s, ImFont* f, float maxW) { - bool t = false; - while (s.size() > 1 && f->CalcTextSizeA(f->LegacySize, FLT_MAX, 0, s.c_str()).x > maxW) { s.pop_back(); t = true; } - if (t) s += "\xE2\x80\xA6"; - return s; - }; + 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){ s.pop_back(); t=true; } if (t) s += "\xE2\x80\xA6"; return s; }; - // Value + DRGX strings per basis. - char vb[80]; std::string valueStr; bool hasValue = false; - if (e.priceBasis == 0 && market.price_usd > 0) { snprintf(vb, sizeof(vb), "$%.2f", bal * market.price_usd); valueStr = vb; hasValue = true; } - else if (e.priceBasis == 1 && market.price_btc > 0) { snprintf(vb, sizeof(vb), "%.8f BTC", bal * market.price_btc); valueStr = vb; hasValue = true; } - else if (e.priceBasis == 3 && e.manualPrice > 0.0) { snprintf(vb, sizeof(vb), "%.2f %s", bal * e.manualPrice, e.manualCurrency.c_str()); valueStr = vb; hasValue = true; } - char db[48]; snprintf(db, sizeof(db), "%.4f %s", bal, DRAGONX_TICKER); - bool heroIsValue = e.showValue && hasValue; - std::string heroStr = heroIsValue ? valueStr : (e.showDrgx ? std::string(db) : (hasValue ? valueStr : std::string(db))); - std::string footStr = (heroIsValue && e.showDrgx) ? std::string(db) : std::string(); + char vb[80]; std::string valStr; bool hasValue=false; + if (e.priceBasis==0 && market.price_usd>0){ snprintf(vb,sizeof(vb),"$%.2f",bal*market.price_usd); valStr=vb; hasValue=true; } + else if (e.priceBasis==1 && market.price_btc>0){ snprintf(vb,sizeof(vb),"%.8f BTC",bal*market.price_btc); valStr=vb; hasValue=true; } + else if (e.priceBasis==3 && e.manualPrice>0.0){ snprintf(vb,sizeof(vb),"%.2f %s",bal*e.manualPrice,e.manualCurrency.c_str()); valStr=vb; hasValue=true; } + char db[48]; snprintf(db,sizeof(db),"%.4f %s",bal,DRAGONX_TICKER); + std::string drgxStr = e.showDrgx ? std::string(db) : std::string(); + std::string valueStr = (e.showValue && hasValue) ? valStr : std::string(); + bool showChange = e.show24h && (e.priceBasis==0||e.priceBasis==1) && market.change_24h!=0.0; + std::string chgStr; if (showChange){ char cb[32]; snprintf(cb,sizeof(cb),"%+.2f%%",market.change_24h); chgStr=cb; } + ImU32 chgCol = market.change_24h>=0 ? Success() : Error(); + bool showSpark = e.showSparkline && (e.priceBasis==0||e.priceBasis==1); + std::vector spark; + if (showSpark){ spark=data::sparklineSeries(market,e.sparklineInterval); if (spark.size()<2) showSpark=false; } + ImU32 sparkCol = (!spark.empty() && spark.back()>=spark.front()) ? Success() : Error(); - // Compact stat card: name (+ secondary DRGX) on the left; value with % change stacked - // below it in the top-right corner; the sparkline fills the body — so it renders even - // on a 2-cell card. Tighter padding on short cards. - float cardHeight = gcMax.y - gcMin.y; - bool cardShort = cardHeight < 90.0f * mktDp; - float hpad = cardShort ? Layout::spacingSm() : pad; - float hleft = gcMin.x + hpad, hright = gcMax.x - hpad; - float hAvailW = hright - hleft; - float topY = gcMin.y + hpad; + const float padX = Layout::spacingMd(), padY = Layout::spacingSm(), sp = Layout::spacingMd(); + float left = rowMin.x + stripW + padX; + float right = rowMax.x - padX; + float midY = (rowMin.y + rowMax.y) * 0.5f; + float iconSz = sub1->LegacySize; - // Right column: value (top-right), % change just below it. - float rightBottom = topY; - float valLeft = hright; - bool showChange = e.show24h && (e.priceBasis == 0 || e.priceBasis == 1) && market.change_24h != 0.0; - if (!heroStr.empty()) { - ImFont* vf = sub1; - std::string vs = fit(heroStr, vf, hAvailW * 0.66f); - float vw = vf->CalcTextSizeA(vf->LegacySize, FLT_MAX, 0, vs.c_str()).x; - valLeft = hright - vw; - dl->AddText(vf, vf->LegacySize, ImVec2(valLeft, topY), OnSurface(), vs.c_str()); - rightBottom = topY + vf->LegacySize; - if (showChange) { - char cb[32]; snprintf(cb, sizeof(cb), "%+.2f%%", market.change_24h); - float cw = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, cb).x; - dl->AddText(capFont, capFont->LegacySize, ImVec2(hright - cw, rightBottom + 1.0f * mktDp), - market.change_24h >= 0 ? Success() : Error(), cb); - valLeft = std::min(valLeft, hright - cw); - rightBottom += capFont->LegacySize + 1.0f * mktDp; + if (style == 0) { + // Single line — icon + label (left); drgx / value / 24h / sparkline right-aligned. + float y = midY - sub1->LegacySize * 0.5f, x = left, rx = right; + if (!e.icon.empty()) { + material::project_icons::drawByName(dl, e.icon.c_str(), ImVec2(x+iconSz*0.5f, midY), accent?accent:OnSurfaceMedium(), Type().iconSmall(), iconSz); + x += iconSz + Layout::spacingSm(); } - } - - // Left column: icon + name (larger, top), secondary DRGX amount below (always shown). - float nameX = hleft; - if (!e.icon.empty()) { - material::project_icons::drawByName(dl, e.icon.c_str(), - ImVec2(nameX + sub1->LegacySize * 0.5f, topY + sub1->LegacySize * 0.5f), - accent ? accent : OnSurfaceMedium(), Type().iconSmall(), sub1->LegacySize); - nameX += sub1->LegacySize + Layout::spacingSm(); - } - float nameMaxW = std::max(24.0f * mktDp, valLeft - Layout::spacingSm() - nameX); - dl->AddText(sub1, sub1->LegacySize, ImVec2(nameX, topY), OnSurfaceMedium(), - fit(e.label, sub1, nameMaxW).c_str()); - float leftBottom = topY + sub1->LegacySize; - if (!footStr.empty()) { - dl->AddText(capFont, capFont->LegacySize, ImVec2(nameX, leftBottom + 1.0f * mktDp), - OnSurfaceDisabled(), fit(footStr, capFont, nameMaxW).c_str()); - leftBottom += capFont->LegacySize + 1.0f * mktDp; - } - - // Body: sparkline fills from below the header to the card bottom. - if (e.showSparkline && (e.priceBasis == 0 || e.priceBasis == 1)) { - std::vector hh = data::sparklineSeries(market, e.sparklineInterval); - if (hh.size() >= 2) { - float sparkTop = std::max(leftBottom, rightBottom) + Layout::spacingXs(); - float sparkBot = gcMax.y - hpad; - if (sparkBot - sparkTop >= 8.0f * mktDp) - pfDrawSparklineFilled(dl, ImVec2(hleft, sparkTop), ImVec2(hright, sparkBot), - hh, hh.back() >= hh.front() ? Success() : Error()); + if (showSpark){ float sw=110*dp; pfDrawSparkline(dl, ImVec2(rx-sw, rowMin.y+padY), ImVec2(rx, rowMax.y-padY), spark, sparkCol); rx -= sw + sp; } + if (!chgStr.empty()){ float w=tw(capFont,chgStr); dl->AddText(capFont,capFont->LegacySize,ImVec2(rx-w, midY-capFont->LegacySize*0.5f), chgCol, chgStr.c_str()); rx -= w + sp; } + if (!valueStr.empty()){ float w=tw(sub1,valueStr); dl->AddText(sub1,sub1->LegacySize,ImVec2(rx-w,y),OnSurface(),valueStr.c_str()); rx -= w + sp; } + if (!drgxStr.empty()){ float w=tw(capFont,drgxStr); dl->AddText(capFont,capFont->LegacySize,ImVec2(rx-w, midY-capFont->LegacySize*0.5f),OnSurfaceMedium(),drgxStr.c_str()); rx -= w + sp; } + float lblMaxW = std::max(24.0f*dp, rx - x - Layout::spacingSm()); + dl->AddText(sub1,sub1->LegacySize,ImVec2(x,y),OnSurface(),fit(e.label,sub1,lblMaxW).c_str()); + } else if (style == 1) { + // Two lines — label + (24h, sparkline) on top; DRGX + value below. + float topY = rowMin.y + padY, botY = rowMax.y - padY - capFont->LegacySize, x = left, rx = right; + if (!e.icon.empty()) { + material::project_icons::drawByName(dl, e.icon.c_str(), ImVec2(x+iconSz*0.5f, topY+iconSz*0.5f), accent?accent:OnSurfaceMedium(), Type().iconSmall(), iconSz); + x += iconSz + Layout::spacingSm(); } + if (showSpark){ float sw=130*dp; pfDrawSparkline(dl, ImVec2(rx-sw, topY), ImVec2(rx, midY), spark, sparkCol); rx -= sw + sp; } + if (!chgStr.empty()){ float w=tw(capFont,chgStr); dl->AddText(capFont,capFont->LegacySize,ImVec2(rx-w,topY),chgCol,chgStr.c_str()); rx -= w + sp; } + dl->AddText(sub1,sub1->LegacySize,ImVec2(x,topY),OnSurface(),fit(e.label,sub1,std::max(24.0f*dp, rx-x)).c_str()); + if (!drgxStr.empty()) dl->AddText(capFont,capFont->LegacySize,ImVec2(x,botY),OnSurfaceMedium(),drgxStr.c_str()); + if (!valueStr.empty()){ float w=tw(body2,valueStr); dl->AddText(body2,body2->LegacySize,ImVec2(right-w, botY-(body2->LegacySize-capFont->LegacySize)),OnSurface(),valueStr.c_str()); } + } else { + // Value-hero — label; big value; DRGX · 24h; sparkline fills the right. + float topY = rowMin.y + padY, x = left; + if (!e.icon.empty()) { + material::project_icons::drawByName(dl, e.icon.c_str(), ImVec2(x+iconSz*0.5f, topY+capFont->LegacySize*0.5f), accent?accent:OnSurfaceMedium(), Type().iconSmall(), capFont->LegacySize); + x += capFont->LegacySize + Layout::spacingSm(); + } + dl->AddText(capFont,capFont->LegacySize,ImVec2(x,topY),OnSurfaceMedium(),fit(e.label,capFont,right-x-140*dp).c_str()); + float valY = topY + capFont->LegacySize + Layout::spacingXs(); + std::string hero = !valueStr.empty()? valueStr : drgxStr; + DrawTextShadow(dl, sub1, sub1->LegacySize, ImVec2(left, valY), accent?accent:Success(), hero.c_str()); + float botY = valY + sub1->LegacySize + Layout::spacingXs(); + std::string sub = (!valueStr.empty() && !drgxStr.empty()) ? drgxStr : std::string(); + if (!sub.empty()) dl->AddText(capFont,capFont->LegacySize,ImVec2(left,botY),OnSurfaceDisabled(),sub.c_str()); + if (!chgStr.empty()){ float sw = sub.empty()?0.0f:(tw(capFont,sub)+tw(capFont," \xC2\xB7 ")); if(!sub.empty()) dl->AddText(capFont,capFont->LegacySize,ImVec2(left+tw(capFont,sub),botY),OnSurfaceDisabled()," \xC2\xB7 "); dl->AddText(capFont,capFont->LegacySize,ImVec2(left+sw,botY),chgCol,chgStr.c_str()); } + if (showSpark){ float sw=(right-left)*0.42f; pfDrawSparklineFilled(dl, ImVec2(right-sw, valY-2*dp), ImVec2(right, rowMax.y-padY), spark, sparkCol); } } } @@ -1237,10 +1169,6 @@ struct MktCtx { bool chartUp; double periodChangePct; std::string periodSuffix; - const std::vector* pfEntriesGeo; - const std::vector* pfLayout; - int pfN, pfCols, pfMinW, pfMinH, pfMaxRow; - float pfCellW, pfCellH, pfInset, pfCardPad, pfCardGap; }; // Combined hero card: draws the ONE glass panel spanning the price header AND the chart below it, @@ -1659,10 +1587,6 @@ static void mktDrawPortfolio(const MktCtx& cx) ImFont* sub1 = Type().subtitle1(); ImFont* body2 = Type().body2(); float ratioBarH = cx.ratioBarH, pfSummaryH = cx.pfSummaryH, portfolioH = cx.portfolioH; - const auto& pfEntriesGeo = *cx.pfEntriesGeo; - const auto& pfLayout = *cx.pfLayout; - int pfN = cx.pfN, pfCols = cx.pfCols, pfMinW = cx.pfMinW, pfMinH = cx.pfMinH, pfMaxRow = cx.pfMaxRow; - float pfCellW = cx.pfCellW, pfCellH = cx.pfCellH, pfInset = cx.pfInset, pfCardPad = cx.pfCardPad, pfCardGap = cx.pfCardGap; char buf[128]; Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("market_portfolio")); @@ -1760,157 +1684,51 @@ static void mktDrawPortfolio(const MktCtx& cx) ImVec2(cx0, cy + ratioBarH + Layout::spacingXs()), OnSurfaceDisabled(), buf); } - // ---- CUSTOM GROUPS — dashboard grid: drag a card to move it, drag its corner to resize. - // A subtle dot grid shows while rearranging. Click (no drag) opens the editor. ---- - if (pfN > 0) { - float gy = cardMin.y + pfSummaryH; - ImGuiIO& io = ImGui::GetIO(); - bool interacting = (s_grid.drag >= 0 || s_grid.resize >= 0); - float rh = 20.0f * mktDp; // corner resize-grip size (generous so a short resize is easy to grab) - - auto cellMin = [&](const PfCell& L) { - return ImVec2(cx0 + L.col * (pfCellW + pfCardGap), gy + L.row * (pfCellH + pfCardGap)); - }; - auto cellSize = [&](int w, int h) { - return ImVec2(w * pfCellW + (w - 1) * pfCardGap, h * pfCellH + (h - 1) * pfCardGap); - }; - - // Draw one card's content into [gcMin, gcMax]. - // Stat tile: header (icon + name, 24h badge) / hero value (large, centered band) / - // footer (DRGX amount) + a dedicated sparkline strip along the bottom. - MktCardCtx cardCx{ dl, &state, &market, mktDp, pfInset, pfCardPad, sub1, capFont }; - - // Subtle dot grid while rearranging (fine 32px cells). Fill the whole visible grid area - // — down to the bottom of the scroll viewport, not just the occupied rows — so a card can - // be dropped anywhere, including the empty space below the current cards. - if (interacting) { - ImU32 dotc = WithAlpha(OnSurface(), 38); - float visibleBottom = dl->GetClipRectMax().y; // bottom edge of the scroll child - int rowsToBottom = (int)((visibleBottom - gy) / pfCellH) + 1; - int lastRow = std::max(pfMaxRow + 2, rowsToBottom); - for (int r = 0; r <= lastRow; r++) - for (int c = 0; c <= pfCols; c++) - dl->AddCircleFilled(ImVec2(cx0 + c * pfCellW, gy + r * pfCellH), 1.2f * mktDp, dotc); + // ---- CUSTOM GROUPS — full-width rows in the selected style (single-line / two-line / value-hero), + // inside an internal scroll region so the portfolio stays within its height budget. Left/Right + // arrows switch styles (handled in RenderMarketTab). Click a row to edit it. ---- + { + const auto& allEntries = app->settings()->getPortfolioEntries(); + const std::string activeHash = app->activeWalletIdentityHash(); + // Visible = this wallet's groups + legacy (empty scope). Keep storage indices for edit targets. + std::vector vis; + for (int i = 0; i < (int)allEntries.size(); i++) { + const auto& e = allEntries[i]; + if (e.scope.empty() || activeHash.empty() || e.scope == activeHash) vis.push_back(i); } - // Draw + hit-test each card. Skip the one being dragged/resized (drawn specially below). - for (int i = 0; i < pfN; i++) { - ImVec2 gcMin = cellMin(pfLayout[i]); - ImVec2 sz = cellSize(pfLayout[i].w, pfLayout[i].h); - ImVec2 gcMax(gcMin.x + sz.x, gcMin.y + sz.y); + int style = app->settings() ? app->settings()->getPortfolioStyle() : 0; + float rowH = (style == 0 ? 46.0f : style == 1 ? 64.0f : 84.0f) * mktDp; + float rowGap = Layout::spacingSm(); + float rowsH = std::max(rowH, portfolioH - pfSummaryH); - if (i == s_grid.drag && s_grid.moved) { - dl->AddRect(gcMin, gcMax, WithAlpha(OnSurface(), 45), 10.0f, 0, 1.0f); // source ghost - continue; - } - if (i == s_grid.resize) continue; - - bool hov = !interacting && material::IsRectHovered(gcMin, gcMax); - mktDrawCard(cardCx, gcMin, gcMax, pfEntriesGeo[i], hov); - - // Resize grip (bottom-right). - bool gripHov = material::IsRectHovered(ImVec2(gcMax.x - rh, gcMax.y - rh), gcMax); - ImU32 gcc = WithAlpha(OnSurface(), gripHov ? 170 : 80); - for (int k = 1; k <= 2; k++) { - float o = k * 4.0f * mktDp; - dl->AddLine(ImVec2(gcMax.x - o, gcMax.y - 3.0f), ImVec2(gcMax.x - 3.0f, gcMax.y - o), gcc, 1.3f * mktDp); - } - - // Start a gesture only when idle. One body button; the press location (corner vs body) - // decides resize vs move — avoids overlapping-item hit-test issues. - if (s_grid.drag < 0 && s_grid.resize < 0) { - ImGui::PushID(3000 + i); - ImGui::SetCursorScreenPos(gcMin); - ImGui::InvisibleButton("##pfBody", sz); - bool bodyAct = ImGui::IsItemActivated(); - bool bodyHov = ImGui::IsItemHovered(); + ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMin.y + pfSummaryH)); + // Flush-left child (no window padding) so rows align with the summary; a scrollbar appears + // only when the visible groups overflow the bounded height. + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0)); + ImGui::BeginChild("##pfRows", ImVec2(availWidth, rowsH), false); + ImGui::PopStyleVar(); + ImDrawList* rdl = ImGui::GetWindowDrawList(); + float rowW = ImGui::GetContentRegionAvail().x; // narrows automatically if a scrollbar shows + if (vis.empty()) { + ImGui::Dummy(ImVec2(0, Layout::spacingSm())); + Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("portfolio_no_entries")); + } else { + for (int vi = 0; vi < (int)vis.size(); vi++) { + int i = vis[vi]; + ImVec2 rMin = ImGui::GetCursorScreenPos(); + ImVec2 rMax(rMin.x + rowW, rMin.y + rowH); + ImGui::PushID(4000 + i); + bool clicked = ImGui::InvisibleButton("##pfRowBtn", ImVec2(rowW, rowH)); + bool hov = ImGui::IsItemHovered(); + if (hov) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); ImGui::PopID(); - if (bodyHov) ImGui::SetMouseCursor(gripHov ? ImGuiMouseCursor_ResizeNWSE : ImGuiMouseCursor_Hand); - if (bodyAct) { - if (gripHov) { s_grid.resize = i; s_grid.old = pfLayout[i]; } - else { - s_grid.drag = i; s_grid.moved = false; s_grid.old = pfLayout[i]; - s_grid.press = io.MousePos; - s_grid.grab = ImVec2(io.MousePos.x - gcMin.x, io.MousePos.y - gcMin.y); - } - } - } - } - - auto pfPersist = [&](const std::vector& v) { - app->settings()->setPortfolioEntries(v); app->settings()->save(); - }; - - // ----- Resize in progress (corner drag) ----- - if (s_grid.resize >= 0 && s_grid.resize < pfN) { - int i = s_grid.resize; - ImVec2 mn = cellMin(pfLayout[i]); - int nw = std::max(pfMinW, std::min(pfCols - pfLayout[i].col, - (int)((io.MousePos.x - mn.x + pfCardGap) / (pfCellW + pfCardGap) + 0.5f))); - int nh = std::max(pfMinH, (int)((io.MousePos.y - mn.y + pfCardGap) / (pfCellH + pfCardGap) + 0.5f)); - ImVec2 sz = cellSize(nw, nh), mx(mn.x + sz.x, mn.y + sz.y); - mktDrawCard(cardCx, mn, mx, pfEntriesGeo[i], true); - dl->AddRect(mn, mx, WithAlpha(Primary(), 200), 10.0f, 0, 2.0f * mktDp); - // Live size readout (e.g. "8\xC3\x972") so the reached grid span is visible — the - // minimum is pfMinW x pfMinH (8x2). - { - char szb[32]; snprintf(szb, sizeof(szb), "%d\xC3\x97%d", nw, nh); - ImVec2 tsz = sub1->CalcTextSizeA(sub1->LegacySize, FLT_MAX, 0, szb); - ImVec2 tp((mn.x + mx.x) * 0.5f - tsz.x * 0.5f, (mn.y + mx.y) * 0.5f - tsz.y * 0.5f); - dl->AddRectFilled(ImVec2(tp.x - 6, tp.y - 3), ImVec2(tp.x + tsz.x + 6, tp.y + tsz.y + 3), - IM_COL32(20, 20, 30, 205), 4.0f); - dl->AddText(sub1, sub1->LegacySize, tp, OnSurface(), szb); - } - ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeNWSE); - if (!io.MouseDown[0]) { - auto entries = app->settings()->getPortfolioEntries(); - if (i < (int)entries.size()) { - entries[i].gridCol = pfLayout[i].col; entries[i].gridRow = pfLayout[i].row; - entries[i].gridW = nw; entries[i].gridH = nh; - pfPersist(entries); - } - s_grid.resize = -1; - } - } - - // ----- Drag in progress ----- - if (s_grid.drag >= 0 && s_grid.drag < pfN) { - int i = s_grid.drag; - if (io.MouseDown[0]) { - if (std::abs(io.MousePos.x - s_grid.press.x) > 4.0f || std::abs(io.MousePos.y - s_grid.press.y) > 4.0f) - s_grid.moved = true; - if (s_grid.moved) { - int w = pfLayout[i].w, h = pfLayout[i].h; - float tx = io.MousePos.x - s_grid.grab.x, ty = io.MousePos.y - s_grid.grab.y; - int tc = std::max(0, std::min(pfCols - w, (int)((tx - cx0) / (pfCellW + pfCardGap) + 0.5f))); - int tr = std::max(0, (int)((ty - gy) / (pfCellH + pfCardGap) + 0.5f)); - s_grid.target = {tc, tr, w, h}; - ImVec2 tmn(cx0 + tc * (pfCellW + pfCardGap), gy + tr * (pfCellH + pfCardGap)); - ImVec2 tsz = cellSize(w, h), tmx(tmn.x + tsz.x, tmn.y + tsz.y); - dl->AddRectFilled(tmn, tmx, WithAlpha(Primary(), 25), 10.0f); - dl->AddRect(tmn, tmx, WithAlpha(Primary(), 200), 10.0f, 0, 2.0f * mktDp); - ImVec2 fmn(tx, ty), fmx(fmn.x + tsz.x, fmn.y + tsz.y); - mktDrawCard(cardCx, fmn, fmx, pfEntriesGeo[i], true); // dragged card follows the mouse - ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeAll); - } - } else { - if (!s_grid.moved) { PortfolioBeginEdit(app, i); s_pfEdit.open = true; } - else { - auto entries = app->settings()->getPortfolioEntries(); - if (i < (int)entries.size()) { - int occ = -1; - for (int j = 0; j < pfN; j++) - if (j != i && pfLayout[j].col == s_grid.target.col && pfLayout[j].row == s_grid.target.row) { occ = j; break; } - entries[i].gridCol = s_grid.target.col; entries[i].gridRow = s_grid.target.row; - if (occ >= 0 && occ < (int)entries.size()) { - entries[occ].gridCol = s_grid.old.col; entries[occ].gridRow = s_grid.old.row; - } - pfPersist(entries); - } - } - s_grid.drag = -1; s_grid.moved = false; + pfDrawRow(rdl, rMin, rMax, allEntries[i], state, market, style, mktDp, sub1, body2, capFont, hov); + if (clicked) { PortfolioBeginEdit(app, i); s_pfEdit.open = true; } + if (vi + 1 < (int)vis.size()) ImGui::Dummy(ImVec2(rowW, rowGap)); } } + ImGui::EndChild(); } ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMin.y + portfolioH)); @@ -1949,6 +1767,22 @@ void RenderMarketTab(App* app) const auto& currentExchange = registry[s_mkt.exchangeIdx]; if (s_mkt.pairIdx >= (int)currentExchange.pairs.size()) s_mkt.pairIdx = 0; + // Left/Right arrows: cycle portfolio row styles (compact / detailed / featured), mirroring the + // Overview tab's layout switch. Skip while typing, when Ctrl is held (theme cycle), or while the + // portfolio editor modal is open. + if (app->settings() && !s_pfEdit.open && !ImGui::GetIO().WantTextInput && !ImGui::GetIO().KeyCtrl) { + bool prev = ImGui::IsKeyPressed(ImGuiKey_LeftArrow); + bool next = ImGui::IsKeyPressed(ImGuiKey_RightArrow); + if (prev || next) { + int st = app->settings()->getPortfolioStyle(); + st = next ? (st + 1) % 3 : (st + 2) % 3; + app->settings()->setPortfolioStyle(st); + const char* names[3] = { TR("portfolio_style_compact"), TR("portfolio_style_detailed"), + TR("portfolio_style_featured") }; + Notifications::instance().info(std::string(TR("portfolio_style_label")) + ": " + names[st]); + } + } + // (Panel theme-effects are suppressed frame-wide in App::render() while the modal is open, so // their foreground borders don't bleed over the overlay — see PortfolioEditorActive().) ImVec2 marketAvail = ImGui::GetContentRegionAvail(); @@ -1991,23 +1825,20 @@ void RenderMarketTab(App* app) + body2->LegacySize + Layout::spacingSm() // DRGX balance row + ratioBarH + Layout::spacingXs() + capFont->LegacySize // ratio bar + % label + Layout::spacingMd(); // gap before the group cards - int pfN = (int)pfEntriesGeo.size(); - float pfCardPad = Layout::spacingMd(); // inner padding of each group card - // Fine SQUARE grid: cells are ~32px minimum and scale up to evenly fill the width. Cards snap to - // this grid and span many cells; they're drawn with an inner inset so adjacent cards don't touch. - float pfCardGap = 0.0f; // gapless cell grid - float pfInset = 3.0f * mktDp; // per-card inset for visual separation - const int pfMinW = 8, pfMinH = 2; // minimum card size in cells (~256x64) - int pfCols = std::max(pfMinW, (int)(availWidth / (32.0f * mktDp))); - float pfCellW = availWidth / (float)pfCols; // square cell edge (>= ~32px) - float pfCellH = pfCellW; - // Resolve each group's grid cell: honor a stored placement when it fits, else auto-place - // row-major into the first free cells. Deterministic each frame. - std::vector pfLayout = pfComputeGridLayout(pfEntriesGeo, pfCols, pfMinW, pfMinH); - int pfMaxRow = 0; for (const auto& L : pfLayout) pfMaxRow = std::max(pfMaxRow, L.row + L.h); - // Content height = the grid plus a couple of empty rows so cards can be dragged below the - // current bottom (the tab scrolls when the total exceeds the window). - float pfGroupsH = (pfN > 0) ? ((pfMaxRow + 2) * pfCellH) : 0.0f; + // Content height = full-width rows in the selected style, confined to a bounded area (up to + // pfMaxVisibleRows) with an internal scroll when the visible groups overflow. Only groups + // visible in the active wallet (own scope or legacy/global) count toward the height. + const std::string pfActiveHash = app->activeWalletIdentityHash(); + int pfVisN = 0; + for (const auto& e : pfEntriesGeo) + if (e.scope.empty() || pfActiveHash.empty() || e.scope == pfActiveHash) pfVisN++; + int pfStyle = app->settings()->getPortfolioStyle(); + float pfRowH = (pfStyle == 0 ? 46.0f : pfStyle == 1 ? 64.0f : 84.0f) * mktDp; + float pfRowGap = Layout::spacingSm(); + const int pfMaxVisibleRows = 4; // rows shown before the list scrolls + float pfWantH = (pfVisN > 0) ? (pfVisN * pfRowH + std::max(0, pfVisN - 1) * pfRowGap) : 0.0f; + float pfCapH = pfMaxVisibleRows * pfRowH + (pfMaxVisibleRows - 1) * pfRowGap; + float pfGroupsH = std::min(pfWantH, pfCapH); float portfolioH = pfSummaryH + pfGroupsH; // Chart series for the selected interval (timestamped), shared by the hero change badge and @@ -2040,9 +1871,7 @@ void RenderMarketTab(App* app) MktCtx mktc{ app, ¤tExchange, chartH, heroHeaderH, pfSummaryH, portfolioH, ratioBarH, - &chartTimes, nowSec, chartUp, periodChangePct, periodSuffix, - &pfEntriesGeo, &pfLayout, pfN, pfCols, pfMinW, pfMinH, pfMaxRow, - pfCellW, pfCellH, pfInset, pfCardPad, pfCardGap + &chartTimes, nowSec, chartUp, periodChangePct, periodSuffix }; mktDrawPriceHero(mktc); diff --git a/src/ui/windows/wallets_dialog.h b/src/ui/windows/wallets_dialog.h index 6785ea5..42a8d16 100644 --- a/src/ui/windows/wallets_dialog.h +++ b/src/ui/windows/wallets_dialog.h @@ -177,6 +177,9 @@ public: ImGui::Separator(); if (StyledButton(TR("refresh"), ImVec2(110.0f * dp, 0))) s_needScan = true; ImGui::SameLine(); + if (StyledButton(TR("wallets_reveal"), ImVec2(150.0f * dp, 0))) + util::Platform::openFolder(util::Platform::getDragonXDataDir()); + ImGui::SameLine(); if (StyledButton(TR("close"), ImVec2(110.0f * dp, 0))) s_open = false; EndOverlayDialog(); diff --git a/src/util/i18n.cpp b/src/util/i18n.cpp index 905ad6c..1de7d20 100644 --- a/src/util/i18n.cpp +++ b/src/util/i18n.cpp @@ -279,6 +279,7 @@ void I18n::loadBuiltinEnglish() strings_["wallets_imported"] = "Wallet imported — switching…"; strings_["wallets_import_failed"] = "Could not import that wallet file."; strings_["wallets_creating"] = "Creating wallet — the node will restart…"; + strings_["wallets_reveal"] = "Reveal folder"; strings_["tt_seed_migrate"] = "Create a new seed-phrase wallet and move your funds into it"; // Migrate-to-seed modal strings_["mig_title"] = "Migrate to a seed wallet"; @@ -1300,6 +1301,10 @@ void I18n::loadBuiltinEnglish() strings_["portfolio_all_transparent"] = "All transparent"; strings_["portfolio_clear_sel"] = "Clear"; strings_["portfolio_no_entries"] = "No custom entries yet. Add one to track a group of addresses."; + strings_["portfolio_style_label"] = "Portfolio style"; + strings_["portfolio_style_compact"] = "Compact rows"; + strings_["portfolio_style_detailed"] = "Detailed rows"; + strings_["portfolio_style_featured"] = "Featured rows"; strings_["portfolio_edit"] = "Edit"; strings_["portfolio_delete"] = "Delete"; strings_["portfolio_save"] = "Save";