refactor(market): extract drawCard lambda to mktDrawCard function

Lift the ~90-line [&]-capturing drawCard closure out of RenderMarketTab's grid
code into a file-static mktDrawCard(MktCardCtx, min, max, entry, hov). The
captured locals (dl/state/market/mktDp/pfInset/pfCardPad/sub1/capFont) are
bundled into a MktCardCtx bound once per frame. Verbatim body. No behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-04 00:40:56 -05:00
parent 0ae52ee095
commit 7e8628f94c

View File

@@ -1181,6 +1181,122 @@ static std::vector<PfCell> pfComputeGridLayout(
return layout; 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<double> 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) void RenderMarketTab(App* app)
{ {
auto& S = schema::UI(); auto& S = schema::UI();
@@ -1874,100 +1990,7 @@ void RenderMarketTab(App* app)
// Draw one card's content into [gcMin, gcMax]. // Draw one card's content into [gcMin, gcMax].
// Stat tile: header (icon + name, 24h badge) / hero value (large, centered band) / // Stat tile: header (icon + name, 24h badge) / hero value (large, centered band) /
// footer (DRGX amount) + a dedicated sparkline strip along the bottom. // footer (DRGX amount) + a dedicated sparkline strip along the bottom.
auto drawCard = [&](ImVec2 gcMin, ImVec2 gcMax, const config::Settings::PortfolioEntry& e, bool hov) { MktCardCtx cardCx{ dl, &state, &market, mktDp, pfInset, pfCardPad, sub1, 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 = 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<double> 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());
}
}
};
// Subtle dot grid while rearranging (fine 32px cells). Fill the whole visible grid area // 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 // — 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; if (i == s_grid.resize) continue;
bool hov = !interacting && material::IsRectHovered(gcMin, gcMax); bool hov = !interacting && material::IsRectHovered(gcMin, gcMax);
drawCard(gcMin, gcMax, pfEntriesGeo[i], hov); mktDrawCard(cardCx, gcMin, gcMax, pfEntriesGeo[i], hov);
// Resize grip (bottom-right). // Resize grip (bottom-right).
bool gripHov = material::IsRectHovered(ImVec2(gcMax.x - rh, gcMax.y - rh), gcMax); 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)((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)); 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); 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); 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 // Live size readout (e.g. "8\xC3\x972") so the reached grid span is visible — the
// minimum is pfMinW x pfMinH (8x2). // minimum is pfMinW x pfMinH (8x2).
@@ -2079,7 +2102,7 @@ void RenderMarketTab(App* app)
dl->AddRectFilled(tmn, tmx, WithAlpha(Primary(), 25), 10.0f); dl->AddRectFilled(tmn, tmx, WithAlpha(Primary(), 25), 10.0f);
dl->AddRect(tmn, tmx, WithAlpha(Primary(), 200), 10.0f, 0, 2.0f * mktDp); 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); 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); ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeAll);
} }
} else { } else {