diff --git a/src/ui/windows/market_tab.cpp b/src/ui/windows/market_tab.cpp index f6dc62b..535388d 100644 --- a/src/ui/windows/market_tab.cpp +++ b/src/ui/windows/market_tab.cpp @@ -164,16 +164,16 @@ static bool pfProjectSeries(const std::vector& hist, ImVec2 mn, ImVec2 m } static void pfDrawSparkline(ImDrawList* dl, ImVec2 mn, ImVec2 mx, - const std::vector& hist, ImU32 col) + const std::vector& hist, ImU32 col, float dp) { std::vector pts; if (!pfProjectSeries(hist, mn, mx, pts)) return; - dl->AddPolyline(pts.data(), (int)pts.size(), col, ImDrawFlags_None, 1.2f); + dl->AddPolyline(pts.data(), (int)pts.size(), col, ImDrawFlags_None, 1.2f * dp); } // Sparkline with a soft fill under the line — for the card's dedicated bottom strip. static void pfDrawSparklineFilled(ImDrawList* dl, ImVec2 mn, ImVec2 mx, - const std::vector& hist, ImU32 lineCol) + const std::vector& hist, ImU32 lineCol, float dp) { std::vector pts; if (!pfProjectSeries(hist, mn, mx, pts)) return; @@ -182,7 +182,7 @@ static void pfDrawSparklineFilled(ImDrawList* dl, ImVec2 mn, ImVec2 mx, dl->PathLineTo(ImVec2(pts[n - 1].x, mx.y)); dl->PathLineTo(ImVec2(pts[0].x, mx.y)); dl->PathFillConcave(WithAlpha(lineCol, 28)); - dl->AddPolyline(pts.data(), n, WithAlpha(lineCol, 220), ImDrawFlags_None, 1.4f); + dl->AddPolyline(pts.data(), n, WithAlpha(lineCol, 220), ImDrawFlags_None, 1.4f * dp); } @@ -974,7 +974,7 @@ static void RenderPortfolioEditor(App* app) ImU32 spCol = WithAlpha(h.back() >= h.front() ? Success() : Error(), 80); ImVec2 spMin(pMin.x + ppad, pMin.y + ph * 0.46f); ImVec2 spMax(pMax.x - ppad, pMax.y - ppad * 0.6f); - pfDrawSparkline(pdl, spMin, spMax, h, spCol); + pfDrawSparkline(pdl, spMin, spMax, h, spCol, dp); } } float rowY = pMin.y + ppad, nameX = pMin.x + ppad; @@ -1094,6 +1094,7 @@ bool PortfolioEditorActive() { return s_pfEdit.open; } static constexpr float kPfChgColW = 62.0f; static constexpr float kPfValColW = 108.0f; static constexpr float kPfDrgxColW = 118.0f; +static constexpr float kPfSparkColW = 72.0f; // Table trend column, reserved only when a group shows one struct PfTableCols { float labelR, drgxR, valR, chgR; }; static PfTableCols pfTableCols(float right, float dp) @@ -1115,7 +1116,7 @@ static float pfRowGapFor(int style, float dp) { return style == 0 ? 2.0f * dp : 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* capFont, bool hov) + ImFont* sub1, ImFont* capFont, bool hov, bool tableReserveSpark = false) { // One pass over the wallet's addresses: the shielded/transparent split (used by the Cards Z/T bar) // also yields the total, so we never scan twice. @@ -1193,9 +1194,12 @@ static void pfDrawRow(ImDrawList* dl, ImVec2 rowMin, ImVec2 rowMax, const int zdVtx = zeroBal ? dl->VtxBuffer.Size : -1; if (style == 0) { - // ---- TABLE: icon + label (left); tight numeric columns DRGX | value | 24h pill (right). - // No sparkline — the trend rides in a small coloured % pill, keeping this a scannable grid. - const PfTableCols cols = pfTableCols(right, dp); + // ---- TABLE: icon + label (left); tight numeric columns DRGX | value | 24h pill (right), and a + // small trend sparkline in a reserved far-right column when any group opts into one. The + // column is reserved for ALL rows (tableReserveSpark) so they stay aligned with the header. + const float sparkW = tableReserveSpark ? kPfSparkColW * dp : 0.0f; + const float colsRight = right - (sparkW > 0.0f ? sparkW + colGap : 0.0f); + const PfTableCols cols = pfTableCols(colsRight, dp); const float chgR = cols.chgR, valR = cols.valR, drgxR = cols.drgxR, labelR = cols.labelR; const float valW = kPfValColW*dp, drgxW = kPfDrgxColW*dp; // fit() widths (shared with pfTableCols) float iconSz = capFont->LegacySize, x = left; @@ -1213,6 +1217,8 @@ static void pfDrawRow(ImDrawList* dl, ImVec2 rowMin, ImVec2 rowMax, dl->AddText(capFont, capFont->LegacySize, ImVec2(pmn.x + 6.0f*dp, midY - capFont->LegacySize*0.5f), hasChange?chgCol:OnSurfaceDisabled(), s.c_str()); } + if (hasSpark && sparkW > 0.0f) + pfDrawSparkline(dl, ImVec2(right - sparkW, rowMin.y + padY), ImVec2(right, rowMax.y - padY), spark, sparkCol, dp); const float lblMaxW = std::max(24.0f*dp, labelR - x); dl->AddText(sub1, sub1->LegacySize, ImVec2(x, midY - sub1->LegacySize*0.5f), OnSurface(), fit(e.label,sub1,lblMaxW).c_str()); } else if (style == 1) { @@ -1222,6 +1228,10 @@ static void pfDrawRow(ImDrawList* dl, ImVec2 rowMin, ImVec2 rowMax, float iconSz = capFont->LegacySize, x = left; const float topY = rowMin.y + padY + 1.0f*dp; const float botY = rowMax.y - padY - zbarH - 3.0f*dp - capFont->LegacySize; + // Optional trend sparkline strip on the right (above the Z/T bar); text right-aligns to its left. + const float cSparkW = hasSpark ? std::min(110.0f * dp, (right - left) * 0.34f) : 0.0f; + const float cSparkL = right - cSparkW; + const float cRight = hasSpark ? (cSparkL - colGap) : right; if (!e.icon.empty()) { material::project_icons::drawByName(dl, e.icon.c_str(), ImVec2(x+iconSz*0.5f, topY+sub1->LegacySize*0.5f), accent?accent:OnSurfaceMedium(), Type().iconSmall(), iconSz); x += iconSz + Layout::spacingSm(); @@ -1229,13 +1239,14 @@ static void pfDrawRow(ImDrawList* dl, ImVec2 rowMin, ImVec2 rowMax, // value (top-right, leads via a soft shadow) std::string vTop = wantValue ? (hasValue?valStr:kDash) : std::string(); float vW = wantValue ? tw(sub1, vTop) : 0.0f; - if (wantValue) DrawTextShadow(dl, sub1, sub1->LegacySize, ImVec2(right - vW, topY), hasValue?OnSurface():OnSurfaceDisabled(), vTop.c_str()); + if (wantValue) DrawTextShadow(dl, sub1, sub1->LegacySize, ImVec2(cRight - vW, topY), hasValue?OnSurface():OnSurfaceDisabled(), vTop.c_str()); // label (top-left, muted so the value leads) - float labelR = wantValue ? (right - vW - colGap) : right; + float labelR = wantValue ? (cRight - vW - colGap) : cRight; dl->AddText(capFont, capFont->LegacySize, ImVec2(x, topY + (sub1->LegacySize - capFont->LegacySize)*0.5f), OnSurfaceMedium(), fit(e.label,capFont,std::max(24.0f*dp, labelR - x)).c_str()); // DRGX (bottom-left) + 24h (bottom-right) if (e.showDrgx) dl->AddText(capFont, capFont->LegacySize, ImVec2(left, botY), OnSurfaceMedium(), drgxStr.c_str()); - if (wantChange) { std::string s = hasChange?chgStr:kDash; dl->AddText(capFont, capFont->LegacySize, ImVec2(right - tw(capFont,s), botY), hasChange?chgCol:OnSurfaceDisabled(), s.c_str()); } + if (wantChange) { std::string s = hasChange?chgStr:kDash; dl->AddText(capFont, capFont->LegacySize, ImVec2(cRight - tw(capFont,s), botY), hasChange?chgCol:OnSurfaceDisabled(), s.c_str()); } + if (hasSpark) pfDrawSparklineFilled(dl, ImVec2(cSparkL, topY), ImVec2(right, rowMax.y - padY - zbarH - 2.0f*dp), spark, sparkCol, dp); // shielded/transparent split bar (Cards-only signature; reuses the split computed above) { const data::PortfolioSplit& sp = split; @@ -1289,7 +1300,7 @@ static void pfDrawRow(ImDrawList* dl, ImVec2 rowMin, ImVec2 rowMax, dl->AddText(capFont, capFont->LegacySize, ImVec2(chipX+6.0f*dp, chipY + (ph-capFont->LegacySize)*0.5f), chgCol, chgStr.c_str()); } } - if (hasSpark) pfDrawSparklineFilled(dl, ImVec2(sparkL, rowMin.y + padY), ImVec2(right, rowMax.y - padY), spark, sparkCol); + if (hasSpark) pfDrawSparklineFilled(dl, ImVec2(sparkL, rowMin.y + padY), ImVec2(right, rowMax.y - padY), spark, sparkCol, dp); } if (zdVtx >= 0) { // fade the just-emitted body vertices for an empty group @@ -2090,6 +2101,13 @@ static void mktDrawPortfolio(const MktCtx& cx) } int style = app->settings() ? app->settings()->getPortfolioStyle() : 0; + // Reserve the Table trend column for ALL rows + the header when ANY visible group opts into a + // sparkline, so the columns stay aligned regardless of which individual rows draw one. + bool anySpark = false; + for (int i : vis) { + const auto& e = allEntries[i]; + if (e.showSparkline && (e.priceBasis == 0 || e.priceBasis == 1)) { anySpark = true; break; } + } float rowH = pfRowHeight(style, mktDp); // Table rows abut like a ledger (thin gap); Cards/Spotlight breathe. float rowGap = pfRowGapFor(style, mktDp); @@ -2118,7 +2136,8 @@ static void mktDrawPortfolio(const MktCtx& cx) const ImVec2 hp = ImGui::GetCursorScreenPos(); const float hLeft = hp.x + Layout::spacingMd(); const float hRight = hp.x + rowW - Layout::spacingMd(); - const PfTableCols hc = pfTableCols(hRight, mktDp); + const float hColsRight = hRight - (anySpark ? kPfSparkColW * mktDp + Layout::spacingMd() : 0.0f); + const PfTableCols hc = pfTableCols(hColsRight, mktDp); ImFont* ovF = Type().overline(); const float ty = hp.y + (headerH - ovF->LegacySize) * 0.5f; const ImU32 hcol = OnSurfaceDisabled(); @@ -2143,7 +2162,7 @@ static void mktDrawPortfolio(const MktCtx& cx) bool hov = ImGui::IsItemHovered(); if (hov) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); ImGui::PopID(); - pfDrawRow(rdl, rMin, rMax, allEntries[i], state, market, style, mktDp, sub1, capFont, hov); + pfDrawRow(rdl, rMin, rMax, allEntries[i], state, market, style, mktDp, sub1, capFont, hov, anySpark); if (clicked) { PortfolioBeginEdit(app, i); s_pfEdit.open = true; } if (vi + 1 < (int)vis.size()) ImGui::Dummy(ImVec2(rowW, rowGap)); } @@ -2260,6 +2279,10 @@ void RenderMarketTab(App* app) // Also kick the historical price-chart fetch (self-throttled) so the chart's hour/day/week/ // month intervals populate promptly when the user opens the Market tab. app->refreshMarketChart(); + // ...and the SELECTED pair's per-exchange OHLC candles (self-throttled, in-flight-guarded). Without + // this, the candle series only loaded on a pair-chip / refresh click, so the candlestick toggle was + // missing (and "switch to candlesticks" silently did nothing) when the tab was just opened. + app->refreshExchangeChart(); const auto& registry = EffectiveRegistry(market); // Load persisted exchange/pair on first frame