feat(market): refine the three portfolio row styles
Act on the design review of the row layouts: - Fixed right-aligned columns (DRGX | value | 24h | sparkline) so the numbers line up across rows and the value no longer jumps rightward when a group has no sparkline/24h. - Emphasize the fiat value (larger + brighter than the muted DRGX amount) — the number users actually scan for. - Graceful fallback: on-but-absent fields (e.g. price not loaded yet) show a muted em-dash instead of a hole; deliberately-hidden fields stay blank. - Detailed style reworked into a 2x2 grid (label/value on top, DRGX/24h below) with a tall sparkline strip spanning both lines, so its extra height earns its keep; when a group has no sparkline the value/DRGX fill the space. - Featured hero value is now neutral bold (OnSurface) instead of the accent color, so it can't collide with the green 24h; DRGX right-aligns to fill the right when there is no sparkline. - Stronger row separation (more card fill) and REMOVED the left accent strip — group identity now reads from the tinted icon + a subtle accent border. Verified via the headless sweep across all three styles on dark + light. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -971,93 +971,130 @@ bool PortfolioEditorActive() { return s_pfEdit.open; }
|
||||
|
||||
|
||||
// ---- 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.
|
||||
// Draw one group as a full-width row card in [rowMin,rowMax]; style 0=compact, 1=detailed,
|
||||
// 2=value-hero. No left accent strip — identity comes from the tinted icon + a subtle accent
|
||||
// border. Fixed right-aligned columns keep numbers aligned across rows; on-but-absent fields show a
|
||||
// muted em-dash so a row never looks broken. 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)
|
||||
ImFont* sub1, ImFont* capFont, bool hov)
|
||||
{
|
||||
double bal = data::SumPortfolioBalance(e.addresses, state.addresses);
|
||||
ImU32 accent = e.color ? (ImU32)e.color : 0;
|
||||
float stripW = (style == 2 ? 5.0f : 3.0f) * dp;
|
||||
|
||||
GlassPanelSpec g; g.rounding = 10.0f; g.fillAlpha = hov ? 34 : 22; g.borderAlpha = 40;
|
||||
// Card — a touch more fill than before so rows read as distinct cards, plus a subtle accent
|
||||
// border for identity (the left accent strip was removed).
|
||||
GlassPanelSpec g; g.rounding = 10.0f; g.fillAlpha = hov ? 42 : 30; g.borderAlpha = 55;
|
||||
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(rowMin, rowMax, WithAlpha(accent, oa), 10.0f, 0, hov ? 2.0f : 1.5f);
|
||||
dl->AddRect(rowMin, rowMax, WithAlpha(accent, hov ? std::min(255, oa + 45) : oa), 10.0f, 0, hov ? 1.8f : 1.2f);
|
||||
} else if (hov) {
|
||||
dl->AddRect(rowMin, rowMax, WithAlpha(OnSurface(), 70), 10.0f, 0, 1.0f);
|
||||
dl->AddRect(rowMin, rowMax, WithAlpha(OnSurface(), 80), 10.0f, 0, 1.2f);
|
||||
}
|
||||
|
||||
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; };
|
||||
// Right-aligned text: right edge at xr, vertically centred on yc.
|
||||
auto rtext = [&](ImFont* f, float xr, float yc, ImU32 col, const std::string& s){
|
||||
dl->AddText(f, f->LegacySize, ImVec2(xr - tw(f, s), yc - f->LegacySize * 0.5f), col, s.c_str());
|
||||
};
|
||||
const std::string kDash = "\xE2\x80\x94"; // em-dash for on-but-absent fields
|
||||
|
||||
// ---- Field values + presence ----
|
||||
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; }
|
||||
std::string drgxStr = std::string(db);
|
||||
bool wantValue = e.showValue;
|
||||
bool wantChange = e.show24h && (e.priceBasis==0 || e.priceBasis==1);
|
||||
bool hasChange = wantChange && market.change_24h != 0.0;
|
||||
std::string chgStr; if (hasChange){ 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);
|
||||
bool wantSpark = e.showSparkline && (e.priceBasis==0 || e.priceBasis==1);
|
||||
std::vector<double> spark;
|
||||
if (showSpark){ spark=data::sparklineSeries(market,e.sparklineInterval); if (spark.size()<2) showSpark=false; }
|
||||
if (wantSpark) spark = data::sparklineSeries(market, e.sparklineInterval);
|
||||
bool hasSpark = wantSpark && spark.size() >= 2;
|
||||
ImU32 sparkCol = (!spark.empty() && spark.back()>=spark.front()) ? Success() : Error();
|
||||
|
||||
const float padX = Layout::spacingMd(), padY = Layout::spacingSm(), sp = Layout::spacingMd();
|
||||
float left = rowMin.x + stripW + padX;
|
||||
float right = rowMax.x - padX;
|
||||
const float padX = Layout::spacingMd(), padY = Layout::spacingSm(), colGap = Layout::spacingMd();
|
||||
float left = rowMin.x + padX;
|
||||
float right = rowMax.x - padX;
|
||||
float midY = (rowMin.y + rowMax.y) * 0.5f;
|
||||
float iconSz = sub1->LegacySize;
|
||||
|
||||
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;
|
||||
// ---- Compact: icon + label (left); fixed right-aligned columns DRGX | value | 24h | spark.
|
||||
float iconSz = sub1->LegacySize, x = left;
|
||||
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();
|
||||
}
|
||||
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());
|
||||
const float sparkW = 92.0f*dp, chgW = 60.0f*dp, valW = 112.0f*dp, drgxW = 122.0f*dp;
|
||||
float sparkL = right - sparkW;
|
||||
float chgR = sparkL - colGap;
|
||||
float valR = chgR - chgW - colGap;
|
||||
float drgxR = valR - valW - colGap;
|
||||
float labelR = drgxR - drgxW - colGap;
|
||||
|
||||
if (e.showDrgx) rtext(capFont, drgxR, midY, OnSurfaceMedium(), fit(drgxStr, capFont, drgxW));
|
||||
if (wantValue) rtext(sub1, valR, midY, hasValue?OnSurface():OnSurfaceDisabled(), hasValue?fit(valStr,sub1,valW):kDash);
|
||||
if (wantChange) rtext(capFont, chgR, midY, hasChange?chgCol:OnSurfaceDisabled(), hasChange?chgStr:kDash);
|
||||
if (hasSpark) pfDrawSparkline(dl, ImVec2(sparkL, rowMin.y+padY), ImVec2(right, rowMax.y-padY), spark, sparkCol);
|
||||
|
||||
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) {
|
||||
// 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;
|
||||
// ---- Detailed: 2x2 grid — label/value on top, DRGX/24h below — plus a tall sparkline strip.
|
||||
float iconSz = sub1->LegacySize, x = left;
|
||||
const float sparkW = 150.0f*dp;
|
||||
float sparkL = right - sparkW;
|
||||
float textR = hasSpark ? (sparkL - colGap) : right;
|
||||
float topY = rowMin.y + padY;
|
||||
float botY = rowMax.y - padY - capFont->LegacySize;
|
||||
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()); }
|
||||
// value (top-right, emphasized)
|
||||
std::string vTop = wantValue ? (hasValue?valStr:kDash) : std::string();
|
||||
if (wantValue) dl->AddText(sub1, sub1->LegacySize, ImVec2(textR - tw(sub1,vTop), topY), hasValue?OnSurface():OnSurfaceDisabled(), vTop.c_str());
|
||||
// label (top-left, fills up to the value column)
|
||||
float labelR = wantValue ? (textR - tw(sub1,vTop) - colGap) : textR;
|
||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(x, topY), OnSurface(), fit(e.label,sub1,std::max(24.0f*dp, labelR - x)).c_str());
|
||||
// DRGX (bottom-left, muted) + 24h (bottom-right, colored)
|
||||
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(textR - tw(capFont,s), botY), hasChange?chgCol:OnSurfaceDisabled(), s.c_str()); }
|
||||
// tall sparkline strip spanning both lines
|
||||
if (hasSpark) pfDrawSparklineFilled(dl, ImVec2(sparkL, topY), ImVec2(right, rowMax.y-padY), spark, sparkCol);
|
||||
} else {
|
||||
// Value-hero — label; big value; DRGX · 24h; sparkline fills the right.
|
||||
float topY = rowMin.y + padY, x = left;
|
||||
// ---- Value-hero: label; big neutral value; 24h; DRGX + sparkline fill the right.
|
||||
float iconSz = capFont->LegacySize, x = left;
|
||||
const float sparkW = (right-left)*0.40f;
|
||||
float sparkL = right - sparkW;
|
||||
float topY = rowMin.y + padY;
|
||||
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();
|
||||
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();
|
||||
}
|
||||
dl->AddText(capFont,capFont->LegacySize,ImVec2(x,topY),OnSurfaceMedium(),fit(e.label,capFont,right-x-140*dp).c_str());
|
||||
float rightZoneL = hasSpark ? sparkL : right;
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(x, topY), OnSurfaceMedium(), fit(e.label,capFont,std::max(24.0f*dp, rightZoneL - x - colGap)).c_str());
|
||||
// hero value — neutral bold (accent stays in the icon/border, so it can't clash with the 24h)
|
||||
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());
|
||||
std::string hero = hasValue ? valStr : (e.showDrgx ? drgxStr : kDash);
|
||||
DrawTextShadow(dl, sub1, sub1->LegacySize, ImVec2(left, valY), hasValue?OnSurface():OnSurfaceDisabled(), hero.c_str());
|
||||
// 24h below the value (colored / dash)
|
||||
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); }
|
||||
if (wantChange) dl->AddText(capFont, capFont->LegacySize, ImVec2(left, botY), hasChange?chgCol:OnSurfaceDisabled(), (hasChange?chgStr:kDash).c_str());
|
||||
// DRGX — right-aligned on the value line; fills the right when there's no sparkline.
|
||||
if (e.showDrgx && hasValue) {
|
||||
float dr = hasSpark ? (sparkL - colGap) : right;
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(dr - tw(capFont,drgxStr), valY + (sub1->LegacySize - capFont->LegacySize)*0.5f), OnSurfaceMedium(), drgxStr.c_str());
|
||||
}
|
||||
// filled sparkline (right ~40%)
|
||||
if (hasSpark) pfDrawSparklineFilled(dl, ImVec2(sparkL, valY - 2*dp), ImVec2(right, rowMax.y-padY), spark, sparkCol);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1727,7 +1764,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, body2, capFont, hov);
|
||||
pfDrawRow(rdl, rMin, rMax, allEntries[i], state, market, style, mktDp, sub1, capFont, hov);
|
||||
if (clicked) { PortfolioBeginEdit(app, i); s_pfEdit.open = true; }
|
||||
if (vi + 1 < (int)vis.size()) ImGui::Dummy(ImVec2(rowW, rowGap));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user