|
|
|
|
@@ -99,6 +99,7 @@ static char s_pf_label[64] = {0};
|
|
|
|
|
static std::vector<std::string> s_pf_addrs;
|
|
|
|
|
static std::string s_pf_icon; // selected wallet-icon name ("" = none)
|
|
|
|
|
static unsigned int s_pf_color = 0; // selected accent (packed IM_COL32; 0 = default)
|
|
|
|
|
static int s_pf_outline_opacity = 25; // accent-outline opacity (percent)
|
|
|
|
|
static char s_pf_search[64] = {0}; // address-list filter text
|
|
|
|
|
static int s_pf_type_filter = 0; // 0 = all, 1 = shielded, 2 = transparent
|
|
|
|
|
static bool s_pf_funded_only = false;
|
|
|
|
|
@@ -164,6 +165,21 @@ static std::vector<double> pfSparklineSeries(const MarketInfo& m, int interval)
|
|
|
|
|
return pfResampleHistory(m.price_history, interval);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Main price-chart interval selector (same semantics as pfSparklineSeries):
|
|
|
|
|
// 0=Live (in-session minute buffer) 1=hour 2=day 3=week 4=month. Session-scoped.
|
|
|
|
|
static int s_chart_interval = 2; // default: Day (historical)
|
|
|
|
|
|
|
|
|
|
// Approx seconds represented by one plotted point at the given chart interval, for X-axis labels.
|
|
|
|
|
static float pfChartSecPerPoint(int interval) {
|
|
|
|
|
switch (interval) {
|
|
|
|
|
case 1: return 3600.0f; // hour
|
|
|
|
|
case 2: return 86400.0f; // day
|
|
|
|
|
case 3: return 604800.0f; // week
|
|
|
|
|
case 4: return 2592000.0f; // month (~30d)
|
|
|
|
|
default: return 60.0f; // minute (live buffer)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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.
|
|
|
|
|
static void pfDrawSparkline(ImDrawList* dl, ImVec2 mn, ImVec2 mx,
|
|
|
|
|
@@ -275,6 +291,7 @@ static void PortfolioBeginEdit(App* app, int index)
|
|
|
|
|
s_pf_addrs = src.addresses;
|
|
|
|
|
s_pf_icon = src.icon;
|
|
|
|
|
s_pf_color = src.color;
|
|
|
|
|
s_pf_outline_opacity = src.outlineOpacity;
|
|
|
|
|
s_pf_price_basis = src.priceBasis;
|
|
|
|
|
s_pf_manual_price = src.manualPrice;
|
|
|
|
|
snprintf(s_pf_manual_ccy, sizeof(s_pf_manual_ccy), "%s", src.manualCurrency.c_str());
|
|
|
|
|
@@ -288,6 +305,7 @@ static void PortfolioBeginEdit(App* app, int index)
|
|
|
|
|
s_pf_addrs.clear();
|
|
|
|
|
s_pf_icon.clear();
|
|
|
|
|
s_pf_color = 0;
|
|
|
|
|
s_pf_outline_opacity = 25;
|
|
|
|
|
s_pf_price_basis = 0;
|
|
|
|
|
s_pf_manual_price = 0.0;
|
|
|
|
|
snprintf(s_pf_manual_ccy, sizeof(s_pf_manual_ccy), "USD");
|
|
|
|
|
@@ -384,7 +402,10 @@ static void RenderPortfolioEditor(App* app)
|
|
|
|
|
GlassPanelSpec g; g.rounding = 10.0f; g.fillAlpha = 22; g.borderAlpha = 40;
|
|
|
|
|
DrawGlassPanel(pdl, pMin, pMax, g);
|
|
|
|
|
// Colored outline around the whole preview card (matches the live group cards).
|
|
|
|
|
if (accent) pdl->AddRect(pMin, pMax, accent, 10.0f, 0, 2.0f);
|
|
|
|
|
if (accent) {
|
|
|
|
|
int oa = (int)(std::max(0, std::min(100, s_pf_outline_opacity)) * 2.55f + 0.5f);
|
|
|
|
|
pdl->AddRect(pMin, pMax, WithAlpha(accent, oa), 10.0f, 0, 2.0f);
|
|
|
|
|
}
|
|
|
|
|
if (s_pf_show_sparkline && (s_pf_price_basis == 0 || s_pf_price_basis == 1)) {
|
|
|
|
|
std::vector<double> h = pfSparklineSeries(state.market, s_pf_spark_interval);
|
|
|
|
|
if (h.size() >= 2) {
|
|
|
|
|
@@ -502,6 +523,15 @@ static void RenderPortfolioEditor(App* app)
|
|
|
|
|
ImGui::EndPopup();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Accent-outline opacity (only meaningful when an accent color is set).
|
|
|
|
|
ImGui::BeginDisabled(s_pf_color == 0u);
|
|
|
|
|
ImGui::TextUnformatted(TR("portfolio_outline_opacity"));
|
|
|
|
|
ImGui::SetNextItemWidth(-1);
|
|
|
|
|
ImGui::SliderInt("##pfOutlineOpacity", &s_pf_outline_opacity, 0, 100, "%d%%");
|
|
|
|
|
if (s_pf_outline_opacity < 0) s_pf_outline_opacity = 0;
|
|
|
|
|
if (s_pf_outline_opacity > 100) s_pf_outline_opacity = 100;
|
|
|
|
|
ImGui::EndDisabled();
|
|
|
|
|
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
|
|
|
|
|
|
|
|
|
// Price data: how this group's value is priced + which fields the card shows.
|
|
|
|
|
@@ -752,6 +782,7 @@ static void RenderPortfolioEditor(App* app)
|
|
|
|
|
e.addresses = s_pf_addrs;
|
|
|
|
|
e.icon = s_pf_icon;
|
|
|
|
|
e.color = s_pf_color;
|
|
|
|
|
e.outlineOpacity = s_pf_outline_opacity;
|
|
|
|
|
e.priceBasis = s_pf_price_basis;
|
|
|
|
|
e.manualPrice = s_pf_manual_price;
|
|
|
|
|
e.manualCurrency = s_pf_manual_ccy;
|
|
|
|
|
@@ -792,6 +823,9 @@ void RenderMarketTab(App* app)
|
|
|
|
|
// Kick a once-per-session live exchange-list fetch (self-guarded), then source the
|
|
|
|
|
// registry from it (falling back to the compiled-in list until it arrives).
|
|
|
|
|
app->refreshExchanges();
|
|
|
|
|
// 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();
|
|
|
|
|
const auto& registry = EffectiveRegistry(market);
|
|
|
|
|
|
|
|
|
|
// Load persisted exchange/pair on first frame
|
|
|
|
|
@@ -836,11 +870,11 @@ void RenderMarketTab(App* app)
|
|
|
|
|
// -- Hero header: size to the ACTUAL content (price row + separator gap + stats row) so the
|
|
|
|
|
// chart starts right after "24H VOLUME"/"Market Cap" instead of below a reserved empty band. --
|
|
|
|
|
ImFont* mktH3 = Type().h3();
|
|
|
|
|
float heroHeaderH = Layout::spacingLg() // top pad
|
|
|
|
|
+ mktH3->LegacySize + Layout::spacingMd() // price row -> separator
|
|
|
|
|
+ Layout::spacingSm() // separator -> stats
|
|
|
|
|
+ ovFont->LegacySize + Layout::spacingXs() + sub1->LegacySize // stats label + value
|
|
|
|
|
+ Layout::spacingSm(); // bottom pad before the chart (tight)
|
|
|
|
|
// Header now holds only the price row + separator; 24H volume + market cap moved onto the
|
|
|
|
|
// chart's top strip (with the interval buttons) so they no longer create an empty band.
|
|
|
|
|
float heroHeaderH = Layout::spacingLg() // top pad
|
|
|
|
|
+ mktH3->LegacySize + Layout::spacingMd() // price row -> separator
|
|
|
|
|
+ Layout::spacingSm(); // gap below separator, before the chart
|
|
|
|
|
|
|
|
|
|
// -- Portfolio geometry. No parent card background: the summary floats under the header
|
|
|
|
|
// and the custom-group cards float below it (each with its own glass background). --
|
|
|
|
|
@@ -944,41 +978,13 @@ void RenderMarketTab(App* app)
|
|
|
|
|
dl->AddRectFilled(bMin, bMax, badgeBg, 4.0f * dp);
|
|
|
|
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(bMin.x + badgePadH, bMin.y + badgePadV), chgCol, buf);
|
|
|
|
|
|
|
|
|
|
// ---- SEPARATOR ----
|
|
|
|
|
// ---- SEPARATOR (below the price row; 24H volume + market cap now overlay the chart) ----
|
|
|
|
|
float sepY = cy + h3->LegacySize + Layout::spacingMd();
|
|
|
|
|
float rnd = glassSpec.rounding;
|
|
|
|
|
dl->AddLine(ImVec2(cardMin.x + rnd * 0.5f, sepY),
|
|
|
|
|
ImVec2(cardMax.x - rnd * 0.5f, sepY),
|
|
|
|
|
WithAlpha(OnSurface(), 15), 1.0f * dp);
|
|
|
|
|
|
|
|
|
|
// ---- STATS ROW (24h Volume | Market Cap) ----
|
|
|
|
|
// (BTC-price column + the "updated Ns ago" staleness line were dropped to
|
|
|
|
|
// simplify; freshness is still shown by the attribution "Updated" line.)
|
|
|
|
|
float statsY = sepY + Layout::spacingSm();
|
|
|
|
|
float colW = (availWidth - Layout::spacingLg() * 2) / 2.0f;
|
|
|
|
|
|
|
|
|
|
struct StatItem { const char* label; std::string value; ImU32 valueCol; };
|
|
|
|
|
StatItem stats[2] = {
|
|
|
|
|
{TR("market_24h_volume"), FormatCompactUSD(market.volume_24h), OnSurface()},
|
|
|
|
|
{TR("market_cap"), FormatCompactUSD(market.market_cap), OnSurface()},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < 2; i++) {
|
|
|
|
|
float sx = cardMin.x + Layout::spacingLg() + i * colW;
|
|
|
|
|
float centerX = sx + colW * 0.5f;
|
|
|
|
|
|
|
|
|
|
// Label (overline, centered)
|
|
|
|
|
ImVec2 lblSz = ovFont->CalcTextSizeA(ovFont->LegacySize, 10000, 0, stats[i].label);
|
|
|
|
|
dl->AddText(ovFont, ovFont->LegacySize,
|
|
|
|
|
ImVec2(centerX - lblSz.x * 0.5f, statsY), OnSurfaceMedium(), stats[i].label);
|
|
|
|
|
|
|
|
|
|
// Value (subtitle1, centered)
|
|
|
|
|
float valY = statsY + ovFont->LegacySize + Layout::spacingXs();
|
|
|
|
|
ImVec2 valSz = sub1->CalcTextSizeA(sub1->LegacySize, 10000, 0, stats[i].value.c_str());
|
|
|
|
|
dl->AddText(sub1, sub1->LegacySize,
|
|
|
|
|
ImVec2(centerX - valSz.x * 0.5f, valY), stats[i].valueCol, stats[i].value.c_str());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---- TRADE BUTTON (top-right of card) ----
|
|
|
|
|
if (!currentExchange.pairs.empty()) {
|
|
|
|
|
const char* pairName = currentExchange.pairs[s_pair_idx].displayName.c_str();
|
|
|
|
|
@@ -1053,30 +1059,100 @@ void RenderMarketTab(App* app)
|
|
|
|
|
// PRICE CHART — drawn inside the combined hero card's glass panel (above)
|
|
|
|
|
// ================================================================
|
|
|
|
|
{
|
|
|
|
|
// Plot the REAL accumulated price history (one point per refresh, oldest→newest).
|
|
|
|
|
// No synthetic data: until at least two real samples exist, the empty-state below
|
|
|
|
|
// ("not enough history yet") is shown rather than inventing a price curve.
|
|
|
|
|
s_price_history = market.price_history;
|
|
|
|
|
// Plot the historical price series for the selected interval (Live/1H/1D/1W/1M). The
|
|
|
|
|
// historical intervals come from CoinGecko market_chart; "Live" is the in-session buffer.
|
|
|
|
|
// Until two points exist, the empty-state below is shown instead of a synthetic curve.
|
|
|
|
|
s_price_history = pfSparklineSeries(market, s_chart_interval);
|
|
|
|
|
s_history_initialized = true;
|
|
|
|
|
|
|
|
|
|
// Chart height precomputed above (responsive — the chart absorbs the vertical slack
|
|
|
|
|
// left by the hero header + portfolio card). No glass panel here — it shares the
|
|
|
|
|
// combined hero+chart card's panel drawn in PRICE SUMMARY above.
|
|
|
|
|
// The chart shares the combined hero+chart glass panel drawn in PRICE SUMMARY above.
|
|
|
|
|
ImVec2 chartMin = ImGui::GetCursorScreenPos();
|
|
|
|
|
ImVec2 chartMax(chartMin.x + availWidth, chartMin.y + chartH);
|
|
|
|
|
|
|
|
|
|
if (!s_price_history.empty() && s_price_history.size() >= 2) {
|
|
|
|
|
float chartPad = pad;
|
|
|
|
|
float labelPadLeft = std::max(S.drawElement("tabs.market", "chart-y-axis-min-padding").size, S.drawElement("tabs.market", "chart-y-axis-padding").size * hs);
|
|
|
|
|
float labelPadBottom = Layout::spacingXl();
|
|
|
|
|
float chartPad = pad;
|
|
|
|
|
float pillH = capFont->LegacySize + Layout::spacingXs() * 2.0f;
|
|
|
|
|
float stripH = pillH + Layout::spacingSm(); // top control/info strip height
|
|
|
|
|
|
|
|
|
|
float plotLeft = chartMin.x + labelPadLeft;
|
|
|
|
|
float plotRight = chartMax.x - chartPad;
|
|
|
|
|
float plotTop = chartMin.y + chartPad;
|
|
|
|
|
float plotBottom = chartMax.y - labelPadBottom;
|
|
|
|
|
float plotW = plotRight - plotLeft;
|
|
|
|
|
float plotH = plotBottom - plotTop;
|
|
|
|
|
// ---- Top strip: interval buttons (left) + 24H volume / market cap + refresh (right) ----
|
|
|
|
|
{
|
|
|
|
|
float rowTop = chartMin.y + chartPad;
|
|
|
|
|
float textY = rowTop + (pillH - capFont->LegacySize) * 0.5f;
|
|
|
|
|
|
|
|
|
|
// Interval buttons (left). Live = in-session buffer; the rest are historical.
|
|
|
|
|
const struct { const char* lbl; int iv; } kIvs[] = {
|
|
|
|
|
{ TR("market_iv_live"), 0 }, { "1H", 1 }, { "1D", 2 }, { "1W", 3 }, { "1M", 4 }
|
|
|
|
|
};
|
|
|
|
|
float bx = chartMin.x + chartPad;
|
|
|
|
|
for (int b = 0; b < 5; b++) {
|
|
|
|
|
float tw = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, kIvs[b].lbl).x;
|
|
|
|
|
float bw = tw + Layout::spacingSm() * 2.0f;
|
|
|
|
|
ImVec2 bmn(bx, rowTop), bmx(bx + bw, rowTop + pillH);
|
|
|
|
|
bool sel = (s_chart_interval == kIvs[b].iv);
|
|
|
|
|
bool bhov = material::IsRectHovered(bmn, bmx);
|
|
|
|
|
ImU32 bg = sel ? WithAlpha(Primary(), 200)
|
|
|
|
|
: (bhov ? WithAlpha(OnSurface(), 35) : WithAlpha(OnSurface(), 18));
|
|
|
|
|
dl->AddRectFilled(bmn, bmx, bg, 4.0f);
|
|
|
|
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(bx + Layout::spacingSm(), textY),
|
|
|
|
|
sel ? IM_COL32(255, 255, 255, 255) : OnSurface(), kIvs[b].lbl);
|
|
|
|
|
ImGui::SetCursorScreenPos(bmn);
|
|
|
|
|
ImGui::PushID(9100 + b);
|
|
|
|
|
if (ImGui::InvisibleButton("##civ", ImVec2(bw, pillH))) {
|
|
|
|
|
s_chart_interval = kIvs[b].iv;
|
|
|
|
|
s_history_initialized = false;
|
|
|
|
|
}
|
|
|
|
|
if (ImGui::IsItemHovered()) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
|
|
|
|
ImGui::PopID();
|
|
|
|
|
bx += bw + Layout::spacingXs();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Refresh button (far right).
|
|
|
|
|
float rEdge = chartMax.x - chartPad;
|
|
|
|
|
ImFont* iconSmall = material::Typography::instance().iconSmall();
|
|
|
|
|
ImVec2 rbMin(rEdge - pillH, rowTop), rbMax(rEdge, rowTop + pillH);
|
|
|
|
|
bool refreshHov = material::IsRectHovered(rbMin, rbMax);
|
|
|
|
|
if (refreshHov) { dl->AddRectFilled(rbMin, rbMax, IM_COL32(255, 255, 255, 20), 4.0f);
|
|
|
|
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); }
|
|
|
|
|
ImVec2 icSz = iconSmall->CalcTextSizeA(iconSmall->LegacySize, FLT_MAX, 0, ICON_MD_REFRESH);
|
|
|
|
|
dl->AddText(iconSmall, iconSmall->LegacySize,
|
|
|
|
|
ImVec2(rbMin.x + (pillH - icSz.x) * 0.5f, rbMin.y + (pillH - icSz.y) * 0.5f),
|
|
|
|
|
refreshHov ? OnSurface() : OnSurfaceMedium(), ICON_MD_REFRESH);
|
|
|
|
|
ImGui::SetCursorScreenPos(rbMin);
|
|
|
|
|
if (ImGui::InvisibleButton("##RefreshMarket", ImVec2(pillH, pillH))) {
|
|
|
|
|
app->refreshMarketData();
|
|
|
|
|
s_history_initialized = false;
|
|
|
|
|
s_last_refresh_time = ImGui::GetTime();
|
|
|
|
|
}
|
|
|
|
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("market_refresh_price"));
|
|
|
|
|
|
|
|
|
|
// 24H volume + market cap, right-aligned to the left of the refresh button. Skipped
|
|
|
|
|
// when there's no room (narrow window) so they never overlap the interval buttons.
|
|
|
|
|
float sxr = rbMin.x - Layout::spacingMd();
|
|
|
|
|
auto drawStat = [&](const char* label, const std::string& val) {
|
|
|
|
|
char sb[64]; snprintf(sb, sizeof(sb), "%s %s", label, val.c_str());
|
|
|
|
|
float w = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, sb).x;
|
|
|
|
|
if (sxr - w < bx + Layout::spacingMd()) return;
|
|
|
|
|
sxr -= w;
|
|
|
|
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(sxr, textY), OnSurfaceMedium(), sb);
|
|
|
|
|
sxr -= Layout::spacingMd();
|
|
|
|
|
};
|
|
|
|
|
if (market.price_usd > 0) {
|
|
|
|
|
drawStat(TR("market_cap_short"), FormatCompactUSD(market.market_cap));
|
|
|
|
|
drawStat(TR("market_vol_short"), FormatCompactUSD(market.volume_24h));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
float labelPadLeft = std::max(S.drawElement("tabs.market", "chart-y-axis-min-padding").size,
|
|
|
|
|
S.drawElement("tabs.market", "chart-y-axis-padding").size * hs);
|
|
|
|
|
// Extra bottom room so the time labels aren't crowded against the card's bottom edge.
|
|
|
|
|
float labelPadBottom = Layout::spacingXl() + Layout::spacingSm();
|
|
|
|
|
float plotLeft = chartMin.x + labelPadLeft;
|
|
|
|
|
float plotRight = chartMax.x - chartPad;
|
|
|
|
|
float plotTop = chartMin.y + chartPad + stripH;
|
|
|
|
|
float plotBottom = chartMax.y - labelPadBottom;
|
|
|
|
|
float plotW = plotRight - plotLeft;
|
|
|
|
|
float plotH = plotBottom - plotTop;
|
|
|
|
|
|
|
|
|
|
if (s_price_history.size() >= 2) {
|
|
|
|
|
// Compute Y range with padding
|
|
|
|
|
double yMin = *std::min_element(s_price_history.begin(), s_price_history.end());
|
|
|
|
|
double yMax = *std::max_element(s_price_history.begin(), s_price_history.end());
|
|
|
|
|
@@ -1138,28 +1214,30 @@ void RenderMarketTab(App* app)
|
|
|
|
|
dl->AddCircleFilled(points[i], dotR, dotCol);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// X-axis time labels. The price series is one sample per market refresh (~60s
|
|
|
|
|
// cadence, see RefreshScheduler::kPrice), so label the axis in approximate
|
|
|
|
|
// minutes-ago (rightmost = "now"). Approximate — a refresh can be delayed — hence "~".
|
|
|
|
|
// X-axis time labels. Each point spans one interval unit (minute/hour/day/week/month);
|
|
|
|
|
// convert points-before-now to a "~<time> ago" label (rightmost = "now"). Approximate — "~".
|
|
|
|
|
{
|
|
|
|
|
const float kSecPerPoint = 60.0f; // RefreshScheduler::kPrice
|
|
|
|
|
float secPerPoint = pfChartSecPerPoint(s_chart_interval);
|
|
|
|
|
int nPts = (int)n;
|
|
|
|
|
int ticks = std::min(4, nPts);
|
|
|
|
|
for (int tk = 0; tk < ticks; tk++) {
|
|
|
|
|
float t = (ticks > 1) ? (float)tk / (float)(ticks - 1) : 1.0f;
|
|
|
|
|
float xpos = plotLeft + t * plotW;
|
|
|
|
|
int idx = (int)(t * (nPts - 1) + 0.5f);
|
|
|
|
|
int minsAgo = (int)(((float)(nPts - 1 - idx) * kSecPerPoint) / 60.0f + 0.5f);
|
|
|
|
|
float secAgo = (float)(nPts - 1 - idx) * secPerPoint;
|
|
|
|
|
char tlbl[32];
|
|
|
|
|
if (minsAgo <= 0) snprintf(tlbl, sizeof(tlbl), "%s", TR("market_now"));
|
|
|
|
|
else if (minsAgo < 90) snprintf(tlbl, sizeof(tlbl), "~%dm", minsAgo);
|
|
|
|
|
else snprintf(tlbl, sizeof(tlbl), "~%.1fh", minsAgo / 60.0f);
|
|
|
|
|
if (secAgo < 45.0f) snprintf(tlbl, sizeof(tlbl), "%s", TR("market_now"));
|
|
|
|
|
else if (secAgo < 5400.0f) snprintf(tlbl, sizeof(tlbl), "~%.0fm", secAgo / 60.0f);
|
|
|
|
|
else if (secAgo < 172800.0f) snprintf(tlbl, sizeof(tlbl), "~%.0fh", secAgo / 3600.0f);
|
|
|
|
|
else if (secAgo < 1209600.0f) snprintf(tlbl, sizeof(tlbl), "~%.0fd", secAgo / 86400.0f);
|
|
|
|
|
else if (secAgo < 7776000.0f) snprintf(tlbl, sizeof(tlbl), "~%.0fw", secAgo / 604800.0f);
|
|
|
|
|
else snprintf(tlbl, sizeof(tlbl), "~%.0fmo", secAgo / 2592000.0f);
|
|
|
|
|
ImVec2 lblSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, tlbl);
|
|
|
|
|
float lx = (tk == 0) ? plotLeft
|
|
|
|
|
: (tk == ticks - 1) ? plotRight - lblSz.x
|
|
|
|
|
: xpos - lblSz.x * 0.5f;
|
|
|
|
|
dl->AddText(capFont, capFont->LegacySize,
|
|
|
|
|
ImVec2(lx, plotBottom + 2), OnSurfaceDisabled(), tlbl);
|
|
|
|
|
ImVec2(lx, plotBottom + 4), OnSurfaceDisabled(), tlbl);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -1206,58 +1284,14 @@ void RenderMarketTab(App* app)
|
|
|
|
|
ImVec2(tipX + tipPad, tipY + tipPad), dotCol, buf);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Empty state, centered in the plot area (the interval strip + refresh are already drawn).
|
|
|
|
|
const char* msg = TR("market_no_history");
|
|
|
|
|
ImVec2 ts = sub1->CalcTextSizeA(sub1->LegacySize, FLT_MAX, 0, msg);
|
|
|
|
|
dl->AddText(sub1, sub1->LegacySize,
|
|
|
|
|
ImVec2(chartMin.x + (availWidth - ts.x) * 0.5f, chartMin.y + chartH * 0.45f),
|
|
|
|
|
ImVec2(chartMin.x + (availWidth - ts.x) * 0.5f, (plotTop + plotBottom) * 0.5f - ts.y * 0.5f),
|
|
|
|
|
OnSurfaceDisabled(), msg);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- Refresh button + timestamp pinned in chart top-right ---
|
|
|
|
|
{
|
|
|
|
|
float iconBtnSz = capFont->LegacySize + 8.0f;
|
|
|
|
|
float refreshX = chartMax.x - pad;
|
|
|
|
|
float refreshY = chartMin.y + pad * 0.5f;
|
|
|
|
|
|
|
|
|
|
// Draw refresh icon button
|
|
|
|
|
ImVec2 btnMin(refreshX - iconBtnSz, refreshY);
|
|
|
|
|
ImVec2 btnMax(refreshX, refreshY + iconBtnSz);
|
|
|
|
|
bool refreshHov = material::IsRectHovered(btnMin, btnMax);
|
|
|
|
|
if (refreshHov) {
|
|
|
|
|
dl->AddRectFilled(btnMin, btnMax, IM_COL32(255, 255, 255, 20), 4.0f);
|
|
|
|
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ImFont* iconSmall = material::Typography::instance().iconSmall();
|
|
|
|
|
ImVec2 iconSz = iconSmall->CalcTextSizeA(iconSmall->LegacySize, FLT_MAX, 0, ICON_MD_REFRESH);
|
|
|
|
|
dl->AddText(iconSmall, iconSmall->LegacySize,
|
|
|
|
|
ImVec2(btnMin.x + (iconBtnSz - iconSz.x) * 0.5f, btnMin.y + (iconBtnSz - iconSz.y) * 0.5f),
|
|
|
|
|
refreshHov ? OnSurface() : OnSurfaceMedium(), ICON_MD_REFRESH);
|
|
|
|
|
|
|
|
|
|
ImGui::SetCursorScreenPos(btnMin);
|
|
|
|
|
if (ImGui::InvisibleButton("##RefreshMarket", ImVec2(iconBtnSz, iconBtnSz))) {
|
|
|
|
|
app->refreshMarketData();
|
|
|
|
|
s_history_initialized = false;
|
|
|
|
|
s_last_refresh_time = ImGui::GetTime();
|
|
|
|
|
}
|
|
|
|
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("market_refresh_price"));
|
|
|
|
|
|
|
|
|
|
// Timestamp text to the left of refresh button
|
|
|
|
|
if (s_last_refresh_time > 0.0) {
|
|
|
|
|
double elapsed = ImGui::GetTime() - s_last_refresh_time;
|
|
|
|
|
if (elapsed < 60.0)
|
|
|
|
|
snprintf(buf, sizeof(buf), "%.0fs ago", elapsed);
|
|
|
|
|
else if (elapsed < 3600.0)
|
|
|
|
|
snprintf(buf, sizeof(buf), "%.0fm ago", elapsed / 60.0);
|
|
|
|
|
else
|
|
|
|
|
snprintf(buf, sizeof(buf), "%.1fh ago", elapsed / 3600.0);
|
|
|
|
|
ImVec2 tsSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, buf);
|
|
|
|
|
dl->AddText(capFont, capFont->LegacySize,
|
|
|
|
|
ImVec2(btnMin.x - tsSz.x - 6, btnMin.y + (iconBtnSz - tsSz.y) * 0.5f),
|
|
|
|
|
OnSurfaceDisabled(), buf);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ImGui::SetCursorScreenPos(ImVec2(chartMin.x, chartMin.y + chartH));
|
|
|
|
|
ImGui::Dummy(ImVec2(availWidth, 0));
|
|
|
|
|
}
|
|
|
|
|
@@ -1448,7 +1482,7 @@ void RenderMarketTab(App* app)
|
|
|
|
|
float gy = cardMin.y + pfSummaryH;
|
|
|
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
|
|
|
bool interacting = (s_pf_drag >= 0 || s_pf_resize >= 0);
|
|
|
|
|
float rh = 14.0f * mktDp; // corner resize-grip size
|
|
|
|
|
float rh = 20.0f * mktDp; // corner resize-grip size (generous so a short resize is easy to grab)
|
|
|
|
|
|
|
|
|
|
auto cellMin = [&](const PfCell& L) {
|
|
|
|
|
return ImVec2(cx + L.col * (pfCellW + pfCardGap), gy + L.row * (pfCellH + pfCardGap));
|
|
|
|
|
@@ -1469,9 +1503,12 @@ void RenderMarketTab(App* app)
|
|
|
|
|
GlassPanelSpec gcGlass; gcGlass.rounding = 10.0f;
|
|
|
|
|
gcGlass.fillAlpha = hov ? 34 : 22; gcGlass.borderAlpha = 40;
|
|
|
|
|
DrawGlassPanel(dl, gcMin, gcMax, gcGlass);
|
|
|
|
|
// Accented groups get a colored outline around the whole card; others a subtle hover ring.
|
|
|
|
|
if (accent) dl->AddRect(gcMin, gcMax, accent, 10.0f, 0, hov ? 2.5f : 2.0f);
|
|
|
|
|
else if (hov) dl->AddRect(gcMin, gcMax, WithAlpha(OnSurface(), 70), 10.0f, 0, 1.0f);
|
|
|
|
|
// Accented groups get a colored outline around the whole card (configurable opacity);
|
|
|
|
|
// others get a subtle hover ring.
|
|
|
|
|
if (accent) {
|
|
|
|
|
int oa = (int)(std::max(0, std::min(100, e.outlineOpacity)) * 2.55f + 0.5f);
|
|
|
|
|
dl->AddRect(gcMin, gcMax, WithAlpha(accent, oa), 10.0f, 0, hov ? 2.5f : 2.0f);
|
|
|
|
|
} else if (hov) dl->AddRect(gcMin, gcMax, WithAlpha(OnSurface(), 70), 10.0f, 0, 1.0f);
|
|
|
|
|
|
|
|
|
|
float left = gcMin.x + pad;
|
|
|
|
|
float right = gcMax.x - pad;
|
|
|
|
|
@@ -1620,6 +1657,16 @@ void RenderMarketTab(App* app)
|
|
|
|
|
ImVec2 sz = cellSize(nw, nh), mx(mn.x + sz.x, mn.y + sz.y);
|
|
|
|
|
drawCard(mn, mx, pfEntriesGeo[i], true);
|
|
|
|
|
dl->AddRect(mn, mx, WithAlpha(Primary(), 200), 10.0f, 0, 2.0f * mktDp);
|
|
|
|
|
// Live size readout (e.g. "6\xC3\x973") so the reached grid span is visible — the
|
|
|
|
|
// minimum is pfMinW x pfMinH (4x3).
|
|
|
|
|
{
|
|
|
|
|
char szb[16]; snprintf(szb, sizeof(szb), "%d\xC3\x97%d", nw, nh);
|
|
|
|
|
ImVec2 tsz = sub1->CalcTextSizeA(sub1->LegacySize, FLT_MAX, 0, szb);
|
|
|
|
|
ImVec2 tp((mn.x + mx.x) * 0.5f - tsz.x * 0.5f, (mn.y + mx.y) * 0.5f - tsz.y * 0.5f);
|
|
|
|
|
dl->AddRectFilled(ImVec2(tp.x - 6, tp.y - 3), ImVec2(tp.x + tsz.x + 6, tp.y + tsz.y + 3),
|
|
|
|
|
IM_COL32(20, 20, 30, 205), 4.0f);
|
|
|
|
|
dl->AddText(sub1, sub1->LegacySize, tp, OnSurface(), szb);
|
|
|
|
|
}
|
|
|
|
|
ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeNWSE);
|
|
|
|
|
if (!io.MouseDown[0]) {
|
|
|
|
|
auto entries = app->settings()->getPortfolioEntries();
|
|
|
|
|
|