diff --git a/src/ui/windows/market_tab.cpp b/src/ui/windows/market_tab.cpp index c2b0e5a..22b65c3 100644 --- a/src/ui/windows/market_tab.cpp +++ b/src/ui/windows/market_tab.cpp @@ -1181,6 +1181,122 @@ static std::vector pfComputeGridLayout( 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) +{ + 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. + if (accent) { + 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); + + 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; + }; + + // 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(); + + // 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; + + // 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; + } + } + + // 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()); + } + } +} + void RenderMarketTab(App* app) { auto& S = schema::UI(); @@ -1874,100 +1990,7 @@ void RenderMarketTab(App* app) // 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. - auto drawCard = [&](ImVec2 gcMin, ImVec2 gcMax, const config::Settings::PortfolioEntry& e, bool hov) { - // 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 = 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. - if (accent) { - 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); - - 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; - }; - - // 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(); - - // 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; - - // 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; - } - } - - // 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()); - } - } - }; + 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 @@ -1995,7 +2018,7 @@ void RenderMarketTab(App* app) if (i == s_grid.resize) continue; bool hov = !interacting && material::IsRectHovered(gcMin, gcMax); - drawCard(gcMin, gcMax, pfEntriesGeo[i], hov); + mktDrawCard(cardCx, gcMin, gcMax, pfEntriesGeo[i], hov); // Resize grip (bottom-right). bool gripHov = material::IsRectHovered(ImVec2(gcMax.x - rh, gcMax.y - rh), gcMax); @@ -2038,7 +2061,7 @@ void RenderMarketTab(App* app) (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); - drawCard(mn, mx, pfEntriesGeo[i], true); + 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). @@ -2079,7 +2102,7 @@ void RenderMarketTab(App* app) 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); - drawCard(fmn, fmx, pfEntriesGeo[i], true); // dragged card follows the mouse + mktDrawCard(cardCx, fmn, fmx, pfEntriesGeo[i], true); // dragged card follows the mouse ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeAll); } } else {