From b8cfc1c79c42030bfe7dcab28f8a4897c0a5436a Mon Sep 17 00:00:00 2001 From: DanS Date: Sat, 4 Jul 2026 00:10:59 -0500 Subject: [PATCH] refactor(market): group ~35 file-scope statics into three structs Collapse the loose market_tab.cpp statics into MarketViewState (s_mkt), PfEditState (s_pfEdit) and PfGridDrag (s_grid). Pure mechanical rename (compile-verified, no behavior change) that shrinks the global surface and gives the upcoming function decompositions a single state handle per concern instead of ~35 free names. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/ui/windows/market_tab.cpp | 516 +++++++++++++++++----------------- 1 file changed, 261 insertions(+), 255 deletions(-) diff --git a/src/ui/windows/market_tab.cpp b/src/ui/windows/market_tab.cpp index 1dce347..c543dc7 100644 --- a/src/ui/windows/market_tab.cpp +++ b/src/ui/windows/market_tab.cpp @@ -36,12 +36,14 @@ namespace ui { using namespace material; // ---- Market tab persistent state ---- -static std::vector s_price_history; // mirror of WalletState market.price_history - -// Exchange / pair selection -static int s_exchange_idx = 0; -static int s_pair_idx = 0; -static bool s_market_state_loaded = false; +struct MarketViewState { + std::vector history; // mirror of WalletState market.price_history + int exchangeIdx = 0; // selected exchange / pair + int pairIdx = 0; + bool stateLoaded = false; + int chartInterval = 4; // main chart range: 0=Live 1=1H 2=1D 3=1W 4=1M (default 1M) +}; +static MarketViewState s_mkt; // The effective exchange registry: the live CoinGecko list when available, else the // compiled-in fallback. @@ -53,18 +55,18 @@ static const std::vector& EffectiveRegistry(const MarketInfo // Helper: load selected exchange/pair from settings static void LoadMarketState(config::Settings* settings, const std::vector& registry) { - if (s_market_state_loaded || !settings) return; - s_market_state_loaded = true; + if (s_mkt.stateLoaded || !settings) return; + s_mkt.stateLoaded = true; std::string savedExchange = settings->getSelectedExchange(); std::string savedPair = settings->getSelectedPair(); for (int ei = 0; ei < (int)registry.size(); ei++) { if (registry[ei].name == savedExchange) { - s_exchange_idx = ei; + s_mkt.exchangeIdx = ei; for (int pi = 0; pi < (int)registry[ei].pairs.size(); pi++) { if (registry[ei].pairs[pi].displayName == savedPair) { - s_pair_idx = pi; + s_mkt.pairIdx = pi; break; } } @@ -94,38 +96,40 @@ static std::string FormatPrice(double price) return std::string(buf); } -// ---- Portfolio editor (modal) state ---- -static bool s_portfolio_editor_open = false; -static int s_pf_sel = -1; // selected group index in the master list; -1 = none -static int s_pf_section = 0; // right-pane segmented control: 0=Appearance 1=Price 2=Addresses -// Backdrop live-capture: capture once on open + on viewport resize (not every frame). The blur is a -// frozen snapshot while the modal is open — efficient and flicker-free. -static bool s_pf_was_open = false; -static int s_pf_capture_frames = 0; // frames left to (re)capture the live backdrop -static bool s_pf_capturing_frame = false; // true on any frame a live-backdrop capture is armed -static float s_pf_capture_w = 0.0f, s_pf_capture_h = 0.0f; // viewport size at the last capture trigger -static char s_pf_label[64] = {0}; -static std::vector 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 char s_pf_icon_search[64] = {0}; // icon-picker filter text -static int s_pf_type_filter = 0; // 0 = all, 1 = shielded, 2 = transparent -static bool s_pf_funded_only = false; -// Edge-fade shaders for the two internal scroll regions (one persistent instance each). -static effects::ScrollFadeShader s_pf_icon_fade; -static effects::ScrollFadeShader s_pf_addr_fade; -static float s_pf_custom_rgb[3] = {0.31f, 0.62f, 1.0f}; // working RGB for the custom color picker -// Price-data config (working copy while editing a group). -static int s_pf_price_basis = 0; -static double s_pf_manual_price = 0.0; -static char s_pf_manual_ccy[12] = "USD"; -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 +// ---- Portfolio editor (modal) working state ---- +struct PfEditState { + bool open = false; // modal visible (queried by PortfolioEditorActive()) + int sel = -1; // selected group index in the master list; -1 = none + int section = 0; // right-pane segmented control: 0=Appearance 1=Price 2=Addresses + // Backdrop live-capture: capture once on open + on viewport resize (frozen snapshot while open). + bool wasOpen = false; + int captureFrames = 0; // frames left to (re)capture the live backdrop + bool capturingFrame = false; // true on any frame a live-backdrop capture is armed + float captureW = 0.0f, captureH = 0.0f; // viewport size at the last capture trigger + // Working copy of the group being edited. + char label[64] = {0}; + std::vector addrs; + std::string icon; // selected wallet-icon name ("" = none) + unsigned int color = 0; // selected accent (packed IM_COL32; 0 = default) + int outlineOpacity = 25; // accent-outline opacity (percent) + char search[64] = {0}; // address-list filter text + char iconSearch[64] = {0}; // icon-picker filter text + int typeFilter = 0; // 0 = all, 1 = shielded, 2 = transparent + bool fundedOnly = false; + effects::ScrollFadeShader iconFade; // edge-fade shaders for the two internal scroll regions + effects::ScrollFadeShader addrFade; + float customRgb[3] = {0.31f, 0.62f, 1.0f}; // working RGB for the custom color picker + // Price-data config (working copy while editing a group). + int priceBasis = 0; + double manualPrice = 0.0; + char manualCcy[12] = "USD"; + bool showDrgx = true; + bool showValue = true; + bool show24h = false; + bool showSparkline = false; + int sparkInterval = 0; // 0=min 1=hour 2=day 3=week 4=month +}; +static PfEditState s_pfEdit; // Wrap a scroll region with smooth scrolling + the Settings-tab edge fade + a thicker, rounded // scrollbar that is inset from the container edge. Uses a bordered/rounded OUTER frame whose @@ -153,7 +157,7 @@ static ImDrawList* pfBeginScrollChild(const char* id, effects::ScrollFadeShader& // Fade the top/bottom edges; off in low-spec and while the backdrop is being captured (the // acrylic overlay rebinds render state on those frames, which would clash with the fade shader). const float fadeH = 22.0f * dp; - if (!effects::isLowSpecMode() && !s_pf_capturing_frame && fade.init()) { + if (!effects::isLowSpecMode() && !s_pfEdit.capturingFrame && fade.init()) { ImVec2 cMin = ImGui::GetWindowPos(); ImVec2 cMax(cMin.x + ImGui::GetWindowSize().x, cMin.y + ImGui::GetWindowSize().y); float sY = ImGui::GetScrollY(), sMax = ImGui::GetScrollMaxY(); @@ -168,16 +172,15 @@ static ImDrawList* pfBeginScrollChild(const char* id, effects::ScrollFadeShader& static void pfEndScrollChild(effects::ScrollFadeShader& fade, ImDrawList* dl) { - if (!effects::isLowSpecMode() && !s_pf_capturing_frame && fade.ready) + if (!effects::isLowSpecMode() && !s_pfEdit.capturingFrame && fade.ready) effects::ScrollFadeShader::addUnbind(dl); ImGui::EndChild(); // inner ImGui::PopStyleVar(2); // ScrollbarSize + ScrollbarRounding ImGui::EndChild(); // outer } -// Main price-chart interval selector: 0=Live (in-session minute buffer) 1=hour 2=day 3=week -// 4=month. Session-scoped. The pure series math lives in data/market_series.h. -static int s_chart_interval = 4; // default: 1M (last 30 days) +// The pure price-series math lives in data/market_series.h; the selected chart range is +// s_mkt.chartInterval (0=Live 1=1H 2=1D 3=1W 4=1M, session-scoped, default 1M). // A group's value trend tracks the DRGX price, so a group sparkline plots the price history @@ -266,13 +269,16 @@ static void pfDrawSparklineFilled(ImDrawList* dl, ImVec2 mn, ImVec2 mx, // Dashboard-grid state for the portfolio cards (drag to move, corner-drag to resize). struct PfCell { int col, row, w, h; }; -static int s_pf_drag = -1; // card index being dragged (-1 = none) -static int s_pf_resize = -1; // card index being resized -static bool s_pf_moved = false; // drag passed the click threshold this gesture -static ImVec2 s_pf_press; // mouse pos at grab -static ImVec2 s_pf_grab; // mouse offset within the card at grab -static PfCell s_pf_old = {0,0,1,1};// dragged card's cell at grab (for swap-on-drop) -static PfCell s_pf_target = {0,0,1,1}; // live drop target cell +struct PfGridDrag { + int drag = -1; // card index being dragged (-1 = none) + int resize = -1; // card index being resized + bool moved = false; // drag passed the click threshold this gesture + ImVec2 press; // mouse pos at grab + ImVec2 grab; // mouse offset within the card at grab + PfCell old = {0,0,1,1}; // dragged card's cell at grab (for swap-on-drop) + PfCell target = {0,0,1,1};// live drop target cell +}; +static PfGridDrag s_grid; // Build the right-hand amount strings for a group card / preview from its price-data config. struct PfDisplay { @@ -344,47 +350,47 @@ static const ImU32 kPfPalette[] = { static void PortfolioBeginEdit(App* app, int index) { const auto& entries = app->settings()->getPortfolioEntries(); - s_pf_sel = index; + s_pfEdit.sel = index; if (index >= 0 && index < (int)entries.size()) { const auto& src = entries[index]; - snprintf(s_pf_label, sizeof(s_pf_label), "%s", src.label.c_str()); - 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()); - s_pf_show_drgx = src.showDrgx; - s_pf_show_value = src.showValue; - s_pf_show_24h = src.show24h; - s_pf_show_sparkline = src.showSparkline; - s_pf_spark_interval = src.sparklineInterval; + snprintf(s_pfEdit.label, sizeof(s_pfEdit.label), "%s", src.label.c_str()); + s_pfEdit.addrs = src.addresses; + s_pfEdit.icon = src.icon; + s_pfEdit.color = src.color; + s_pfEdit.outlineOpacity = src.outlineOpacity; + s_pfEdit.priceBasis = src.priceBasis; + s_pfEdit.manualPrice = src.manualPrice; + snprintf(s_pfEdit.manualCcy, sizeof(s_pfEdit.manualCcy), "%s", src.manualCurrency.c_str()); + s_pfEdit.showDrgx = src.showDrgx; + s_pfEdit.showValue = src.showValue; + s_pfEdit.show24h = src.show24h; + s_pfEdit.showSparkline = src.showSparkline; + s_pfEdit.sparkInterval = src.sparklineInterval; } else { - s_pf_label[0] = '\0'; - 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"); - s_pf_show_drgx = true; - s_pf_show_value = true; - s_pf_show_24h = false; - s_pf_show_sparkline = false; - s_pf_spark_interval = 0; + s_pfEdit.label[0] = '\0'; + s_pfEdit.addrs.clear(); + s_pfEdit.icon.clear(); + s_pfEdit.color = 0; + s_pfEdit.outlineOpacity = 25; + s_pfEdit.priceBasis = 0; + s_pfEdit.manualPrice = 0.0; + snprintf(s_pfEdit.manualCcy, sizeof(s_pfEdit.manualCcy), "USD"); + s_pfEdit.showDrgx = true; + s_pfEdit.showValue = true; + s_pfEdit.show24h = false; + s_pfEdit.showSparkline = false; + s_pfEdit.sparkInterval = 0; } - s_pf_search[0] = '\0'; - s_pf_type_filter = 0; - s_pf_funded_only = false; + s_pfEdit.search[0] = '\0'; + s_pfEdit.typeFilter = 0; + s_pfEdit.fundedOnly = false; } // The "Manage portfolio" modal: list/add/edit/delete entries, each a label tied to a group // of wallet addresses. Persists to Settings on every mutation (like the address book). static void RenderPortfolioEditor(App* app) { - if (!s_portfolio_editor_open) return; + if (!s_pfEdit.open) return; config::Settings* settings = app->settings(); const auto& state = app->getWalletState(); @@ -402,31 +408,31 @@ static void RenderPortfolioEditor(App* app) // Build a PortfolioEntry from the working set, preserving off-form fields (grid geometry) from base. auto buildWorking = [&](const config::Settings::PortfolioEntry& base) { config::Settings::PortfolioEntry e = base; - e.label = s_pf_label; 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; - e.showDrgx = s_pf_show_drgx; e.showValue = s_pf_show_value; e.show24h = s_pf_show_24h; - e.showSparkline = s_pf_show_sparkline; e.sparklineInterval = s_pf_spark_interval; + e.label = s_pfEdit.label; e.addresses = s_pfEdit.addrs; e.icon = s_pfEdit.icon; + e.color = s_pfEdit.color; e.outlineOpacity = s_pfEdit.outlineOpacity; + e.priceBasis = s_pfEdit.priceBasis; e.manualPrice = s_pfEdit.manualPrice; e.manualCurrency = s_pfEdit.manualCcy; + e.showDrgx = s_pfEdit.showDrgx; e.showValue = s_pfEdit.showValue; e.show24h = s_pfEdit.show24h; + e.showSparkline = s_pfEdit.showSparkline; e.sparklineInterval = s_pfEdit.sparkInterval; return e; }; auto workingMatches = [&](const config::Settings::PortfolioEntry& e) { - return e.label == std::string(s_pf_label) && 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 == std::string(s_pf_manual_ccy) - && e.showDrgx == s_pf_show_drgx && e.showValue == s_pf_show_value && e.show24h == s_pf_show_24h - && e.showSparkline == s_pf_show_sparkline && e.sparklineInterval == s_pf_spark_interval; + return e.label == std::string(s_pfEdit.label) && e.addresses == s_pfEdit.addrs && e.icon == s_pfEdit.icon + && e.color == s_pfEdit.color && e.outlineOpacity == s_pfEdit.outlineOpacity + && e.priceBasis == s_pfEdit.priceBasis && e.manualPrice == s_pfEdit.manualPrice + && e.manualCurrency == std::string(s_pfEdit.manualCcy) + && e.showDrgx == s_pfEdit.showDrgx && e.showValue == s_pfEdit.showValue && e.show24h == s_pfEdit.show24h + && e.showSparkline == s_pfEdit.showSparkline && e.sparklineInterval == s_pfEdit.sparkInterval; }; // Persist the working group when it is named and actually changed; unnamed drafts are dropped. auto commitIfNeeded = [&]() { - if (s_pf_label[0] == '\0') return; + if (s_pfEdit.label[0] == '\0') return; auto entries = settings->getPortfolioEntries(); - bool existing = (s_pf_sel >= 0 && s_pf_sel < (int)entries.size()); + bool existing = (s_pfEdit.sel >= 0 && s_pfEdit.sel < (int)entries.size()); config::Settings::PortfolioEntry base; - if (existing) { base = entries[s_pf_sel]; if (workingMatches(base)) return; } + if (existing) { base = entries[s_pfEdit.sel]; if (workingMatches(base)) return; } auto e = buildWorking(base); - if (existing) entries[s_pf_sel] = e; - else { entries.push_back(e); s_pf_sel = (int)entries.size() - 1; } + if (existing) entries[s_pfEdit.sel] = e; + else { entries.push_back(e); s_pfEdit.sel = (int)entries.size() - 1; } persist(entries); }; @@ -436,10 +442,10 @@ static void RenderPortfolioEditor(App* app) ImVec2 vpPos = vp->Pos, vpSize = vp->Size; // Trigger a live backdrop (re)capture on OPEN and on viewport RESIZE only. A few frames are used // so the capture lands after the theme-effect suppression (frame after open) settles. - bool justOpened = !s_pf_was_open; - s_pf_was_open = true; - if (justOpened || s_pf_capture_w != vpSize.x || s_pf_capture_h != vpSize.y) s_pf_capture_frames = 3; - s_pf_capture_w = vpSize.x; s_pf_capture_h = vpSize.y; + bool justOpened = !s_pfEdit.wasOpen; + s_pfEdit.wasOpen = true; + if (justOpened || s_pfEdit.captureW != vpSize.x || s_pfEdit.captureH != vpSize.y) s_pfEdit.captureFrames = 3; + s_pfEdit.captureW = vpSize.x; s_pfEdit.captureH = vpSize.y; // Don't force-focus the scrim while a popup (basis/interval combo, color picker) is open, or it // would close the popup; the same guard gates the outside-click dismiss below. const bool popupOpen = ImGui::IsPopupOpen("", ImGuiPopupFlags_AnyPopupId | ImGuiPopupFlags_AnyPopupLevel); @@ -461,13 +467,13 @@ static void RenderPortfolioEditor(App* app) // Latch "capturing" for the WHOLE frame before decrementing, so the scroll children (which draw // later this frame) suppress their fade shader on every capture frame — including the last one, // where the counter would otherwise already read 0 and clash with the capture's render-state reset. - s_pf_capturing_frame = (s_pf_capture_frames > 0); - if (s_pf_capturing_frame) { + s_pfEdit.capturingFrame = (s_pfEdit.captureFrames > 0); + if (s_pfEdit.capturingFrame) { if (ImDrawCallback liveCap = effects::ImGuiAcrylic::GetLiveCaptureCallback()) { dl->AddCallback(liveCap, nullptr); dl->AddCallback(ImDrawCallback_ResetRenderState, nullptr); } - s_pf_capture_frames--; + s_pfEdit.captureFrames--; } // Skip the blur on the very first frame (live content isn't captured until this frame's render) — // shows the plain opaque scrim for one frame instead of a blur of stale background pixels. @@ -500,11 +506,11 @@ static void RenderPortfolioEditor(App* app) ImGui::PopFont(); ImGui::Dummy(ImVec2(0, Layout::spacingXs())); } - if (ImGui::IsKeyPressed(ImGuiKey_Escape)) s_portfolio_editor_open = false; + if (ImGui::IsKeyPressed(ImGuiKey_Escape)) s_pfEdit.open = false; { // clamp/validate the selection against the current entry list const auto& entriesRO = settings->getPortfolioEntries(); - if (s_pf_sel >= (int)entriesRO.size()) s_pf_sel = entriesRO.empty() ? -1 : 0; + if (s_pfEdit.sel >= (int)entriesRO.size()) s_pfEdit.sel = entriesRO.empty() ? -1 : 0; } float footerH = 56.0f * dp; // room for the 40px buttons + separator so they aren't clipped @@ -520,7 +526,7 @@ static void RenderPortfolioEditor(App* app) bool selDirty = false; { const auto& es = settings->getPortfolioEntries(); - if (s_pf_sel >= 0 && s_pf_sel < (int)es.size()) selDirty = !workingMatches(es[s_pf_sel]); + if (s_pfEdit.sel >= 0 && s_pfEdit.sel < (int)es.size()) selDirty = !workingMatches(es[s_pfEdit.sel]); } // ================= MASTER: scrollable group list + a pinned Add button below it ================= @@ -539,10 +545,10 @@ static void RenderPortfolioEditor(App* app) for (int i = 0; i < (int)entries.size(); i++) { ImGui::PushID(i); const auto& en = entries[i]; - bool selRow = (s_pf_sel == i); + bool selRow = (s_pfEdit.sel == i); ImVec2 rmn = ImGui::GetCursorScreenPos(); if (ImGui::Selectable("##pfgrp", selRow, ImGuiSelectableFlags_SpanAvailWidth, ImVec2(0, rowH))) { - if (i != s_pf_sel) clickedSel = i; + if (i != s_pfEdit.sel) clickedSel = i; } ImVec2 rmx(rmn.x + ImGui::GetItemRectSize().x, rmn.y + rowH); bool rowHov = ImGui::IsItemHovered(); @@ -596,10 +602,10 @@ static void RenderPortfolioEditor(App* app) if (delRow < (int)es.size()) { es.erase(es.begin() + delRow); persist(es); - if (es.empty()) s_pf_sel = -1; - else if (delRow < s_pf_sel) s_pf_sel -= 1; - else if (delRow == s_pf_sel) s_pf_sel = std::min(delRow, (int)es.size() - 1); - PortfolioBeginEdit(app, s_pf_sel); + if (es.empty()) s_pfEdit.sel = -1; + else if (delRow < s_pfEdit.sel) s_pfEdit.sel -= 1; + else if (delRow == s_pfEdit.sel) s_pfEdit.sel = std::min(delRow, (int)es.size() - 1); + PortfolioBeginEdit(app, s_pfEdit.sel); } } else if (clickedSel != -999) { PortfolioBeginEdit(app, clickedSel); // switching groups discards uncommitted edits @@ -615,8 +621,8 @@ static void RenderPortfolioEditor(App* app) ne.label = TR("portfolio_untitled"); es.push_back(ne); persist(es); - s_pf_sel = (int)es.size() - 1; - PortfolioBeginEdit(app, s_pf_sel); + s_pfEdit.sel = (int)es.size() - 1; + PortfolioBeginEdit(app, s_pfEdit.sel); } } ImGui::EndGroup(); @@ -625,7 +631,7 @@ static void RenderPortfolioEditor(App* app) // ================= DETAIL: options for the selected group ================= ImGui::BeginChild("##pfDetail", ImVec2(detailW, bodyH), false); - if (s_pf_sel == -1) { + if (s_pfEdit.sel == -1) { const char* msg = TR("portfolio_detail_empty"); ImVec2 av = ImGui::GetContentRegionAvail(); ImVec2 ts = capF->CalcTextSizeA(capF->LegacySize, FLT_MAX, 0, msg); @@ -646,15 +652,15 @@ static void RenderPortfolioEditor(App* app) float ph = ppad * 2.0f + sub1f->LegacySize + Layout::spacingXs() + capF->LegacySize; ImVec2 pMin = ImGui::GetCursorScreenPos(); ImVec2 pMax(pMin.x + pw, pMin.y + ph); - ImU32 accent = s_pf_color ? (ImU32)s_pf_color : 0; + ImU32 accent = s_pfEdit.color ? (ImU32)s_pfEdit.color : 0; GlassPanelSpec g; g.rounding = 10.0f; g.fillAlpha = 22; g.borderAlpha = 40; DrawGlassPanel(pdl, pMin, pMax, g); if (accent) { - int oa = (int)(std::max(0, std::min(100, s_pf_outline_opacity)) * 2.55f + 0.5f); + int oa = (int)(std::max(0, std::min(100, s_pfEdit.outlineOpacity)) * 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 h = data::sparklineSeries(state.market, s_pf_spark_interval); + if (s_pfEdit.showSparkline && (s_pfEdit.priceBasis == 0 || s_pfEdit.priceBasis == 1)) { + std::vector h = data::sparklineSeries(state.market, s_pfEdit.sparkInterval); if (h.size() >= 2) { ImU32 spCol = WithAlpha(h.back() >= h.front() ? Success() : Error(), 80); ImVec2 spMin(pMin.x + ppad, pMin.y + ph * 0.46f); @@ -663,23 +669,23 @@ static void RenderPortfolioEditor(App* app) } } float rowY = pMin.y + ppad, nameX = pMin.x + ppad; - if (!s_pf_icon.empty()) { - material::project_icons::drawByName(pdl, s_pf_icon, + if (!s_pfEdit.icon.empty()) { + material::project_icons::drawByName(pdl, s_pfEdit.icon, ImVec2(nameX + sub1f->LegacySize * 0.5f, rowY + sub1f->LegacySize * 0.5f), accent ? accent : OnSurface(), iconFont, sub1f->LegacySize); nameX += sub1f->LegacySize + Layout::spacingSm(); } - const char* nm = s_pf_label[0] ? s_pf_label : TR("portfolio_group_name"); + const char* nm = s_pfEdit.label[0] ? s_pfEdit.label : TR("portfolio_group_name"); pdl->AddText(sub1f, sub1f->LegacySize, ImVec2(nameX, rowY), - s_pf_label[0] ? OnSurface() : OnSurfaceDisabled(), nm); - double pbal = data::SumPortfolioBalance(s_pf_addrs, state.addresses); + s_pfEdit.label[0] ? OnSurface() : OnSurfaceDisabled(), nm); + double pbal = data::SumPortfolioBalance(s_pfEdit.addrs, state.addresses); config::Settings::PortfolioEntry tmp; - tmp.priceBasis = s_pf_price_basis; - tmp.manualPrice = s_pf_manual_price; - tmp.manualCurrency = s_pf_manual_ccy; - tmp.showDrgx = s_pf_show_drgx; - tmp.showValue = s_pf_show_value; - tmp.show24h = s_pf_show_24h; + tmp.priceBasis = s_pfEdit.priceBasis; + tmp.manualPrice = s_pfEdit.manualPrice; + tmp.manualCurrency = s_pfEdit.manualCcy; + tmp.showDrgx = s_pfEdit.showDrgx; + tmp.showValue = s_pfEdit.showValue; + tmp.show24h = s_pfEdit.show24h; PfDisplay pd = pfBuildDisplay(tmp, pbal, state.market); float primW = pd.primary.empty() ? 0.0f : body2f->CalcTextSizeA(body2f->LegacySize, FLT_MAX, 0, pd.primary.c_str()).x; @@ -704,7 +710,7 @@ static void RenderPortfolioEditor(App* app) } ImGui::Dummy(ImVec2(0, Layout::spacingSm())); ImGui::SetNextItemWidth(-1); - ImGui::InputTextWithHint("##pfLabel", TR("portfolio_group_name"), s_pf_label, sizeof(s_pf_label)); + ImGui::InputTextWithHint("##pfLabel", TR("portfolio_group_name"), s_pfEdit.label, sizeof(s_pfEdit.label)); ImGui::Dummy(ImVec2(0, Layout::spacingSm())); // Segmented control switches the detail section (no collapsible dropdowns) — one at a time. @@ -715,15 +721,15 @@ static void RenderPortfolioEditor(App* app) float segW = ImGui::GetContentRegionAvail().x; ImVec2 sMin = ImGui::GetCursorScreenPos(); int clk = pfSegmentedControl(ImGui::GetWindowDrawList(), sMin, segW, segH, - secs, 3, s_pf_section, body2f, "##pfseg", dp); - if (clk >= 0) s_pf_section = clk; + secs, 3, s_pfEdit.section, body2f, "##pfseg", dp); + if (clk >= 0) s_pfEdit.section = clk; ImGui::SetCursorScreenPos(ImVec2(sMin.x, sMin.y + segH)); ImGui::Dummy(ImVec2(segW, 0)); } ImGui::Dummy(ImVec2(0, Layout::spacingMd())); // ---- APPEARANCE (color + outline opacity + icon) ---- - if (s_pf_section == 0) { + if (s_pfEdit.section == 0) { // Color picker: preset swatches in ONE row (overflow hidden) + a custom "+" cell that is // always visible at the right and opens a picker. { @@ -736,8 +742,8 @@ static void RenderPortfolioEditor(App* app) // Reserved "+" slot at the right; inset by a few px so its selection outline // (drawn at radius sw*0.5) stays inside the modal instead of clipping at the edge. float plusX = rowStart.x + std::max(sw, availW - sw - 4.0f * dp); - bool isCustom = (s_pf_color != 0u); - for (int c = 0; c < nColors; c++) if (s_pf_color == kPfPalette[c]) isCustom = false; + bool isCustom = (s_pfEdit.color != 0u); + for (int c = 0; c < nColors; c++) if (s_pfEdit.color == kPfPalette[c]) isCustom = false; bool openCustom = false; // Preset swatches (0 = default/no color, 1..N = palette); stop before the "+" slot. float x = rowStart.x; @@ -747,7 +753,7 @@ static void RenderPortfolioEditor(App* app) ImVec2 cc(mn.x + sw * 0.5f, mn.y + sw * 0.5f); bool isDefault = (idx == 0); ImU32 col32 = isDefault ? 0u : kPfPalette[idx - 1]; - bool sel = isDefault ? (s_pf_color == 0u) : (s_pf_color == col32); + bool sel = isDefault ? (s_pfEdit.color == 0u) : (s_pfEdit.color == col32); bool hov = ImGui::IsMouseHoveringRect(mn, ImVec2(mn.x + sw, mn.y + sw)); if (isDefault) cdl->AddCircle(cc, sw * 0.40f, OnSurfaceMedium(), 0, 1.5f * dp); else cdl->AddCircleFilled(cc, sw * 0.40f, col32); @@ -756,7 +762,7 @@ static void RenderPortfolioEditor(App* app) ImGui::PushID(2000 + idx); ImGui::SetCursorScreenPos(mn); ImGui::InvisibleButton("##pfcol", ImVec2(sw, sw)); - if (ImGui::IsItemClicked()) s_pf_color = isDefault ? 0u : col32; + if (ImGui::IsItemClicked()) s_pfEdit.color = isDefault ? 0u : col32; ImGui::PopID(); x += sw + gap; } @@ -766,7 +772,7 @@ static void RenderPortfolioEditor(App* app) ImVec2 cc(mn.x + sw * 0.5f, mn.y + sw * 0.5f); bool hov = ImGui::IsMouseHoveringRect(mn, ImVec2(mn.x + sw, mn.y + sw)); if (isCustom) { - cdl->AddCircleFilled(cc, sw * 0.40f, (ImU32)s_pf_color); + cdl->AddCircleFilled(cc, sw * 0.40f, (ImU32)s_pfEdit.color); } else { cdl->AddCircle(cc, sw * 0.40f, OnSurfaceMedium(), 0, 1.5f * dp); float pr = sw * 0.18f; @@ -779,10 +785,10 @@ static void RenderPortfolioEditor(App* app) ImGui::SetCursorScreenPos(mn); ImGui::InvisibleButton("##pfcustomcol", ImVec2(sw, sw)); if (ImGui::IsItemClicked()) { - ImU32 base = s_pf_color ? (ImU32)s_pf_color : IM_COL32(0x4F, 0x9D, 0xFF, 0xFF); - s_pf_custom_rgb[0] = ((base >> IM_COL32_R_SHIFT) & 0xFF) / 255.0f; - s_pf_custom_rgb[1] = ((base >> IM_COL32_G_SHIFT) & 0xFF) / 255.0f; - s_pf_custom_rgb[2] = ((base >> IM_COL32_B_SHIFT) & 0xFF) / 255.0f; + ImU32 base = s_pfEdit.color ? (ImU32)s_pfEdit.color : IM_COL32(0x4F, 0x9D, 0xFF, 0xFF); + s_pfEdit.customRgb[0] = ((base >> IM_COL32_R_SHIFT) & 0xFF) / 255.0f; + s_pfEdit.customRgb[1] = ((base >> IM_COL32_G_SHIFT) & 0xFF) / 255.0f; + s_pfEdit.customRgb[2] = ((base >> IM_COL32_B_SHIFT) & 0xFF) / 255.0f; openCustom = true; } if (hov) material::Tooltip("%s", TR("portfolio_custom_color")); @@ -792,21 +798,21 @@ static void RenderPortfolioEditor(App* app) ImGui::Dummy(ImVec2(availW, 0)); if (openCustom) ImGui::OpenPopup("##pfCustomColor"); if (ImGui::BeginPopup("##pfCustomColor")) { - ImGui::ColorPicker3("##pfpick", s_pf_custom_rgb, + ImGui::ColorPicker3("##pfpick", s_pfEdit.customRgb, ImGuiColorEditFlags_NoSidePreview | ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_NoLabel); - s_pf_color = IM_COL32((int)(s_pf_custom_rgb[0] * 255.0f + 0.5f), - (int)(s_pf_custom_rgb[1] * 255.0f + 0.5f), - (int)(s_pf_custom_rgb[2] * 255.0f + 0.5f), 255); + s_pfEdit.color = IM_COL32((int)(s_pfEdit.customRgb[0] * 255.0f + 0.5f), + (int)(s_pfEdit.customRgb[1] * 255.0f + 0.5f), + (int)(s_pfEdit.customRgb[2] * 255.0f + 0.5f), 255); ImGui::EndPopup(); } } // Accent-outline opacity (only meaningful when an accent color is set). - ImGui::BeginDisabled(s_pf_color == 0u); + ImGui::BeginDisabled(s_pfEdit.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::SliderInt("##pfOutlineOpacity", &s_pfEdit.outlineOpacity, 0, 100, "%d%%"); + if (s_pfEdit.outlineOpacity < 0) s_pfEdit.outlineOpacity = 0; + if (s_pfEdit.outlineOpacity > 100) s_pfEdit.outlineOpacity = 100; ImGui::EndDisabled(); ImGui::Dummy(ImVec2(0, Layout::spacingSm())); // Icon picker: search on the heading line + a 12-per-row grid that scales to width, @@ -822,10 +828,10 @@ static void RenderPortfolioEditor(App* app) if (off > 0.0f) ImGui::SetCursorPosX(ImGui::GetCursorPosX() + off); ImGui::SetNextItemWidth(searchW); ImGui::InputTextWithHint("##pfIconSearch", TR("portfolio_search_icons"), - s_pf_icon_search, sizeof(s_pf_icon_search)); + s_pfEdit.iconSearch, sizeof(s_pfEdit.iconSearch)); } // Filtered icon list (-1 = "None", shown only when not searching). - std::string isearch = s_pf_icon_search; + std::string isearch = s_pfEdit.iconSearch; std::vector iconIdx; if (isearch.empty()) iconIdx.push_back(-1); int nIcons = material::project_icons::walletIconCount(); @@ -835,7 +841,7 @@ static void RenderPortfolioEditor(App* app) continue; iconIdx.push_back(i); } - ImDrawList* gdl = pfBeginScrollChild("##pfIconGrid", s_pf_icon_fade, + ImDrawList* gdl = pfBeginScrollChild("##pfIconGrid", s_pfEdit.iconFade, ImVec2(Layout::spacingSm(), Layout::spacingSm()), dp); const int cols = 12; float availW = ImGui::GetContentRegionAvail().x; @@ -854,7 +860,7 @@ static void RenderPortfolioEditor(App* app) bool hov = ImGui::IsMouseHoveringRect(mn, mx); bool isNone = (idx < 0); const char* nm2 = isNone ? "" : material::project_icons::walletIconName(idx); - bool sel = isNone ? s_pf_icon.empty() : (s_pf_icon == nm2); + bool sel = isNone ? s_pfEdit.icon.empty() : (s_pfEdit.icon == nm2); if (sel) { gdl->AddRectFilled(mn, mx, WithAlpha(Primary(), 40), 6.0f * dp); gdl->AddRect(mn, mx, WithAlpha(Primary(), 120), 6.0f * dp, 0, 1.5f * dp); @@ -872,64 +878,64 @@ static void RenderPortfolioEditor(App* app) } ImGui::PushID(idx + 2); ImGui::InvisibleButton("##pfic", ImVec2(cell, cell)); - if (ImGui::IsItemClicked()) s_pf_icon = isNone ? std::string() : std::string(nm2); + if (ImGui::IsItemClicked()) s_pfEdit.icon = isNone ? std::string() : std::string(nm2); if (hov) material::Tooltip("%s", isNone ? TR("portfolio_no_icon") : nm2); ImGui::PopID(); gcol = (gcol + 1) % cols; } - pfEndScrollChild(s_pf_icon_fade, gdl); + pfEndScrollChild(s_pfEdit.iconFade, gdl); } } // ---- PRICE (basis, manual price, shown fields, sparkline) ---- - if (s_pf_section == 1) { + if (s_pfEdit.section == 1) { // Price basis — radio group (all options visible; no dropdown). const char* basisItems[4] = { TR("portfolio_price_usd"), TR("portfolio_price_btc"), TR("portfolio_price_drgx"), TR("portfolio_price_manual") }; - for (int b = 0; b < 4; b++) { if (b) ImGui::SameLine(); ImGui::RadioButton(basisItems[b], &s_pf_price_basis, b); } - if (s_pf_price_basis == 3) { // Manual: price-per-DRGX + currency label + for (int b = 0; b < 4; b++) { if (b) ImGui::SameLine(); ImGui::RadioButton(basisItems[b], &s_pfEdit.priceBasis, b); } + if (s_pfEdit.priceBasis == 3) { // Manual: price-per-DRGX + currency label float half = (ImGui::GetContentRegionAvail().x - Layout::spacingSm()) * 0.62f; ImGui::SetNextItemWidth(half); - ImGui::InputDouble("##pfManPrice", &s_pf_manual_price, 0.0, 0.0, "%.6f"); - if (s_pf_manual_price < 0.0) s_pf_manual_price = 0.0; + ImGui::InputDouble("##pfManPrice", &s_pfEdit.manualPrice, 0.0, 0.0, "%.6f"); + if (s_pfEdit.manualPrice < 0.0) s_pfEdit.manualPrice = 0.0; ImGui::SameLine(); ImGui::SetNextItemWidth(-1); - ImGui::InputTextWithHint("##pfManCcy", TR("portfolio_currency"), s_pf_manual_ccy, sizeof(s_pf_manual_ccy)); + ImGui::InputTextWithHint("##pfManCcy", TR("portfolio_currency"), s_pfEdit.manualCcy, sizeof(s_pfEdit.manualCcy)); } ImGui::Dummy(ImVec2(0, Layout::spacingSm())); // Shown fields. ImGui::TextUnformatted(TR("portfolio_show")); - ImGui::Checkbox(DRAGONX_TICKER "##pfShowDrgx", &s_pf_show_drgx); + ImGui::Checkbox(DRAGONX_TICKER "##pfShowDrgx", &s_pfEdit.showDrgx); ImGui::SameLine(); - ImGui::BeginDisabled(s_pf_price_basis == 2); // DRGX-only -> no value field - ImGui::Checkbox(TR("portfolio_show_value"), &s_pf_show_value); + ImGui::BeginDisabled(s_pfEdit.priceBasis == 2); // DRGX-only -> no value field + ImGui::Checkbox(TR("portfolio_show_value"), &s_pfEdit.showValue); ImGui::EndDisabled(); ImGui::SameLine(); // 24h + sparkline toggles share one line (both live-market only). - ImGui::BeginDisabled(!(s_pf_price_basis == 0 || s_pf_price_basis == 1)); - ImGui::Checkbox(TR("portfolio_show_24h"), &s_pf_show_24h); + ImGui::BeginDisabled(!(s_pfEdit.priceBasis == 0 || s_pfEdit.priceBasis == 1)); + ImGui::Checkbox(TR("portfolio_show_24h"), &s_pfEdit.show24h); ImGui::SameLine(); - ImGui::Checkbox(TR("portfolio_show_sparkline"), &s_pf_show_sparkline); + ImGui::Checkbox(TR("portfolio_show_sparkline"), &s_pfEdit.showSparkline); ImGui::EndDisabled(); ImGui::Dummy(ImVec2(0, Layout::spacingSm())); // Sparkline interval — radio group (enabled only when the sparkline is shown). - ImGui::BeginDisabled(!s_pf_show_sparkline || !(s_pf_price_basis == 0 || s_pf_price_basis == 1)); + ImGui::BeginDisabled(!s_pfEdit.showSparkline || !(s_pfEdit.priceBasis == 0 || s_pfEdit.priceBasis == 1)); const char* spItems[5] = { TR("portfolio_spark_min"), TR("portfolio_spark_hour"), TR("portfolio_spark_day"), TR("portfolio_spark_week"), TR("portfolio_spark_month") }; - for (int s = 0; s < 5; s++) { if (s) ImGui::SameLine(); ImGui::RadioButton(spItems[s], &s_pf_spark_interval, s); } + for (int s = 0; s < 5; s++) { if (s) ImGui::SameLine(); ImGui::RadioButton(spItems[s], &s_pfEdit.sparkInterval, s); } ImGui::EndDisabled(); } // ---- ADDRESSES (search, filter, select) ---- - if (s_pf_section == 2) { + if (s_pfEdit.section == 2) { { char selc[48]; - snprintf(selc, sizeof(selc), TR("portfolio_addresses_sel"), (int)s_pf_addrs.size()); + snprintf(selc, sizeof(selc), TR("portfolio_addresses_sel"), (int)s_pfEdit.addrs.size()); Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), selc); } ImGui::SetNextItemWidth(-1); - ImGui::InputTextWithHint("##pfSearch", TR("portfolio_search"), s_pf_search, sizeof(s_pf_search)); + ImGui::InputTextWithHint("##pfSearch", TR("portfolio_search"), s_pfEdit.search, sizeof(s_pfEdit.search)); ImGui::Dummy(ImVec2(0, Layout::spacingXs())); // Filter + selection on one row: type-filter segmented (left) + Select all / Clear / // Funded (right). Select-all is deferred so it applies to the freshly-filtered set below. @@ -954,8 +960,8 @@ static void RenderPortfolioEditor(App* app) // Segmented type filter (left), vertically centered in the row. float segY = rowStart.y + (rowH - segH) * 0.5f; int tclk = pfSegmentedControl(ImGui::GetWindowDrawList(), ImVec2(rowStart.x, segY), - segW, segH, flabels, 3, s_pf_type_filter, capF, "##pftf", dp); - if (tclk >= 0) s_pf_type_filter = tclk; + segW, segH, flabels, 3, s_pfEdit.typeFilter, capF, "##pftf", dp); + if (tclk >= 0) s_pfEdit.typeFilter = tclk; // Select all / Clear (rounded pills) + Funded toggle (right), vertically centered. float grpX = rowStart.x + std::max(0.0f, rowW - groupW); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 12.0f * dp); @@ -963,22 +969,22 @@ static void RenderPortfolioEditor(App* app) ImGui::SetCursorScreenPos(ImVec2(grpX, rowStart.y + (rowH - pillH) * 0.5f)); if (ImGui::Button(selLbl)) selAllClicked = true; ImGui::SameLine(); - if (ImGui::Button(clrLbl)) s_pf_addrs.clear(); + if (ImGui::Button(clrLbl)) s_pfEdit.addrs.clear(); ImGui::PopStyleVar(2); ImGui::SameLine(); ImGui::SetCursorScreenPos(ImVec2(ImGui::GetCursorScreenPos().x, rowStart.y + (rowH - defFrameH) * 0.5f)); // center vs. pills - ImGui::Checkbox(TR("portfolio_funded"), &s_pf_funded_only); + ImGui::Checkbox(TR("portfolio_funded"), &s_pfEdit.fundedOnly); ImGui::SetCursorScreenPos(ImVec2(rowStart.x, rowStart.y + rowH)); ImGui::Dummy(ImVec2(rowW, 0)); } ImGui::Dummy(ImVec2(0, Layout::spacingXs())); - std::string search = s_pf_search; + std::string search = s_pfEdit.search; std::vector filtered; for (const auto& a : state.addresses) { - if (s_pf_type_filter == 1 && a.type != "shielded") continue; - if (s_pf_type_filter == 2 && a.type != "transparent") continue; - if (s_pf_funded_only && a.balance <= 1e-9) continue; + if (s_pfEdit.typeFilter == 1 && a.type != "shielded") continue; + if (s_pfEdit.typeFilter == 2 && a.type != "transparent") continue; + if (s_pfEdit.fundedOnly && a.balance <= 1e-9) continue; if (!search.empty()) { std::string hay = a.address + " " + app->getAddressLabel(a.address); if (!util::containsIgnoreCase(hay, search)) continue; @@ -986,15 +992,15 @@ static void RenderPortfolioEditor(App* app) filtered.push_back(&a); } std::sort(filtered.begin(), filtered.end(), [&](const AddressInfo* x, const AddressInfo* y) { - bool sx = data::PortfolioEntryContains(s_pf_addrs, x->address); - bool sy = data::PortfolioEntryContains(s_pf_addrs, y->address); + bool sx = data::PortfolioEntryContains(s_pfEdit.addrs, x->address); + bool sy = data::PortfolioEntryContains(s_pfEdit.addrs, y->address); if (sx != sy) return sx; return x->balance > y->balance; }); if (selAllClicked) - for (const AddressInfo* a : filtered) data::PortfolioEntryAdd(s_pf_addrs, a->address); + for (const AddressInfo* a : filtered) data::PortfolioEntryAdd(s_pfEdit.addrs, a->address); ImGui::Dummy(ImVec2(0, Layout::spacingXs())); - ImDrawList* ldl = pfBeginScrollChild("##pfAddrList", s_pf_addr_fade, + ImDrawList* ldl = pfBeginScrollChild("##pfAddrList", s_pfEdit.addrFade, ImVec2(Layout::spacingMd(), Layout::spacingSm()), dp); float rowH = std::max(26.0f * dp, body2f->LegacySize + Layout::spacingSm()); float lw = ImGui::GetContentRegionAvail().x; @@ -1002,7 +1008,7 @@ static void RenderPortfolioEditor(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_pf_addrs, a.address); + bool inSet = data::PortfolioEntryContains(s_pfEdit.addrs, a.address); ImVec2 rmn = ImGui::GetCursorScreenPos(); ImVec2 rmx(rmn.x + lw, rmn.y + rowH); bool rhov = ImGui::IsMouseHoveringRect(rmn, rmx); @@ -1052,12 +1058,12 @@ static void RenderPortfolioEditor(App* app) ImGui::PushID(a.address.c_str()); ImGui::InvisibleButton("##pfrow", ImVec2(lw, rowH)); if (ImGui::IsItemClicked()) { - if (inSet) data::PortfolioEntryRemove(s_pf_addrs, a.address); - else data::PortfolioEntryAdd(s_pf_addrs, a.address); + if (inSet) data::PortfolioEntryRemove(s_pfEdit.addrs, a.address); + else data::PortfolioEntryAdd(s_pfEdit.addrs, a.address); } ImGui::PopID(); } - pfEndScrollChild(s_pf_addr_fade, ldl); + pfEndScrollChild(s_pfEdit.addrFade, ldl); } ImGui::EndChild(); // ##pfDetailBody // Group-scoped actions (Add-entry height → lower hierarchy than the modal Close in the footer). @@ -1068,9 +1074,9 @@ static void RenderPortfolioEditor(App* app) ImGui::SetCursorPosY(ImGui::GetCursorPosY() + Layout::spacingSm()); ImGui::BeginDisabled(!selDirty); if (ImGui::Button(TR("portfolio_revert"), ImVec2(bw, addH))) - PortfolioBeginEdit(app, s_pf_sel); // reload the working set from the stored entry + PortfolioBeginEdit(app, s_pfEdit.sel); // reload the working set from the stored entry ImGui::SameLine(0, sp); - ImGui::BeginDisabled(s_pf_label[0] == '\0'); + ImGui::BeginDisabled(s_pfEdit.label[0] == '\0'); if (ImGui::Button(TR("portfolio_save"), ImVec2(bw, addH))) commitIfNeeded(); ImGui::EndDisabled(); ImGui::EndDisabled(); @@ -1082,7 +1088,7 @@ static void RenderPortfolioEditor(App* app) { float bh = 40.0f * dp, bw = 120.0f * dp; pfRightAlignX(bw); - if (ImGui::Button(TR("portfolio_close"), ImVec2(bw, bh))) { commitIfNeeded(); s_portfolio_editor_open = false; } + if (ImGui::Button(TR("portfolio_close"), ImVec2(bw, bh))) { commitIfNeeded(); s_pfEdit.open = false; } } ImGui::EndChild(); // ##pfCard @@ -1092,13 +1098,13 @@ static void RenderPortfolioEditor(App* app) // Outside-click dismiss (guarded against the appearing frame and open popups). if (!popupOpen && !ImGui::IsWindowAppearing() && ImGui::IsMouseClicked(ImGuiMouseButton_Left) && !ImGui::IsMouseHoveringRect(cardMin, cardMax)) - s_portfolio_editor_open = false; + s_pfEdit.open = false; // Esc / outside-click discard uncommitted edits (only the Close button above commits explicitly). // Invalidate the acrylic capture so the background (not the last live capture) is re-captured for // the other glass panels once the overlay is gone, and arm a fresh capture for the next open. - if (!s_portfolio_editor_open) { + if (!s_pfEdit.open) { effects::ImGuiAcrylic::InvalidateCapture(); - s_pf_was_open = false; + s_pfEdit.wasOpen = false; } ImGui::End(); @@ -1106,7 +1112,7 @@ static void RenderPortfolioEditor(App* app) ImGui::PopStyleVar(3); } -bool PortfolioEditorActive() { return s_portfolio_editor_open; } +bool PortfolioEditorActive() { return s_pfEdit.open; } void RenderMarketTab(App* app) { @@ -1135,9 +1141,9 @@ void RenderMarketTab(App* app) // Load persisted exchange/pair on first frame LoadMarketState(app->settings(), registry); - if (s_exchange_idx >= (int)registry.size()) s_exchange_idx = 0; - const auto& currentExchange = registry[s_exchange_idx]; - if (s_pair_idx >= (int)currentExchange.pairs.size()) s_pair_idx = 0; + if (s_mkt.exchangeIdx >= (int)registry.size()) s_mkt.exchangeIdx = 0; + const auto& currentExchange = registry[s_mkt.exchangeIdx]; + if (s_mkt.pairIdx >= (int)currentExchange.pairs.size()) s_mkt.pairIdx = 0; // (Panel theme-effects are suppressed frame-wide in App::render() while the modal is open, so // their foreground borders don't bleed over the overlay — see PortfolioEditorActive().) @@ -1243,19 +1249,19 @@ void RenderMarketTab(App* app) // the chart block below. Computed once here so the badge can report the displayed period's change. std::time_t nowSec = std::time(nullptr); std::vector chartTimes; - s_price_history.clear(); - for (const auto& pr : data::chartSeries(market, s_chart_interval, nowSec)) { - s_price_history.push_back(pr.second); + s_mkt.history.clear(); + for (const auto& pr : data::chartSeries(market, s_mkt.chartInterval, nowSec)) { + s_mkt.history.push_back(pr.second); chartTimes.push_back(pr.first); } // Change over the displayed period (Live falls back to the market's 24h figure). double periodChangePct = market.change_24h; - if (s_chart_interval != 0 && s_price_history.size() >= 2 && s_price_history.front() > 0.0) - periodChangePct = (s_price_history.back() - s_price_history.front()) / s_price_history.front() * 100.0; + if (s_mkt.chartInterval != 0 && s_mkt.history.size() >= 2 && s_mkt.history.front() > 0.0) + periodChangePct = (s_mkt.history.back() - s_mkt.history.front()) / s_mkt.history.front() * 100.0; bool chartUp = periodChangePct >= 0.0; // Suffix = the SELECTED range (not the raw data span), so 1D reads "24h", 1W reads "7d", etc. std::string periodSuffix = "24h"; - switch (s_chart_interval) { + switch (s_mkt.chartInterval) { case 1: periodSuffix = "1h"; break; // 1H case 2: periodSuffix = "24h"; break; // 1D case 3: periodSuffix = "7d"; break; // 1W @@ -1311,7 +1317,7 @@ void RenderMarketTab(App* app) // ---- TRADE BUTTON (top-right of card) ---- if (!currentExchange.pairs.empty()) { - const char* pairName = currentExchange.pairs[s_pair_idx].displayName.c_str(); + const char* pairName = currentExchange.pairs[s_mkt.pairIdx].displayName.c_str(); ImFont* iconFont = Type().iconSmall(); ImVec2 textSz = body2->CalcTextSizeA(body2->LegacySize, FLT_MAX, 0, pairName); ImVec2 iconSz = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, ICON_MD_OPEN_IN_NEW); @@ -1349,7 +1355,7 @@ void RenderMarketTab(App* app) ImGui::SetCursorScreenPos(tMin); ImGui::InvisibleButton("##TradeOnExchange", ImVec2(tradeBtnW, tradeBtnH)); if (ImGui::IsItemClicked()) { - util::Platform::openUrl(currentExchange.pairs[s_pair_idx].tradeUrl); + util::Platform::openUrl(currentExchange.pairs[s_mkt.pairIdx].tradeUrl); } if (ImGui::IsItemHovered()) { ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); @@ -1383,7 +1389,7 @@ void RenderMarketTab(App* app) // PRICE CHART — drawn inside the combined hero card's glass panel (above) // ================================================================ { - // The chart series (s_price_history) + timestamps (chartTimes) were computed in the + // The chart series (s_mkt.history) + timestamps (chartTimes) were computed in the // precompute above. Historical intervals come from CoinGecko market_chart; "Live" is the // in-session buffer. Until two points exist, the empty-state below is shown. // The chart shares the combined hero+chart glass panel drawn in PRICE SUMMARY above. @@ -1409,7 +1415,7 @@ void RenderMarketTab(App* app) 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 sel = (s_mkt.chartInterval == kIvs[b].iv); bool bhov = material::IsRectHovered(bmn, bmx); ImU32 bg = sel ? WithAlpha(Primary(), 200) : (bhov ? WithAlpha(OnSurface(), 35) : WithAlpha(OnSurface(), 18)); @@ -1419,7 +1425,7 @@ void RenderMarketTab(App* app) ImGui::SetCursorScreenPos(bmn); ImGui::PushID(9100 + b); if (ImGui::InvisibleButton("##civ", ImVec2(bw, pillH))) { - s_chart_interval = kIvs[b].iv; + s_mkt.chartInterval = kIvs[b].iv; } if (ImGui::IsItemHovered()) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); ImGui::PopID(); @@ -1471,10 +1477,10 @@ void RenderMarketTab(App* app) float plotW = plotRight - plotLeft; float plotH = plotBottom - plotTop; - if (s_price_history.size() >= 2) { + if (s_mkt.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()); + double yMin = *std::min_element(s_mkt.history.begin(), s_mkt.history.end()); + double yMax = *std::max_element(s_mkt.history.begin(), s_mkt.history.end()); if (yMax <= yMin) { yMax = yMin + 1e-8; } double yRange = yMax - yMin; double yPadding = yRange * 0.12; @@ -1499,7 +1505,7 @@ void RenderMarketTab(App* app) } // Build points - size_t n = s_price_history.size(); + size_t n = s_mkt.history.size(); std::vector points(n); // Colored by the DISPLAYED period's direction (chartUp), not just 24h. @@ -1510,7 +1516,7 @@ void RenderMarketTab(App* app) for (size_t i = 0; i < n; i++) { float t = (n > 1) ? (float)i / (float)(n - 1) : 0.0f; float x = plotLeft + t * plotW; - float y = plotBottom - (float)((s_price_history[i] - yMin) / (yMax - yMin)) * plotH; + float y = plotBottom - (float)((s_mkt.history[i] - yMin) / (yMax - yMin)) * plotH; points[i] = ImVec2(x, y); } @@ -1525,11 +1531,11 @@ void RenderMarketTab(App* app) // High/low price labels at the displayed range's extremes (no dot markers). if (n >= 3) { - size_t hiIdx = (size_t)(std::max_element(s_price_history.begin(), s_price_history.end()) - s_price_history.begin()); - size_t loIdx = (size_t)(std::min_element(s_price_history.begin(), s_price_history.end()) - s_price_history.begin()); + size_t hiIdx = (size_t)(std::max_element(s_mkt.history.begin(), s_mkt.history.end()) - s_mkt.history.begin()); + size_t loIdx = (size_t)(std::min_element(s_mkt.history.begin(), s_mkt.history.end()) - s_mkt.history.begin()); auto markExtreme = [&](size_t idx, bool high) { ImVec2 p = points[idx]; - std::string lbl = FormatPrice(s_price_history[idx]); + std::string lbl = FormatPrice(s_mkt.history[idx]); ImVec2 ls = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, lbl.c_str()); float ly = high ? p.y - ls.y - 5.0f * mktDp : p.y + 5.0f * mktDp; float lx = std::min(std::max(plotLeft, p.x - ls.x * 0.5f), plotRight - ls.x); @@ -1597,12 +1603,12 @@ void RenderMarketTab(App* app) std::time_t tt = chartTimes[idx]; std::tm* tmv = std::localtime(&tt); if (tmv) std::strftime(whenBuf, sizeof(whenBuf), - s_chart_interval <= 1 ? "%b %d %H:%M" : "%b %d, %Y", tmv); + s_mkt.chartInterval <= 1 ? "%b %d %H:%M" : "%b %d, %Y", tmv); } if (whenBuf[0]) - snprintf(buf, sizeof(buf), "%s \xC2\xB7 %s", whenBuf, FormatPrice(s_price_history[idx]).c_str()); + snprintf(buf, sizeof(buf), "%s \xC2\xB7 %s", whenBuf, FormatPrice(s_mkt.history[idx]).c_str()); else - snprintf(buf, sizeof(buf), "%s", FormatPrice(s_price_history[idx]).c_str()); + 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; @@ -1667,7 +1673,7 @@ void RenderMarketTab(App* app) ImVec2 cMin(cx, cy); ImVec2 cMax(cx + cw, cy + chipH); - bool selected = (ex == s_exchange_idx && pr == s_pair_idx); + bool selected = (ex == s_mkt.exchangeIdx && pr == s_mkt.pairIdx); bool hov = material::IsRectHovered(cMin, cMax); ImU32 chipBg = selected ? WithAlpha(Primary(), 200) : (hov ? WithAlpha(OnSurface(), 35) : WithAlpha(OnSurface(), 20)); @@ -1688,9 +1694,9 @@ void RenderMarketTab(App* app) ImGui::SetCursorScreenPos(cMin); snprintf(buf, sizeof(buf), "##PairBtn%d", btnCounter++); if (ImGui::InvisibleButton(buf, ImVec2(cw, chipH))) { - if (ex != s_exchange_idx || pr != s_pair_idx) { - s_exchange_idx = ex; - s_pair_idx = pr; + if (ex != s_mkt.exchangeIdx || pr != s_mkt.pairIdx) { + s_mkt.exchangeIdx = ex; + s_mkt.pairIdx = pr; app->settings()->setSelectedExchange(exName); app->settings()->setSelectedPair(pairName); app->settings()->save(); @@ -1732,7 +1738,7 @@ void RenderMarketTab(App* app) if (ImGui::Button(ml, ImVec2(mBtnW, 0))) { // Open the combined editor selecting the first group (or the empty state if none). PortfolioBeginEdit(app, app->settings()->getPortfolioEntries().empty() ? -1 : 0); - s_portfolio_editor_open = true; + s_pfEdit.open = true; } } ImGui::Dummy(ImVec2(0, Layout::spacingXs())); @@ -1818,7 +1824,7 @@ void RenderMarketTab(App* app) if (pfN > 0) { float gy = cardMin.y + pfSummaryH; ImGuiIO& io = ImGui::GetIO(); - bool interacting = (s_pf_drag >= 0 || s_pf_resize >= 0); + bool interacting = (s_grid.drag >= 0 || s_grid.resize >= 0); float rh = 20.0f * mktDp; // corner resize-grip size (generous so a short resize is easy to grab) auto cellMin = [&](const PfCell& L) { @@ -1945,11 +1951,11 @@ void RenderMarketTab(App* app) ImVec2 sz = cellSize(pfLayout[i].w, pfLayout[i].h); ImVec2 gcMax(gcMin.x + sz.x, gcMin.y + sz.y); - if (i == s_pf_drag && s_pf_moved) { + if (i == s_grid.drag && s_grid.moved) { dl->AddRect(gcMin, gcMax, WithAlpha(OnSurface(), 45), 10.0f, 0, 1.0f); // source ghost continue; } - if (i == s_pf_resize) continue; + if (i == s_grid.resize) continue; bool hov = !interacting && material::IsRectHovered(gcMin, gcMax); drawCard(gcMin, gcMax, pfEntriesGeo[i], hov); @@ -1964,7 +1970,7 @@ void RenderMarketTab(App* app) // Start a gesture only when idle. One body button; the press location (corner vs body) // decides resize vs move — avoids overlapping-item hit-test issues. - if (s_pf_drag < 0 && s_pf_resize < 0) { + if (s_grid.drag < 0 && s_grid.resize < 0) { ImGui::PushID(3000 + i); ImGui::SetCursorScreenPos(gcMin); ImGui::InvisibleButton("##pfBody", sz); @@ -1973,11 +1979,11 @@ void RenderMarketTab(App* app) ImGui::PopID(); if (bodyHov) ImGui::SetMouseCursor(gripHov ? ImGuiMouseCursor_ResizeNWSE : ImGuiMouseCursor_Hand); if (bodyAct) { - if (gripHov) { s_pf_resize = i; s_pf_old = pfLayout[i]; } + if (gripHov) { s_grid.resize = i; s_grid.old = pfLayout[i]; } else { - s_pf_drag = i; s_pf_moved = false; s_pf_old = pfLayout[i]; - s_pf_press = io.MousePos; - s_pf_grab = ImVec2(io.MousePos.x - gcMin.x, io.MousePos.y - gcMin.y); + s_grid.drag = i; s_grid.moved = false; s_grid.old = pfLayout[i]; + s_grid.press = io.MousePos; + s_grid.grab = ImVec2(io.MousePos.x - gcMin.x, io.MousePos.y - gcMin.y); } } } @@ -1988,8 +1994,8 @@ void RenderMarketTab(App* app) }; // ----- Resize in progress (corner drag) ----- - if (s_pf_resize >= 0 && s_pf_resize < pfN) { - int i = s_pf_resize; + if (s_grid.resize >= 0 && s_grid.resize < pfN) { + int i = s_grid.resize; ImVec2 mn = cellMin(pfLayout[i]); int nw = std::max(pfMinW, std::min(pfCols - pfLayout[i].col, (int)((io.MousePos.x - mn.x + pfCardGap) / (pfCellW + pfCardGap) + 0.5f))); @@ -2015,22 +2021,22 @@ void RenderMarketTab(App* app) entries[i].gridW = nw; entries[i].gridH = nh; pfPersist(entries); } - s_pf_resize = -1; + s_grid.resize = -1; } } // ----- Drag in progress ----- - if (s_pf_drag >= 0 && s_pf_drag < pfN) { - int i = s_pf_drag; + if (s_grid.drag >= 0 && s_grid.drag < pfN) { + int i = s_grid.drag; if (io.MouseDown[0]) { - if (std::abs(io.MousePos.x - s_pf_press.x) > 4.0f || std::abs(io.MousePos.y - s_pf_press.y) > 4.0f) - s_pf_moved = true; - if (s_pf_moved) { + if (std::abs(io.MousePos.x - s_grid.press.x) > 4.0f || std::abs(io.MousePos.y - s_grid.press.y) > 4.0f) + s_grid.moved = true; + if (s_grid.moved) { int w = pfLayout[i].w, h = pfLayout[i].h; - float tx = io.MousePos.x - s_pf_grab.x, ty = io.MousePos.y - s_pf_grab.y; + float tx = io.MousePos.x - s_grid.grab.x, ty = io.MousePos.y - s_grid.grab.y; int tc = std::max(0, std::min(pfCols - w, (int)((tx - cx) / (pfCellW + pfCardGap) + 0.5f))); int tr = std::max(0, (int)((ty - gy) / (pfCellH + pfCardGap) + 0.5f)); - s_pf_target = {tc, tr, w, h}; + s_grid.target = {tc, tr, w, h}; ImVec2 tmn(cx + tc * (pfCellW + pfCardGap), gy + tr * (pfCellH + pfCardGap)); ImVec2 tsz = cellSize(w, h), tmx(tmn.x + tsz.x, tmn.y + tsz.y); dl->AddRectFilled(tmn, tmx, WithAlpha(Primary(), 25), 10.0f); @@ -2040,21 +2046,21 @@ void RenderMarketTab(App* app) ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeAll); } } else { - if (!s_pf_moved) { PortfolioBeginEdit(app, i); s_portfolio_editor_open = true; } + if (!s_grid.moved) { PortfolioBeginEdit(app, i); s_pfEdit.open = true; } else { auto entries = app->settings()->getPortfolioEntries(); if (i < (int)entries.size()) { int occ = -1; for (int j = 0; j < pfN; j++) - if (j != i && pfLayout[j].col == s_pf_target.col && pfLayout[j].row == s_pf_target.row) { occ = j; break; } - entries[i].gridCol = s_pf_target.col; entries[i].gridRow = s_pf_target.row; + if (j != i && pfLayout[j].col == s_grid.target.col && pfLayout[j].row == s_grid.target.row) { occ = j; break; } + entries[i].gridCol = s_grid.target.col; entries[i].gridRow = s_grid.target.row; if (occ >= 0 && occ < (int)entries.size()) { - entries[occ].gridCol = s_pf_old.col; entries[occ].gridRow = s_pf_old.row; + entries[occ].gridCol = s_grid.old.col; entries[occ].gridRow = s_grid.old.row; } pfPersist(entries); } } - s_pf_drag = -1; s_pf_moved = false; + s_grid.drag = -1; s_grid.moved = false; } } }