From 0e8816e220e25f2e6934310023d0f3fe4e311645 Mon Sep 17 00:00:00 2001 From: DanS Date: Fri, 3 Jul 2026 20:26:11 -0500 Subject: [PATCH] feat(effects): overlay-time live-framebuffer blur behind the portfolio modal The acrylic system only blurred a frozen snapshot of the static app background. Add an overlay-time re-capture so the Manage-portfolio modal blurs the LIVE tab content behind it. - AcrylicMaterial::captureLiveFramebuffer() (GL blit of FB 0 / DX11 CopyResource of the backbuffer + no-backend stub) mirrors captureBackgroundDirect but bypasses the dirtyFrames_ gate so it refreshes every frame the overlay is open, forcing a re-blur. - ImGuiAcrylic::GetLiveCaptureCallback() returns a draw callback for it. - market_tab: the modal inserts that callback (+ ImDrawCallback_ResetRenderState) at the START of its overlay draw list, before the backdrop, so the capture holds the app UI drawn below and the backdrop blurs it. On close, InvalidateCapture() re-captures the background so other glass panels don't keep blurring the stale live capture. - Panel theme-effects are suppressed during the market render while the modal is open (their foreground-draw-list borders draw above all windows and would otherwise bleed over the overlay). Market tab body is rendered again (no longer skipped). - Backdrop draws the acrylic at full strength (fallbackColor.w=1) over an opaque base so the blurred content reads clearly; lighter dim. Design mapped via an Explore investigation of the capture/blur pipeline; compiles on both OpenGL (Linux) and DX11 (Windows cross-build). Needs on-device visual verification. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/ui/effects/acrylic.cpp | 35 ++++++++++++++++++++++++++++++++ src/ui/effects/acrylic.h | 10 +++++++++ src/ui/effects/imgui_acrylic.cpp | 12 +++++++++++ src/ui/effects/imgui_acrylic.h | 13 ++++++++++++ src/ui/material/draw_helpers.h | 5 +++-- src/ui/windows/market_tab.cpp | 29 +++++++++++++++++++------- 6 files changed, 95 insertions(+), 9 deletions(-) diff --git a/src/ui/effects/acrylic.cpp b/src/ui/effects/acrylic.cpp index 145b50d..07ac359 100644 --- a/src/ui/effects/acrylic.cpp +++ b/src/ui/effects/acrylic.cpp @@ -355,6 +355,20 @@ void AcrylicMaterial::captureBackgroundDirect() blurCacheValid_ = false; // force re-blur with fresh capture } +void AcrylicMaterial::captureLiveFramebuffer() +{ + // Like captureBackgroundDirect but with NO dirtyFrames_ gate — refresh every frame the + // overlay is open so the blur tracks the live UI. Fires mid-RenderDrawData (from a callback + // inserted at the overlay window's draw-list start), so FB 0 already holds the app UI drawn + // below the overlay. A paired ImDrawCallback_ResetRenderState restores ImGui's GL state. + if (!initialized_ || shouldSkipEffect()) return; + if (viewportWidth_ <= 0 || viewportHeight_ <= 0) return; + glDisable(GL_SCISSOR_TEST); + captureBuffer_.captureScreen(viewportWidth_, viewportHeight_); + hasValidCapture_ = true; + blurCacheValid_ = false; // force re-blur with the fresh live capture +} + bool AcrylicMaterial::shouldSkipEffect() const { return !settings_.enabled || @@ -1238,6 +1252,26 @@ void AcrylicMaterial::captureBackgroundDirect() blurCacheValid_ = false; } +void AcrylicMaterial::captureLiveFramebuffer() +{ + // DX11 twin of the GL live capture: copy the currently-bound backbuffer (which holds the app + // UI drawn below the overlay) into the capture texture, with NO dirtyFrames_ gate. + if (!initialized_ || shouldSkipEffect()) return; + if (viewportWidth_ <= 0 || viewportHeight_ <= 0) return; + if (!dx_captureTex_ || !dx_context_) return; + ID3D11RenderTargetView* curRTV = nullptr; + dx_context_->OMGetRenderTargets(1, &curRTV, nullptr); + if (!curRTV) return; + ID3D11Resource* bbRes = nullptr; + curRTV->GetResource(&bbRes); + curRTV->Release(); + if (!bbRes) return; + dx_context_->CopyResource(dx_captureTex_, bbRes); + bbRes->Release(); + hasValidCapture_ = true; + blurCacheValid_ = false; +} + void AcrylicMaterial::applyBlur(float radius) { if (!dx_blurSRV_[0] || !dx_blurPS_ || !dx_context_) return; @@ -1666,6 +1700,7 @@ void AcrylicMaterial::shutdown() {} void AcrylicMaterial::resize(int, int) {} void AcrylicMaterial::captureBackground() {} void AcrylicMaterial::captureBackgroundDirect() {} +void AcrylicMaterial::captureLiveFramebuffer() {} void AcrylicMaterial::applyBlur(float) {} ImTextureID AcrylicMaterial::getBlurredTexture() const { return 0; } ImTextureID AcrylicMaterial::getNoiseTexture() const { return 0; } diff --git a/src/ui/effects/acrylic.h b/src/ui/effects/acrylic.h index b966d15..75933e7 100644 --- a/src/ui/effects/acrylic.h +++ b/src/ui/effects/acrylic.h @@ -166,6 +166,16 @@ public: */ void captureBackgroundDirect(); + /** + * @brief Capture the LIVE framebuffer (whatever has been drawn so far this + * frame), bypassing the dirtyFrames_ gate. Used by a full-window modal + * overlay to blur the live UI behind it: insert GetLiveCaptureCallback() + * at the start of the overlay window's draw list so it fires mid-pass, + * after the app UI is rasterized and before the overlay's own backdrop. + * Always forces a re-blur of the fresh capture. + */ + void captureLiveFramebuffer(); + /** * @brief Returns true once a valid background capture has been made. * diff --git a/src/ui/effects/imgui_acrylic.cpp b/src/ui/effects/imgui_acrylic.cpp index 1ab77e1..94715ba 100644 --- a/src/ui/effects/imgui_acrylic.cpp +++ b/src/ui/effects/imgui_acrylic.cpp @@ -236,6 +236,17 @@ ImDrawCallback GetBackgroundCaptureCallback() return BackgroundCaptureCallback; } +static void LiveCaptureCallback(const ImDrawList*, const ImDrawCmd*) +{ + if (!s_initialized) return; + getAcrylicMaterial().captureLiveFramebuffer(); +} + +ImDrawCallback GetLiveCaptureCallback() +{ + return LiveCaptureCallback; +} + // ============================================================================ // Drawing Functions // ============================================================================ @@ -657,6 +668,7 @@ void BeginFrame(int, int) {} void CaptureBackground() {} void InvalidateCapture() {} ImDrawCallback GetBackgroundCaptureCallback() { return nullptr; } +ImDrawCallback GetLiveCaptureCallback() { return nullptr; } void DrawAcrylicRect(ImDrawList* drawList, const ImVec2& pMin, const ImVec2& pMax, diff --git a/src/ui/effects/imgui_acrylic.h b/src/ui/effects/imgui_acrylic.h index 551bc39..936badb 100644 --- a/src/ui/effects/imgui_acrylic.h +++ b/src/ui/effects/imgui_acrylic.h @@ -89,6 +89,19 @@ void InvalidateCapture(); */ ImDrawCallback GetBackgroundCaptureCallback(); +/** + * @brief Returns a draw callback that captures the LIVE framebuffer (no dirtyFrames_ + * gate). Insert it at the START of a full-window modal overlay's draw list — before the + * overlay draws its own backdrop — so the acrylic blurs the live app UI behind the modal: + * @code + * ImDrawList* dl = ImGui::GetWindowDrawList(); + * dl->AddCallback(ImGuiAcrylic::GetLiveCaptureCallback(), nullptr); + * dl->AddCallback(ImDrawCallback_ResetRenderState, nullptr); + * // ... then draw the overlay's acrylic/dim backdrop ... + * @endcode + */ +ImDrawCallback GetLiveCaptureCallback(); + // ============================================================================ // Drawing Functions // ============================================================================ diff --git a/src/ui/material/draw_helpers.h b/src/ui/material/draw_helpers.h index a1709f5..4c94e5a 100644 --- a/src/ui/material/draw_helpers.h +++ b/src/ui/material/draw_helpers.h @@ -484,13 +484,14 @@ inline void DrawGlassPanel(ImDrawList* dl, const ImVec2& pMin, // mirroring DrawGlassPanel's gating. inline void DrawFullWindowBlurBackdrop(ImDrawList* dl, const ImVec2& pMin, const ImVec2& pMax) { - dl->AddRectFilled(pMin, pMax, IM_COL32(9, 10, 16, 255)); // opaque base — guarantees opacity + dl->AddRectFilled(pMin, pMax, IM_COL32(9, 10, 16, 255)); // opaque base (shows through if the blur is faint) if (IsBackdropActive() && !dragonx::ui::effects::isLowSpecMode() && effects::ImGuiAcrylic::IsEnabled() && effects::ImGuiAcrylic::IsAvailable()) { auto params = GetCurrentAcrylicTheme().card; params.blurRadius = 40.0f; // strong — spans the whole window + params.fallbackColor.w = 1.0f; // full-strength blur so the live content reads effects::ImGuiAcrylic::DrawAcrylicRect(dl, pMin, pMax, params, 0.0f); - dl->AddRectFilled(pMin, pMax, IM_COL32(6, 8, 18, 90)); // dim so the card reads as foreground + dl->AddRectFilled(pMin, pMax, IM_COL32(6, 8, 18, 70)); // slight dim so the card reads as foreground } } diff --git a/src/ui/windows/market_tab.cpp b/src/ui/windows/market_tab.cpp index c46cfaa..1e94fce 100644 --- a/src/ui/windows/market_tab.cpp +++ b/src/ui/windows/market_tab.cpp @@ -16,6 +16,8 @@ #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" #include "../../util/platform.h" @@ -410,6 +412,12 @@ static void RenderPortfolioEditor(App* app) ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse | ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoSavedSettings); ImDrawList* dl = ImGui::GetWindowDrawList(); + // Capture the live UI drawn below this overlay (then reset ImGui's render state) so the backdrop + // blurs the live tab content, not the frozen wallpaper. Must precede the backdrop draw. + if (ImDrawCallback liveCap = effects::ImGuiAcrylic::GetLiveCaptureCallback()) { + dl->AddCallback(liveCap, nullptr); + dl->AddCallback(ImDrawCallback_ResetRenderState, nullptr); + } material::DrawFullWindowBlurBackdrop(dl, vpPos, ImVec2(vpPos.x + vpSize.x, vpPos.y + vpSize.y)); ImGui::SetCursorScreenPos(vpPos); ImGui::InvisibleButton("##pfInputBlocker", vpSize, @@ -933,8 +941,10 @@ static void RenderPortfolioEditor(App* app) if (!popupOpen && !ImGui::IsWindowAppearing() && ImGui::IsMouseClicked(ImGuiMouseButton_Left) && !ImGui::IsMouseHoveringRect(cardMin, cardMax)) s_portfolio_editor_open = false; - // Commit named edits on any close path (X / Esc / outside-click); unnamed drafts are dropped. - if (!s_portfolio_editor_open) commitIfNeeded(); + // Commit named edits on any close path (Esc / outside-click / Close); unnamed drafts are dropped. + // Also invalidate the acrylic capture so the background (not the last live capture) is re-captured + // for the other glass panels once the overlay is gone. + if (!s_portfolio_editor_open) { commitIfNeeded(); effects::ImGuiAcrylic::InvalidateCapture(); } ImGui::End(); ImGui::PopStyleColor(); @@ -972,10 +982,11 @@ void RenderMarketTab(App* app) const auto& currentExchange = registry[s_exchange_idx]; if (s_pair_idx >= (int)currentExchange.pairs.size()) s_pair_idx = 0; - // When the Manage-portfolio modal is open it fully covers the tab, so skip rendering the tab - // body (and its foreground theme effects, which would otherwise bleed over the overlay) — the - // modal floats on the app's blurred backdrop instead of the sharp tab content. - if (s_portfolio_editor_open) { RenderPortfolioEditor(app); return; } + // 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); ImVec2 marketAvail = ImGui::GetContentRegionAvail(); // Scrollable (the portfolio grid can extend past the window) but with no visible scrollbar — @@ -1908,7 +1919,11 @@ void RenderMarketTab(App* app) } ImGui::EndChild(); // ##MarketScroll - // (When the editor is open the tab body is skipped above and the modal is rendered there.) + + // 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