feat(market): icon-picker search + Settings-style scroll for portfolio modal
Rework the Manage-portfolio detail pane's Appearance and Addresses sections: - Icon picker: search field inline with the "Icon" heading (filters by name, hides the "None" slot while searching); grid now always fits 12 icons per row, scaling cell size to width and centering the block. - Both the icon grid and the address list scroll via a shared helper (pfBeginScrollChild/pfEndScrollChild): smooth scrolling + the Settings-tab edge-fade shader + a thicker, rounded scrollbar inset from the container edge (bordered outer frame wrapping an inner scrolling child). Smaller container radius. Fade gates off in low-spec and while the acrylic backdrop is being captured — latched per-frame so the last capture frame no longer clashes with the render-state reset. - Addresses: All/Shielded/Transparent segmented control moved to its own row, capped at 580px; Select all / Clear pills moved left of the Funded checkbox on one left-aligned row (Funded vertically centered against the pills). - Custom colour "+" swatch inset a few px so its selection outline no longer clips at the modal's right edge. - Remove the divider above the footer Revert/Save/Close buttons. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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<std::string> 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<double> pfResampleHistory(const std::vector<double>& 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<int> 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;
|
||||
|
||||
Reference in New Issue
Block a user