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)
|
// Process deferred encryption from wizard (runs in background)
|
||||||
processDeferredEncryption();
|
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)
|
// Main content area - use full window (no menu bar)
|
||||||
ImGuiViewport* viewport = ImGui::GetMainViewport();
|
ImGuiViewport* viewport = ImGui::GetMainViewport();
|
||||||
ImGui::SetNextWindowPos(viewport->WorkPos);
|
ImGui::SetNextWindowPos(viewport->WorkPos);
|
||||||
|
|||||||
@@ -16,7 +16,6 @@
|
|||||||
#include "../material/colors.h"
|
#include "../material/colors.h"
|
||||||
#include "../material/typography.h"
|
#include "../material/typography.h"
|
||||||
#include "../material/project_icons.h"
|
#include "../material/project_icons.h"
|
||||||
#include "../effects/theme_effects.h"
|
|
||||||
#include "../effects/imgui_acrylic.h"
|
#include "../effects/imgui_acrylic.h"
|
||||||
#include "../../util/text_format.h"
|
#include "../../util/text_format.h"
|
||||||
#include "../../embedded/IconsMaterialDesign.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::PopStyleVar(); // pop WindowPadding — the 28x20 applies to ##pfCard only, not nested children
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_ButtonTextAlign, ImVec2(0.5f, 0.5f)); // centered button labels
|
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;
|
if (ImGui::IsKeyPressed(ImGuiKey_Escape)) s_portfolio_editor_open = false;
|
||||||
|
|
||||||
{ // clamp/validate the selection against the current entry list
|
{ // 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;
|
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 bodyH = std::max(120.0f, ImGui::GetContentRegionAvail().y - footerH - Layout::spacingSm());
|
||||||
float contentW = ImGui::GetContentRegionAvail().x;
|
float contentW = ImGui::GetContentRegionAvail().x;
|
||||||
float gap = Layout::spacingLg();
|
float gap = Layout::spacingLg();
|
||||||
@@ -464,18 +470,16 @@ static void RenderPortfolioEditor(App* app)
|
|||||||
if (entries.empty())
|
if (entries.empty())
|
||||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("portfolio_no_entries"));
|
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("portfolio_no_entries"));
|
||||||
float rowH = 46.0f * dp;
|
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
|
int clickedSel = -999, delRow = -1; // deferred: don't mutate `entries` mid-loop
|
||||||
for (int i = 0; i < (int)entries.size(); i++) {
|
for (int i = 0; i < (int)entries.size(); i++) {
|
||||||
ImGui::PushID(i);
|
ImGui::PushID(i);
|
||||||
const auto& en = entries[i];
|
const auto& en = entries[i];
|
||||||
bool selRow = (s_pf_sel == i);
|
bool selRow = (s_pf_sel == i);
|
||||||
ImVec2 rmn = ImGui::GetCursorScreenPos();
|
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 (ImGui::Selectable("##pfgrp", selRow, ImGuiSelectableFlags_SpanAvailWidth, ImVec2(0, rowH))) {
|
||||||
if (i != s_pf_sel) clickedSel = i;
|
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);
|
ImVec2 rmx(rmn.x + ImGui::GetItemRectSize().x, rmn.y + rowH);
|
||||||
bool rowHov = ImGui::IsItemHovered();
|
bool rowHov = ImGui::IsItemHovered();
|
||||||
ImU32 accent = en.color ? (ImU32)en.color : WithAlpha(OnSurface(), 60);
|
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);
|
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());
|
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);
|
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) {
|
if (rowHov || selRow) {
|
||||||
float ds = 22.0f * dp;
|
ImFont* delFont = Type().iconMed();
|
||||||
ImVec2 dmn(rmx.x - ds - Layout::spacingXs(), rmn.y + (rowH - ds) * 0.5f);
|
float ds = delFont->LegacySize;
|
||||||
ImGui::SetCursorScreenPos(dmn);
|
ImVec2 dc(rmx.x - Layout::spacingMd() - ds * 0.5f, rmn.y + rowH * 0.5f);
|
||||||
if (ImGui::InvisibleButton("##pfdel", ImVec2(ds, ds))) delRow = i;
|
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::IsItemHovered();
|
bool dhov = ImGui::IsMouseHoveringRect(dmn, dmx);
|
||||||
if (dhov) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
if (dhov) {
|
||||||
ImVec2 isz = iconFont->CalcTextSizeA(iconFsz, FLT_MAX, 0, ICON_MD_DELETE_OUTLINE);
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
mdl->AddText(iconFont, iconFsz, ImVec2(dmn.x + (ds - isz.x) * 0.5f, dmn.y + (ds - isz.y) * 0.5f),
|
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);
|
dhov ? Error() : OnSurfaceMedium(), ICON_MD_DELETE_OUTLINE);
|
||||||
ImGui::SetCursorScreenPos(rowNext); // restore flow so the next row isn't offset
|
|
||||||
}
|
}
|
||||||
ImGui::PopID();
|
ImGui::PopID();
|
||||||
}
|
}
|
||||||
@@ -951,6 +958,8 @@ static void RenderPortfolioEditor(App* app)
|
|||||||
ImGui::PopStyleVar(3);
|
ImGui::PopStyleVar(3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool PortfolioEditorActive() { return s_portfolio_editor_open; }
|
||||||
|
|
||||||
void RenderMarketTab(App* app)
|
void RenderMarketTab(App* app)
|
||||||
{
|
{
|
||||||
auto& S = schema::UI();
|
auto& S = schema::UI();
|
||||||
@@ -982,12 +991,8 @@ void RenderMarketTab(App* app)
|
|||||||
const auto& currentExchange = registry[s_exchange_idx];
|
const auto& currentExchange = registry[s_exchange_idx];
|
||||||
if (s_pair_idx >= (int)currentExchange.pairs.size()) s_pair_idx = 0;
|
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 are suppressed frame-wide in App::render() while the modal is open, so
|
||||||
// panel theme-effects for this render so their foreground-draw-list borders don't bleed over the
|
// their foreground borders don't bleed over the overlay — see PortfolioEditorActive().)
|
||||||
// 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);
|
|
||||||
|
|
||||||
ImVec2 marketAvail = ImGui::GetContentRegionAvail();
|
ImVec2 marketAvail = ImGui::GetContentRegionAvail();
|
||||||
// Scrollable (the portfolio grid can extend past the window) but with no visible scrollbar —
|
// Scrollable (the portfolio grid can extend past the window) but with no visible scrollbar —
|
||||||
// scrolling still works via the mouse wheel.
|
// 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
|
// 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.
|
// + blurs the live content drawn above, so it must render after the tab body.
|
||||||
RenderPortfolioEditor(app);
|
RenderPortfolioEditor(app);
|
||||||
if (fxSuppressed) effects::ThemeEffects::instance().setEnabled(true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace ui
|
} // namespace ui
|
||||||
|
|||||||
@@ -15,5 +15,9 @@ namespace ui {
|
|||||||
*/
|
*/
|
||||||
void RenderMarketTab(App* app);
|
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 ui
|
||||||
} // namespace dragonx
|
} // namespace dragonx
|
||||||
|
|||||||
Reference in New Issue
Block a user