feat(market): settings gear, options modal & distinct portfolio styles
Add a settings-gear modal (portfolio layout / chart style / fetch prices) and persist the portfolio style. Redesign the three row styles into distinct purposes: Table (borderless grid + column header + % pill), Cards (glass card + muted label/shadow value + shielded/transparent split bar), Spotlight (heavy tile + enlarged direction-coloured hero value + delta chip). Dim empty groups. Add data::SumPortfolioSplit. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -31,6 +31,19 @@ inline double SumPortfolioBalance(const std::vector<std::string>& entryAddresses
|
||||
return sum;
|
||||
}
|
||||
|
||||
// Shielded/transparent breakdown of an entry's balance (for the per-group Z/T split bar). Pure.
|
||||
struct PortfolioSplit { double shielded = 0.0; double transparent = 0.0; };
|
||||
inline PortfolioSplit SumPortfolioSplit(const std::vector<std::string>& entryAddresses,
|
||||
const std::vector<AddressInfo>& walletAddresses)
|
||||
{
|
||||
PortfolioSplit s;
|
||||
if (entryAddresses.empty()) return s;
|
||||
std::unordered_set<std::string> want(entryAddresses.begin(), entryAddresses.end());
|
||||
for (const auto& a : walletAddresses)
|
||||
if (want.count(a.address)) { if (a.isShielded()) s.shielded += a.balance; else s.transparent += a.balance; }
|
||||
return s;
|
||||
}
|
||||
|
||||
// Whether `address` is part of the entry's address group.
|
||||
inline bool PortfolioEntryContains(const std::vector<std::string>& entryAddresses,
|
||||
const std::string& address)
|
||||
|
||||
@@ -47,6 +47,7 @@ struct MarketViewState {
|
||||
int chartStyle = 1; // 0 = line, 1 = candlestick (only applies when per-exchange OHLC exists)
|
||||
};
|
||||
static MarketViewState s_mkt;
|
||||
static bool s_show_market_settings = false; // Market general-options modal (gear button, like chat)
|
||||
|
||||
// The effective exchange registry: the live CoinGecko list when available, else the
|
||||
// compiled-in fallback.
|
||||
@@ -1080,6 +1081,21 @@ bool PortfolioEditorActive() { return s_pfEdit.open; }
|
||||
// 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.
|
||||
// TABLE-style (portfolio_style 0) right-edge column anchors — shared by pfDrawRow's row body and the
|
||||
// column-header strip so the header labels sit exactly over the values they name.
|
||||
struct PfTableCols { float labelR, drgxR, valR, chgR; };
|
||||
static PfTableCols pfTableCols(float right, float dp)
|
||||
{
|
||||
const float colGap = Layout::spacingMd();
|
||||
const float chgW = 62.0f*dp, valW = 108.0f*dp, drgxW = 118.0f*dp;
|
||||
PfTableCols c;
|
||||
c.chgR = right;
|
||||
c.valR = c.chgR - chgW - colGap;
|
||||
c.drgxR = c.valR - valW - colGap;
|
||||
c.labelR = c.drgxR - drgxW - colGap;
|
||||
return c;
|
||||
}
|
||||
|
||||
static void pfDrawRow(ImDrawList* dl, ImVec2 rowMin, ImVec2 rowMax,
|
||||
const config::Settings::PortfolioEntry& e, const WalletState& state,
|
||||
const MarketInfo& market, int style, float dp,
|
||||
@@ -1087,16 +1103,38 @@ static void pfDrawRow(ImDrawList* dl, ImVec2 rowMin, ImVec2 rowMax,
|
||||
{
|
||||
double bal = data::SumPortfolioBalance(e.addresses, state.addresses);
|
||||
ImU32 accent = e.color ? (ImU32)e.color : 0;
|
||||
const bool zeroBal = (bal <= 0.0); // empty groups render dimmed (content only, not the container)
|
||||
|
||||
// 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) {
|
||||
int oa = (int)(std::max(0, std::min(100, e.outlineOpacity)) * 2.55f + 0.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(), 80), 10.0f, 0, 1.2f);
|
||||
// ---- Per-style CONTAINER (the enabler): the three styles differ before any field is read.
|
||||
// Table (0) = borderless ledger row (hairline + hover wash + a slim left identity tick);
|
||||
// Cards (1) = glass card with a faint accent tint + a left accent spine;
|
||||
// Spotlight (2) = a heavier tile (higher fill + a bold accent spine). ----
|
||||
const float round = 10.0f * dp;
|
||||
const int oa = (int)(std::max(0, std::min(100, e.outlineOpacity)) * 2.55f + 0.5f);
|
||||
if (style == 0) {
|
||||
if (hov) dl->AddRectFilled(rowMin, rowMax, WithAlpha(OnSurface(), 14), 0.0f);
|
||||
dl->AddLine(ImVec2(rowMin.x, rowMax.y - 0.5f), ImVec2(rowMax.x, rowMax.y - 0.5f),
|
||||
WithAlpha(OnSurface(), 24), 1.0f);
|
||||
dl->AddRectFilled(ImVec2(rowMin.x, rowMin.y + 5.0f * dp), ImVec2(rowMin.x + 3.0f * dp, rowMax.y - 5.0f * dp),
|
||||
accent ? accent : WithAlpha(OnSurface(), 55), 1.5f * dp);
|
||||
} else if (style == 1) {
|
||||
GlassPanelSpec g; g.rounding = 10.0f; g.fillAlpha = hov ? 42 : 30; g.borderAlpha = 55;
|
||||
DrawGlassPanel(dl, rowMin, rowMax, g);
|
||||
if (accent) {
|
||||
dl->AddRectFilled(rowMin, rowMax, WithAlpha(accent, hov ? 16 : 10), round);
|
||||
dl->AddRectFilled(rowMin, ImVec2(rowMin.x + 3.0f * dp, rowMax.y), WithAlpha(accent, 205),
|
||||
round, ImDrawFlags_RoundCornersLeft);
|
||||
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(), 80), 10.0f, 0, 1.2f);
|
||||
}
|
||||
} else {
|
||||
GlassPanelSpec g; g.rounding = 10.0f; g.fillAlpha = hov ? 56 : 46; g.borderAlpha = 60;
|
||||
DrawGlassPanel(dl, rowMin, rowMax, g);
|
||||
dl->AddRectFilled(rowMin, ImVec2(rowMin.x + 4.0f * dp, rowMax.y),
|
||||
WithAlpha(accent ? accent : Primary(), 220), round, ImDrawFlags_RoundCornersLeft);
|
||||
if (accent) 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.2f);
|
||||
}
|
||||
|
||||
auto tw = [](ImFont* f, const std::string& s){ return f->CalcTextSizeA(f->LegacySize, FLT_MAX, 0, s.c_str()).x; };
|
||||
@@ -1130,76 +1168,116 @@ static void pfDrawRow(ImDrawList* dl, ImVec2 rowMin, ImVec2 rowMax,
|
||||
float right = rowMax.x - padX;
|
||||
float midY = (rowMin.y + rowMax.y) * 0.5f;
|
||||
|
||||
// Empty groups: dim the BODY content (text/pill/bar/sparkline) to ~half alpha so a $0 group reads as
|
||||
// muted, while the container (card/tick) stays full-strength. Done by scaling the alpha of the
|
||||
// vertices this body emits — one place instead of tinting every draw call.
|
||||
const int zdVtx = zeroBal ? dl->VtxBuffer.Size : -1;
|
||||
|
||||
if (style == 0) {
|
||||
// ---- Compact: icon + label (left); fixed right-aligned columns DRGX | value | 24h | spark.
|
||||
float iconSz = sub1->LegacySize, x = left;
|
||||
// ---- 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);
|
||||
const float chgR = cols.chgR, valR = cols.valR, drgxR = cols.drgxR, labelR = cols.labelR;
|
||||
const float valW = 108.0f*dp, drgxW = 118.0f*dp; // fit() widths for the value / DRGX columns
|
||||
float iconSz = capFont->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();
|
||||
}
|
||||
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);
|
||||
if (wantChange) {
|
||||
const std::string s = hasChange ? chgStr : kDash;
|
||||
const float pw = tw(capFont, s) + 12.0f*dp, ph = capFont->LegacySize + 5.0f*dp;
|
||||
const ImVec2 pmn(chgR - pw, midY - ph*0.5f);
|
||||
if (hasChange) dl->AddRectFilled(pmn, ImVec2(chgR, midY + ph*0.5f), WithAlpha(chgCol, 32), ph*0.5f);
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(pmn.x + 6.0f*dp, midY - capFont->LegacySize*0.5f),
|
||||
hasChange?chgCol:OnSurfaceDisabled(), s.c_str());
|
||||
}
|
||||
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) {
|
||||
// ---- 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();
|
||||
}
|
||||
// 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 neutral value; 24h; DRGX + sparkline fill the right.
|
||||
// ---- CARDS: muted label + a leading (shadowed) value on top; DRGX + 24h beneath; and a
|
||||
// per-group shielded/transparent split bar along the bottom edge as the signature.
|
||||
const float zbarH = 3.0f*dp;
|
||||
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;
|
||||
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();
|
||||
}
|
||||
// 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());
|
||||
// label (top-left, muted so the value leads)
|
||||
float labelR = wantValue ? (right - vW - colGap) : right;
|
||||
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()); }
|
||||
// shielded/transparent split bar (Cards-only signature; read-only balance data)
|
||||
{
|
||||
const data::PortfolioSplit sp = data::SumPortfolioSplit(e.addresses, state.addresses);
|
||||
const double tot = sp.shielded + sp.transparent;
|
||||
const float barY = rowMax.y - padY - zbarH;
|
||||
const bool barLight = IsLightTheme();
|
||||
dl->AddRectFilled(ImVec2(left, barY), ImVec2(right, barY + zbarH),
|
||||
barLight ? IM_COL32(0,0,0,20) : IM_COL32(255,255,255,12), zbarH*0.5f);
|
||||
if (tot > 0) {
|
||||
float sr = (float)(sp.shielded / tot); sr = sr<0?0.0f:(sr>1?1.0f:sr);
|
||||
float sw = (right - left) * sr;
|
||||
const int fa = barLight ? 205 : 175;
|
||||
if (sw > 0.5f) dl->AddRectFilled(ImVec2(left, barY), ImVec2(left+sw, barY+zbarH), WithAlpha(Success(), fa),
|
||||
(right-left-sw > 0.5f) ? ImDrawFlags_RoundCornersLeft : ImDrawFlags_RoundCornersAll, zbarH*0.5f);
|
||||
if (right-left-sw > 0.5f) dl->AddRectFilled(ImVec2(left+sw, barY), ImVec2(right, barY+zbarH), WithAlpha(Warning(), fa),
|
||||
sw > 0.5f ? ImDrawFlags_RoundCornersRight : ImDrawFlags_RoundCornersAll, zbarH*0.5f);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// ---- SPOTLIGHT: muted name + DRGX on top, then a LARGE value coloured by 24h direction with a
|
||||
// delta chip, and a filled sparkline on the right. A third font tier no other style uses.
|
||||
const float heroSz = sub1->LegacySize * 1.5f; // ImGui 1.92 rebakes at this size (crisp, not upscaled)
|
||||
const float sparkW = (right - left) * 0.34f;
|
||||
const float sparkL = right - sparkW;
|
||||
const float rightZoneL = hasSpark ? (sparkL - colGap) : right;
|
||||
const float blockH = capFont->LegacySize + Layout::spacingXs() + heroSz;
|
||||
const float topY = midY - blockH * 0.5f;
|
||||
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+iconSz*0.5f), accent?accent:OnSurfaceMedium(), Type().iconSmall(), iconSz);
|
||||
x += iconSz + Layout::spacingSm();
|
||||
}
|
||||
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 = 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();
|
||||
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());
|
||||
// name (top-left, muted) + DRGX (top-right, muted)
|
||||
const float drgxW2 = e.showDrgx ? tw(capFont, drgxStr) : 0.0f;
|
||||
if (e.showDrgx) rtext(capFont, rightZoneL, topY + capFont->LegacySize*0.5f, OnSurfaceMedium(), drgxStr);
|
||||
const float nameMaxW = std::max(24.0f*dp, (rightZoneL - (e.showDrgx ? drgxW2 + colGap : 0.0f)) - x);
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(x, topY), OnSurfaceMedium(), fit(e.label,capFont,nameMaxW).c_str());
|
||||
// hero value — coloured by 24h direction (neutral when no live change)
|
||||
const float valY = topY + capFont->LegacySize + Layout::spacingXs();
|
||||
const std::string hero = hasValue ? valStr : (e.showDrgx ? drgxStr : kDash);
|
||||
const ImU32 heroCol = !hasValue ? OnSurfaceDisabled() : (hasChange ? chgCol : OnSurface());
|
||||
DrawTextShadow(dl, sub1, heroSz, ImVec2(left, valY), heroCol, hero.c_str());
|
||||
const float heroW = sub1->CalcTextSizeA(heroSz, FLT_MAX, 0, hero.c_str()).x;
|
||||
// delta chip trailing the hero value (only if it fits before the sparkline zone)
|
||||
if (wantChange && hasChange) {
|
||||
const float pw = tw(capFont, chgStr) + 12.0f*dp, ph = capFont->LegacySize + 5.0f*dp;
|
||||
const float chipX = left + heroW + Layout::spacingSm();
|
||||
const float chipY = valY + (heroSz - ph)*0.5f;
|
||||
if (chipX + pw <= rightZoneL) {
|
||||
dl->AddRectFilled(ImVec2(chipX, chipY), ImVec2(chipX+pw, chipY+ph), WithAlpha(chgCol, 34), ph*0.5f);
|
||||
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 (zdVtx >= 0) { // fade the just-emitted body vertices for an empty group
|
||||
for (int vi = zdVtx; vi < dl->VtxBuffer.Size; ++vi) {
|
||||
ImDrawVert& v = dl->VtxBuffer[vi];
|
||||
v.col = material::ScaleAlpha(v.col, 0.5f);
|
||||
}
|
||||
// filled sparkline (right ~40%)
|
||||
if (hasSpark) pfDrawSparklineFilled(dl, ImVec2(sparkL, valY - 2*dp), ImVec2(right, rowMax.y-padY), spark, sparkCol);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1867,12 +1945,23 @@ static void mktDrawPortfolio(const MktCtx& cx)
|
||||
}
|
||||
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("market_portfolio"));
|
||||
// "Manage…" button, right-aligned on the header row — opens the portfolio editor.
|
||||
// Settings gear + "Manage…" button, right-aligned on the header row (gear left of Manage). The gear
|
||||
// opens the general market-options modal; Manage opens the portfolio editor.
|
||||
{
|
||||
const char* ml = TR("portfolio_manage");
|
||||
float mBtnW = body2->CalcTextSizeA(body2->LegacySize, FLT_MAX, 0, ml).x + Layout::spacingMd() * 2;
|
||||
const float gearW = ImGui::GetFrameHeight(); // square, matching the Manage button's height
|
||||
const float gGap = Layout::spacingXs();
|
||||
ImGui::SameLine();
|
||||
material::RightAlignX(mBtnW);
|
||||
RightAlignX(mBtnW + gGap + gearW); // reserve the whole group before drawing left-to-right
|
||||
IconButtonStyle gst;
|
||||
gst.tooltip = TR("market_settings_tip");
|
||||
gst.hoverBg = WithAlpha(OnSurface(), 30);
|
||||
gst.restBg = WithAlpha(OnSurface(), 20);
|
||||
gst.bgRounding = 7.0f * mktDp;
|
||||
if (IconButton("##marketSettings", ICON_MD_SETTINGS, Type().iconSmall(), ImVec2(gearW, gearW), gst))
|
||||
s_show_market_settings = true;
|
||||
ImGui::SameLine(0.0f, gGap);
|
||||
if (material::TactileButton(ml, ImVec2(mBtnW, 0))) {
|
||||
// Open the editor on the first group visible to this wallet (or the empty state if none) —
|
||||
// never a raw index 0 that might belong to a different wallet.
|
||||
@@ -1982,8 +2071,9 @@ static void mktDrawPortfolio(const MktCtx& cx)
|
||||
}
|
||||
|
||||
int style = app->settings() ? app->settings()->getPortfolioStyle() : 0;
|
||||
float rowH = (style == 0 ? 46.0f : style == 1 ? 64.0f : 84.0f) * mktDp;
|
||||
float rowGap = Layout::spacingSm();
|
||||
float rowH = (style == 0 ? 40.0f : style == 1 ? 68.0f : 80.0f) * mktDp;
|
||||
// Table rows abut like a ledger (thin gap); Cards/Spotlight breathe.
|
||||
float rowGap = (style == 0 ? 2.0f * mktDp : Layout::spacingSm());
|
||||
float rowsH = std::max(rowH, portfolioH - pfSummaryH);
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMin.y + pfSummaryH));
|
||||
@@ -2002,6 +2092,29 @@ static void mktDrawPortfolio(const MktCtx& cx)
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("portfolio_no_entries"));
|
||||
} else {
|
||||
// TABLE column-header strip (style 0): a thin static header labelling the numeric columns —
|
||||
// a signature the card styles don't have. Uses the shared pfTableCols so it aligns with rows.
|
||||
if (style == 0) {
|
||||
const float headerH = capFont->LegacySize + 8.0f * mktDp;
|
||||
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);
|
||||
ImFont* ovF = Type().overline();
|
||||
const float ty = hp.y + (headerH - ovF->LegacySize) * 0.5f;
|
||||
const ImU32 hcol = OnSurfaceDisabled();
|
||||
auto hdr = [&](float xr, const char* s){
|
||||
const float w = ovF->CalcTextSizeA(ovF->LegacySize, FLT_MAX, 0, s).x;
|
||||
rdl->AddText(ovF, ovF->LegacySize, ImVec2(xr - w, ty), hcol, s);
|
||||
};
|
||||
rdl->AddText(ovF, ovF->LegacySize, ImVec2(hLeft, ty), hcol, TR("market_col_name"));
|
||||
hdr(hc.drgxR, DRAGONX_TICKER);
|
||||
hdr(hc.valR, TR("market_col_value"));
|
||||
hdr(hc.chgR, TR("market_24h"));
|
||||
rdl->AddLine(ImVec2(hLeft, hp.y + headerH - 1.0f), ImVec2(hRight, hp.y + headerH - 1.0f),
|
||||
WithAlpha(OnSurface(), 20), 1.0f);
|
||||
ImGui::Dummy(ImVec2(rowW, headerH));
|
||||
}
|
||||
for (int vi = 0; vi < (int)vis.size(); vi++) {
|
||||
int i = vis[vi];
|
||||
ImVec2 rMin = ImGui::GetCursorScreenPos();
|
||||
@@ -2025,6 +2138,87 @@ static void mktDrawPortfolio(const MktCtx& cx)
|
||||
ImGui::Dummy(ImVec2(0, gap));
|
||||
}
|
||||
|
||||
// General market-tab options modal (opened by the PORTFOLIO-header gear). Mirrors the chat/contacts
|
||||
// settings modals: a BlurFloat overlay of right-aligned segmented controls + a text-sized Done button.
|
||||
// All controls write EXISTING settings (+ sync the live s_mkt mirror for chart range/style so the chart
|
||||
// updates the same frame). Rendered outside the scroll child so it overlays the page.
|
||||
static void RenderMarketSettings(App* app)
|
||||
{
|
||||
if (!s_show_market_settings || !app->settings()) return;
|
||||
auto* st = app->settings();
|
||||
const float dp = Layout::dpiScale();
|
||||
|
||||
OverlayDialogSpec ov;
|
||||
ov.title = TR("market_settings_title");
|
||||
ov.p_open = &s_show_market_settings;
|
||||
ov.style = OverlayStyle::BlurFloat;
|
||||
ov.cardWidth = 480.0f;
|
||||
ov.idSuffix = "marketsettings";
|
||||
if (BeginOverlayDialog(ov)) {
|
||||
const float ctrlW = 280.0f * dp; // roomy enough for the multi-word segment labels
|
||||
auto rowStart = [&](const char* label) {
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted(label);
|
||||
ImGui::SameLine();
|
||||
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + std::max(0.0f, ImGui::GetContentRegionAvail().x - ctrlW));
|
||||
};
|
||||
// A right-aligned segmented control on its own row; returns the clicked index (-1 if none).
|
||||
auto seg = [&](const char* label, const char* const* items, int count, int cur, const char* id) -> int {
|
||||
rowStart(label);
|
||||
const float segH = ImGui::GetFrameHeight();
|
||||
const ImVec2 o = ImGui::GetCursorScreenPos();
|
||||
int clk = SegmentedControl(ImGui::GetWindowDrawList(), o, ctrlW, segH,
|
||||
items, count, cur, Type().caption(), id, dp);
|
||||
ImGui::SetCursorScreenPos(o);
|
||||
ImGui::Dummy(ImVec2(ctrlW, segH)); // restore + reserve the control's rect for layout flow
|
||||
return clk;
|
||||
};
|
||||
|
||||
// Portfolio layout — surfaces the row style that was previously only Left/Right-arrow cyclable,
|
||||
// and (unlike the arrow cycle) persists it.
|
||||
{
|
||||
const char* items[] = { TR("portfolio_style_compact"), TR("portfolio_style_detailed"),
|
||||
TR("portfolio_style_featured") };
|
||||
const int cur = st->getPortfolioStyle();
|
||||
const int clk = seg(TR("portfolio_style_label"), items, 3, cur, "##mktPfStyle");
|
||||
if (clk >= 0 && clk != cur) { st->setPortfolioStyle(clk); st->save(); }
|
||||
}
|
||||
// (Chart RANGE is deliberately not duplicated here — the Live/1H/1D/1W/1M toggle is always
|
||||
// visible on the chart itself, and a 5-segment control can't hold long "Live" translations.)
|
||||
// Chart style (candlesticks only render where per-exchange OHLC exists; the setting is still stored).
|
||||
{
|
||||
ImGui::Dummy(ImVec2(0.0f, 4.0f * dp));
|
||||
const char* items[] = { TR("market_style_line_label"), TR("market_style_candle_label") };
|
||||
const int cur = st->getChartStyle();
|
||||
const int clk = seg(TR("market_opt_chart_style"), items, 2, cur, "##mktChartStyle");
|
||||
if (clk >= 0 && clk != cur) { s_mkt.chartStyle = clk; st->setChartStyle(clk); st->save(); }
|
||||
}
|
||||
// Fetch live prices (master toggle, mirrors the general Settings page).
|
||||
{
|
||||
ImGui::Dummy(ImVec2(0.0f, 6.0f * dp));
|
||||
bool v = st->getFetchPrices();
|
||||
if (ImGui::Checkbox(TR("fetch_prices"), &v)) { st->setFetchPrices(v); st->save(); }
|
||||
}
|
||||
|
||||
// Done — sized to its text (+ a little shoulder room) and centered, not full width.
|
||||
ImGui::Dummy(ImVec2(0.0f, 8.0f * dp));
|
||||
{
|
||||
const char* doneLbl = TR("chat_settings_done");
|
||||
ImGui::PushFont(Type().button());
|
||||
const float bw = ImGui::CalcTextSize(doneLbl).x + ImGui::GetStyle().FramePadding.x * 2.0f + 28.0f * dp;
|
||||
ImGui::PopFont();
|
||||
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + std::max(0.0f, (ImGui::GetContentRegionAvail().x - bw) * 0.5f));
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(WithAlpha(Primary(), 205)));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(Primary()));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImGui::ColorConvertU32ToFloat4(WithAlpha(Primary(), 235)));
|
||||
if (material::TactileButton(doneLbl, ImVec2(bw, 0)))
|
||||
s_show_market_settings = false;
|
||||
ImGui::PopStyleColor(3);
|
||||
}
|
||||
EndOverlayDialog();
|
||||
}
|
||||
}
|
||||
|
||||
void RenderMarketTab(App* app)
|
||||
{
|
||||
auto& S = schema::UI();
|
||||
@@ -2058,14 +2252,16 @@ void RenderMarketTab(App* app)
|
||||
|
||||
// Left/Right arrows: cycle portfolio row styles (compact / detailed / featured), mirroring the
|
||||
// Overview tab's layout switch. Skip while typing, when Ctrl is held (theme cycle), or while the
|
||||
// portfolio editor modal is open.
|
||||
if (app->settings() && !s_pfEdit.open && !ImGui::GetIO().WantTextInput && !ImGui::GetIO().KeyCtrl) {
|
||||
// portfolio editor / market-settings modal is open.
|
||||
if (app->settings() && !s_pfEdit.open && !s_show_market_settings &&
|
||||
!ImGui::GetIO().WantTextInput && !ImGui::GetIO().KeyCtrl) {
|
||||
bool prev = ImGui::IsKeyPressed(ImGuiKey_LeftArrow);
|
||||
bool next = ImGui::IsKeyPressed(ImGuiKey_RightArrow);
|
||||
if (prev || next) {
|
||||
int st = app->settings()->getPortfolioStyle();
|
||||
st = next ? (st + 1) % 3 : (st + 2) % 3;
|
||||
app->settings()->setPortfolioStyle(st);
|
||||
app->settings()->save(); // persist the choice (matches the gear-modal control)
|
||||
const char* names[3] = { TR("portfolio_style_compact"), TR("portfolio_style_detailed"),
|
||||
TR("portfolio_style_featured") };
|
||||
Notifications::instance().info(std::string(TR("portfolio_style_label")) + ": " + names[st]);
|
||||
@@ -2123,15 +2319,16 @@ void RenderMarketTab(App* app)
|
||||
for (const auto& e : pfEntriesGeo)
|
||||
if (e.scope.empty() || (!pfActiveHash.empty() && e.scope == pfActiveHash)) pfVisN++;
|
||||
int pfStyle = app->settings()->getPortfolioStyle();
|
||||
float pfRowH = (pfStyle == 0 ? 46.0f : pfStyle == 1 ? 64.0f : 84.0f) * mktDp;
|
||||
float pfRowGap = Layout::spacingSm();
|
||||
float pfRowH = (pfStyle == 0 ? 40.0f : pfStyle == 1 ? 68.0f : 80.0f) * mktDp;
|
||||
float pfRowGap = (pfStyle == 0 ? 2.0f * mktDp : Layout::spacingSm()); // Table rows abut
|
||||
float pfHeaderH = (pfStyle == 0) ? (capFont->LegacySize + 8.0f * mktDp) : 0.0f; // Table column strip
|
||||
const int pfMaxVisibleRows = 4; // rows shown before the list scrolls
|
||||
// Height for up to pfMaxVisibleRows rows. ItemSpacing is zeroed inside the row child, so the
|
||||
// content is exactly rows + inter-row gaps; one extra gap of bottom breathing room keeps the
|
||||
// last visible row off the clip edge. More groups than this scroll internally.
|
||||
int pfVisRows = std::min(pfVisN, pfMaxVisibleRows);
|
||||
float pfGroupsH = (pfVisRows > 0)
|
||||
? (pfVisRows * pfRowH + (pfVisRows - 1) * pfRowGap + pfRowGap)
|
||||
? (pfHeaderH + pfVisRows * pfRowH + (pfVisRows - 1) * pfRowGap + pfRowGap)
|
||||
: 0.0f;
|
||||
float portfolioH = pfSummaryH + pfGroupsH;
|
||||
|
||||
@@ -2192,6 +2389,7 @@ void RenderMarketTab(App* app)
|
||||
// Portfolio editor modal (rendered outside the scroll child so it overlays the page). It captures
|
||||
// + blurs the live content drawn above, so it must render after the tab body.
|
||||
RenderPortfolioEditor(app);
|
||||
RenderMarketSettings(app);
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
|
||||
Reference in New Issue
Block a user