fix(market): portfolio modal — hover assert, cut-off footer, plain heading, FX bleed, bigger delete icon

- Fix the ImGui error on hovering a group row: the per-row delete was an
  overlapping InvisibleButton + SetCursorScreenPos over the row Selectable. Replace
  it with a manual IsMouseHoveringRect / IsMouseClicked hit-test (no overlapping
  ImGui item, no cursor manipulation).
- Delete icon is larger (iconMed) and inset from the row edge (spacingMd).
- Footer no longer clipped: reserve 56px for the 40px buttons + separator.
- Manage-portfolio heading is now plain text (h6) with no title-bar background or
  divider — content floats on the backdrop.
- Sidebar theme-effect borders no longer bleed through the overlay: suppress panel
  theme-effects frame-wide (RAII guard in App::render via ui::PortfolioEditorActive)
  while the modal is open, instead of only during the market render.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-03 20:47:33 -05:00
parent 0e8816e220
commit 5afb339ba7
3 changed files with 42 additions and 23 deletions

View File

@@ -16,7 +16,6 @@
#include "../material/colors.h"
#include "../material/typography.h"
#include "../material/project_icons.h"
#include "../effects/theme_effects.h"
#include "../effects/imgui_acrylic.h"
#include "../../util/text_format.h"
#include "../../embedded/IconsMaterialDesign.h"
@@ -438,7 +437,14 @@ static void RenderPortfolioEditor(App* app)
ImGui::PopStyleVar(); // pop WindowPadding — the 28x20 applies to ##pfCard only, not nested children
ImGui::PushStyleVar(ImGuiStyleVar_ButtonTextAlign, ImVec2(0.5f, 0.5f)); // centered button labels
material::DrawDialogTitleBar(TR("portfolio_manage_title"), nullptr); // no X — Close is in the footer
// Plain heading — no title-bar background or divider (content floats on the backdrop).
{
ImFont* titleFont = Type().h6();
ImGui::PushFont(titleFont);
ImGui::TextUnformatted(TR("portfolio_manage_title"));
ImGui::PopFont();
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
}
if (ImGui::IsKeyPressed(ImGuiKey_Escape)) s_portfolio_editor_open = false;
{ // clamp/validate the selection against the current entry list
@@ -446,7 +452,7 @@ static void RenderPortfolioEditor(App* app)
if (s_pf_sel >= (int)entriesRO.size()) s_pf_sel = entriesRO.empty() ? -1 : 0;
}
float footerH = 40.0f * dp;
float footerH = 56.0f * dp; // room for the 40px buttons + separator so they aren't clipped
float bodyH = std::max(120.0f, ImGui::GetContentRegionAvail().y - footerH - Layout::spacingSm());
float contentW = ImGui::GetContentRegionAvail().x;
float gap = Layout::spacingLg();
@@ -464,18 +470,16 @@ static void RenderPortfolioEditor(App* app)
if (entries.empty())
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("portfolio_no_entries"));
float rowH = 46.0f * dp;
float delSlot = 26.0f * dp;
float delSlot = 36.0f * dp; // reserved trailing space for the delete icon + margin
int clickedSel = -999, delRow = -1; // deferred: don't mutate `entries` mid-loop
for (int i = 0; i < (int)entries.size(); i++) {
ImGui::PushID(i);
const auto& en = entries[i];
bool selRow = (s_pf_sel == i);
ImVec2 rmn = ImGui::GetCursorScreenPos();
ImGui::SetNextItemAllowOverlap(); // let the per-row delete icon sit on top
if (ImGui::Selectable("##pfgrp", selRow, ImGuiSelectableFlags_SpanAvailWidth, ImVec2(0, rowH))) {
if (i != s_pf_sel) clickedSel = i;
}
ImVec2 rowNext = ImGui::GetCursorScreenPos(); // next-row cursor (the delete button moves it)
ImVec2 rmx(rmn.x + ImGui::GetItemRectSize().x, rmn.y + rowH);
bool rowHov = ImGui::IsItemHovered();
ImU32 accent = en.color ? (ImU32)en.color : WithAlpha(OnSurface(), 60);
@@ -497,18 +501,21 @@ static void RenderPortfolioEditor(App* app)
double bal = data::SumPortfolioBalance(en.addresses, state.addresses);
char vl[80]; snprintf(vl, sizeof(vl), "%.4f %s \xC2\xB7 %d", bal, DRAGONX_TICKER, (int)en.addresses.size());
mdl->AddText(capF, capF->LegacySize, ImVec2(ix, rmn.y + rowH * 0.56f), OnSurfaceMedium(), vl);
// Per-row delete icon (shown on hover or when selected).
// Per-row delete icon (larger, inset from the edge). Hit-tested manually so it is
// NOT an overlapping ImGui item over the row Selectable (which asserted on hover).
if (rowHov || selRow) {
float ds = 22.0f * dp;
ImVec2 dmn(rmx.x - ds - Layout::spacingXs(), rmn.y + (rowH - ds) * 0.5f);
ImGui::SetCursorScreenPos(dmn);
if (ImGui::InvisibleButton("##pfdel", ImVec2(ds, ds))) delRow = i;
bool dhov = ImGui::IsItemHovered();
if (dhov) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImVec2 isz = iconFont->CalcTextSizeA(iconFsz, FLT_MAX, 0, ICON_MD_DELETE_OUTLINE);
mdl->AddText(iconFont, iconFsz, ImVec2(dmn.x + (ds - isz.x) * 0.5f, dmn.y + (ds - isz.y) * 0.5f),
ImFont* delFont = Type().iconMed();
float ds = delFont->LegacySize;
ImVec2 dc(rmx.x - Layout::spacingMd() - ds * 0.5f, rmn.y + rowH * 0.5f);
ImVec2 dmn(dc.x - ds * 0.65f, dc.y - ds * 0.65f), dmx(dc.x + ds * 0.65f, dc.y + ds * 0.65f);
bool dhov = ImGui::IsMouseHoveringRect(dmn, dmx);
if (dhov) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) delRow = i;
}
ImVec2 isz = delFont->CalcTextSizeA(ds, FLT_MAX, 0, ICON_MD_DELETE_OUTLINE);
mdl->AddText(delFont, ds, ImVec2(dc.x - isz.x * 0.5f, dc.y - isz.y * 0.5f),
dhov ? Error() : OnSurfaceMedium(), ICON_MD_DELETE_OUTLINE);
ImGui::SetCursorScreenPos(rowNext); // restore flow so the next row isn't offset
}
ImGui::PopID();
}
@@ -951,6 +958,8 @@ static void RenderPortfolioEditor(App* app)
ImGui::PopStyleVar(3);
}
bool PortfolioEditorActive() { return s_portfolio_editor_open; }
void RenderMarketTab(App* app)
{
auto& S = schema::UI();
@@ -982,12 +991,8 @@ void RenderMarketTab(App* app)
const auto& currentExchange = registry[s_exchange_idx];
if (s_pair_idx >= (int)currentExchange.pairs.size()) s_pair_idx = 0;
// While the modal is open the overlay captures + blurs the live tab content behind it. Suppress
// panel theme-effects for this render so their foreground-draw-list borders don't bleed over the
// overlay (foreground draws above all windows). Restored after the modal is rendered.
bool fxSuppressed = s_portfolio_editor_open && effects::ThemeEffects::instance().isEnabled();
if (fxSuppressed) effects::ThemeEffects::instance().setEnabled(false);
// (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().)
ImVec2 marketAvail = ImGui::GetContentRegionAvail();
// Scrollable (the portfolio grid can extend past the window) but with no visible scrollbar —
// scrolling still works via the mouse wheel.
@@ -1923,7 +1928,6 @@ void RenderMarketTab(App* app)
// Portfolio editor modal (rendered outside the scroll child so it overlays the page). It captures
// + blurs the live content drawn above, so it must render after the tab body.
RenderPortfolioEditor(app);
if (fxSuppressed) effects::ThemeEffects::instance().setEnabled(true);
}
} // namespace ui