// DragonX Wallet - ImGui Edition // Copyright 2024-2026 The Hush Developers // Released under the GPLv3 // Shader-based scroll-edge fade effect. // // Inserts ImDrawCallback entries into the draw list to switch from ImGui's // default shader to a custom shader that multiplies output alpha by a // smoothstep gradient based on screen-Y position. After the fade region, // ImDrawCallback_ResetRenderState restores ImGui's default render state. // // GL path: custom GLSL 130 program (VS + FS), uses gl_FragCoord.y // DX11 path: custom pixel shader only, uses SV_POSITION.y #pragma once #include "imgui.h" #include "imgui_internal.h" // ---- Platform headers ---- #ifdef DRAGONX_HAS_GLAD #include #endif #ifdef DRAGONX_USE_DX11 #include #include #endif #include #include #include #include "../../util/logger.h" namespace dragonx { namespace ui { namespace effects { // ============================================================================ // ScrollFadeShader — per-pixel scroll-edge alpha mask via ImDrawCallback // ============================================================================ struct ScrollFadeShader { // ---- Caller-set parameters (logical screen-space, per-frame) ---- float fadeTopY = 0.0f; // top of scrollable region (screen Y) float fadeBottomY = 0.0f; // bottom of scrollable region (screen Y) float fadeZoneTop = 0.0f; // top fade height (0 = disabled) float fadeZoneBottom = 0.0f; // bottom fade height (0 = disabled) // ---- Internal state ---- bool ready = false; #ifdef DRAGONX_HAS_GLAD GLuint program_ = 0; GLint locProjMtx_ = -1; GLint locTexture_ = -1; GLint locFadeTopY_ = -1; GLint locFadeBottomY_= -1; GLint locFadeZoneTop_= -1; GLint locFadeZoneBot_= -1; GLint locViewportH_ = -1; #endif #ifdef DRAGONX_USE_DX11 ID3D11PixelShader* pFadePS_ = nullptr; ID3D11Buffer* pFadeCB_ = nullptr; #endif // ---------------------------------------------------------------- // Init / Destroy // ---------------------------------------------------------------- bool init() { if (ready) return true; #ifdef DRAGONX_HAS_GLAD ready = initGL(); #elif defined(DRAGONX_USE_DX11) ready = initDX11(); #endif return ready; } void destroy() { #ifdef DRAGONX_HAS_GLAD destroyGL(); #elif defined(DRAGONX_USE_DX11) destroyDX11(); #endif ready = false; } // ---------------------------------------------------------------- // Draw-list integration helpers // ---------------------------------------------------------------- /// Insert a "bind fade shader" callback into the draw list. /// Must be followed later by addUnbind() or AddCallback(ImDrawCallback_ResetRenderState). void addBind(ImDrawList* dl) { dl->AddCallback(BindCB, this); } /// Insert ImDrawCallback_ResetRenderState to restore ImGui's default shader. static void addUnbind(ImDrawList* dl) { dl->AddCallback(ImDrawCallback_ResetRenderState, nullptr); } // ================================================================ // GL implementation // ================================================================ #ifdef DRAGONX_HAS_GLAD static constexpr const char* kFadeVS_130 = #ifdef __APPLE__ "#version 150\n" #else "#version 130\n" #endif "uniform mat4 ProjMtx;\n" "in vec2 Position;\n" "in vec2 UV;\n" "in vec4 Color;\n" "out vec2 Frag_UV;\n" "out vec4 Frag_Color;\n" "void main() {\n" " Frag_UV = UV;\n" " Frag_Color = Color;\n" " gl_Position = ProjMtx * vec4(Position.xy, 0, 1);\n" "}\n"; static constexpr const char* kFadeFS_130 = #ifdef __APPLE__ "#version 150\n" #else "#version 130\n" #endif "uniform sampler2D Texture;\n" "uniform float u_fadeTopY;\n" "uniform float u_fadeBottomY;\n" "uniform float u_fadeZoneTop;\n" "uniform float u_fadeZoneBot;\n" "uniform float u_viewportH;\n" "in vec2 Frag_UV;\n" "in vec4 Frag_Color;\n" "out vec4 Out_Color;\n" "void main() {\n" " Out_Color = Frag_Color * texture(Texture, Frag_UV.st);\n" " float y = u_viewportH - gl_FragCoord.y;\n" // bottom-up → top-down " float topA = (u_fadeZoneTop > 0.0)\n" " ? smoothstep(u_fadeTopY, u_fadeTopY + u_fadeZoneTop, y)\n" " : 1.0;\n" " float botA = (u_fadeZoneBot > 0.0)\n" " ? (1.0 - smoothstep(u_fadeBottomY - u_fadeZoneBot, u_fadeBottomY, y))\n" " : 1.0;\n" " Out_Color.a *= topA * botA;\n" "}\n"; bool initGL() { GLuint vs = glCreateShader(GL_VERTEX_SHADER); GLuint fs = glCreateShader(GL_FRAGMENT_SHADER); glShaderSource(vs, 1, &kFadeVS_130, nullptr); glCompileShader(vs); if (!checkShader(vs, "fade VS")) { glDeleteShader(vs); glDeleteShader(fs); return false; } glShaderSource(fs, 1, &kFadeFS_130, nullptr); glCompileShader(fs); if (!checkShader(fs, "fade FS")) { glDeleteShader(vs); glDeleteShader(fs); return false; } program_ = glCreateProgram(); glAttachShader(program_, vs); glAttachShader(program_, fs); // Force attribute locations to match ImGui's typical assignment // (Position=0, UV=1, Color=2) so that the VAO set up by ImGui's // backend is compatible with our program. glBindAttribLocation(program_, 0, "Position"); glBindAttribLocation(program_, 1, "UV"); glBindAttribLocation(program_, 2, "Color"); glLinkProgram(program_); glDetachShader(program_, vs); glDetachShader(program_, fs); glDeleteShader(vs); glDeleteShader(fs); if (!checkProgram(program_, "fade program")) { glDeleteProgram(program_); program_ = 0; return false; } locProjMtx_ = glGetUniformLocation(program_, "ProjMtx"); locTexture_ = glGetUniformLocation(program_, "Texture"); locFadeTopY_ = glGetUniformLocation(program_, "u_fadeTopY"); locFadeBottomY_ = glGetUniformLocation(program_, "u_fadeBottomY"); locFadeZoneTop_ = glGetUniformLocation(program_, "u_fadeZoneTop"); locFadeZoneBot_ = glGetUniformLocation(program_, "u_fadeZoneBot"); locViewportH_ = glGetUniformLocation(program_, "u_viewportH"); DEBUG_LOGF("ScrollFadeShader: GL program %u (ProjMtx=%d Tex=%d top=%d bot=%d zt=%d zb=%d vh=%d)\n", program_, locProjMtx_, locTexture_, locFadeTopY_, locFadeBottomY_, locFadeZoneTop_, locFadeZoneBot_, locViewportH_); return true; } void destroyGL() { if (program_) { glDeleteProgram(program_); program_ = 0; } } static bool checkShader(GLuint s, const char* label) { GLint ok = 0; glGetShaderiv(s, GL_COMPILE_STATUS, &ok); if (!ok) { char log[512]; glGetShaderInfoLog(s, sizeof(log), nullptr, log); DEBUG_LOGF("ScrollFadeShader: %s compile error:\n%s\n", label, log); } return ok != 0; } static bool checkProgram(GLuint p, const char* label) { GLint ok = 0; glGetProgramiv(p, GL_LINK_STATUS, &ok); if (!ok) { char log[512]; glGetProgramInfoLog(p, sizeof(log), nullptr, log); DEBUG_LOGF("ScrollFadeShader: %s link error:\n%s\n", label, log); } return ok != 0; } #endif // DRAGONX_HAS_GLAD // ================================================================ // DX11 implementation // ================================================================ #ifdef DRAGONX_USE_DX11 // Pixel-shader constant buffer layout (must be 16-byte aligned) struct alignas(16) FadeCBData { float fadeTopY; float fadeBottomY; float fadeZoneTop; float fadeZoneBot; }; static constexpr const char* kFadePS_HLSL = "cbuffer FadeParams : register(b1) {\n" " float u_fadeTopY;\n" " float u_fadeBottomY;\n" " float u_fadeZoneTop;\n" " float u_fadeZoneBot;\n" "};\n" "struct PS_INPUT {\n" " float4 pos : SV_POSITION;\n" " float4 col : COLOR0;\n" " float2 uv : TEXCOORD0;\n" "};\n" "sampler sampler0;\n" "Texture2D texture0;\n" "float4 main(PS_INPUT input) : SV_Target {\n" " float4 out_col = input.col * texture0.Sample(sampler0, input.uv);\n" " float y = input.pos.y;\n" // SV_POSITION.y is top-down in FB pixels " float topA = (u_fadeZoneTop > 0.0)\n" " ? smoothstep(u_fadeTopY, u_fadeTopY + u_fadeZoneTop, y)\n" " : 1.0;\n" " float botA = (u_fadeZoneBot > 0.0)\n" " ? (1.0 - smoothstep(u_fadeBottomY - u_fadeZoneBot, u_fadeBottomY, y))\n" " : 1.0;\n" " out_col.a *= topA * botA;\n" " return out_col;\n" "}\n"; bool initDX11() { // Get DX11 device from ImGui backend auto* bd = ImGui::GetIO().BackendRendererUserData; if (!bd) { DEBUG_LOGF("ScrollFadeShader: no DX11 backend data\n"); return false; } // ImGui_ImplDX11_Data layout: first field is ID3D11Device* ID3D11Device* device = *(ID3D11Device**)bd; if (!device) { DEBUG_LOGF("ScrollFadeShader: null DX11 device\n"); return false; } // Compile pixel shader ID3DBlob* psBlob = nullptr; ID3DBlob* errBlob = nullptr; HRESULT hr = D3DCompile(kFadePS_HLSL, strlen(kFadePS_HLSL), nullptr, nullptr, nullptr, "main", "ps_4_0", 0, 0, &psBlob, &errBlob); if (FAILED(hr)) { if (errBlob) { DEBUG_LOGF("ScrollFadeShader: PS compile error:\n%s\n", (const char*)errBlob->GetBufferPointer()); errBlob->Release(); } return false; } if (errBlob) errBlob->Release(); hr = device->CreatePixelShader(psBlob->GetBufferPointer(), psBlob->GetBufferSize(), nullptr, &pFadePS_); psBlob->Release(); if (FAILED(hr)) { DEBUG_LOGF("ScrollFadeShader: CreatePixelShader failed 0x%08lx\n", hr); return false; } // Create constant buffer for fade params D3D11_BUFFER_DESC cbDesc = {}; cbDesc.ByteWidth = sizeof(FadeCBData); cbDesc.Usage = D3D11_USAGE_DYNAMIC; cbDesc.BindFlags = D3D11_BIND_CONSTANT_BUFFER; cbDesc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE; hr = device->CreateBuffer(&cbDesc, nullptr, &pFadeCB_); if (FAILED(hr)) { DEBUG_LOGF("ScrollFadeShader: CreateBuffer (CB) failed 0x%08lx\n", hr); pFadePS_->Release(); pFadePS_ = nullptr; return false; } VERBOSE_LOGF("ScrollFadeShader: DX11 pixel shader + CB created\n"); return true; } void destroyDX11() { if (pFadeCB_) { pFadeCB_->Release(); pFadeCB_ = nullptr; } if (pFadePS_) { pFadePS_->Release(); pFadePS_ = nullptr; } } #endif // DRAGONX_USE_DX11 // ================================================================ // Callback — called by ImGui's render backend during RenderDrawData // ================================================================ static void BindCB(const ImDrawList* /*dl*/, const ImDrawCmd* cmd) { auto* self = static_cast(cmd->UserCallbackData); if (!self || !self->ready) return; ImGuiIO& io = ImGui::GetIO(); float fbScaleY = io.DisplayFramebufferScale.y; float fb_height = io.DisplaySize.y * fbScaleY; // In multi-viewport mode ImGui coordinates are OS-absolute, but // SV_POSITION / gl_FragCoord are render-target-relative. Subtract // the main viewport origin to convert screen-space → RT-local. float vpY = ImGui::GetMainViewport()->Pos.y; // Convert logical screen-space params to framebuffer pixels float topY = (self->fadeTopY - vpY) * fbScaleY; float botY = (self->fadeBottomY - vpY) * fbScaleY; float zoneTop = self->fadeZoneTop * fbScaleY; float zoneBot = self->fadeZoneBottom * fbScaleY; #ifdef DRAGONX_HAS_GLAD glUseProgram(self->program_); // Rebind vertex attribute pointers so our forced locations (0=Position, // 1=UV, 2=Color) match the already-bound VBO data. ImGui's linker may // assign different locations (e.g. Mesa uses alphabetical order: // Color=0, Position=1, UV=2), but the VBO layout is fixed, so we // must explicitly point our attributes at the correct offsets. glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(ImDrawVert), (GLvoid*)offsetof(ImDrawVert, pos)); glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(ImDrawVert), (GLvoid*)offsetof(ImDrawVert, uv)); glVertexAttribPointer(2, 4, GL_UNSIGNED_BYTE, GL_TRUE, sizeof(ImDrawVert), (GLvoid*)offsetof(ImDrawVert, col)); glEnableVertexAttribArray(0); glEnableVertexAttribArray(1); glEnableVertexAttribArray(2); // Ortho projection — must match ImGui's SetupRenderState which uses // draw_data->DisplayPos as the origin. With ViewportsEnable, vertex // positions are in OS screen-space; the projection matrix maps them // to window-local framebuffer coordinates. float vpX = ImGui::GetMainViewport()->Pos.x; float L = vpX; float R = vpX + io.DisplaySize.x; float T = vpY; float B = vpY + io.DisplaySize.y; const float ortho[4][4] = { { 2.0f/(R-L), 0.0f, 0.0f, 0.0f }, { 0.0f, 2.0f/(T-B), 0.0f, 0.0f }, { 0.0f, 0.0f, -1.0f, 0.0f }, { (R+L)/(L-R), (T+B)/(B-T), 0.0f, 1.0f }, }; glUniformMatrix4fv(self->locProjMtx_, 1, GL_FALSE, &ortho[0][0]); glUniform1i(self->locTexture_, 0); glUniform1f(self->locFadeTopY_, topY); glUniform1f(self->locFadeBottomY_, botY); glUniform1f(self->locFadeZoneTop_, zoneTop); glUniform1f(self->locFadeZoneBot_, zoneBot); glUniform1f(self->locViewportH_, fb_height); #endif #ifdef DRAGONX_USE_DX11 (void)fb_height; // Get device context from ImGui backend (first two fields of ImGui_ImplDX11_Data) auto* bd = io.BackendRendererUserData; if (!bd) return; struct DX11BD { ID3D11Device* dev; ID3D11DeviceContext* ctx; }; auto* dx = reinterpret_cast(bd); ID3D11DeviceContext* ctx = dx->ctx; if (!ctx) return; // Update constant buffer D3D11_MAPPED_SUBRESOURCE mapped; if (SUCCEEDED(ctx->Map(self->pFadeCB_, 0, D3D11_MAP_WRITE_DISCARD, 0, &mapped))) { auto* cb = static_cast(mapped.pData); cb->fadeTopY = topY; cb->fadeBottomY = botY; cb->fadeZoneTop = zoneTop; cb->fadeZoneBot = zoneBot; ctx->Unmap(self->pFadeCB_, 0); } // Bind our pixel shader + constant buffer (slot 1 = register(b1)) ctx->PSSetShader(self->pFadePS_, nullptr, 0); ctx->PSSetConstantBuffers(1, 1, &self->pFadeCB_); #endif } }; } // namespace effects } // namespace ui } // namespace dragonx