Files
ObsidianDragon/src/ui/effects/imgui_acrylic.cpp
T
DanSandClaude Opus 4.8 968d6d3875 fix(effects): capture the acrylic backdrop at framebuffer size on HiDPI
The acrylic effect blits its backdrop from the default framebuffer using
the logical viewport size as the source rectangle. On a HiDPI display
the framebuffer is larger than the logical size, so the blur sampled
only the lower-left corner of the screen and the glass showed the wrong
region stretched across each panel.

- The capture blit source now uses the real framebuffer pixel size
  (logical size times ImGui's DisplayFramebufferScale, truncated the
  same way the GL backend does), while the capture and blur targets stay
  at logical size. The framebuffer size is tracked separately from the
  viewport size so quality or buffer-lifetime resets cannot lose it, and
  moving the window between a Retina and a non-Retina display redoes the
  capture. DX11 is unaffected; its scale is one.
- The debug screenshot sweep captures at framebuffer size too, so HiDPI
  captures are no longer cropped.

Tests: the framebuffer-extent helper matches the GL backend's
truncation and falls back to the logical size when the scale is unknown.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-13 13:30:20 -05:00

813 lines
23 KiB
C++

// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#include "imgui_acrylic.h"
#include "../schema/ui_schema.h"
#include "../../util/noise_texture.h"
#ifdef DRAGONX_HAS_GLAD
#include <glad/gl.h>
#endif
#include <cstdio>
#include <cmath>
#include "../../util/logger.h"
// ============================================================================
// Frosted scrim — shared between GLAD and non-GLAD builds.
// Drawn on the popup's draw list as the first command so it appears behind
// the popup content but over the rest of the UI. Replaces ImGui's default
// solid-colour ModalWindowDimBg with a translucent frosted overlay + noise.
// ============================================================================
namespace dragonx { namespace ui { namespace effects { namespace ImGuiAcrylic {
static void DrawFrostedScrim(ImDrawList* dl)
{
const auto& S = ui::schema::UI();
auto sde = [&](const char* key, float fb) -> float {
float v = S.drawElement("components.modal-scrim", key).size;
return v >= 0.0f ? v : fb;
};
ImGuiViewport* vp = ImGui::GetMainViewport();
ImVec2 vpMin = vp->Pos;
ImVec2 vpMax(vp->Pos.x + vp->Size.x, vp->Pos.y + vp->Size.y);
// Semi-transparent white fill (the "frost")
int fillAlpha = (int)sde("fill-alpha", 12.0f);
dl->AddRectFilled(vpMin, vpMax, IM_COL32(255, 255, 255, fillAlpha));
// Noise grain overlay — single draw call via pre-tiled texture
{
int noiseAlpha = (int)sde("noise-alpha", 14.0f);
ImU32 noiseTint = IM_COL32(255, 255, 255, noiseAlpha);
dragonx::util::DrawTiledNoiseRect(dl, vpMin, vpMax, noiseTint);
}
}
} } } } // namespace dragonx::ui::effects::ImGuiAcrylic
// ============================================================================
// Preset table — shared by all backends
// ============================================================================
namespace dragonx { namespace ui { namespace effects { namespace ImGuiAcrylic {
struct PresetDef {
const char* label;
AcrylicQuality quality;
float blur;
};
static const PresetDef kPresets[kPresetCount] = {
{ "Off", AcrylicQuality::Off, 0.0f },
{ "Subtle", AcrylicQuality::Low, 0.6f },
{ "Light", AcrylicQuality::Medium, 1.0f },
{ "Standard", AcrylicQuality::Medium, 1.5f },
{ "Strong", AcrylicQuality::High, 2.5f },
{ "Frosted", AcrylicQuality::High, 4.0f },
};
const char* GetPresetLabel(int preset)
{
if (preset < 0 || preset >= kPresetCount) return "?";
return kPresets[preset].label;
}
AcrylicQuality PresetQuality(int preset)
{
if (preset < 0 || preset >= kPresetCount) return AcrylicQuality::Medium;
return kPresets[preset].quality;
}
float PresetBlur(int preset)
{
if (preset < 0 || preset >= kPresetCount) return 1.0f;
return kPresets[preset].blur;
}
// Find closest preset matching given quality + blur
int MatchPreset(AcrylicQuality q, float blur)
{
// Exact match first
for (int i = 0; i < kPresetCount; ++i) {
if (kPresets[i].quality == q &&
std::abs(kPresets[i].blur - blur) < 0.01f)
return i;
}
// Closest blur within same quality
int best = 3; // Standard fallback
float bestDist = 999.f;
for (int i = 0; i < kPresetCount; ++i) {
if (kPresets[i].quality == q) {
float d = std::abs(kPresets[i].blur - blur);
if (d < bestDist) { bestDist = d; best = i; }
}
}
return best;
}
} } } } // namespace dragonx::ui::effects::ImGuiAcrylic
// Single ImGuiAcrylic frontend for BOTH GL (GLAD) and DX11: the frontend makes no
// direct GL/DX calls — every backend op is delegated to AcrylicMaterial, which has a
// backend for each API — so it must not fork per-backend.
#if defined(DRAGONX_HAS_GLAD) || defined(DRAGONX_USE_DX11)
namespace dragonx {
namespace ui {
namespace effects {
namespace ImGuiAcrylic {
// ============================================================================
// Static State
// ============================================================================
static bool s_initialized = false;
static int s_viewportWidth = 0;
static int s_viewportHeight = 0;
static int s_fbWidth = 0;
static int s_fbHeight = 0;
static bool s_backgroundCaptured = false;
// Track window state for acrylic rendering
struct AcrylicWindowState {
ImVec2 windowPos;
ImVec2 windowSize;
AcrylicParams params;
bool active;
};
static AcrylicWindowState s_currentWindow = {};
// ============================================================================
// Initialization
// ============================================================================
bool Init()
{
if (s_initialized) {
return true;
}
AcrylicMaterial& acrylic = getAcrylicMaterial();
if (!acrylic.init()) {
DEBUG_LOGF("ImGuiAcrylic: Failed to initialize acrylic material\n");
return false;
}
s_initialized = true;
DEBUG_LOGF("ImGuiAcrylic: Initialized successfully\n");
return true;
}
void Shutdown()
{
if (!s_initialized) {
return;
}
getAcrylicMaterial().shutdown();
s_initialized = false;
DEBUG_LOGF("ImGuiAcrylic: Shut down\n");
}
bool IsAvailable()
{
return s_initialized && getAcrylicMaterial().isInitialized();
}
// ============================================================================
// Frame Operations
// ============================================================================
void BeginFrame(int width, int height)
{
BeginFrame(width, height, width, height);
}
void BeginFrame(int width, int height, int fbWidth, int fbHeight)
{
if (!s_initialized) {
return;
}
// Moving the window between a Retina and a non-Retina display changes the framebuffer size while the
// logical size stays put: the capture must be redone against the new pixel grid.
if (fbWidth != s_fbWidth || fbHeight != s_fbHeight) {
s_fbWidth = fbWidth;
s_fbHeight = fbHeight;
getAcrylicMaterial().setFramebufferSize(fbWidth, fbHeight);
}
// Update viewport dimensions if changed
if (width != s_viewportWidth || height != s_viewportHeight) {
s_viewportWidth = width;
s_viewportHeight = height;
getAcrylicMaterial().resize(width, height);
}
// Capture the first few frames to ensure a clean initial blur,
// then stop. The background (gradient/image/noise) is static;
// re-capturing + re-blurring every frame causes a subtle flicker
// from GPU floating-point non-determinism in the blur shader.
// Resize / theme change already sets dirtyFrames_ via markBackgroundDirty().
static int s_warmupFrames = 3;
if (s_warmupFrames > 0) {
getAcrylicMaterial().markBackgroundDirty();
--s_warmupFrames;
}
s_backgroundCaptured = false;
}
void CaptureBackground()
{
if (!s_initialized || s_backgroundCaptured) {
return;
}
getAcrylicMaterial().captureBackground();
s_backgroundCaptured = true;
}
void InvalidateCapture()
{
if (!s_initialized) return;
getAcrylicMaterial().markBackgroundDirty();
}
void ReleaseIdleBuffers()
{
if (s_initialized) getAcrylicMaterial().syncBufferLifetime();
}
// Draw callback: captures framebuffer 0 (which at this point during
// RenderDrawData() contains only the rasterized BackgroundDrawList —
// no UI windows yet) into the acrylic capture buffer.
static void BackgroundCaptureCallback(const ImDrawList*, const ImDrawCmd*)
{
if (!s_initialized) return;
getAcrylicMaterial().captureBackgroundDirect();
s_backgroundCaptured = true;
}
ImDrawCallback GetBackgroundCaptureCallback()
{
return BackgroundCaptureCallback;
}
static void LiveCaptureCallback(const ImDrawList*, const ImDrawCmd*)
{
if (!s_initialized) return;
getAcrylicMaterial().captureLiveFramebuffer();
}
ImDrawCallback GetLiveCaptureCallback()
{
return LiveCaptureCallback;
}
// ============================================================================
// Drawing Functions
// ============================================================================
void DrawAcrylicRect(ImDrawList* drawList,
const ImVec2& pMin, const ImVec2& pMax,
const AcrylicParams& params,
float rounding)
{
if (!drawList) {
return;
}
// If acrylic not available or disabled, draw fallback
if (!IsAvailable() || !IsEnabled() || !params.enabled) {
drawList->AddRectFilled(pMin, pMax,
ImGui::ColorConvertFloat4ToU32(params.fallbackColor), rounding);
return;
}
// Background capture is now handled by a draw callback inserted
// into the BackgroundDrawList (see GetBackgroundCaptureCallback()).
// On the very first frame the callback hasn't fired yet, so
// drawRect() will fall back to a tinted fill automatically.
// Draw using acrylic material
getAcrylicMaterial().drawRect(drawList, pMin, pMax, params, rounding);
}
void DrawAcrylicRect(ImDrawList* drawList,
const ImVec2& pMin, const ImVec2& pMax,
float rounding)
{
DrawAcrylicRect(drawList, pMin, pMax, AcrylicMaterial::getDarkPreset(), rounding);
}
// ============================================================================
// Window Wrappers
// ============================================================================
bool BeginAcrylicWindow(const char* name, bool* p_open,
ImGuiWindowFlags flags,
const AcrylicParams& params)
{
// Store params for use in EndAcrylicWindow
s_currentWindow.params = params;
s_currentWindow.active = true;
// Ensure dialog windows stay above background windows (MainContent, StatusBar, MenuBar)
if (!(flags & ImGuiWindowFlags_NoBringToFrontOnFocus))
ImGui::SetNextWindowFocus();
// Make window background transparent so acrylic shows through
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0, 0, 0, 0));
bool result = ImGui::Begin(name, p_open, flags);
if (result) {
// Get window bounds for acrylic rendering
s_currentWindow.windowPos = ImGui::GetWindowPos();
s_currentWindow.windowSize = ImGui::GetWindowSize();
// Draw acrylic background
ImDrawList* drawList = ImGui::GetWindowDrawList();
ImVec2 pMin = s_currentWindow.windowPos;
ImVec2 pMax = ImVec2(pMin.x + s_currentWindow.windowSize.x,
pMin.y + s_currentWindow.windowSize.y);
// Get window rounding from style
float rounding = ImGui::GetStyle().WindowRounding;
DrawAcrylicRect(drawList, pMin, pMax, params, rounding);
}
return result;
}
void EndAcrylicWindow()
{
ImGui::End();
ImGui::PopStyleColor(); // WindowBg
s_currentWindow.active = false;
}
// ============================================================================
// Child Region Wrappers
// ============================================================================
bool BeginAcrylicChild(const char* str_id,
const ImVec2& size,
const AcrylicParams& params,
ImGuiChildFlags child_flags,
ImGuiWindowFlags window_flags)
{
// Make child background transparent
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0, 0, 0, 0));
bool result = ImGui::BeginChild(str_id, size, child_flags, window_flags);
if (result) {
// Get child bounds
ImVec2 childPos = ImGui::GetWindowPos();
ImVec2 childSize = ImGui::GetWindowSize();
// Draw acrylic background
ImDrawList* drawList = ImGui::GetWindowDrawList();
ImVec2 pMin = childPos;
ImVec2 pMax = ImVec2(pMin.x + childSize.x, pMin.y + childSize.y);
float rounding = ImGui::GetStyle().ChildRounding;
DrawAcrylicRect(drawList, pMin, pMax, params, rounding);
}
return result;
}
void EndAcrylicChild()
{
ImGui::EndChild();
ImGui::PopStyleColor(); // ChildBg
}
// ============================================================================
// Popup Wrappers
// ============================================================================
bool BeginAcrylicPopup(const char* str_id,
ImGuiWindowFlags flags,
const AcrylicParams& params)
{
ImGui::PushStyleColor(ImGuiCol_PopupBg, ImVec4(0, 0, 0, 0));
bool result = ImGui::BeginPopup(str_id, flags);
if (result) {
ImVec2 popupPos = ImGui::GetWindowPos();
ImVec2 popupSize = ImGui::GetWindowSize();
ImDrawList* drawList = ImGui::GetWindowDrawList();
ImVec2 pMin = popupPos;
ImVec2 pMax = ImVec2(pMin.x + popupSize.x, pMin.y + popupSize.y);
float rounding = ImGui::GetStyle().PopupRounding;
DrawAcrylicRect(drawList, pMin, pMax, params, rounding);
} else {
ImGui::PopStyleColor(); // Only pop if not opened
}
return result;
}
void EndAcrylicPopup()
{
ImGui::EndPopup();
ImGui::PopStyleColor(); // PopupBg
}
bool BeginAcrylicPopupModal(const char* name, bool* p_open,
ImGuiWindowFlags flags,
const AcrylicParams& params)
{
// Suppress ImGui's built-in dark scrim — we draw our own frosted one
ImGui::PushStyleColor(ImGuiCol_ModalWindowDimBg, ImVec4(0, 0, 0, 0));
ImGui::PushStyleColor(ImGuiCol_PopupBg, ImVec4(0, 0, 0, 0));
bool result = ImGui::BeginPopupModal(name, p_open, flags);
ImGui::PopStyleColor(); // ModalWindowDimBg (only needed during Begin)
if (result) {
// Draw frosted scrim behind the popup on its own draw list
ImDrawList* drawList = ImGui::GetWindowDrawList();
DrawFrostedScrim(drawList);
ImVec2 popupPos = ImGui::GetWindowPos();
ImVec2 popupSize = ImGui::GetWindowSize();
ImVec2 pMin = popupPos;
ImVec2 pMax = ImVec2(pMin.x + popupSize.x, pMin.y + popupSize.y);
float rounding = ImGui::GetStyle().PopupRounding;
DrawAcrylicRect(drawList, pMin, pMax, params, rounding);
} else {
ImGui::PopStyleColor();
}
return result;
}
// ============================================================================
// Context Menu Helpers
// ============================================================================
bool BeginAcrylicContextItem(const char* str_id,
ImGuiPopupFlags popup_flags,
const AcrylicParams& params)
{
ImGui::PushStyleColor(ImGuiCol_PopupBg, ImVec4(0, 0, 0, 0));
bool result = ImGui::BeginPopupContextItem(str_id, popup_flags);
if (result) {
ImVec2 popupPos = ImGui::GetWindowPos();
ImVec2 popupSize = ImGui::GetWindowSize();
ImDrawList* drawList = ImGui::GetWindowDrawList();
ImVec2 pMin = popupPos;
ImVec2 pMax = ImVec2(pMin.x + popupSize.x, pMin.y + popupSize.y);
float rounding = ImGui::GetStyle().PopupRounding;
DrawAcrylicRect(drawList, pMin, pMax, params, rounding);
} else {
ImGui::PopStyleColor();
}
return result;
}
bool BeginAcrylicContextWindow(const char* str_id,
ImGuiPopupFlags popup_flags,
const AcrylicParams& params)
{
ImGui::PushStyleColor(ImGuiCol_PopupBg, ImVec4(0, 0, 0, 0));
bool result = ImGui::BeginPopupContextWindow(str_id, popup_flags);
if (result) {
ImVec2 popupPos = ImGui::GetWindowPos();
ImVec2 popupSize = ImGui::GetWindowSize();
ImDrawList* drawList = ImGui::GetWindowDrawList();
ImVec2 pMin = popupPos;
ImVec2 pMax = ImVec2(pMin.x + popupSize.x, pMin.y + popupSize.y);
float rounding = ImGui::GetStyle().PopupRounding;
DrawAcrylicRect(drawList, pMin, pMax, params, rounding);
} else {
ImGui::PopStyleColor();
}
return result;
}
// ============================================================================
// Sidebar Helper
// ============================================================================
void DrawSidebarBackground(float width, const AcrylicParams& params)
{
ImGuiViewport* viewport = ImGui::GetMainViewport();
// Sidebar starts below menu bar
float menuBarHeight = ImGui::GetFrameHeight();
ImVec2 pMin = ImVec2(viewport->WorkPos.x, viewport->WorkPos.y + menuBarHeight);
ImVec2 pMax = ImVec2(pMin.x + width, viewport->WorkPos.y + viewport->WorkSize.y);
// Draw to background draw list
ImDrawList* drawList = ImGui::GetBackgroundDrawList();
DrawAcrylicRect(drawList, pMin, pMax, params, 0.0f);
}
// ============================================================================
// Settings
// ============================================================================
void SetQuality(AcrylicQuality quality)
{
if (s_initialized) {
getAcrylicMaterial().setQuality(quality);
}
}
AcrylicQuality GetQuality()
{
if (s_initialized) {
return getAcrylicMaterial().getQuality();
}
return AcrylicQuality::Off;
}
void SetEnabled(bool enabled)
{
if (s_initialized) {
getAcrylicMaterial().setEnabled(enabled);
}
}
bool IsEnabled()
{
if (s_initialized) {
return getAcrylicMaterial().isEnabled();
}
return false;
}
void SetBlurMultiplier(float multiplier)
{
if (s_initialized) {
getAcrylicMaterial().setBlurMultiplier(multiplier);
}
}
float GetBlurMultiplier()
{
if (s_initialized) {
return getAcrylicMaterial().getBlurMultiplier();
}
return 1.0f;
}
void SetReducedTransparency(bool reduced)
{
if (s_initialized) {
getAcrylicMaterial().setReducedTransparency(reduced);
}
}
bool GetReducedTransparency()
{
if (s_initialized) {
return getAcrylicMaterial().getReducedTransparency();
}
return false;
}
void SetUIOpacity(float opacity)
{
if (s_initialized) {
getAcrylicMaterial().setUIOpacity(opacity);
}
}
float GetUIOpacity()
{
if (s_initialized) {
return getAcrylicMaterial().getUIOpacity();
}
return 1.0f;
}
void SetNoiseOpacity(float multiplier)
{
if (s_initialized) {
getAcrylicMaterial().setNoiseOpacityMultiplier(multiplier);
}
}
float GetNoiseOpacity()
{
if (s_initialized) {
return getAcrylicMaterial().getNoiseOpacityMultiplier();
}
return 1.0f;
}
const AcrylicSettings& GetSettings()
{
return getAcrylicMaterial().getSettings();
}
void SetSettings(const AcrylicSettings& settings)
{
if (s_initialized) {
getAcrylicMaterial().setSettings(settings);
}
}
void SetPreset(int preset)
{
if (preset < 0 || preset >= kPresetCount) preset = 3;
SetEnabled(preset != 0);
SetQuality(PresetQuality(preset));
SetBlurMultiplier(PresetBlur(preset));
}
int GetPreset()
{
return MatchPreset(GetQuality(), GetBlurMultiplier());
}
void ApplyBlurAmount(float blur)
{
if (blur < 0.001f) {
SetEnabled(false);
SetQuality(AcrylicQuality::Off);
SetBlurMultiplier(0.0f);
} else {
SetEnabled(true);
SetQuality(AcrylicQuality::Low);
SetBlurMultiplier(blur);
}
}
} // namespace ImGuiAcrylic
} // namespace effects
} // namespace ui
} // namespace dragonx
#else // neither GLAD nor DX11
namespace dragonx {
namespace ui {
namespace effects {
namespace ImGuiAcrylic {
bool Init() { return false; }
void Shutdown() {}
bool IsAvailable() { return false; }
void BeginFrame(int, int) {}
void BeginFrame(int, int, int, int) {}
void CaptureBackground() {}
void InvalidateCapture() {}
void ReleaseIdleBuffers() {}
ImDrawCallback GetBackgroundCaptureCallback() { return nullptr; }
ImDrawCallback GetLiveCaptureCallback() { return nullptr; }
void DrawAcrylicRect(ImDrawList* drawList,
const ImVec2& pMin, const ImVec2& pMax,
const AcrylicParams& params,
float rounding)
{
if (drawList) {
drawList->AddRectFilled(pMin, pMax,
ImGui::ColorConvertFloat4ToU32(params.fallbackColor), rounding);
}
}
void DrawAcrylicRect(ImDrawList* drawList,
const ImVec2& pMin, const ImVec2& pMax,
float rounding)
{
if (drawList) {
AcrylicParams params;
drawList->AddRectFilled(pMin, pMax,
ImGui::ColorConvertFloat4ToU32(params.fallbackColor), rounding);
}
}
bool BeginAcrylicWindow(const char* name, bool* p_open,
ImGuiWindowFlags flags,
const AcrylicParams&)
{
if (!(flags & ImGuiWindowFlags_NoBringToFrontOnFocus))
ImGui::SetNextWindowFocus();
return ImGui::Begin(name, p_open, flags);
}
void EndAcrylicWindow()
{
ImGui::End();
}
bool BeginAcrylicChild(const char* str_id,
const ImVec2& size,
const AcrylicParams&,
ImGuiChildFlags child_flags,
ImGuiWindowFlags window_flags)
{
return ImGui::BeginChild(str_id, size, child_flags, window_flags);
}
void EndAcrylicChild()
{
ImGui::EndChild();
}
bool BeginAcrylicPopup(const char* str_id,
ImGuiWindowFlags flags,
const AcrylicParams&)
{
return ImGui::BeginPopup(str_id, flags);
}
void EndAcrylicPopup()
{
ImGui::EndPopup();
}
bool BeginAcrylicPopupModal(const char* name, bool* p_open,
ImGuiWindowFlags flags,
const AcrylicParams&)
{
ImGui::PushStyleColor(ImGuiCol_ModalWindowDimBg, ImVec4(0, 0, 0, 0));
bool result = ImGui::BeginPopupModal(name, p_open, flags);
ImGui::PopStyleColor();
if (result) {
DrawFrostedScrim(ImGui::GetWindowDrawList());
}
return result;
}
bool BeginAcrylicContextItem(const char* str_id,
ImGuiPopupFlags popup_flags,
const AcrylicParams&)
{
return ImGui::BeginPopupContextItem(str_id, popup_flags);
}
bool BeginAcrylicContextWindow(const char* str_id,
ImGuiPopupFlags popup_flags,
const AcrylicParams&)
{
return ImGui::BeginPopupContextWindow(str_id, popup_flags);
}
void DrawSidebarBackground(float, const AcrylicParams&) {}
void SetQuality(AcrylicQuality) {}
AcrylicQuality GetQuality() { return AcrylicQuality::Off; }
void SetEnabled(bool) {}
bool IsEnabled() { return false; }
void SetBlurMultiplier(float) {}
float GetBlurMultiplier() { return 1.0f; }
void SetReducedTransparency(bool) {}
bool GetReducedTransparency() { return false; }
void SetUIOpacity(float) {}
float GetUIOpacity() { return 1.0f; }
void SetNoiseOpacity(float) {}
float GetNoiseOpacity() { return 1.0f; }
static AcrylicSettings s_stubSettings;
const AcrylicSettings& GetSettings() { return s_stubSettings; }
void SetSettings(const AcrylicSettings&) {}
void SetPreset(int) {}
int GetPreset() { return 0; }
void ApplyBlurAmount(float) {}
} // namespace ImGuiAcrylic
} // namespace effects
} // namespace ui
} // namespace dragonx
#endif // DRAGONX_HAS_GLAD || DRAGONX_USE_DX11