From 9bcdf51895ef1fc5c27950c857d7648d90385f98 Mon Sep 17 00:00:00 2001 From: DanS Date: Thu, 2 Jul 2026 22:32:19 -0500 Subject: [PATCH] feat(market): compact chart w/ time axis; floating portfolio + redesigned group cards MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to the Market tab rework, per feedback: - Chart is more compact: a modest responsive height (~22% of the tab, floored at 110px) instead of stretching to fill. - Chart now has a time axis. The price series is one sample per ~60s market refresh (RefreshScheduler::kPrice), so the x-axis is labelled in approximate minutes-ago ("~45m … ~15m … now") instead of only "Now" — you can now read the interval/span of each plot point. - Portfolio: removed the top-level card background — the summary (fiat hero + 24h change + BTC, DRGX balance + Z/T, ratio bar) now floats directly under the MY DRGX header, and the "GROUPS" heading is gone. - Group cards redesigned: a max width (280px, shrinks on narrow tabs) so they float rather than stretch; the group name is larger (subtitle font) top-left; the DRGX + fiat amounts moved to the card's top-right corner; and more inner padding around the content. Removed the now-unused portfolio_groups i18n key. Full-node + Lite build clean; ctest 1/1; hygiene clean. Rendering change — needs a screenshot check. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/ui/windows/market_tab.cpp | 160 +++++++++++++++++----------------- src/util/i18n.cpp | 1 - 2 files changed, 81 insertions(+), 80 deletions(-) diff --git a/src/ui/windows/market_tab.cpp b/src/ui/windows/market_tab.cpp index fcf8caf..bfefb0f 100644 --- a/src/ui/windows/market_tab.cpp +++ b/src/ui/windows/market_tab.cpp @@ -276,39 +276,12 @@ void RenderMarketTab(App* app) char buf[128]; // ================================================================ - // Section geometry — this tab does NOT scroll, so everything must fit. Precompute the - // portfolio card height (refined summary + a grid of group cards) and the hero header, - // then let the price chart absorb the remaining vertical space (capped at its schema - // height, floored so it stays useful). Fixes the old layout where an oversized chart - // squeezed the portfolio into a cramped strip. + // Section geometry. // ================================================================ float mktDp = Layout::dpiScale(); - // -- Portfolio card geometry -- - const auto& pfEntriesGeo = app->settings()->getPortfolioEntries(); - float pfInnerW = availWidth - Layout::spacingLg() * 2.0f; - float ratioBarH = std::max(S.drawElement("tabs.market", "ratio-bar-min-height").size, - S.drawElement("tabs.market", "ratio-bar-height").size * vs); - float pfSummaryH = Layout::spacingLg() - + sub1->LegacySize + Layout::spacingSm() // fiat hero row - + body2->LegacySize + Layout::spacingSm() // DRGX balance row - + ratioBarH + Layout::spacingXs() + capFont->LegacySize // ratio bar + % label - + Layout::spacingMd(); // bottom pad - int pfN = (int)pfEntriesGeo.size(); - float pfCardGap = Layout::spacingSm(); - float pfMinCardW = 200.0f * mktDp; - int pfCardsPerRow = std::max(1, (int)((pfInnerW + pfCardGap) / (pfMinCardW + pfCardGap))); - if (pfN > 0 && pfCardsPerRow > pfN) pfCardsPerRow = pfN; - float pfCardW = (pfN > 0) ? (pfInnerW - (pfCardsPerRow - 1) * pfCardGap) / (float)pfCardsPerRow : 0.0f; - float pfCardH = body2->LegacySize + Layout::spacingXs() + body2->LegacySize - + capFont->LegacySize + Layout::spacingSm() * 2.0f; - int pfRows = (pfN > 0) ? (pfN + pfCardsPerRow - 1) / pfCardsPerRow : 0; - float pfGroupsH = (pfN > 0) - ? (ovFont->LegacySize + Layout::spacingSm() - + pfRows * pfCardH + std::max(0, pfRows - 1) * pfCardGap - + Layout::spacingMd()) - : 0.0f; - float portfolioCardH = pfSummaryH + pfGroupsH; + // -- Compact price chart: a modest responsive height, no longer stretched to fill the tab. -- + float chartH = std::max(110.0f * vs, std::min(chartElem.height * vs, marketAvail.y * 0.22f)); // -- Hero header (price + stats, excluding the chart) -- float mktStatsRowH = ovFont->LegacySize + Layout::spacingXs() + sub1->LegacySize + pad; @@ -316,12 +289,28 @@ void RenderMarketTab(App* app) S.drawElement("tabs.market", "hero-card-min-height").size + mktStatsRowH + pad, (S.drawElement("tabs.market", "hero-card-height").size + mktStatsRowH + pad) * vs); - // -- Chart absorbs the slack: capped at its schema height, floored so it stays useful -- - float mktChipH = S.drawElement("tabs.market", "pair-chip-height").height; - float mktPairsReserve = mktChipH * 2.0f + Layout::spacingXl() + capFont->LegacySize; - float chartH = std::min(chartElem.height * vs, - marketAvail.y - heroHeaderH - portfolioCardH - mktPairsReserve - gap * 3.0f); - chartH = std::max(chartH, 130.0f * vs); + // -- Portfolio geometry. No parent card background: the summary floats under the header + // and the custom-group cards float below it (each with its own glass background). -- + const auto& pfEntriesGeo = app->settings()->getPortfolioEntries(); + float ratioBarH = std::max(S.drawElement("tabs.market", "ratio-bar-min-height").size, + S.drawElement("tabs.market", "ratio-bar-height").size * vs); + float pfSummaryH = sub1->LegacySize + Layout::spacingSm() // fiat hero row + + 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 pfCardGap = Layout::spacingSm(); + float pfCardPad = Layout::spacingMd(); // inner padding of each group card + float pfCardW = std::min(280.0f * mktDp, availWidth); // max card width (shrinks on a narrow tab) + int pfCardsPerRow = std::max(1, (int)((availWidth + pfCardGap) / (pfCardW + pfCardGap))); + if (pfN > 0 && pfCardsPerRow > pfN) pfCardsPerRow = pfN; + // Card: a name row (sub1) + a fiat row (caption); the DRGX/fiat amounts sit top-right. + float pfCardH = pfCardPad * 2.0f + sub1->LegacySize + Layout::spacingXs() + capFont->LegacySize; + int pfRows = (pfN > 0) ? (pfN + pfCardsPerRow - 1) / pfCardsPerRow : 0; + float pfGroupsH = (pfN > 0) + ? (pfRows * pfCardH + std::max(0, pfRows - 1) * pfCardGap) + : 0.0f; + float portfolioH = pfSummaryH + pfGroupsH; // ================================================================ // PRICE SUMMARY — Combined hero card with price, stats, and exchange @@ -558,14 +547,29 @@ void RenderMarketTab(App* app) dl->AddCircleFilled(points[i], dotR, dotCol); } - // X-axis: samples are per-refresh, not hourly-timestamped, so only the most - // recent point can be labelled truthfully ("Now"). The series simply runs - // older→newer, left→right. (Avoids the old fake "24h…6h" hourly gradations.) + // X-axis time labels. The price series is one sample per market refresh (~60s + // cadence, see RefreshScheduler::kPrice), so label the axis in approximate + // minutes-ago (rightmost = "now"). Approximate — a refresh can be delayed — hence "~". { - const char* nowText = TR("market_now"); - ImVec2 lblSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, nowText); - dl->AddText(capFont, capFont->LegacySize, - ImVec2(plotRight - lblSz.x, plotBottom + 2), OnSurfaceDisabled(), nowText); + const float kSecPerPoint = 60.0f; // RefreshScheduler::kPrice + int nPts = (int)n; + int ticks = std::min(4, nPts); + for (int tk = 0; tk < ticks; tk++) { + float t = (ticks > 1) ? (float)tk / (float)(ticks - 1) : 1.0f; + float xpos = plotLeft + t * plotW; + int idx = (int)(t * (nPts - 1) + 0.5f); + int minsAgo = (int)(((float)(nPts - 1 - idx) * kSecPerPoint) / 60.0f + 0.5f); + char tlbl[32]; + if (minsAgo <= 0) snprintf(tlbl, sizeof(tlbl), "%s", TR("market_now")); + else if (minsAgo < 90) snprintf(tlbl, sizeof(tlbl), "~%dm", minsAgo); + else snprintf(tlbl, sizeof(tlbl), "~%.1fh", minsAgo / 60.0f); + ImVec2 lblSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, tlbl); + float lx = (tk == 0) ? plotLeft + : (tk == ticks - 1) ? plotRight - lblSz.x + : xpos - lblSz.x * 0.5f; + dl->AddText(capFont, capFont->LegacySize, + ImVec2(lx, plotBottom + 2), OnSurfaceDisabled(), tlbl); + } } // Hover crosshair + tooltip @@ -756,7 +760,7 @@ void RenderMarketTab(App* app) } // ================================================================ - // PORTFOLIO — Glass card with balance breakdown + // PORTFOLIO — floating summary (no card background) + custom-group cards // ================================================================ { Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("market_portfolio")); @@ -775,13 +779,11 @@ void RenderMarketTab(App* app) double private_balance = state.privateBalance; double transparent_balance = state.transparentBalance; + // No parent card background — the summary floats directly under the "MY DRGX" header. ImVec2 cardMin = ImGui::GetCursorScreenPos(); - float cardH = portfolioCardH; // precomputed: summary block + group-card grid - ImVec2 cardMax(cardMin.x + availWidth, cardMin.y + cardH); - DrawGlassPanel(dl, cardMin, cardMax, glassSpec); - - float cx = cardMin.x + Layout::spacingLg(); - float cy = cardMin.y + Layout::spacingLg(); + float rightEdge = cardMin.x + availWidth; + float cx = cardMin.x; + float cy = cardMin.y; // ---- SUMMARY: fiat value (hero) + 24h change, BTC right-aligned ---- if (market.price_usd > 0) { @@ -805,7 +807,7 @@ void RenderMarketTab(App* app) snprintf(buf, sizeof(buf), "\xE2\x89\x88 %.8f BTC", portfolio_btc); // "≈ BTC" float btcW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, buf).x; dl->AddText(capFont, capFont->LegacySize, - ImVec2(cardMax.x - btcW - pad, cy + (sub1->LegacySize - capFont->LegacySize)), + ImVec2(rightEdge - btcW, cy + (sub1->LegacySize - capFont->LegacySize)), OnSurfaceMedium(), buf); } else { dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, cy), OnSurfaceDisabled(), TR("market_no_price")); @@ -818,12 +820,12 @@ void RenderMarketTab(App* app) snprintf(buf, sizeof(buf), "Z %.4f \xC2\xB7 T %.4f", private_balance, transparent_balance); float brkW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, buf).x; dl->AddText(capFont, capFont->LegacySize, - ImVec2(cardMax.x - brkW - pad, cy + 2), OnSurfaceDisabled(), buf); + ImVec2(rightEdge - brkW, cy + 2), OnSurfaceDisabled(), buf); cy += body2->LegacySize + Layout::spacingSm(); // Full-width shielded/transparent ratio bar + % label. if (total_balance > 0) { - float barW = pfInnerW; + float barW = availWidth; float shieldedRatio = (float)(private_balance / total_balance); if (shieldedRatio > 1.0f) shieldedRatio = 1.0f; if (shieldedRatio < 0.0f) shieldedRatio = 0.0f; @@ -849,12 +851,9 @@ void RenderMarketTab(App* app) ImVec2(cx, cy + ratioBarH + Layout::spacingXs()), OnSurfaceDisabled(), buf); } - // ---- CUSTOM GROUPS — one glass card per entry, wrapping into a grid ---- + // ---- CUSTOM GROUPS — floating glass cards below the summary (no heading) ---- if (pfN > 0) { float gy = cardMin.y + pfSummaryH; - dl->AddText(ovFont, ovFont->LegacySize, ImVec2(cx, gy), OnSurfaceMedium(), TR("portfolio_groups")); - gy += ovFont->LegacySize + Layout::spacingSm(); - for (int i = 0; i < pfN; i++) { const auto& e = pfEntriesGeo[i]; int col = i % pfCardsPerRow; @@ -863,42 +862,45 @@ void RenderMarketTab(App* app) ImVec2 gcMax(gcMin.x + pfCardW, gcMin.y + pfCardH); GlassPanelSpec gcGlass; - gcGlass.rounding = 8.0f; - gcGlass.fillAlpha = 18; - gcGlass.borderAlpha = 30; + gcGlass.rounding = 10.0f; + gcGlass.fillAlpha = 22; + gcGlass.borderAlpha = 40; DrawGlassPanel(dl, gcMin, gcMax, gcGlass); double bal = data::SumPortfolioBalance(e.addresses, state.addresses); - float tcx = gcMin.x + Layout::spacingSm(); - float tcy = gcMin.y + Layout::spacingSm(); - float maxTextW = pfCardW - Layout::spacingSm() * 2.0f; + float rowY = gcMin.y + pfCardPad; + float gcRight = gcMax.x - pfCardPad; - // Label (truncated with an ellipsis to the card width). + // Amounts, top-right corner: DRGX on the name row, fiat directly below it. + snprintf(buf, sizeof(buf), "%.4f %s", bal, DRAGONX_TICKER); + float drgxW = body2->CalcTextSizeA(body2->LegacySize, FLT_MAX, 0, buf).x; + dl->AddText(body2, body2->LegacySize, + ImVec2(gcRight - drgxW, rowY + (sub1->LegacySize - body2->LegacySize) * 0.5f), + OnSurface(), buf); + if (market.price_usd > 0) { + char usd[48]; + snprintf(usd, sizeof(usd), "$%.2f", bal * market.price_usd); + float usdW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, usd).x; + dl->AddText(capFont, capFont->LegacySize, + ImVec2(gcRight - usdW, rowY + sub1->LegacySize + Layout::spacingXs()), + OnSurfaceDisabled(), usd); + } + + // Group name, top-left, larger — truncated so it never overlaps the amount. + float nameMaxW = (gcRight - drgxW - Layout::spacingMd()) - (gcMin.x + pfCardPad); std::string label = e.label; bool truncated = false; while (label.size() > 1 && - body2->CalcTextSizeA(body2->LegacySize, FLT_MAX, 0, label.c_str()).x > maxTextW) { + sub1->CalcTextSizeA(sub1->LegacySize, FLT_MAX, 0, label.c_str()).x > nameMaxW) { label.pop_back(); truncated = true; } if (truncated) label += "\xE2\x80\xA6"; - dl->AddText(body2, body2->LegacySize, ImVec2(tcx, tcy), OnSurfaceMedium(), label.c_str()); - tcy += body2->LegacySize + Layout::spacingXs(); - - // DRGX amount. - snprintf(buf, sizeof(buf), "%.4f %s", bal, DRAGONX_TICKER); - dl->AddText(body2, body2->LegacySize, ImVec2(tcx, tcy), OnSurface(), buf); - tcy += body2->LegacySize; - - // Fiat value. - if (market.price_usd > 0) { - snprintf(buf, sizeof(buf), "$%.2f", bal * market.price_usd); - dl->AddText(capFont, capFont->LegacySize, ImVec2(tcx, tcy), OnSurfaceDisabled(), buf); - } + dl->AddText(sub1, sub1->LegacySize, ImVec2(gcMin.x + pfCardPad, rowY), OnSurface(), label.c_str()); } } - ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMin.y + cardH)); + ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMin.y + portfolioH)); ImGui::Dummy(ImVec2(availWidth, 0)); ImGui::Dummy(ImVec2(0, gap)); } diff --git a/src/util/i18n.cpp b/src/util/i18n.cpp index 25dc5b9..97484d4 100644 --- a/src/util/i18n.cpp +++ b/src/util/i18n.cpp @@ -1119,7 +1119,6 @@ void I18n::loadBuiltinEnglish() strings_["market_pct_shielded"] = "%.0f%% Shielded"; strings_["market_portfolio"] = "MY DRGX"; strings_["portfolio_all_funds"] = "All funds"; - strings_["portfolio_groups"] = "GROUPS"; strings_["portfolio_manage"] = "Manage\xE2\x80\xA6"; strings_["portfolio_manage_title"] = "Manage portfolio"; strings_["portfolio_add_entry"] = "Add entry";