// DragonX Wallet - ImGui Edition // Copyright 2024-2026 The Hush Developers // Released under the GPLv3 // // Offscreen render-target scroll fade — the ImGui equivalent of CSS mask-image. // Renders scrollable content to an offscreen surface, then composites it back // as a textured mesh strip with vertex alpha for edge fading. // This produces a true per-pixel fade that works with any background // (including acrylic/backdrop transparency). // // Supports both OpenGL (DRAGONX_HAS_GLAD) and DX11 (DRAGONX_USE_DX11). #pragma once #include "imgui.h" #include "imgui_internal.h" #include // ============================================================================ // Platform detection // ============================================================================ #if defined(DRAGONX_USE_DX11) #include #define SCROLL_FADE_HAS_OFFSCREEN 1 #define SCROLL_FADE_DX11 1 #elif defined(DRAGONX_HAS_GLAD) #include #include "../../util/logger.h" #ifndef GL_FRAMEBUFFER_BINDING #define GL_FRAMEBUFFER_BINDING 0x8CA6 #endif #ifndef GL_VIEWPORT #define GL_VIEWPORT 0x0BA2 #endif #ifndef GL_SCISSOR_TEST #define GL_SCISSOR_TEST 0x0C11 #endif #define SCROLL_FADE_HAS_OFFSCREEN 1 #define SCROLL_FADE_GL 1 #endif #ifdef SCROLL_FADE_HAS_OFFSCREEN namespace dragonx { namespace ui { namespace effects { // ============================================================================ // ScrollFadeRT — manages an offscreen render target for scroll-fade rendering // ============================================================================ class ScrollFadeRT { public: ScrollFadeRT() = default; ~ScrollFadeRT() { destroy(); } // Non-copyable ScrollFadeRT(const ScrollFadeRT&) = delete; ScrollFadeRT& operator=(const ScrollFadeRT&) = delete; /// Ensure RT matches the required dimensions. Returns true if ready. bool ensure(int w, int h) { if (w <= 0 || h <= 0) return false; if (isValid() && w == width_ && h == height_) return true; return init(w, h); } void destroy(); bool isValid() const; /// Get the texture as an ImTextureID for compositing. ImTextureID textureID() const; int width() const { return width_; } int height() const { return height_; } #ifdef SCROLL_FADE_DX11 ID3D11RenderTargetView* rtv() const { return rtv_; } #endif #ifdef SCROLL_FADE_GL unsigned int fbo() const { return fbo_; } #endif private: bool init(int w, int h); int width_ = 0; int height_ = 0; #ifdef SCROLL_FADE_DX11 ID3D11Texture2D* tex_ = nullptr; ID3D11RenderTargetView* rtv_ = nullptr; ID3D11ShaderResourceView* srv_ = nullptr; #endif #ifdef SCROLL_FADE_GL unsigned int fbo_ = 0; unsigned int colorTex_ = 0; #endif }; // ============================================================================ // Implementations // ============================================================================ #ifdef SCROLL_FADE_DX11 // --- DX11 helpers to get device/context from ImGui backend --- inline ID3D11Device* GetDX11Device() { ImGuiIO& io = ImGui::GetIO(); if (!io.BackendRendererUserData) return nullptr; return *reinterpret_cast(io.BackendRendererUserData); } inline ID3D11DeviceContext* GetDX11Context() { ID3D11Device* dev = GetDX11Device(); if (!dev) return nullptr; ID3D11DeviceContext* ctx = nullptr; dev->GetImmediateContext(&ctx); return ctx; // caller must Release() } inline bool ScrollFadeRT::init(int w, int h) { destroy(); ID3D11Device* dev = GetDX11Device(); if (!dev) return false; width_ = w; height_ = h; // Create texture D3D11_TEXTURE2D_DESC td = {}; td.Width = (UINT)w; td.Height = (UINT)h; td.MipLevels = 1; td.ArraySize = 1; td.Format = DXGI_FORMAT_R8G8B8A8_UNORM; td.SampleDesc.Count = 1; td.Usage = D3D11_USAGE_DEFAULT; td.BindFlags = D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE; if (FAILED(dev->CreateTexture2D(&td, nullptr, &tex_))) { DEBUG_LOGF("ScrollFadeRT: CreateTexture2D failed\n"); destroy(); return false; } // Render target view if (FAILED(dev->CreateRenderTargetView(tex_, nullptr, &rtv_))) { DEBUG_LOGF("ScrollFadeRT: CreateRenderTargetView failed\n"); destroy(); return false; } // Shader resource view (for sampling as texture) if (FAILED(dev->CreateShaderResourceView(tex_, nullptr, &srv_))) { DEBUG_LOGF("ScrollFadeRT: CreateShaderResourceView failed\n"); destroy(); return false; } return true; } inline void ScrollFadeRT::destroy() { if (srv_) { srv_->Release(); srv_ = nullptr; } if (rtv_) { rtv_->Release(); rtv_ = nullptr; } if (tex_) { tex_->Release(); tex_ = nullptr; } width_ = height_ = 0; } inline bool ScrollFadeRT::isValid() const { return rtv_ != nullptr; } inline ImTextureID ScrollFadeRT::textureID() const { return (ImTextureID)srv_; } #endif // SCROLL_FADE_DX11 #ifdef SCROLL_FADE_GL inline bool ScrollFadeRT::init(int w, int h) { destroy(); width_ = w; height_ = h; glGenFramebuffers(1, &fbo_); glBindFramebuffer(GL_FRAMEBUFFER, fbo_); glGenTextures(1, &colorTex_); glBindTexture(GL_TEXTURE_2D, colorTex_); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, colorTex_, 0); GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER); glBindFramebuffer(GL_FRAMEBUFFER, 0); glBindTexture(GL_TEXTURE_2D, 0); if (status != GL_FRAMEBUFFER_COMPLETE) { DEBUG_LOGF("ScrollFadeRT: FBO incomplete (0x%X)\n", status); destroy(); return false; } return true; } inline void ScrollFadeRT::destroy() { if (colorTex_) { glDeleteTextures(1, &colorTex_); colorTex_ = 0; } if (fbo_) { glDeleteFramebuffers(1, &fbo_); fbo_ = 0; } width_ = height_ = 0; } inline bool ScrollFadeRT::isValid() const { return fbo_ != 0; } inline ImTextureID ScrollFadeRT::textureID() const { return (ImTextureID)(intptr_t)colorTex_; } #endif // SCROLL_FADE_GL // ============================================================================ // Callback state — singleton storage for bind/unbind data // ============================================================================ struct ScrollFadeState { #ifdef SCROLL_FADE_DX11 ID3D11RenderTargetView* offscreenRTV = nullptr; ID3D11RenderTargetView* savedRTV = nullptr; ID3D11DepthStencilView* savedDSV = nullptr; D3D11_VIEWPORT savedVP = {}; #endif #ifdef SCROLL_FADE_GL unsigned int fbo = 0; int savedFBO = 0; int savedVP[4] = {}; bool savedScissorEnabled = true; // ImGui always has scissor enabled #endif int vpW = 0, vpH = 0; // framebuffer pixel dimensions for viewport }; inline ScrollFadeState& GetScrollFadeState() { static ScrollFadeState s; return s; } // ============================================================================ // Callbacks — inserted into the draw list via AddCallback // ============================================================================ #ifdef SCROLL_FADE_DX11 inline void BindRTCallback(const ImDrawList*, const ImDrawCmd*) { auto& st = GetScrollFadeState(); ID3D11DeviceContext* ctx = GetDX11Context(); if (!ctx) return; // Save current RT and viewport UINT numVP = 1; ctx->OMGetRenderTargets(1, &st.savedRTV, &st.savedDSV); ctx->RSGetViewports(&numVP, &st.savedVP); // Bind offscreen RT ctx->OMSetRenderTargets(1, &st.offscreenRTV, nullptr); // Set viewport to match RT size D3D11_VIEWPORT vp = {}; vp.Width = (FLOAT)st.vpW; vp.Height = (FLOAT)st.vpH; vp.MaxDepth = 1.0f; ctx->RSSetViewports(1, &vp); // Clear to transparent float clearColor[4] = { 0.0f, 0.0f, 0.0f, 0.0f }; ctx->ClearRenderTargetView(st.offscreenRTV, clearColor); ctx->Release(); } inline void UnbindRTCallback(const ImDrawList*, const ImDrawCmd*) { auto& st = GetScrollFadeState(); ID3D11DeviceContext* ctx = GetDX11Context(); if (!ctx) return; // Restore previous RT and viewport ctx->OMSetRenderTargets(1, &st.savedRTV, st.savedDSV); ctx->RSSetViewports(1, &st.savedVP); // Release the refs from OMGetRenderTargets if (st.savedRTV) { st.savedRTV->Release(); st.savedRTV = nullptr; } if (st.savedDSV) { st.savedDSV->Release(); st.savedDSV = nullptr; } ctx->Release(); } #endif // SCROLL_FADE_DX11 #ifdef SCROLL_FADE_GL inline void BindRTCallback(const ImDrawList*, const ImDrawCmd*) { auto& st = GetScrollFadeState(); // Save current FBO and viewport glGetIntegerv(GL_FRAMEBUFFER_BINDING, &st.savedFBO); glGetIntegerv(GL_VIEWPORT, st.savedVP); glBindFramebuffer(GL_FRAMEBUFFER, st.fbo); glViewport(0, 0, st.vpW, st.vpH); // Disable scissor test inside the FBO. ImGui's renderer computes // scissor rects relative to the main framebuffer dimensions — those // coordinates would be wrong for our offscreen surface. The child // window's content is already bounded by ImGui's layout, and the // composite step applies its own clip rect, so skipping scissor // in the FBO is safe. glDisable(GL_SCISSOR_TEST); glClearColor(0.0f, 0.0f, 0.0f, 0.0f); glClear(GL_COLOR_BUFFER_BIT); } inline void UnbindRTCallback(const ImDrawList*, const ImDrawCmd*) { auto& st = GetScrollFadeState(); glBindFramebuffer(GL_FRAMEBUFFER, (unsigned int)st.savedFBO); glViewport(st.savedVP[0], st.savedVP[1], st.savedVP[2], st.savedVP[3]); if (st.savedScissorEnabled) glEnable(GL_SCISSOR_TEST); } #endif // SCROLL_FADE_GL // ============================================================================ // Composite helper — draw the RT texture as a mesh strip with alpha fade // ============================================================================ /// Draw the offscreen texture onto `dl` as a vertical strip with alpha=0 at /// the faded edges and alpha=1 in the middle. Produces a true CSS-like /// mask-image: linear-gradient() result. /// /// @param logicalW, logicalH Logical display dimensions (DisplaySize) for /// UV calculation — NOT the RT pixel dimensions. ImGui screen coords /// are in logical units, and the FBO projection maps them 1:1 to the /// logical coordinate space, so UVs must divide by logical size. inline void CompositeWithFade(ImDrawList* dl, ImTextureID texID, const ImVec2& screenMin, const ImVec2& screenMax, int logicalW, int logicalH, float fadeTop, float fadeBot, bool needTop, bool needBot) { float left = screenMin.x; float right = screenMax.x; float y0 = screenMin.y; float y1 = screenMin.y + (needTop ? fadeTop : 0.0f); float y2 = screenMax.y - (needBot ? fadeBot : 0.0f); float y3 = screenMax.y; // Clamp in case fade zones overlap if (y1 > y2) { float mid = (y0 + y3) * 0.5f; y1 = y2 = mid; } // UV coordinates — map screen position (logical) to render target texture. // Screen coords are in logical (DisplaySize) space. The FBO projection // maps these 1:1, so divide by logical dimensions to get [0,1] UVs. float uL = screenMin.x / (float)logicalW; float uR = screenMax.x / (float)logicalW; #ifdef SCROLL_FADE_GL // OpenGL: FBO Y is flipped (ImGui top=0 → GL bottom=0) auto uvY = [&](float y) -> float { return 1.0f - y / (float)logicalH; }; #else // DX11: no Y flip (both ImGui and DX11 have (0,0) at top-left) auto uvY = [&](float y) -> float { return y / (float)logicalH; }; #endif ImU32 colOpaque = IM_COL32(255, 255, 255, 255); ImU32 colClear = IM_COL32(255, 255, 255, 0); ImU32 colTop = needTop ? colClear : colOpaque; ImU32 colBot = needBot ? colClear : colOpaque; dl->PushTextureID(texID); dl->PrimReserve(18, 8); ImDrawVert* vtx = dl->_VtxWritePtr; ImDrawIdx* idx = dl->_IdxWritePtr; ImDrawIdx base = (ImDrawIdx)dl->_VtxCurrentIdx; vtx[0] = { ImVec2(left, y0), ImVec2(uL, uvY(y0)), colTop }; vtx[1] = { ImVec2(right, y0), ImVec2(uR, uvY(y0)), colTop }; vtx[2] = { ImVec2(left, y1), ImVec2(uL, uvY(y1)), colOpaque }; vtx[3] = { ImVec2(right, y1), ImVec2(uR, uvY(y1)), colOpaque }; vtx[4] = { ImVec2(left, y2), ImVec2(uL, uvY(y2)), colOpaque }; vtx[5] = { ImVec2(right, y2), ImVec2(uR, uvY(y2)), colOpaque }; vtx[6] = { ImVec2(left, y3), ImVec2(uL, uvY(y3)), colBot }; vtx[7] = { ImVec2(right, y3), ImVec2(uR, uvY(y3)), colBot }; idx[0] = base+0; idx[1] = base+1; idx[2] = base+3; idx[3] = base+0; idx[4] = base+3; idx[5] = base+2; idx[6] = base+2; idx[7] = base+3; idx[8] = base+5; idx[9] = base+2; idx[10] = base+5; idx[11] = base+4; idx[12] = base+4; idx[13] = base+5; idx[14] = base+7; idx[15] = base+4; idx[16] = base+7; idx[17] = base+6; dl->_VtxWritePtr += 8; dl->_IdxWritePtr += 18; dl->_VtxCurrentIdx += 8; dl->PopTextureID(); } } // namespace effects } // namespace ui } // namespace dragonx #endif // SCROLL_FADE_HAS_OFFSCREEN