fix(market): HiDPI-scale hand-drawn geometry + dedup row/column helpers
Market-tab audit follow-up (all 9 confirmed findings): - Scale the unscaled corner radii on the Cards/Spotlight row rects, the address-preview glass panel, the chart Y-axis labels, and both chart hover tooltips by dpiScale() — they mismatched the coincident already-scaled rects on HiDPI (the CLAUDE.md hand-drawn-geometry rule). - Share the portfolio row-height/gap formula (pfRowHeight/pfRowGapFor) and the TABLE column widths (kPfValColW/kPfDrgxColW) between the draw loop and the scroll-region height budget so they can't drift and clip. - Derive the portfolio total from the shielded/transparent split (one address scan instead of two in Cards). - Build the address-picker selected-set once (unordered_set) instead of a linear PortfolioEntryContains per sort-compare and per row. No correctness/security/threading defects were found in the audit. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -31,6 +31,7 @@
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <ctime>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace dragonx {
|
||||
namespace ui {
|
||||
@@ -668,14 +669,18 @@ static void pfDrawAddressSection(App* app)
|
||||
}
|
||||
filtered.push_back(&a);
|
||||
}
|
||||
// Build the selected-address set once — the sort comparator and the per-row membership test below
|
||||
// both consulted it via a linear PortfolioEntryContains scan (O(n) each) every frame.
|
||||
std::unordered_set<std::string> selSet(s_pfEdit.addrs.begin(), s_pfEdit.addrs.end());
|
||||
std::sort(filtered.begin(), filtered.end(), [&](const AddressInfo* x, const AddressInfo* y) {
|
||||
bool sx = data::PortfolioEntryContains(s_pfEdit.addrs, x->address);
|
||||
bool sy = data::PortfolioEntryContains(s_pfEdit.addrs, y->address);
|
||||
bool sx = selSet.count(x->address) != 0;
|
||||
bool sy = selSet.count(y->address) != 0;
|
||||
if (sx != sy) return sx;
|
||||
return x->balance > y->balance;
|
||||
});
|
||||
if (selAllClicked)
|
||||
for (const AddressInfo* a : filtered) data::PortfolioEntryAdd(s_pfEdit.addrs, a->address);
|
||||
for (const AddressInfo* a : filtered)
|
||||
if (data::PortfolioEntryAdd(s_pfEdit.addrs, a->address)) selSet.insert(a->address);
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||
ImDrawList* ldl = material::BeginFadeScrollChild("##pfAddrList", s_pfEdit.addrFade,
|
||||
ImVec2(Layout::spacingMd(), Layout::spacingSm()), dp);
|
||||
@@ -685,7 +690,7 @@ static void pfDrawAddressSection(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_pfEdit.addrs, a.address);
|
||||
bool inSet = selSet.count(a.address) != 0;
|
||||
ImVec2 rmn = ImGui::GetCursorScreenPos();
|
||||
ImVec2 rmx(rmn.x + lw, rmn.y + rowH);
|
||||
bool rhov = ImGui::IsMouseHoveringRect(rmn, rmx);
|
||||
@@ -734,8 +739,8 @@ static void pfDrawAddressSection(App* app)
|
||||
ImGui::PushID(a.address.c_str());
|
||||
ImGui::InvisibleButton("##pfrow", ImVec2(lw, rowH));
|
||||
if (ImGui::IsItemClicked()) {
|
||||
if (inSet) data::PortfolioEntryRemove(s_pfEdit.addrs, a.address);
|
||||
else data::PortfolioEntryAdd(s_pfEdit.addrs, a.address);
|
||||
if (inSet) { data::PortfolioEntryRemove(s_pfEdit.addrs, a.address); selSet.erase(a.address); }
|
||||
else { data::PortfolioEntryAdd(s_pfEdit.addrs, a.address); selSet.insert(a.address); }
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
@@ -957,7 +962,7 @@ static void RenderPortfolioEditor(App* app)
|
||||
ImVec2 pMin = ImGui::GetCursorScreenPos();
|
||||
ImVec2 pMax(pMin.x + pw, pMin.y + ph);
|
||||
ImU32 accent = s_pfEdit.color ? (ImU32)s_pfEdit.color : 0;
|
||||
GlassPanelSpec g; g.rounding = 10.0f; g.fillAlpha = 22; g.borderAlpha = 40;
|
||||
GlassPanelSpec g; g.rounding = 10.0f * dp; g.fillAlpha = 22; g.borderAlpha = 40;
|
||||
DrawGlassPanel(pdl, pMin, pMax, g);
|
||||
if (accent) {
|
||||
int oa = (int)(std::max(0, std::min(100, s_pfEdit.outlineOpacity)) * 2.55f + 0.5f);
|
||||
@@ -1083,25 +1088,39 @@ bool PortfolioEditorActive() { return s_pfEdit.open; }
|
||||
// muted em-dash so a row never looks broken. Pure drawing into dl.
|
||||
// TABLE-style (portfolio_style 0) right-edge column anchors — shared by pfDrawRow's row body and the
|
||||
// column-header strip so the header labels sit exactly over the values they name.
|
||||
// TABLE-style (style 0) column widths (logical px) — shared by pfTableCols (which turns them into
|
||||
// right-edge anchors) and pfDrawRow (which feeds valW/drgxW to fit() truncation), so truncated text
|
||||
// always lines up with the column edges. Change here and both stay in sync.
|
||||
static constexpr float kPfChgColW = 62.0f;
|
||||
static constexpr float kPfValColW = 108.0f;
|
||||
static constexpr float kPfDrgxColW = 118.0f;
|
||||
|
||||
struct PfTableCols { float labelR, drgxR, valR, chgR; };
|
||||
static PfTableCols pfTableCols(float right, float dp)
|
||||
{
|
||||
const float colGap = Layout::spacingMd();
|
||||
const float chgW = 62.0f*dp, valW = 108.0f*dp, drgxW = 118.0f*dp;
|
||||
PfTableCols c;
|
||||
c.chgR = right;
|
||||
c.valR = c.chgR - chgW - colGap;
|
||||
c.drgxR = c.valR - valW - colGap;
|
||||
c.labelR = c.drgxR - drgxW - colGap;
|
||||
c.valR = c.chgR - kPfChgColW * dp - colGap;
|
||||
c.drgxR = c.valR - kPfValColW * dp - colGap;
|
||||
c.labelR = c.drgxR - kPfDrgxColW * dp - colGap;
|
||||
return c;
|
||||
}
|
||||
|
||||
// Portfolio row height + inter-row gap by style — one source shared by the row drawing
|
||||
// (mktDrawPortfolio) and the scroll-region height budget (RenderMarketTab) so they can't drift and clip.
|
||||
static float pfRowHeight(int style, float dp) { return (style == 0 ? 40.0f : style == 1 ? 68.0f : 80.0f) * dp; }
|
||||
static float pfRowGapFor(int style, float dp) { return style == 0 ? 2.0f * dp : Layout::spacingSm(); }
|
||||
|
||||
static void pfDrawRow(ImDrawList* dl, ImVec2 rowMin, ImVec2 rowMax,
|
||||
const config::Settings::PortfolioEntry& e, const WalletState& state,
|
||||
const MarketInfo& market, int style, float dp,
|
||||
ImFont* sub1, ImFont* capFont, bool hov)
|
||||
{
|
||||
double bal = data::SumPortfolioBalance(e.addresses, state.addresses);
|
||||
// One pass over the wallet's addresses: the shielded/transparent split (used by the Cards Z/T bar)
|
||||
// also yields the total, so we never scan twice.
|
||||
const data::PortfolioSplit split = data::SumPortfolioSplit(e.addresses, state.addresses);
|
||||
const double bal = split.shielded + split.transparent;
|
||||
ImU32 accent = e.color ? (ImU32)e.color : 0;
|
||||
const bool zeroBal = (bal <= 0.0); // empty groups render dimmed (content only, not the container)
|
||||
|
||||
@@ -1118,23 +1137,23 @@ static void pfDrawRow(ImDrawList* dl, ImVec2 rowMin, ImVec2 rowMax,
|
||||
dl->AddRectFilled(ImVec2(rowMin.x, rowMin.y + 5.0f * dp), ImVec2(rowMin.x + 3.0f * dp, rowMax.y - 5.0f * dp),
|
||||
accent ? accent : WithAlpha(OnSurface(), 55), 1.5f * dp);
|
||||
} else if (style == 1) {
|
||||
GlassPanelSpec g; g.rounding = 10.0f; g.fillAlpha = hov ? 42 : 30; g.borderAlpha = 55;
|
||||
GlassPanelSpec g; g.rounding = round; g.fillAlpha = hov ? 42 : 30; g.borderAlpha = 55;
|
||||
DrawGlassPanel(dl, rowMin, rowMax, g);
|
||||
if (accent) {
|
||||
dl->AddRectFilled(rowMin, rowMax, WithAlpha(accent, hov ? 16 : 10), round);
|
||||
dl->AddRectFilled(rowMin, ImVec2(rowMin.x + 3.0f * dp, rowMax.y), WithAlpha(accent, 205),
|
||||
round, ImDrawFlags_RoundCornersLeft);
|
||||
dl->AddRect(rowMin, rowMax, WithAlpha(accent, hov ? std::min(255, oa + 45) : oa), 10.0f, 0, hov ? 1.8f : 1.2f);
|
||||
dl->AddRect(rowMin, rowMax, WithAlpha(accent, hov ? std::min(255, oa + 45) : oa), round, 0, hov ? 1.8f : 1.2f);
|
||||
} else if (hov) {
|
||||
dl->AddRect(rowMin, rowMax, WithAlpha(OnSurface(), 80), 10.0f, 0, 1.2f);
|
||||
dl->AddRect(rowMin, rowMax, WithAlpha(OnSurface(), 80), round, 0, 1.2f);
|
||||
}
|
||||
} else {
|
||||
GlassPanelSpec g; g.rounding = 10.0f; g.fillAlpha = hov ? 56 : 46; g.borderAlpha = 60;
|
||||
GlassPanelSpec g; g.rounding = round; g.fillAlpha = hov ? 56 : 46; g.borderAlpha = 60;
|
||||
DrawGlassPanel(dl, rowMin, rowMax, g);
|
||||
dl->AddRectFilled(rowMin, ImVec2(rowMin.x + 4.0f * dp, rowMax.y),
|
||||
WithAlpha(accent ? accent : Primary(), 220), round, ImDrawFlags_RoundCornersLeft);
|
||||
if (accent) dl->AddRect(rowMin, rowMax, WithAlpha(accent, hov ? std::min(255, oa + 45) : oa), 10.0f, 0, hov ? 1.8f : 1.2f);
|
||||
else if (hov) dl->AddRect(rowMin, rowMax, WithAlpha(OnSurface(), 70), 10.0f, 0, 1.2f);
|
||||
if (accent) dl->AddRect(rowMin, rowMax, WithAlpha(accent, hov ? std::min(255, oa + 45) : oa), round, 0, hov ? 1.8f : 1.2f);
|
||||
else if (hov) dl->AddRect(rowMin, rowMax, WithAlpha(OnSurface(), 70), round, 0, 1.2f);
|
||||
}
|
||||
|
||||
auto tw = [](ImFont* f, const std::string& s){ return f->CalcTextSizeA(f->LegacySize, FLT_MAX, 0, s.c_str()).x; };
|
||||
@@ -1178,7 +1197,7 @@ static void pfDrawRow(ImDrawList* dl, ImVec2 rowMin, ImVec2 rowMax,
|
||||
// No sparkline — the trend rides in a small coloured % pill, keeping this a scannable grid.
|
||||
const PfTableCols cols = pfTableCols(right, dp);
|
||||
const float chgR = cols.chgR, valR = cols.valR, drgxR = cols.drgxR, labelR = cols.labelR;
|
||||
const float valW = 108.0f*dp, drgxW = 118.0f*dp; // fit() widths for the value / DRGX columns
|
||||
const float valW = kPfValColW*dp, drgxW = kPfDrgxColW*dp; // fit() widths (shared with pfTableCols)
|
||||
float iconSz = capFont->LegacySize, x = left;
|
||||
if (!e.icon.empty()) {
|
||||
material::project_icons::drawByName(dl, e.icon.c_str(), ImVec2(x+iconSz*0.5f, midY), accent?accent:OnSurfaceMedium(), Type().iconSmall(), iconSz);
|
||||
@@ -1217,9 +1236,9 @@ static void pfDrawRow(ImDrawList* dl, ImVec2 rowMin, ImVec2 rowMax,
|
||||
// DRGX (bottom-left) + 24h (bottom-right)
|
||||
if (e.showDrgx) dl->AddText(capFont, capFont->LegacySize, ImVec2(left, botY), OnSurfaceMedium(), drgxStr.c_str());
|
||||
if (wantChange) { std::string s = hasChange?chgStr:kDash; dl->AddText(capFont, capFont->LegacySize, ImVec2(right - tw(capFont,s), botY), hasChange?chgCol:OnSurfaceDisabled(), s.c_str()); }
|
||||
// shielded/transparent split bar (Cards-only signature; read-only balance data)
|
||||
// shielded/transparent split bar (Cards-only signature; reuses the split computed above)
|
||||
{
|
||||
const data::PortfolioSplit sp = data::SumPortfolioSplit(e.addresses, state.addresses);
|
||||
const data::PortfolioSplit& sp = split;
|
||||
const double tot = sp.shielded + sp.transparent;
|
||||
const float barY = rowMax.y - padY - zbarH;
|
||||
const bool barLight = IsLightTheme();
|
||||
@@ -1703,7 +1722,7 @@ static void mktDrawPriceChart(const MktCtx& cx)
|
||||
ImVec2 labelSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, buf);
|
||||
// Keep the axis label inside the card even on narrow windows (min-padding may be
|
||||
// smaller than the label width) so it never spills onto the tab background.
|
||||
float lblX = std::max(chartMin.x + 3.0f, plotLeft - labelSz.x - 6);
|
||||
float lblX = std::max(chartMin.x + 3.0f * mktDp, plotLeft - labelSz.x - 6.0f * mktDp);
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(lblX, gy - labelSz.y * 0.5f),
|
||||
OnSurfaceDisabled(), buf);
|
||||
@@ -1766,8 +1785,8 @@ static void mktDrawPriceChart(const MktCtx& cx)
|
||||
tipX = std::max(plotLeft, tipX);
|
||||
float tipY = plotTop + 4.0f * mktDp;
|
||||
ImVec2 tMin(tipX, tipY), tMax(tipX + tw + pad * 2, tipY + th);
|
||||
dl->AddRectFilled(tMin, tMax, IM_COL32(20, 20, 30, 235), 4.0f);
|
||||
dl->AddRect(tMin, tMax, IM_COL32(255, 255, 255, 30), 4.0f, 0, 1.0f);
|
||||
dl->AddRectFilled(tMin, tMax, IM_COL32(20, 20, 30, 235), 4.0f * mktDp);
|
||||
dl->AddRect(tMin, tMax, IM_COL32(255, 255, 255, 30), 4.0f * mktDp, 0, 1.0f);
|
||||
ImU32 cCol = (hc.close >= hc.open) ? Success() : Error();
|
||||
dl->AddText(capFont, lh, ImVec2(tipX + pad, tipY + pad), OnSurface(), when);
|
||||
dl->AddText(capFont, lh, ImVec2(tipX + pad, tipY + pad + lh + gap), cCol, l2);
|
||||
@@ -1870,16 +1889,16 @@ static void mktDrawPriceChart(const MktCtx& cx)
|
||||
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;
|
||||
float tipY = py - tipSz.y - tipPad * 2 - 4;
|
||||
float tipX = px + 10.0f * mktDp;
|
||||
float tipY = py - tipSz.y - tipPad * 2 - 4.0f * mktDp;
|
||||
if (tipX + tipSz.x + tipPad * 2 > plotRight)
|
||||
tipX = px - tipSz.x - tipPad * 2 - 10;
|
||||
if (tipY < plotTop) tipY = py + 10;
|
||||
tipX = px - tipSz.x - tipPad * 2 - 10.0f * mktDp;
|
||||
if (tipY < plotTop) tipY = py + 10.0f * mktDp;
|
||||
|
||||
ImVec2 tipMin(tipX, tipY);
|
||||
ImVec2 tipMax(tipX + tipSz.x + tipPad * 2, tipY + tipSz.y + tipPad * 2);
|
||||
dl->AddRectFilled(tipMin, tipMax, IM_COL32(20, 20, 30, 230), 4.0f);
|
||||
dl->AddRect(tipMin, tipMax, IM_COL32(255, 255, 255, 30), 4.0f, 0, 1.0f);
|
||||
dl->AddRectFilled(tipMin, tipMax, IM_COL32(20, 20, 30, 230), 4.0f * mktDp);
|
||||
dl->AddRect(tipMin, tipMax, IM_COL32(255, 255, 255, 30), 4.0f * mktDp, 0, 1.0f);
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(tipX + tipPad, tipY + tipPad), dotCol, buf);
|
||||
}
|
||||
@@ -2071,9 +2090,9 @@ static void mktDrawPortfolio(const MktCtx& cx)
|
||||
}
|
||||
|
||||
int style = app->settings() ? app->settings()->getPortfolioStyle() : 0;
|
||||
float rowH = (style == 0 ? 40.0f : style == 1 ? 68.0f : 80.0f) * mktDp;
|
||||
float rowH = pfRowHeight(style, mktDp);
|
||||
// Table rows abut like a ledger (thin gap); Cards/Spotlight breathe.
|
||||
float rowGap = (style == 0 ? 2.0f * mktDp : Layout::spacingSm());
|
||||
float rowGap = pfRowGapFor(style, mktDp);
|
||||
float rowsH = std::max(rowH, portfolioH - pfSummaryH);
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMin.y + pfSummaryH));
|
||||
@@ -2319,8 +2338,8 @@ void RenderMarketTab(App* app)
|
||||
for (const auto& e : pfEntriesGeo)
|
||||
if (e.scope.empty() || (!pfActiveHash.empty() && e.scope == pfActiveHash)) pfVisN++;
|
||||
int pfStyle = app->settings()->getPortfolioStyle();
|
||||
float pfRowH = (pfStyle == 0 ? 40.0f : pfStyle == 1 ? 68.0f : 80.0f) * mktDp;
|
||||
float pfRowGap = (pfStyle == 0 ? 2.0f * mktDp : Layout::spacingSm()); // Table rows abut
|
||||
float pfRowH = pfRowHeight(pfStyle, mktDp);
|
||||
float pfRowGap = pfRowGapFor(pfStyle, mktDp); // Table rows abut
|
||||
float pfHeaderH = (pfStyle == 0) ? (capFont->LegacySize + 8.0f * mktDp) : 0.0f; // Table column strip
|
||||
const int pfMaxVisibleRows = 4; // rows shown before the list scrolls
|
||||
// Height for up to pfMaxVisibleRows rows. ItemSpacing is zeroed inside the row child, so the
|
||||
|
||||
Reference in New Issue
Block a user