From 5db7941fbc0dbc026792ae01ad166be9e97d90b2 Mon Sep 17 00:00:00 2001 From: DanS Date: Thu, 9 Jul 2026 19:03:07 -0500 Subject: [PATCH] feat(market): portfolio row-style redesign + per-wallet scope MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the drag/resize dashboard grid on the Market tab's portfolio with three full-width row styles — compact (single-line), detailed (two-line), and value-hero — cycled with Left/Right arrows like the Overview layouts. Rows live in an internal scroll region bounded to the portfolio's height budget, so the section stays put as groups are added. - pfDrawRow renders each group in the selected style, reusing the icon / value / DRGX / 24h / sparkline pieces; per-entry field toggles honored. - portfolio_style setting (0/1/2) persisted; arrow-key switcher + toast. - Portfolio groups gain a per-wallet scope: new groups default to the active wallet's identity hash; legacy groups (empty scope) stay visible in every wallet. Scope filter drives both the height budget and the rows. - Remove the now-dead grid machinery (mktDrawCard, pfComputeGridLayout, PfCell, PfGridDrag, MktCardCtx + MktCtx grid fields) — net code drop. - Full UI sweep: seed demo portfolio groups + wavy price history and add market-rows-{compact,detailed,featured} surfaces so the styles are captured per skin (snapshot/restore the real portfolio + market data). - i18n: portfolio_style_{label,compact,detailed,featured} + 8 langs; CJK subset rebuilt. Co-Authored-By: Claude Opus 4.8 (1M context) --- res/fonts/NotoSansCJK-Subset.ttf | Bin 602408 -> 603784 bytes res/lang/de.json | 5 + res/lang/es.json | 5 + res/lang/fr.json | 5 + res/lang/ja.json | 5 + res/lang/ko.json | 5 + res/lang/pt.json | 5 + res/lang/ru.json | 5 + res/lang/zh.json | 5 + src/app.h | 1 + src/app_sweep.cpp | 70 +++++ src/config/settings.cpp | 5 + src/config/settings.h | 18 +- src/ui/windows/market_tab.cpp | 483 ++++++++++--------------------- src/ui/windows/wallets_dialog.h | 3 + src/util/i18n.cpp | 5 + 16 files changed, 294 insertions(+), 331 deletions(-) diff --git a/res/fonts/NotoSansCJK-Subset.ttf b/res/fonts/NotoSansCJK-Subset.ttf index 0e7849605eb55fbf127cfa06fe83beb3e6cceeaf..0ff306e16748c89750db35634905513aca51481a 100644 GIT binary patch delta 22876 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 WDKp*HF1buYXjQiM%xCV(`u_nGc1gPc delta 21582 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 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";