|
|
|
|
@@ -111,6 +111,23 @@ static bool s_pf_show_drgx = true;
|
|
|
|
|
static bool s_pf_show_value = true;
|
|
|
|
|
static bool s_pf_show_24h = false;
|
|
|
|
|
static bool s_pf_show_sparkline = false;
|
|
|
|
|
static int s_pf_spark_interval = 0; // 0=min 1=hour 2=day 3=week 4=month
|
|
|
|
|
|
|
|
|
|
// price_history is ~1 sample/minute; resample it to the group's chosen interval by averaging
|
|
|
|
|
// each block of K minute-samples into one point.
|
|
|
|
|
static std::vector<double> pfResampleHistory(const std::vector<double>& hist, int interval)
|
|
|
|
|
{
|
|
|
|
|
static const int kMins[5] = {1, 60, 1440, 10080, 43200};
|
|
|
|
|
int k = kMins[(interval >= 0 && interval < 5) ? interval : 0];
|
|
|
|
|
if (k <= 1) return hist;
|
|
|
|
|
std::vector<double> out;
|
|
|
|
|
for (size_t i = 0; i < hist.size(); i += (size_t)k) {
|
|
|
|
|
double sum = 0.0; size_t cnt = 0;
|
|
|
|
|
for (size_t j = i; j < hist.size() && j < i + (size_t)k; ++j) { sum += hist[j]; ++cnt; }
|
|
|
|
|
if (cnt) out.push_back(sum / (double)cnt);
|
|
|
|
|
}
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// A group's value trend tracks the DRGX price, so a group sparkline plots the price history
|
|
|
|
|
// (normalized) across a rect. Colored by overall direction. Only meaningful for market bases.
|
|
|
|
|
@@ -208,6 +225,7 @@ static void PortfolioBeginEdit(App* app, int index)
|
|
|
|
|
s_pf_show_value = src.showValue;
|
|
|
|
|
s_pf_show_24h = src.show24h;
|
|
|
|
|
s_pf_show_sparkline = src.showSparkline;
|
|
|
|
|
s_pf_spark_interval = src.sparklineInterval;
|
|
|
|
|
} else {
|
|
|
|
|
s_pf_label[0] = '\0';
|
|
|
|
|
s_pf_addrs.clear();
|
|
|
|
|
@@ -220,6 +238,7 @@ static void PortfolioBeginEdit(App* app, int index)
|
|
|
|
|
s_pf_show_value = true;
|
|
|
|
|
s_pf_show_24h = false;
|
|
|
|
|
s_pf_show_sparkline = false;
|
|
|
|
|
s_pf_spark_interval = 0;
|
|
|
|
|
}
|
|
|
|
|
s_pf_search[0] = '\0';
|
|
|
|
|
s_pf_type_filter = 0;
|
|
|
|
|
@@ -312,13 +331,14 @@ static void RenderPortfolioEditor(App* app)
|
|
|
|
|
pdl->AddRectFilled(ImVec2(bx, pMin.y + ppad * 0.6f),
|
|
|
|
|
ImVec2(bx + 3.0f, pMax.y - ppad * 0.6f), accent, 2.0f);
|
|
|
|
|
}
|
|
|
|
|
if (s_pf_show_sparkline && (s_pf_price_basis == 0 || s_pf_price_basis == 1) &&
|
|
|
|
|
state.market.price_history.size() >= 2) {
|
|
|
|
|
const auto& h = state.market.price_history;
|
|
|
|
|
ImU32 spCol = WithAlpha(h.back() >= h.front() ? Success() : Error(), 80);
|
|
|
|
|
ImVec2 spMin(pMin.x + ppad + (accent ? 6.0f : 0.0f), pMin.y + ph * 0.46f);
|
|
|
|
|
ImVec2 spMax(pMax.x - ppad, pMax.y - ppad * 0.6f);
|
|
|
|
|
pfDrawSparkline(pdl, spMin, spMax, h, spCol);
|
|
|
|
|
if (s_pf_show_sparkline && (s_pf_price_basis == 0 || s_pf_price_basis == 1)) {
|
|
|
|
|
std::vector<double> h = pfResampleHistory(state.market.price_history, s_pf_spark_interval);
|
|
|
|
|
if (h.size() >= 2) {
|
|
|
|
|
ImU32 spCol = WithAlpha(h.back() >= h.front() ? Success() : Error(), 80);
|
|
|
|
|
ImVec2 spMin(pMin.x + ppad + (accent ? 6.0f : 0.0f), pMin.y + ph * 0.46f);
|
|
|
|
|
ImVec2 spMax(pMax.x - ppad, pMax.y - ppad * 0.6f);
|
|
|
|
|
pfDrawSparkline(pdl, spMin, spMax, h, spCol);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
float rowY = pMin.y + ppad, nameX = pMin.x + ppad + (accent ? 6.0f : 0.0f);
|
|
|
|
|
if (!s_pf_icon.empty()) {
|
|
|
|
|
@@ -458,9 +478,17 @@ static void RenderPortfolioEditor(App* app)
|
|
|
|
|
ImGui::BeginDisabled(!(s_pf_price_basis == 0 || s_pf_price_basis == 1)); // 24h: live-market only
|
|
|
|
|
ImGui::Checkbox(TR("portfolio_show_24h"), &s_pf_show_24h);
|
|
|
|
|
ImGui::EndDisabled();
|
|
|
|
|
// Sparkline on its own line (also live-market only).
|
|
|
|
|
// Sparkline on its own line (also live-market only) + its interval.
|
|
|
|
|
ImGui::BeginDisabled(!(s_pf_price_basis == 0 || s_pf_price_basis == 1));
|
|
|
|
|
ImGui::Checkbox(TR("portfolio_show_sparkline"), &s_pf_show_sparkline);
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
ImGui::BeginDisabled(!s_pf_show_sparkline);
|
|
|
|
|
const char* spItems[5] = { TR("portfolio_spark_min"), TR("portfolio_spark_hour"),
|
|
|
|
|
TR("portfolio_spark_day"), TR("portfolio_spark_week"),
|
|
|
|
|
TR("portfolio_spark_month") };
|
|
|
|
|
ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x);
|
|
|
|
|
ImGui::Combo("##pfSparkInterval", &s_pf_spark_interval, spItems, 5);
|
|
|
|
|
ImGui::EndDisabled();
|
|
|
|
|
ImGui::EndDisabled();
|
|
|
|
|
}
|
|
|
|
|
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
|
|
|
|
@@ -662,7 +690,10 @@ static void RenderPortfolioEditor(App* app)
|
|
|
|
|
ImGui::BeginDisabled(!canSave);
|
|
|
|
|
if (ImGui::Button(TR("portfolio_save"))) {
|
|
|
|
|
auto entries = settings->getPortfolioEntries();
|
|
|
|
|
// Start from the existing entry when editing so its grid placement/size (and any
|
|
|
|
|
// other fields not on this form) are preserved — editing must not reset the card.
|
|
|
|
|
config::Settings::PortfolioEntry e;
|
|
|
|
|
if (s_pf_editing >= 0 && s_pf_editing < (int)entries.size()) e = entries[s_pf_editing];
|
|
|
|
|
e.label = s_pf_label;
|
|
|
|
|
e.addresses = s_pf_addrs;
|
|
|
|
|
e.icon = s_pf_icon;
|
|
|
|
|
@@ -674,6 +705,7 @@ static void RenderPortfolioEditor(App* app)
|
|
|
|
|
e.showValue = s_pf_show_value;
|
|
|
|
|
e.show24h = s_pf_show_24h;
|
|
|
|
|
e.showSparkline = s_pf_show_sparkline;
|
|
|
|
|
e.sparklineInterval = s_pf_spark_interval;
|
|
|
|
|
if (s_pf_editing >= 0 && s_pf_editing < (int)entries.size()) entries[s_pf_editing] = e;
|
|
|
|
|
else entries.push_back(e);
|
|
|
|
|
persist(entries);
|
|
|
|
|
@@ -767,12 +799,11 @@ void RenderMarketTab(App* app)
|
|
|
|
|
int pfN = (int)pfEntriesGeo.size();
|
|
|
|
|
float pfCardGap = Layout::spacingSm();
|
|
|
|
|
float pfCardPad = Layout::spacingMd(); // inner padding of each group card
|
|
|
|
|
// Dashboard grid: responsive column count; a 1x1 cell is one card. Cards can be dragged to a
|
|
|
|
|
// new cell and corner-resized to span multiple cells.
|
|
|
|
|
int pfCols = std::max(1, std::min(4, (int)(availWidth / (240.0f * mktDp) + 0.5f)));
|
|
|
|
|
// Dashboard grid of SQUARE cells. Column count is responsive; min 2 columns so a card can meet
|
|
|
|
|
// its 2x1 minimum size. Cards drag to a new cell and corner-resize to span multiple cells.
|
|
|
|
|
int pfCols = std::max(2, std::min(8, (int)(availWidth / (110.0f * mktDp) + 0.5f)));
|
|
|
|
|
float pfCellW = (availWidth - (pfCols - 1) * pfCardGap) / (float)pfCols;
|
|
|
|
|
// A 1x1 cell: name row (sub1) + a fiat row (caption) + padding; the amounts sit top-right.
|
|
|
|
|
float pfCellH = pfCardPad * 2.0f + sub1->LegacySize + Layout::spacingXs() + capFont->LegacySize;
|
|
|
|
|
float pfCellH = pfCellW; // square cells; the smallest card is 2x1
|
|
|
|
|
// Resolve each group's grid cell: honor a stored placement when it fits, else auto-place
|
|
|
|
|
// row-major into the first free cells. Deterministic each frame.
|
|
|
|
|
std::vector<PfCell> pfLayout((size_t)std::max(0, pfN));
|
|
|
|
|
@@ -790,7 +821,7 @@ void RenderMarketTab(App* app)
|
|
|
|
|
std::vector<char> placed((size_t)pfN, 0);
|
|
|
|
|
for (int i = 0; i < pfN; i++) {
|
|
|
|
|
const auto& e = pfEntriesGeo[i];
|
|
|
|
|
int w = std::max(1, std::min(e.gridW, pfCols)), h = std::max(1, e.gridH);
|
|
|
|
|
int w = std::max(2, std::min(e.gridW, pfCols)), h = std::max(1, e.gridH);
|
|
|
|
|
if (e.gridCol >= 0 && e.gridRow >= 0 && fits(e.gridCol, e.gridRow, w, h)) {
|
|
|
|
|
pfLayout[i] = {e.gridCol, e.gridRow, w, h}; mark(e.gridCol, e.gridRow, w, h); placed[i] = 1;
|
|
|
|
|
}
|
|
|
|
|
@@ -798,7 +829,7 @@ void RenderMarketTab(App* app)
|
|
|
|
|
for (int i = 0; i < pfN; i++) {
|
|
|
|
|
if (placed[i]) continue;
|
|
|
|
|
const auto& e = pfEntriesGeo[i];
|
|
|
|
|
int w = std::max(1, std::min(e.gridW, pfCols)), h = std::max(1, e.gridH);
|
|
|
|
|
int w = std::max(2, std::min(e.gridW, pfCols)), h = std::max(1, e.gridH);
|
|
|
|
|
int fc = 0, fr = 0; bool found = false;
|
|
|
|
|
for (int r = 0; !found && r < 4096; r++)
|
|
|
|
|
for (int c = 0; c + w <= pfCols; c++)
|
|
|
|
|
@@ -1378,12 +1409,13 @@ void RenderMarketTab(App* app)
|
|
|
|
|
dl->AddRectFilled(ImVec2(bx, gcMin.y + pfCardPad * 0.6f),
|
|
|
|
|
ImVec2(bx + 3.0f, gcMax.y - pfCardPad * 0.6f), accent, 2.0f);
|
|
|
|
|
}
|
|
|
|
|
if (e.showSparkline && (e.priceBasis == 0 || e.priceBasis == 1) &&
|
|
|
|
|
market.price_history.size() >= 2) {
|
|
|
|
|
const auto& hh = market.price_history;
|
|
|
|
|
ImU32 spCol = WithAlpha(hh.back() >= hh.front() ? Success() : Error(), 80);
|
|
|
|
|
pfDrawSparkline(dl, ImVec2(gcMin.x + pfCardPad + (accent ? 6.0f : 0.0f), gcMin.y + ch * 0.46f),
|
|
|
|
|
ImVec2(gcMax.x - pfCardPad, gcMax.y - pfCardPad * 0.6f), hh, spCol);
|
|
|
|
|
if (e.showSparkline && (e.priceBasis == 0 || e.priceBasis == 1)) {
|
|
|
|
|
std::vector<double> hh = pfResampleHistory(market.price_history, e.sparklineInterval);
|
|
|
|
|
if (hh.size() >= 2) {
|
|
|
|
|
ImU32 spCol = WithAlpha(hh.back() >= hh.front() ? Success() : Error(), 80);
|
|
|
|
|
pfDrawSparkline(dl, ImVec2(gcMin.x + pfCardPad + (accent ? 6.0f : 0.0f), gcMin.y + ch * 0.46f),
|
|
|
|
|
ImVec2(gcMax.x - pfCardPad, gcMax.y - pfCardPad * 0.6f), hh, spCol);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
float rowY = gcMin.y + pfCardPad, gcRight = gcMax.x - pfCardPad;
|
|
|
|
|
PfDisplay disp = pfBuildDisplay(e, bal, market);
|
|
|
|
|
@@ -1484,7 +1516,7 @@ void RenderMarketTab(App* app)
|
|
|
|
|
if (s_pf_resize >= 0 && s_pf_resize < pfN) {
|
|
|
|
|
int i = s_pf_resize;
|
|
|
|
|
ImVec2 mn = cellMin(pfLayout[i]);
|
|
|
|
|
int nw = std::max(1, std::min(pfCols - pfLayout[i].col,
|
|
|
|
|
int nw = std::max(2, std::min(pfCols - pfLayout[i].col,
|
|
|
|
|
(int)((io.MousePos.x - mn.x + pfCardGap) / (pfCellW + pfCardGap) + 0.5f)));
|
|
|
|
|
int nh = std::max(1, (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);
|
|
|
|
|
|