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:
11
src/app.cpp
11
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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user