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) <noreply@anthropic.com>
This commit is contained in:
@@ -355,6 +355,20 @@ void AcrylicMaterial::captureBackgroundDirect()
|
|||||||
blurCacheValid_ = false; // force re-blur with fresh capture
|
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
|
bool AcrylicMaterial::shouldSkipEffect() const
|
||||||
{
|
{
|
||||||
return !settings_.enabled ||
|
return !settings_.enabled ||
|
||||||
@@ -1238,6 +1252,26 @@ void AcrylicMaterial::captureBackgroundDirect()
|
|||||||
blurCacheValid_ = false;
|
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)
|
void AcrylicMaterial::applyBlur(float radius)
|
||||||
{
|
{
|
||||||
if (!dx_blurSRV_[0] || !dx_blurPS_ || !dx_context_) return;
|
if (!dx_blurSRV_[0] || !dx_blurPS_ || !dx_context_) return;
|
||||||
@@ -1666,6 +1700,7 @@ void AcrylicMaterial::shutdown() {}
|
|||||||
void AcrylicMaterial::resize(int, int) {}
|
void AcrylicMaterial::resize(int, int) {}
|
||||||
void AcrylicMaterial::captureBackground() {}
|
void AcrylicMaterial::captureBackground() {}
|
||||||
void AcrylicMaterial::captureBackgroundDirect() {}
|
void AcrylicMaterial::captureBackgroundDirect() {}
|
||||||
|
void AcrylicMaterial::captureLiveFramebuffer() {}
|
||||||
void AcrylicMaterial::applyBlur(float) {}
|
void AcrylicMaterial::applyBlur(float) {}
|
||||||
ImTextureID AcrylicMaterial::getBlurredTexture() const { return 0; }
|
ImTextureID AcrylicMaterial::getBlurredTexture() const { return 0; }
|
||||||
ImTextureID AcrylicMaterial::getNoiseTexture() const { return 0; }
|
ImTextureID AcrylicMaterial::getNoiseTexture() const { return 0; }
|
||||||
|
|||||||
@@ -166,6 +166,16 @@ public:
|
|||||||
*/
|
*/
|
||||||
void captureBackgroundDirect();
|
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.
|
* @brief Returns true once a valid background capture has been made.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -236,6 +236,17 @@ ImDrawCallback GetBackgroundCaptureCallback()
|
|||||||
return BackgroundCaptureCallback;
|
return BackgroundCaptureCallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void LiveCaptureCallback(const ImDrawList*, const ImDrawCmd*)
|
||||||
|
{
|
||||||
|
if (!s_initialized) return;
|
||||||
|
getAcrylicMaterial().captureLiveFramebuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImDrawCallback GetLiveCaptureCallback()
|
||||||
|
{
|
||||||
|
return LiveCaptureCallback;
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Drawing Functions
|
// Drawing Functions
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -657,6 +668,7 @@ void BeginFrame(int, int) {}
|
|||||||
void CaptureBackground() {}
|
void CaptureBackground() {}
|
||||||
void InvalidateCapture() {}
|
void InvalidateCapture() {}
|
||||||
ImDrawCallback GetBackgroundCaptureCallback() { return nullptr; }
|
ImDrawCallback GetBackgroundCaptureCallback() { return nullptr; }
|
||||||
|
ImDrawCallback GetLiveCaptureCallback() { return nullptr; }
|
||||||
|
|
||||||
void DrawAcrylicRect(ImDrawList* drawList,
|
void DrawAcrylicRect(ImDrawList* drawList,
|
||||||
const ImVec2& pMin, const ImVec2& pMax,
|
const ImVec2& pMin, const ImVec2& pMax,
|
||||||
|
|||||||
@@ -89,6 +89,19 @@ void InvalidateCapture();
|
|||||||
*/
|
*/
|
||||||
ImDrawCallback GetBackgroundCaptureCallback();
|
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
|
// Drawing Functions
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
@@ -484,13 +484,14 @@ inline void DrawGlassPanel(ImDrawList* dl, const ImVec2& pMin,
|
|||||||
// mirroring DrawGlassPanel's gating.
|
// mirroring DrawGlassPanel's gating.
|
||||||
inline void DrawFullWindowBlurBackdrop(ImDrawList* dl, const ImVec2& pMin, const ImVec2& pMax)
|
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()
|
if (IsBackdropActive() && !dragonx::ui::effects::isLowSpecMode()
|
||||||
&& effects::ImGuiAcrylic::IsEnabled() && effects::ImGuiAcrylic::IsAvailable()) {
|
&& effects::ImGuiAcrylic::IsEnabled() && effects::ImGuiAcrylic::IsAvailable()) {
|
||||||
auto params = GetCurrentAcrylicTheme().card;
|
auto params = GetCurrentAcrylicTheme().card;
|
||||||
params.blurRadius = 40.0f; // strong — spans the whole window
|
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);
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,8 @@
|
|||||||
#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 "../../util/text_format.h"
|
#include "../../util/text_format.h"
|
||||||
#include "../../embedded/IconsMaterialDesign.h"
|
#include "../../embedded/IconsMaterialDesign.h"
|
||||||
#include "../../util/platform.h"
|
#include "../../util/platform.h"
|
||||||
@@ -410,6 +412,12 @@ static void RenderPortfolioEditor(App* app)
|
|||||||
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse |
|
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse |
|
||||||
ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoSavedSettings);
|
ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoSavedSettings);
|
||||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
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));
|
material::DrawFullWindowBlurBackdrop(dl, vpPos, ImVec2(vpPos.x + vpSize.x, vpPos.y + vpSize.y));
|
||||||
ImGui::SetCursorScreenPos(vpPos);
|
ImGui::SetCursorScreenPos(vpPos);
|
||||||
ImGui::InvisibleButton("##pfInputBlocker", vpSize,
|
ImGui::InvisibleButton("##pfInputBlocker", vpSize,
|
||||||
@@ -933,8 +941,10 @@ static void RenderPortfolioEditor(App* app)
|
|||||||
if (!popupOpen && !ImGui::IsWindowAppearing() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)
|
if (!popupOpen && !ImGui::IsWindowAppearing() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)
|
||||||
&& !ImGui::IsMouseHoveringRect(cardMin, cardMax))
|
&& !ImGui::IsMouseHoveringRect(cardMin, cardMax))
|
||||||
s_portfolio_editor_open = false;
|
s_portfolio_editor_open = false;
|
||||||
// Commit named edits on any close path (X / Esc / outside-click); unnamed drafts are dropped.
|
// Commit named edits on any close path (Esc / outside-click / Close); unnamed drafts are dropped.
|
||||||
if (!s_portfolio_editor_open) commitIfNeeded();
|
// 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::End();
|
||||||
ImGui::PopStyleColor();
|
ImGui::PopStyleColor();
|
||||||
@@ -972,10 +982,11 @@ 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;
|
||||||
|
|
||||||
// When the Manage-portfolio modal is open it fully covers the tab, so skip rendering the tab
|
// While the modal is open the overlay captures + blurs the live tab content behind it. Suppress
|
||||||
// body (and its foreground theme effects, which would otherwise bleed over the overlay) — the
|
// panel theme-effects for this render so their foreground-draw-list borders don't bleed over the
|
||||||
// modal floats on the app's blurred backdrop instead of the sharp tab content.
|
// overlay (foreground draws above all windows). Restored after the modal is rendered.
|
||||||
if (s_portfolio_editor_open) { RenderPortfolioEditor(app); return; }
|
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 —
|
||||||
@@ -1908,7 +1919,11 @@ void RenderMarketTab(App* app)
|
|||||||
}
|
}
|
||||||
|
|
||||||
ImGui::EndChild(); // ##MarketScroll
|
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
|
} // namespace ui
|
||||||
|
|||||||
Reference in New Issue
Block a user