diff --git a/src/ui/windows/market_tab.cpp b/src/ui/windows/market_tab.cpp index e2909b7..21d4a2b 100644 --- a/src/ui/windows/market_tab.cpp +++ b/src/ui/windows/market_tab.cpp @@ -17,6 +17,8 @@ #include "../material/typography.h" #include "../material/project_icons.h" #include "../effects/imgui_acrylic.h" +#include "../effects/scroll_fade_shader.h" +#include "../effects/low_spec.h" #include "../../util/text_format.h" #include "../../embedded/IconsMaterialDesign.h" #include "../../util/platform.h" @@ -101,6 +103,7 @@ static int s_pf_section = 0; // right-pane segmented control: 0=Appearance 1= // 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; @@ -108,8 +111,12 @@ 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; @@ -121,6 +128,54 @@ 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 +// 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 +// padding holds an INNER scrolling child, so the inner scrollbar floats inside the frame rather +// than hugging its border. `fade` must persist across frames (one instance per scroll region). +// `pad` is the inner content inset; returns the inner draw list. Pair with pfEndScrollChild(). +static ImDrawList* pfBeginScrollChild(const char* id, effects::ScrollFadeShader& fade, + const ImVec2& pad, float dp) +{ + // Outer: rounded bordered frame; its padding is the gap that insets the scrollbar from the edge. + ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 6.0f * dp); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(6.0f * dp, 6.0f * dp)); + ImGui::BeginChild(id, ImVec2(0, std::max(120.0f * dp, ImGui::GetContentRegionAvail().y)), true, + ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse); + ImGui::PopStyleVar(2); // ChildRounding + outer WindowPadding (both captured by the outer child) + // Inner: borderless scrolling child with a thicker rounded scrollbar at its own (inset) edge. + ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarSize, 11.0f * dp); + ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarRounding, 5.5f * dp); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, pad); + std::string innerId = std::string(id) + "_i"; + ImGui::BeginChild(innerId.c_str(), ImVec2(0, 0), false, ImGuiWindowFlags_NoScrollWithMouse); + ImGui::PopStyleVar(); // inner WindowPadding — captured by this child; don't cascade to tooltips + ApplySmoothScroll(); + ImDrawList* dl = ImGui::GetWindowDrawList(); + // 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()) { + ImVec2 cMin = ImGui::GetWindowPos(); + ImVec2 cMax(cMin.x + ImGui::GetWindowSize().x, cMin.y + ImGui::GetWindowSize().y); + float sY = ImGui::GetScrollY(), sMax = ImGui::GetScrollMaxY(); + fade.fadeTopY = cMin.y; + fade.fadeBottomY = cMax.y; + fade.fadeZoneTop = (sY > 1.0f) ? fadeH : 0.0f; + fade.fadeZoneBottom = (sMax > 0.0f && sY < sMax - 1.0f) ? fadeH : 0.0f; + fade.addBind(dl); + } + return dl; +} + +static void pfEndScrollChild(effects::ScrollFadeShader& fade, ImDrawList* dl) +{ + if (!effects::isLowSpecMode() && !s_pf_capturing_frame && fade.ready) + effects::ScrollFadeShader::addUnbind(dl); + ImGui::EndChild(); // inner + ImGui::PopStyleVar(2); // ScrollbarSize + ScrollbarRounding + ImGui::EndChild(); // outer +} + // 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 pfResampleHistory(const std::vector& hist, int interval) @@ -440,7 +495,11 @@ static void RenderPortfolioEditor(App* app) // Only on capture frames (open/resize): capture the live UI drawn below this overlay (then reset // ImGui's render state) so the backdrop blurs the live tab content. Other frames reuse the frozen // blurred snapshot (blurCacheValid_), so there's no per-frame capture/re-blur cost or flicker. - if (s_pf_capture_frames > 0) { + // 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) { if (ImDrawCallback liveCap = effects::ImGuiAcrylic::GetLiveCaptureCallback()) { dl->AddCallback(liveCap, nullptr); dl->AddCallback(ImDrawCallback_ResetRenderState, nullptr); @@ -706,7 +765,9 @@ static void RenderPortfolioEditor(App* app) int nColors = (int)(sizeof(kPfPalette) / sizeof(kPfPalette[0])); float availW = ImGui::GetContentRegionAvail().x; ImVec2 rowStart = ImGui::GetCursorScreenPos(); - float plusX = rowStart.x + std::max(sw, availW - sw); // reserved "+" slot at the right + // 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 openCustom = false; @@ -780,26 +841,46 @@ static void RenderPortfolioEditor(App* app) if (s_pf_outline_opacity > 100) s_pf_outline_opacity = 100; ImGui::EndDisabled(); ImGui::Dummy(ImVec2(0, Layout::spacingSm())); - // Icon picker grid (large cells, fixed height, scrolls internally). + // Icon picker: search on the heading line + a 12-per-row grid that scales to width, + // smooth-scrolled with the Settings-tab edge fade. { - float cell = 56.0f * dp, cellGap = 6.0f * dp; + float cellGap = 6.0f * dp; ImFont* gIconFont = Type().iconXL(); - float gIconSz = cell * 0.5f; ImGui::TextUnformatted(TR("portfolio_icon")); - ImGui::BeginChild("##pfIconGrid", ImVec2(0, std::max(120.0f * dp, ImGui::GetContentRegionAvail().y)), true); - ImDrawList* gdl = ImGui::GetWindowDrawList(); + ImGui::SameLine(); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + ImGui::InputTextWithHint("##pfIconSearch", TR("portfolio_search"), + s_pf_icon_search, sizeof(s_pf_icon_search)); + // Filtered icon list (-1 = "None", shown only when not searching). + std::string isearch = s_pf_icon_search; + std::vector iconIdx; + if (isearch.empty()) iconIdx.push_back(-1); + int nIcons = material::project_icons::walletIconCount(); + for (int i = 0; i < nIcons; i++) { + if (!isearch.empty() && + !util::containsIgnoreCase(material::project_icons::walletIconName(i), isearch)) + continue; + iconIdx.push_back(i); + } + ImDrawList* gdl = pfBeginScrollChild("##pfIconGrid", s_pf_icon_fade, + ImVec2(Layout::spacingSm(), Layout::spacingSm()), dp); + const int cols = 12; float availW = ImGui::GetContentRegionAvail().x; - int cols = std::max(1, (int)((availW + cellGap) / (cell + cellGap))); - int total = material::project_icons::walletIconCount() + 1; // slot 0 = "None" + float cell = std::max(14.0f * dp, (availW - cellGap * (cols - 1)) / (float)cols); + float gIconSz = cell * 0.5f; + float gridW = cell * cols + cellGap * (cols - 1); + float indent = std::max(0.0f, (availW - gridW) * 0.5f); int gcol = 0; - for (int k = 0; k < total; k++) { - if (gcol != 0) ImGui::SameLine(0, cellGap); + for (size_t n = 0; n < iconIdx.size(); n++) { + int idx = iconIdx[n]; + if (gcol == 0) { if (indent > 0.0f) ImGui::SetCursorPosX(ImGui::GetCursorPosX() + indent); } + else ImGui::SameLine(0, cellGap); ImVec2 mn = ImGui::GetCursorScreenPos(); ImVec2 mx(mn.x + cell, mn.y + cell); ImVec2 cc(mn.x + cell * 0.5f, mn.y + cell * 0.5f); bool hov = ImGui::IsMouseHoveringRect(mn, mx); - bool isNone = (k == 0); - const char* nm2 = isNone ? "" : material::project_icons::walletIconName(k - 1); + bool isNone = (idx < 0); + const char* nm2 = isNone ? "" : material::project_icons::walletIconName(idx); bool sel = isNone ? s_pf_icon.empty() : (s_pf_icon == nm2); if (sel) { gdl->AddRectFilled(mn, mx, WithAlpha(Primary(), 40), 6.0f * dp); @@ -816,14 +897,14 @@ static void RenderPortfolioEditor(App* app) } else { material::project_icons::drawByName(gdl, nm2, cc, icol, gIconFont, gIconSz); } - ImGui::PushID(k); + ImGui::PushID(idx + 2); ImGui::InvisibleButton("##pfic", ImVec2(cell, cell)); if (ImGui::IsItemClicked()) s_pf_icon = isNone ? std::string() : std::string(nm2); if (hov) material::Tooltip("%s", isNone ? TR("portfolio_no_icon") : nm2); ImGui::PopID(); gcol = (gcol + 1) % cols; } - ImGui::EndChild(); + pfEndScrollChild(s_pf_icon_fade, gdl); } } @@ -877,15 +958,12 @@ static void RenderPortfolioEditor(App* app) ImGui::SetNextItemWidth(-1); ImGui::InputTextWithHint("##pfSearch", TR("portfolio_search"), s_pf_search, sizeof(s_pf_search)); ImGui::Dummy(ImVec2(0, Layout::spacingXs())); - // Type filter (segmented) + Funded toggle on one row: the segmented control is sized to - // leave room for the Funded checkbox to its right. + // Type filter — segmented control on its own row, capped at 580px wide. { const char* flabels[3] = { TR("portfolio_select_all"), TR("shielded"), TR("transparent") }; float segH = 26.0f * dp; - float fundedW = ImGui::GetFrameHeight() + ImGui::GetStyle().ItemInnerSpacing.x - + ImGui::CalcTextSize(TR("portfolio_funded")).x; - float rowW = ImGui::GetContentRegionAvail().x; - float segW = std::max(180.0f * dp, rowW - fundedW - Layout::spacingMd()); + float availW = ImGui::GetContentRegionAvail().x; + float segW = std::min(availW, 580.0f * dp); float cellW = segW / 3.0f; ImVec2 sMin = ImGui::GetCursorScreenPos(); ImDrawList* fdl = ImGui::GetWindowDrawList(); @@ -909,12 +987,8 @@ static void RenderPortfolioEditor(App* app) if (ImGui::IsItemHovered()) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); ImGui::PopID(); } - // Funded checkbox — vertically centered, to the right of the segmented control. - ImGui::SetCursorScreenPos(ImVec2(sMin.x + segW + Layout::spacingMd(), - sMin.y + (segH - ImGui::GetFrameHeight()) * 0.5f)); - ImGui::Checkbox(TR("portfolio_funded"), &s_pf_funded_only); ImGui::SetCursorScreenPos(ImVec2(sMin.x, sMin.y + segH)); - ImGui::Dummy(ImVec2(rowW, 0)); + ImGui::Dummy(ImVec2(availW, 0)); } ImGui::Dummy(ImVec2(0, Layout::spacingXs())); std::string search = s_pf_search; @@ -935,27 +1009,26 @@ static void RenderPortfolioEditor(App* app) if (sx != sy) return sx; return x->balance > y->balance; }); - // Select all / Clear — rounded (pill) buttons with padding, right-aligned. + // Select all / Clear (rounded pills) + Funded toggle — one row, left-aligned. { const char* selLbl = TR("portfolio_select_shown"); const char* clrLbl = TR("portfolio_clear_sel"); + float rowY = ImGui::GetCursorPosY(); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 12.0f * dp); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(Layout::spacingMd(), Layout::spacingXs())); - ImGuiStyle& st = ImGui::GetStyle(); - float grpW = ImGui::CalcTextSize(selLbl).x + ImGui::CalcTextSize(clrLbl).x - + st.FramePadding.x * 4.0f + st.ItemSpacing.x; - float avail = ImGui::GetContentRegionAvail().x; - if (avail > grpW) ImGui::SetCursorPosX(ImGui::GetCursorPosX() + avail - grpW); + float pillH = ImGui::GetFrameHeight(); if (ImGui::Button(selLbl)) for (const AddressInfo* a : filtered) data::PortfolioEntryAdd(s_pf_addrs, a->address); ImGui::SameLine(); if (ImGui::Button(clrLbl)) s_pf_addrs.clear(); ImGui::PopStyleVar(2); + ImGui::SameLine(); + ImGui::SetCursorPosY(rowY + (pillH - ImGui::GetFrameHeight()) * 0.5f); // center vs. pills + ImGui::Checkbox(TR("portfolio_funded"), &s_pf_funded_only); } ImGui::Dummy(ImVec2(0, Layout::spacingXs())); - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(Layout::spacingMd(), Layout::spacingSm())); - ImGui::BeginChild("##pfAddrList", ImVec2(0, std::max(120.0f * dp, ImGui::GetContentRegionAvail().y)), true); - ImDrawList* ldl = ImGui::GetWindowDrawList(); + ImDrawList* ldl = pfBeginScrollChild("##pfAddrList", s_pf_addr_fade, + ImVec2(Layout::spacingMd(), Layout::spacingSm()), dp); float rowH = std::max(26.0f * dp, body2f->LegacySize + Layout::spacingSm()); float lw = ImGui::GetContentRegionAvail().x; if (filtered.empty()) @@ -1017,14 +1090,12 @@ static void RenderPortfolioEditor(App* app) } ImGui::PopID(); } - ImGui::EndChild(); - ImGui::PopStyleVar(); // ##pfAddrList WindowPadding + pfEndScrollChild(s_pf_addr_fade, ldl); } } ImGui::EndChild(); // ##pfDetail // ================= FOOTER: revert / save / close (right-aligned, large) ================= - ImGui::Separator(); { float bh = 40.0f * dp, bw = 112.0f * dp, sp = Layout::spacingSm(); float aw = bw * 3 + sp * 2;