From 5afb339ba71ed4dbc180aa84149ad1de72e129fa Mon Sep 17 00:00:00 2001 From: DanS Date: Fri, 3 Jul 2026 20:47:33 -0500 Subject: [PATCH] =?UTF-8?q?fix(market):=20portfolio=20modal=20=E2=80=94=20?= =?UTF-8?q?hover=20assert,=20cut-off=20footer,=20plain=20heading,=20FX=20b?= =?UTF-8?q?leed,=20bigger=20delete=20icon?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- src/app.cpp | 11 ++++++++ src/ui/windows/market_tab.cpp | 50 +++++++++++++++++++---------------- src/ui/windows/market_tab.h | 4 +++ 3 files changed, 42 insertions(+), 23 deletions(-) diff --git a/src/app.cpp b/src/app.cpp index 4f2af83..211463c 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -1243,6 +1243,17 @@ void App::render() // Process deferred encryption from wizard (runs in background) processDeferredEncryption(); + // While the full-window portfolio modal is open, suppress panel theme-effects for the whole + // frame so their foreground-draw-list borders (sidebar included) don't bleed over the overlay. + // RAII-restored to the prior state on any return path. + struct FxSuppressGuard { + bool active; + explicit FxSuppressGuard(bool on) : active(on) { + if (active) ui::effects::ThemeEffects::instance().setEnabled(false); + } + ~FxSuppressGuard() { if (active) ui::effects::ThemeEffects::instance().setEnabled(true); } + } fxGuard(ui::PortfolioEditorActive() && ui::effects::ThemeEffects::instance().isEnabled()); + // Main content area - use full window (no menu bar) ImGuiViewport* viewport = ImGui::GetMainViewport(); ImGui::SetNextWindowPos(viewport->WorkPos); diff --git a/src/ui/windows/market_tab.cpp b/src/ui/windows/market_tab.cpp index 1e94fce..08795ac 100644 --- a/src/ui/windows/market_tab.cpp +++ b/src/ui/windows/market_tab.cpp @@ -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 diff --git a/src/ui/windows/market_tab.h b/src/ui/windows/market_tab.h index cccdbff..df277bd 100644 --- a/src/ui/windows/market_tab.h +++ b/src/ui/windows/market_tab.h @@ -15,5 +15,9 @@ namespace ui { */ void RenderMarketTab(App* app); +// True while the full-window Manage-portfolio modal is open (used to suppress panel theme-effects +// frame-wide so their foreground borders don't bleed over the overlay). +bool PortfolioEditorActive(); + } // namespace ui } // namespace dragonx