|
|
|
|
@@ -31,6 +31,7 @@
|
|
|
|
|
#include <algorithm>
|
|
|
|
|
#include <cmath>
|
|
|
|
|
#include <ctime>
|
|
|
|
|
#include <unordered_set>
|
|
|
|
|
|
|
|
|
|
namespace dragonx {
|
|
|
|
|
namespace ui {
|
|
|
|
|
@@ -47,7 +48,6 @@ 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.
|
|
|
|
|
@@ -133,7 +133,7 @@ struct PfEditState {
|
|
|
|
|
bool showValue = true;
|
|
|
|
|
bool show24h = false;
|
|
|
|
|
bool showSparkline = false;
|
|
|
|
|
int sparkInterval = 0; // 0=min 1=hour 2=day 3=week 4=month
|
|
|
|
|
int sparkInterval = 4; // 0=min 1=hour 2=day 3=week 4=month (default month)
|
|
|
|
|
};
|
|
|
|
|
static PfEditState s_pfEdit;
|
|
|
|
|
|
|
|
|
|
@@ -163,16 +163,16 @@ static bool pfProjectSeries(const std::vector<double>& hist, ImVec2 mn, ImVec2 m
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void pfDrawSparkline(ImDrawList* dl, ImVec2 mn, ImVec2 mx,
|
|
|
|
|
const std::vector<double>& hist, ImU32 col)
|
|
|
|
|
const std::vector<double>& hist, ImU32 col, float dp)
|
|
|
|
|
{
|
|
|
|
|
std::vector<ImVec2> 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<double>& hist, ImU32 lineCol)
|
|
|
|
|
const std::vector<double>& hist, ImU32 lineCol, float dp)
|
|
|
|
|
{
|
|
|
|
|
std::vector<ImVec2> pts;
|
|
|
|
|
if (!pfProjectSeries(hist, mn, mx, pts)) return;
|
|
|
|
|
@@ -181,7 +181,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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -284,7 +284,7 @@ static void PortfolioBeginEdit(App* app, int index)
|
|
|
|
|
s_pfEdit.showValue = true;
|
|
|
|
|
s_pfEdit.show24h = false;
|
|
|
|
|
s_pfEdit.showSparkline = false;
|
|
|
|
|
s_pfEdit.sparkInterval = 0;
|
|
|
|
|
s_pfEdit.sparkInterval = 4; // default month (a real curve from the daily series)
|
|
|
|
|
}
|
|
|
|
|
s_pfEdit.search[0] = '\0';
|
|
|
|
|
s_pfEdit.typeFilter = 0;
|
|
|
|
|
@@ -668,14 +668,18 @@ static void pfDrawAddressSection(App* app)
|
|
|
|
|
}
|
|
|
|
|
filtered.push_back(&a);
|
|
|
|
|
}
|
|
|
|
|
// Build the selected-address set once — the sort comparator and the per-row membership test below
|
|
|
|
|
// both consulted it via a linear PortfolioEntryContains scan (O(n) each) every frame.
|
|
|
|
|
std::unordered_set<std::string> selSet(s_pfEdit.addrs.begin(), s_pfEdit.addrs.end());
|
|
|
|
|
std::sort(filtered.begin(), filtered.end(), [&](const AddressInfo* x, const AddressInfo* y) {
|
|
|
|
|
bool sx = data::PortfolioEntryContains(s_pfEdit.addrs, x->address);
|
|
|
|
|
bool sy = data::PortfolioEntryContains(s_pfEdit.addrs, y->address);
|
|
|
|
|
bool sx = selSet.count(x->address) != 0;
|
|
|
|
|
bool sy = selSet.count(y->address) != 0;
|
|
|
|
|
if (sx != sy) return sx;
|
|
|
|
|
return x->balance > y->balance;
|
|
|
|
|
});
|
|
|
|
|
if (selAllClicked)
|
|
|
|
|
for (const AddressInfo* a : filtered) data::PortfolioEntryAdd(s_pfEdit.addrs, a->address);
|
|
|
|
|
for (const AddressInfo* a : filtered)
|
|
|
|
|
if (data::PortfolioEntryAdd(s_pfEdit.addrs, a->address)) selSet.insert(a->address);
|
|
|
|
|
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
|
|
|
|
ImDrawList* ldl = material::BeginFadeScrollChild("##pfAddrList", s_pfEdit.addrFade,
|
|
|
|
|
ImVec2(Layout::spacingMd(), Layout::spacingSm()), dp);
|
|
|
|
|
@@ -685,7 +689,7 @@ static void pfDrawAddressSection(App* app)
|
|
|
|
|
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("portfolio_no_addr_match"));
|
|
|
|
|
for (const AddressInfo* ap : filtered) {
|
|
|
|
|
const AddressInfo& a = *ap;
|
|
|
|
|
bool inSet = data::PortfolioEntryContains(s_pfEdit.addrs, a.address);
|
|
|
|
|
bool inSet = selSet.count(a.address) != 0;
|
|
|
|
|
ImVec2 rmn = ImGui::GetCursorScreenPos();
|
|
|
|
|
ImVec2 rmx(rmn.x + lw, rmn.y + rowH);
|
|
|
|
|
bool rhov = ImGui::IsMouseHoveringRect(rmn, rmx);
|
|
|
|
|
@@ -734,8 +738,8 @@ static void pfDrawAddressSection(App* app)
|
|
|
|
|
ImGui::PushID(a.address.c_str());
|
|
|
|
|
ImGui::InvisibleButton("##pfrow", ImVec2(lw, rowH));
|
|
|
|
|
if (ImGui::IsItemClicked()) {
|
|
|
|
|
if (inSet) data::PortfolioEntryRemove(s_pfEdit.addrs, a.address);
|
|
|
|
|
else data::PortfolioEntryAdd(s_pfEdit.addrs, a.address);
|
|
|
|
|
if (inSet) { data::PortfolioEntryRemove(s_pfEdit.addrs, a.address); selSet.erase(a.address); }
|
|
|
|
|
else { data::PortfolioEntryAdd(s_pfEdit.addrs, a.address); selSet.insert(a.address); }
|
|
|
|
|
}
|
|
|
|
|
ImGui::PopID();
|
|
|
|
|
}
|
|
|
|
|
@@ -957,7 +961,7 @@ static void RenderPortfolioEditor(App* app)
|
|
|
|
|
ImVec2 pMin = ImGui::GetCursorScreenPos();
|
|
|
|
|
ImVec2 pMax(pMin.x + pw, pMin.y + ph);
|
|
|
|
|
ImU32 accent = s_pfEdit.color ? (ImU32)s_pfEdit.color : 0;
|
|
|
|
|
GlassPanelSpec g; g.rounding = 10.0f; g.fillAlpha = 22; g.borderAlpha = 40;
|
|
|
|
|
GlassPanelSpec g; g.rounding = 10.0f * dp; g.fillAlpha = 22; g.borderAlpha = 40;
|
|
|
|
|
DrawGlassPanel(pdl, pMin, pMax, g);
|
|
|
|
|
if (accent) {
|
|
|
|
|
int oa = (int)(std::max(0, std::min(100, s_pfEdit.outlineOpacity)) * 2.55f + 0.5f);
|
|
|
|
|
@@ -969,7 +973,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;
|
|
|
|
|
@@ -1083,58 +1087,61 @@ bool PortfolioEditorActive() { return s_pfEdit.open; }
|
|
|
|
|
// 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.
|
|
|
|
|
// TABLE-style (style 0) column widths (logical px) — shared by pfTableCols (which turns them into
|
|
|
|
|
// right-edge anchors) and pfDrawRow (which feeds valW/drgxW to fit() truncation), so truncated text
|
|
|
|
|
// always lines up with the column edges. Change here and both stay in sync.
|
|
|
|
|
static constexpr float kPfChgColW = 62.0f;
|
|
|
|
|
static constexpr float kPfValColW = 108.0f;
|
|
|
|
|
static constexpr float kPfDrgxColW = 118.0f;
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
c.valR = c.chgR - kPfChgColW * dp - colGap;
|
|
|
|
|
c.drgxR = c.valR - kPfValColW * dp - colGap;
|
|
|
|
|
c.labelR = c.drgxR - kPfDrgxColW * dp - colGap;
|
|
|
|
|
return c;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Portfolio row height + inter-row gap by style — one source shared by the row drawing
|
|
|
|
|
// (mktDrawPortfolio) and the scroll-region height budget (RenderMarketTab) so they can't drift and clip.
|
|
|
|
|
// Row heights: Table 48dp (a dedicated trend column needs vertical room), Cards 68dp (two-zone),
|
|
|
|
|
// Spotlight 92dp (a dominant bottom chart band under the hero value). Uniform per style.
|
|
|
|
|
static float pfRowHeight(int style, float dp) { return (style == 0 ? 48.0f : style == 1 ? 68.0f : 92.0f) * dp; }
|
|
|
|
|
// Fraction of the Table label+trend zone given to the label; the rest is the centre TREND column.
|
|
|
|
|
// Shared by the row body and the header strip so the sparkline and its "TREND" label line up.
|
|
|
|
|
static constexpr float kPfTableTrendFrac = 0.38f;
|
|
|
|
|
static float pfRowGapFor(int style, float dp) { return style == 0 ? 2.0f * dp : Layout::spacingSm(); }
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
{
|
|
|
|
|
double bal = data::SumPortfolioBalance(e.addresses, state.addresses);
|
|
|
|
|
const 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)
|
|
|
|
|
|
|
|
|
|
// ---- 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). ----
|
|
|
|
|
// ---- Per-style CONTAINER: Table (0) = borderless ledger row (hairline + hover wash); Cards (1) =
|
|
|
|
|
// glass card with a faint identity tint; Spotlight (2) = a heavier glass tile. No accent line —
|
|
|
|
|
// the group's colour already reads from its icon and the sparkline, so it adds nothing here. ----
|
|
|
|
|
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;
|
|
|
|
|
GlassPanelSpec g; g.rounding = round; 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);
|
|
|
|
|
}
|
|
|
|
|
if (accent) dl->AddRectFilled(rowMin, rowMax, WithAlpha(accent, hov ? 16 : 10), round); // faint identity wash
|
|
|
|
|
if (hov) dl->AddRect(rowMin, rowMax, WithAlpha(OnSurface(), 80), round, 0, 1.2f);
|
|
|
|
|
} else {
|
|
|
|
|
GlassPanelSpec g; g.rounding = 10.0f; g.fillAlpha = hov ? 56 : 46; g.borderAlpha = 60;
|
|
|
|
|
GlassPanelSpec g; g.rounding = round; 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);
|
|
|
|
|
if (hov) dl->AddRect(rowMin, rowMax, WithAlpha(OnSurface(), 70), round, 0, 1.2f);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto tw = [](ImFont* f, const std::string& s){ return f->CalcTextSizeA(f->LegacySize, FLT_MAX, 0, s.c_str()).x; };
|
|
|
|
|
@@ -1174,11 +1181,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.
|
|
|
|
|
// ---- TABLE: icon + label (left); a dedicated centre TREND column (aligned + header-labelled);
|
|
|
|
|
// right-aligned numeric columns DRGX | value | 24h pill. The trend column is present for the
|
|
|
|
|
// whole list when any group opts in (tableReserveSpark); each row fills it when it has one.
|
|
|
|
|
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
|
|
|
|
|
const float valW = kPfValColW*dp, drgxW = kPfDrgxColW*dp; // fit() widths (shared with pfTableCols)
|
|
|
|
|
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);
|
|
|
|
|
@@ -1194,65 +1202,81 @@ 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());
|
|
|
|
|
}
|
|
|
|
|
const float lblMaxW = std::max(24.0f*dp, labelR - x);
|
|
|
|
|
// Dedicated centre TREND column: when the list has any sparkline (tableReserveSpark), cap the
|
|
|
|
|
// label to a fixed boundary so the column + its header label align across every row; each row
|
|
|
|
|
// fills it only when that group opts in. Rows without any list-wide trend keep the full label.
|
|
|
|
|
// Trend-column origin from the fixed LEFT (icon-independent) so it lines up with the "TREND"
|
|
|
|
|
// header and across rows whether or not a group has an icon (x may be shifted past the icon).
|
|
|
|
|
const float zoneW = std::max(24.0f*dp, labelR - left);
|
|
|
|
|
const float trendX0 = left + zoneW * kPfTableTrendFrac;
|
|
|
|
|
const float lblMaxW = tableReserveSpark ? std::max(24.0f*dp, trendX0 - colGap - x) : 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());
|
|
|
|
|
if (hasSpark && tableReserveSpark && labelR - trendX0 > 40.0f * dp)
|
|
|
|
|
pfDrawSparklineFilled(dl, ImVec2(trendX0, rowMin.y + padY), ImVec2(labelR, rowMax.y - padY), spark, sparkCol, dp);
|
|
|
|
|
} else if (style == 1) {
|
|
|
|
|
// ---- 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;
|
|
|
|
|
// ---- CARDS: a balanced snapshot — a LEFT-aligned info stack (icon + label / value+delta chip /
|
|
|
|
|
// DRGX) grouped on the left, with a full-height sparkline filling the rest of the width.
|
|
|
|
|
const float zoneTop = rowMin.y + padY;
|
|
|
|
|
const float zoneBot = rowMax.y - padY;
|
|
|
|
|
// Icon centred in the info zone; the label/value/DRGX stack sits to its right.
|
|
|
|
|
const float iconSz = capFont->LegacySize;
|
|
|
|
|
float textX = left;
|
|
|
|
|
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();
|
|
|
|
|
material::project_icons::drawByName(dl, e.icon.c_str(), ImVec2(left + iconSz*0.5f, (zoneTop + zoneBot)*0.5f), accent?accent:OnSurfaceMedium(), Type().iconSmall(), iconSz);
|
|
|
|
|
textX = left + 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);
|
|
|
|
|
// Info zone driven by the ACTUAL content width so a short group hands the extra room to the chart;
|
|
|
|
|
// bounded [28%, 52%] so the info never cramps and the chart always keeps a decent band.
|
|
|
|
|
float infoR = right, cSparkL = right;
|
|
|
|
|
if (hasSpark) {
|
|
|
|
|
const std::string vTxt0 = wantValue ? (hasValue?valStr:kDash) : std::string();
|
|
|
|
|
const float chipW = (wantChange && hasChange) ? (Layout::spacingSm() + tw(capFont, chgStr) + 12.0f*dp) : 0.0f;
|
|
|
|
|
const float valLineW = wantValue ? (tw(sub1, vTxt0) + chipW) : 0.0f;
|
|
|
|
|
const float contentW = std::max(std::max(tw(capFont, e.label), valLineW), e.showDrgx ? tw(capFont, drgxStr) : 0.0f);
|
|
|
|
|
const float span = right - left;
|
|
|
|
|
const float infoW = std::min(std::max((textX - left) + contentW + colGap * 2.0f, span * 0.28f), span * 0.52f);
|
|
|
|
|
cSparkL = left + infoW;
|
|
|
|
|
infoR = cSparkL - colGap;
|
|
|
|
|
}
|
|
|
|
|
const float gap = Layout::spacingXs();
|
|
|
|
|
const float stackH = capFont->LegacySize + gap + sub1->LegacySize + gap + capFont->LegacySize;
|
|
|
|
|
float cy = zoneTop + std::max(0.0f, (zoneBot - zoneTop - stackH) * 0.5f);
|
|
|
|
|
// label (muted)
|
|
|
|
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(textX, cy), OnSurfaceMedium(), fit(e.label, capFont, std::max(24.0f*dp, infoR - textX)).c_str());
|
|
|
|
|
cy += capFont->LegacySize + gap;
|
|
|
|
|
// value (leads via a soft shadow) + delta chip trailing it
|
|
|
|
|
if (wantValue) {
|
|
|
|
|
const std::string vTxt = hasValue ? valStr : kDash;
|
|
|
|
|
DrawTextShadow(dl, sub1, sub1->LegacySize, ImVec2(textX, cy), hasValue?OnSurface():OnSurfaceDisabled(), vTxt.c_str());
|
|
|
|
|
if (wantChange && hasChange) {
|
|
|
|
|
const float pw = tw(capFont, chgStr) + 12.0f*dp, ph = capFont->LegacySize + 5.0f*dp;
|
|
|
|
|
const float chipX = textX + tw(sub1, vTxt) + Layout::spacingSm();
|
|
|
|
|
const float chipY = cy + (sub1->LegacySize - ph)*0.5f;
|
|
|
|
|
if (chipX + pw <= infoR) {
|
|
|
|
|
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());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
cy += sub1->LegacySize + gap;
|
|
|
|
|
// DRGX (muted)
|
|
|
|
|
if (e.showDrgx) dl->AddText(capFont, capFont->LegacySize, ImVec2(textX, cy), OnSurfaceMedium(), fit(drgxStr, capFont, std::max(24.0f*dp, infoR - textX)).c_str());
|
|
|
|
|
if (hasSpark) pfDrawSparklineFilled(dl, ImVec2(cSparkL, zoneTop), ImVec2(right, zoneBot), spark, sparkCol, dp);
|
|
|
|
|
} 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.
|
|
|
|
|
// ---- SPOTLIGHT: "big number over a chart" hero tile — a top zone (muted name + DRGX, then a
|
|
|
|
|
// LARGE value coloured by 24h with a delta chip) over a dominant full-width sparkline band
|
|
|
|
|
// filling the bottom. The value spans the full width now (no competing right strip).
|
|
|
|
|
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;
|
|
|
|
|
const float topY = rowMin.y + padY;
|
|
|
|
|
float iconSz = capFont->LegacySize, x = left;
|
|
|
|
|
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);
|
|
|
|
|
material::project_icons::drawByName(dl, e.icon.c_str(), ImVec2(x+iconSz*0.5f, topY+capFont->LegacySize*0.5f), accent?accent:OnSurfaceMedium(), Type().iconSmall(), iconSz);
|
|
|
|
|
x += iconSz + Layout::spacingSm();
|
|
|
|
|
}
|
|
|
|
|
// 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);
|
|
|
|
|
if (e.showDrgx) rtext(capFont, right, topY + capFont->LegacySize*0.5f, OnSurfaceMedium(), drgxStr);
|
|
|
|
|
const float nameMaxW = std::max(24.0f*dp, (right - (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();
|
|
|
|
|
@@ -1260,17 +1284,22 @@ static void pfDrawRow(ImDrawList* dl, ImVec2 rowMin, ImVec2 rowMax,
|
|
|
|
|
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)
|
|
|
|
|
// delta chip trailing the hero value
|
|
|
|
|
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) {
|
|
|
|
|
if (chipX + pw <= right) {
|
|
|
|
|
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);
|
|
|
|
|
// full-width sparkline band filling the bottom of the tile, below the hero value
|
|
|
|
|
if (hasSpark) {
|
|
|
|
|
const float bandTop = valY + heroSz + Layout::spacingXs();
|
|
|
|
|
if (rowMax.y - padY - bandTop > 12.0f * dp)
|
|
|
|
|
pfDrawSparklineFilled(dl, ImVec2(left, bandTop), ImVec2(right, rowMax.y - padY), spark, sparkCol, dp);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (zdVtx >= 0) { // fade the just-emitted body vertices for an empty group
|
|
|
|
|
@@ -1474,6 +1503,23 @@ static void mktDrawPriceHero(const MktCtx& cx)
|
|
|
|
|
float tradeBtnX = cardMax.x - pad - tradeBtnW;
|
|
|
|
|
float tradeBtnY = cardMin.y + Layout::spacingSm();
|
|
|
|
|
|
|
|
|
|
// Chart line/candle segmented control, immediately left of the trade button — only when the
|
|
|
|
|
// selected range has per-exchange candles (Live / aggregate is line-only, nothing to toggle).
|
|
|
|
|
if (cx.chartCandles && cx.chartCandles->size() >= 2) {
|
|
|
|
|
const char* styleIcons[2] = { ICON_MD_SHOW_CHART, ICON_MD_CANDLESTICK_CHART };
|
|
|
|
|
const float csW = 2.0f * tradeBtnH;
|
|
|
|
|
const ImVec2 csOrigin(tradeBtnX - csW - Layout::spacingSm(), tradeBtnY);
|
|
|
|
|
const ImVec2 csSaved = ImGui::GetCursorScreenPos();
|
|
|
|
|
const int csCur = s_mkt.chartStyle;
|
|
|
|
|
const int csClk = SegmentedControl(dl, csOrigin, csW, tradeBtnH, styleIcons, 2, csCur,
|
|
|
|
|
Type().iconSmall(), "##chartStyleSeg", dp);
|
|
|
|
|
if (csClk >= 0 && csClk != csCur) {
|
|
|
|
|
s_mkt.chartStyle = csClk;
|
|
|
|
|
if (app->settings()) { app->settings()->setChartStyle(csClk); app->settings()->save(); }
|
|
|
|
|
}
|
|
|
|
|
ImGui::SetCursorScreenPos(csSaved);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ImVec2 tMin(tradeBtnX, tradeBtnY), tMax(tradeBtnX + tradeBtnW, tradeBtnY + tradeBtnH);
|
|
|
|
|
bool tradeHov = material::IsRectHovered(tMin, tMax);
|
|
|
|
|
|
|
|
|
|
@@ -1596,30 +1642,7 @@ static void mktDrawPriceChart(const MktCtx& cx)
|
|
|
|
|
bx += bw + Layout::spacingXs();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Line/candle toggle — only when the selected range has per-exchange candles (the aggregate /
|
|
|
|
|
// Live view is line-only, so the toggle is hidden there).
|
|
|
|
|
if (cx.chartCandles && cx.chartCandles->size() >= 2) {
|
|
|
|
|
bx += Layout::spacingSm();
|
|
|
|
|
ImFont* icoF = material::Typography::instance().iconSmall();
|
|
|
|
|
const bool isCandle = (s_mkt.chartStyle == 1);
|
|
|
|
|
const char* styleIcon = isCandle ? ICON_MD_CANDLESTICK_CHART : ICON_MD_SHOW_CHART;
|
|
|
|
|
ImVec2 tmn(bx, rowTop), tmx(bx + pillH, rowTop + pillH);
|
|
|
|
|
bool thov = material::IsRectHovered(tmn, tmx);
|
|
|
|
|
if (thov) { dl->AddRectFilled(tmn, tmx, IM_COL32(255, 255, 255, 20), 4.0f * mktDp);
|
|
|
|
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); }
|
|
|
|
|
ImVec2 tiSz = icoF->CalcTextSizeA(icoF->LegacySize, FLT_MAX, 0, styleIcon);
|
|
|
|
|
dl->AddText(icoF, icoF->LegacySize,
|
|
|
|
|
ImVec2(tmn.x + (pillH - tiSz.x) * 0.5f, tmn.y + (pillH - tiSz.y) * 0.5f),
|
|
|
|
|
thov ? OnSurface() : OnSurfaceMedium(), styleIcon);
|
|
|
|
|
ImGui::SetCursorScreenPos(tmn);
|
|
|
|
|
if (ImGui::InvisibleButton("##ChartStyle", ImVec2(pillH, pillH))) {
|
|
|
|
|
s_mkt.chartStyle = isCandle ? 0 : 1;
|
|
|
|
|
if (app->settings()) { app->settings()->setChartStyle(s_mkt.chartStyle); app->settings()->save(); }
|
|
|
|
|
}
|
|
|
|
|
if (ImGui::IsItemHovered())
|
|
|
|
|
material::Tooltip("%s", TR(isCandle ? "market_style_line" : "market_style_candle"));
|
|
|
|
|
bx += pillH;
|
|
|
|
|
}
|
|
|
|
|
// (Line/candle chart-style toggle now lives top-right of the price hero, left of the trade button.)
|
|
|
|
|
|
|
|
|
|
// Refresh button (far right).
|
|
|
|
|
float rEdge = chartMax.x - chartPad;
|
|
|
|
|
@@ -1703,7 +1726,7 @@ static void mktDrawPriceChart(const MktCtx& cx)
|
|
|
|
|
ImVec2 labelSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, buf);
|
|
|
|
|
// Keep the axis label inside the card even on narrow windows (min-padding may be
|
|
|
|
|
// smaller than the label width) so it never spills onto the tab background.
|
|
|
|
|
float lblX = std::max(chartMin.x + 3.0f, plotLeft - labelSz.x - 6);
|
|
|
|
|
float lblX = std::max(chartMin.x + 3.0f * mktDp, plotLeft - labelSz.x - 6.0f * mktDp);
|
|
|
|
|
dl->AddText(capFont, capFont->LegacySize,
|
|
|
|
|
ImVec2(lblX, gy - labelSz.y * 0.5f),
|
|
|
|
|
OnSurfaceDisabled(), buf);
|
|
|
|
|
@@ -1766,8 +1789,8 @@ static void mktDrawPriceChart(const MktCtx& cx)
|
|
|
|
|
tipX = std::max(plotLeft, tipX);
|
|
|
|
|
float tipY = plotTop + 4.0f * mktDp;
|
|
|
|
|
ImVec2 tMin(tipX, tipY), tMax(tipX + tw + pad * 2, tipY + th);
|
|
|
|
|
dl->AddRectFilled(tMin, tMax, IM_COL32(20, 20, 30, 235), 4.0f);
|
|
|
|
|
dl->AddRect(tMin, tMax, IM_COL32(255, 255, 255, 30), 4.0f, 0, 1.0f);
|
|
|
|
|
dl->AddRectFilled(tMin, tMax, IM_COL32(20, 20, 30, 235), 4.0f * mktDp);
|
|
|
|
|
dl->AddRect(tMin, tMax, IM_COL32(255, 255, 255, 30), 4.0f * mktDp, 0, 1.0f);
|
|
|
|
|
ImU32 cCol = (hc.close >= hc.open) ? Success() : Error();
|
|
|
|
|
dl->AddText(capFont, lh, ImVec2(tipX + pad, tipY + pad), OnSurface(), when);
|
|
|
|
|
dl->AddText(capFont, lh, ImVec2(tipX + pad, tipY + pad + lh + gap), cCol, l2);
|
|
|
|
|
@@ -1870,16 +1893,16 @@ static void mktDrawPriceChart(const MktCtx& cx)
|
|
|
|
|
snprintf(buf, sizeof(buf), "%s", FormatPrice(s_mkt.history[idx]).c_str());
|
|
|
|
|
ImVec2 tipSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, buf);
|
|
|
|
|
float tipPad = Layout::spacingSm() + Layout::spacingXs();
|
|
|
|
|
float tipX = px + 10;
|
|
|
|
|
float tipY = py - tipSz.y - tipPad * 2 - 4;
|
|
|
|
|
float tipX = px + 10.0f * mktDp;
|
|
|
|
|
float tipY = py - tipSz.y - tipPad * 2 - 4.0f * mktDp;
|
|
|
|
|
if (tipX + tipSz.x + tipPad * 2 > plotRight)
|
|
|
|
|
tipX = px - tipSz.x - tipPad * 2 - 10;
|
|
|
|
|
if (tipY < plotTop) tipY = py + 10;
|
|
|
|
|
tipX = px - tipSz.x - tipPad * 2 - 10.0f * mktDp;
|
|
|
|
|
if (tipY < plotTop) tipY = py + 10.0f * mktDp;
|
|
|
|
|
|
|
|
|
|
ImVec2 tipMin(tipX, tipY);
|
|
|
|
|
ImVec2 tipMax(tipX + tipSz.x + tipPad * 2, tipY + tipSz.y + tipPad * 2);
|
|
|
|
|
dl->AddRectFilled(tipMin, tipMax, IM_COL32(20, 20, 30, 230), 4.0f);
|
|
|
|
|
dl->AddRect(tipMin, tipMax, IM_COL32(255, 255, 255, 30), 4.0f, 0, 1.0f);
|
|
|
|
|
dl->AddRectFilled(tipMin, tipMax, IM_COL32(20, 20, 30, 230), 4.0f * mktDp);
|
|
|
|
|
dl->AddRect(tipMin, tipMax, IM_COL32(255, 255, 255, 30), 4.0f * mktDp, 0, 1.0f);
|
|
|
|
|
dl->AddText(capFont, capFont->LegacySize,
|
|
|
|
|
ImVec2(tipX + tipPad, tipY + tipPad), dotCol, buf);
|
|
|
|
|
}
|
|
|
|
|
@@ -1945,23 +1968,29 @@ static void mktDrawPortfolio(const MktCtx& cx)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("market_portfolio"));
|
|
|
|
|
// 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.
|
|
|
|
|
// Portfolio-style segmented control (Table / Cards / Spotlight) + "Manage…" button, right-aligned on
|
|
|
|
|
// the header row (style picker left of Manage). 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();
|
|
|
|
|
const char* styleItems[3] = { TR("portfolio_style_compact"), TR("portfolio_style_detailed"),
|
|
|
|
|
TR("portfolio_style_featured") };
|
|
|
|
|
const float segH = ImGui::GetFrameHeight();
|
|
|
|
|
const float segW = 210.0f * mktDp;
|
|
|
|
|
const float sGap = Layout::spacingSm();
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
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);
|
|
|
|
|
RightAlignX(segW + sGap + mBtnW); // right-align the [style picker | Manage] group
|
|
|
|
|
const ImVec2 segOrigin = ImGui::GetCursorScreenPos();
|
|
|
|
|
const int curStyle = app->settings() ? app->settings()->getPortfolioStyle() : 0;
|
|
|
|
|
const int segClk = SegmentedControl(ImGui::GetWindowDrawList(), segOrigin, segW, segH,
|
|
|
|
|
styleItems, 3, curStyle, Type().caption(), "##pfStyleSeg", mktDp);
|
|
|
|
|
if (segClk >= 0 && segClk != curStyle && app->settings()) {
|
|
|
|
|
app->settings()->setPortfolioStyle(segClk);
|
|
|
|
|
app->settings()->save();
|
|
|
|
|
}
|
|
|
|
|
ImGui::SetCursorScreenPos(segOrigin);
|
|
|
|
|
ImGui::Dummy(ImVec2(segW, segH)); // reserve + restore layout flow
|
|
|
|
|
ImGui::SameLine(0.0f, sGap);
|
|
|
|
|
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.
|
|
|
|
|
@@ -2071,9 +2100,16 @@ static void mktDrawPortfolio(const MktCtx& cx)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int style = app->settings() ? app->settings()->getPortfolioStyle() : 0;
|
|
|
|
|
float rowH = (style == 0 ? 40.0f : style == 1 ? 68.0f : 80.0f) * mktDp;
|
|
|
|
|
// The Table's dedicated TREND column (+ its header label) is present for the whole list when any
|
|
|
|
|
// visible group opts into a sparkline, so the column aligns and doesn't flicker per row.
|
|
|
|
|
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 = (style == 0 ? 2.0f * mktDp : Layout::spacingSm());
|
|
|
|
|
float rowGap = pfRowGapFor(style, mktDp);
|
|
|
|
|
float rowsH = std::max(rowH, portfolioH - pfSummaryH);
|
|
|
|
|
|
|
|
|
|
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMin.y + pfSummaryH));
|
|
|
|
|
@@ -2108,6 +2144,8 @@ static void mktDrawPortfolio(const MktCtx& cx)
|
|
|
|
|
rdl->AddText(ovF, ovF->LegacySize, ImVec2(xr - w, ty), hcol, s);
|
|
|
|
|
};
|
|
|
|
|
rdl->AddText(ovF, ovF->LegacySize, ImVec2(hLeft, ty), hcol, TR("market_col_name"));
|
|
|
|
|
if (anySpark) // TREND column header, left-aligned at the same x the row sparklines start
|
|
|
|
|
rdl->AddText(ovF, ovF->LegacySize, ImVec2(hLeft + (hc.labelR - hLeft) * kPfTableTrendFrac, ty), hcol, TR("market_col_trend"));
|
|
|
|
|
hdr(hc.drgxR, DRAGONX_TICKER);
|
|
|
|
|
hdr(hc.valR, TR("market_col_value"));
|
|
|
|
|
hdr(hc.chgR, TR("market_24h"));
|
|
|
|
|
@@ -2124,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));
|
|
|
|
|
}
|
|
|
|
|
@@ -2138,87 +2176,6 @@ 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();
|
|
|
|
|
@@ -2241,6 +2198,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
|
|
|
|
|
@@ -2253,7 +2214,7 @@ 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 / market-settings modal is open.
|
|
|
|
|
if (app->settings() && !s_pfEdit.open && !s_show_market_settings &&
|
|
|
|
|
if (app->settings() && !s_pfEdit.open &&
|
|
|
|
|
!ImGui::GetIO().WantTextInput && !ImGui::GetIO().KeyCtrl) {
|
|
|
|
|
bool prev = ImGui::IsKeyPressed(ImGuiKey_LeftArrow);
|
|
|
|
|
bool next = ImGui::IsKeyPressed(ImGuiKey_RightArrow);
|
|
|
|
|
@@ -2319,8 +2280,8 @@ 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 ? 40.0f : pfStyle == 1 ? 68.0f : 80.0f) * mktDp;
|
|
|
|
|
float pfRowGap = (pfStyle == 0 ? 2.0f * mktDp : Layout::spacingSm()); // Table rows abut
|
|
|
|
|
float pfRowH = pfRowHeight(pfStyle, mktDp);
|
|
|
|
|
float pfRowGap = pfRowGapFor(pfStyle, mktDp); // 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
|
|
|
|
|
@@ -2389,7 +2350,6 @@ 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
|
|
|
|
|
|