// DragonX Wallet - ImGui Edition // Copyright 2024-2026 The Hush Developers // Released under the GPLv3 #pragma once #include "colors.h" #include "type.h" #include "../layout.h" #include "../schema/element_styles.h" #include "../schema/color_var_resolver.h" #include "../schema/ui_schema.h" #include "../effects/theme_effects.h" #include "../effects/low_spec.h" #include "../effects/imgui_acrylic.h" #include "../theme.h" #include "../../util/noise_texture.h" #include "imgui.h" #include "imgui_internal.h" #include #include #include namespace dragonx { namespace ui { namespace material { // Scale the alpha channel of an ImU32 color by a float factor. inline ImU32 ScaleAlpha(ImU32 col, float scale) { int a = static_cast(((col >> IM_COL32_A_SHIFT) & 0xFF) * scale); if (a > 255) a = 255; return (col & ~IM_COL32_A_MASK) | (static_cast(a) << IM_COL32_A_SHIFT); } // ============================================================================ // Text Drop Shadow // ============================================================================ // Draw text with a subtle dark shadow behind it for readability on // translucent / glassy surfaces. The shadow is a 1px offset copy of // the text in a near-black colour. When the OS backdrop (DWM Acrylic) // is inactive, the shadow is skipped since opaque backgrounds already // provide enough contrast. inline void DrawTextShadow(ImDrawList* dl, ImFont* font, float fontSize, const ImVec2& pos, ImU32 col, const char* text, float offsetX = 1.0f, float offsetY = 1.0f, ImU32 shadowCol = 0) { if (IsBackdropActive()) { if (!shadowCol) { static uint32_t s_gen = 0; static ImU32 s_shadowCol = 0; uint32_t g = schema::UI().generation(); if (g != s_gen) { s_gen = g; s_shadowCol = schema::UI().resolveColor("var(--text-shadow)", IM_COL32(0, 0, 0, 120)); } shadowCol = s_shadowCol; } dl->AddText(font, fontSize, ImVec2(pos.x + offsetX, pos.y + offsetY), shadowCol, text); } dl->AddText(font, fontSize, pos, col, text); } // Convenience overload that uses the current default font. inline void DrawTextShadow(ImDrawList* dl, const ImVec2& pos, ImU32 col, const char* text, float offsetX = 1.0f, float offsetY = 1.0f, ImU32 shadowCol = 0) { if (IsBackdropActive()) { if (!shadowCol) { static uint32_t s_gen = 0; static ImU32 s_shadowCol = 0; uint32_t g = schema::UI().generation(); if (g != s_gen) { s_gen = g; s_shadowCol = schema::UI().resolveColor("var(--text-shadow)", IM_COL32(0, 0, 0, 120)); } shadowCol = s_shadowCol; } dl->AddText(ImVec2(pos.x + offsetX, pos.y + offsetY), shadowCol, text); } dl->AddText(pos, col, text); } // ============================================================================ // Modal-Aware Hover Check // ============================================================================ // Drop-in replacement for ImGui::IsMouseHoveringRect that also respects // modal popup blocking. The raw ImGui helper is a pure geometric test // and will return true even when a modal popup covers the rect, which // causes background elements to show hover highlights through dialogs. inline bool IsRectHovered(const ImVec2& r_min, const ImVec2& r_max, bool clip = true) { if (!ImGui::IsMouseHoveringRect(r_min, r_max, clip)) return false; // If a modal popup is open and it is not the current window, treat // the content as non-hoverable (same logic ImGui uses internally // inside IsWindowContentHoverable for modal blocking). // // We cannot rely solely on GetTopMostAndVisiblePopupModal() because // it checks Active, which is only set when BeginPopupModal() is // called in the current frame. Content tabs render BEFORE their // associated dialogs, so the modal's Active flag is still false at // this point. Checking WasActive (set from the previous frame) // covers this render-order gap. ImGuiContext& g = *ImGui::GetCurrentContext(); for (int n = g.OpenPopupStack.Size - 1; n >= 0; n--) { ImGuiWindow* popup = g.OpenPopupStack.Data[n].Window; if (popup && (popup->Flags & ImGuiWindowFlags_Modal)) { if ((popup->Active || popup->WasActive) && !popup->Hidden) { if (popup != ImGui::GetCurrentWindow()) return false; } } } return true; } // ============================================================================ // Tactile Button Overlay // ============================================================================ // Adds a subtle top-edge highlight and bottom-edge shadow to a button rect, // creating the illusion of a raised physical surface. On active (pressed), // the highlight/shadow swap to create an inset "pushed" feel. // Call this AFTER ImGui::Button() using GetItemRectMin()/GetItemRectMax(). inline void DrawTactileOverlay(ImDrawList* dl, const ImVec2& bMin, const ImVec2& bMax, float rounding, bool active = false) { float h = bMax.y - bMin.y; float edgeH = std::min(h * 0.38f, 6.0f); // highlight/shadow strip height // AddRectFilledMultiColor does not support rounding, so clip to the // button's rounded rect to prevent sharp corners from poking out. if (rounding > 0.0f) { float inset = rounding * 0.29f; // enough to hide corners dl->PushClipRect( ImVec2(bMin.x + inset, bMin.y + inset), ImVec2(bMax.x - inset, bMax.y - inset), true); } ImU32 tHi = schema::UI().resolveColor("var(--tactile-top)", IM_COL32(255, 255, 255, 18)); ImU32 tHiT = tHi & ~IM_COL32_A_MASK; // transparent version if (!active) { // Raised: bright top edge, dark bottom edge // Top highlight dl->AddRectFilledMultiColor( bMin, ImVec2(bMax.x, bMin.y + edgeH), tHi, tHi, tHiT, tHiT); // Bottom shadow dl->AddRectFilledMultiColor( ImVec2(bMin.x, bMax.y - edgeH), bMax, IM_COL32(0, 0, 0, 0), // top-left IM_COL32(0, 0, 0, 0), // top-right IM_COL32(0, 0, 0, 22), // bottom-right IM_COL32(0, 0, 0, 22)); // bottom-left } else { // Pressed: dark top edge, bright bottom edge (inset) // Top shadow dl->AddRectFilledMultiColor( bMin, ImVec2(bMax.x, bMin.y + edgeH), IM_COL32(0, 0, 0, 20), IM_COL32(0, 0, 0, 20), IM_COL32(0, 0, 0, 0), IM_COL32(0, 0, 0, 0)); // Bottom highlight (dimmer when pressed) ImU32 pHi = ScaleAlpha(tHi, 0.55f); ImU32 pHiT = pHi & ~IM_COL32_A_MASK; dl->AddRectFilledMultiColor( ImVec2(bMin.x, bMax.y - edgeH), bMax, pHiT, pHiT, pHi, pHi); } if (rounding > 0.0f) dl->PopClipRect(); } // Convenience: call right after any ImGui::Button() / SmallButton() call // to add tactile depth. Uses the last item rect automatically. inline void ApplyTactile(ImDrawList* dl = nullptr) { if (!dl) dl = ImGui::GetWindowDrawList(); ImVec2 bMin = ImGui::GetItemRectMin(); ImVec2 bMax = ImGui::GetItemRectMax(); float rounding = ImGui::GetStyle().FrameRounding; bool active = ImGui::IsItemActive(); DrawTactileOverlay(dl, bMin, bMax, rounding, active); } // ── Button font tier helper ───────────────────────────────────────────── // Resolves an int tier (0=sm, 1=md/default, 2=lg) to the matching ImFont*. // Passing -1 or any out-of-range value returns the default button font. inline ImFont* resolveButtonFont(int tier) { switch (tier) { case 0: return Type().buttonSm(); case 2: return Type().buttonLg(); default: return Type().button(); // 1 or any other value } } // Resolve per-button font: if perButton >= 0 use it, else fall back to sectionDefault inline ImFont* resolveButtonFont(int perButton, int sectionDefault) { return resolveButtonFont(perButton >= 0 ? perButton : sectionDefault); } // ── Tactile wrappers ──────────────────────────────────────────────────── // Drop-in replacements for ImGui::Button / SmallButton that automatically // add the tactile highlight overlay after rendering. inline bool TactileButton(const char* label, const ImVec2& size = ImVec2(0, 0), ImFont* font = nullptr) { // Draw button with glass-card styling: translucent fill + border + tactile overlay ImDrawList* dl = ImGui::GetWindowDrawList(); ImFont* useFont = font ? font : Type().button(); // For icon fonts, use InvisibleButton + manual centered text rendering // to ensure perfect centering (ImGui::Button alignment can be off for icons) bool isIconFont = font && (font == Type().iconSmall() || font == Type().iconMed() || font == Type().iconLarge() || font == Type().iconXL()); bool pressed; if (isIconFont && size.x > 0 && size.y > 0) { pressed = ImGui::InvisibleButton(label, size); } else { ImGui::PushFont(useFont); pressed = ImGui::Button(label, size); ImGui::PopFont(); } // Glass overlay on the button rect ImVec2 bMin = ImGui::GetItemRectMin(); ImVec2 bMax = ImGui::GetItemRectMax(); // For icon fonts, manually draw centered icon after getting button rect if (isIconFont && size.x > 0 && size.y > 0) { ImVec2 textSz = useFont->CalcTextSizeA(useFont->LegacySize, FLT_MAX, 0, label); ImVec2 textPos(bMin.x + (size.x - textSz.x) * 0.5f, bMin.y + (size.y - textSz.y) * 0.5f); dl->AddText(useFont, useFont->LegacySize, textPos, ImGui::GetColorU32(ImGuiCol_Text), label); } float rounding = ImGui::GetStyle().FrameRounding; bool active = ImGui::IsItemActive(); bool hovered = ImGui::IsItemHovered(); // Frosted glass highlight — subtle fill on top if (!active) { ImU32 col = hovered ? schema::UI().resolveColor("var(--hover-overlay)", IM_COL32(255, 255, 255, 12)) : schema::UI().resolveColor("var(--glass-fill)", IM_COL32(255, 255, 255, 6)); dl->AddRectFilled(bMin, bMax, col, rounding); } // Rim light ImU32 rim = schema::UI().resolveColor("var(--rim-light)", IM_COL32(255, 255, 255, 25)); dl->AddRect(bMin, bMax, active ? ScaleAlpha(rim, 0.6f) : rim, rounding, 0, 1.0f); // Tactile depth DrawTactileOverlay(dl, bMin, bMax, rounding, active); return pressed; } inline bool TactileSmallButton(const char* label, ImFont* font = nullptr) { ImDrawList* dl = ImGui::GetWindowDrawList(); ImGui::PushFont(font ? font : Type().button()); bool pressed = ImGui::SmallButton(label); ImGui::PopFont(); ImVec2 bMin = ImGui::GetItemRectMin(); ImVec2 bMax = ImGui::GetItemRectMax(); float rounding = ImGui::GetStyle().FrameRounding; bool active = ImGui::IsItemActive(); bool hovered = ImGui::IsItemHovered(); if (!active) { ImU32 col = hovered ? schema::UI().resolveColor("var(--hover-overlay)", IM_COL32(255, 255, 255, 12)) : schema::UI().resolveColor("var(--glass-fill)", IM_COL32(255, 255, 255, 6)); dl->AddRectFilled(bMin, bMax, col, rounding); } ImU32 rim = schema::UI().resolveColor("var(--rim-light)", IM_COL32(255, 255, 255, 25)); dl->AddRect(bMin, bMax, active ? ScaleAlpha(rim, 0.6f) : rim, rounding, 0, 1.0f); DrawTactileOverlay(dl, bMin, bMax, rounding, active); return pressed; } // ============================================================================ // Glass Panel (glassmorphism card) // ============================================================================ // Draws a frosted-glass style panel: a semi-transparent fill with a // subtle light border. Used for card/panel containers that sit above // the blurred background. When the backdrop is inactive the fill is // simply the normal surface colour (fully opaque). struct GlassPanelSpec { float rounding = 6.0f; int fillAlpha = 18; // fill brightness (white, 0-255) int borderAlpha = 30; // border brightness (white, 0-255) float borderWidth = 1.0f; }; inline void DrawGlassPanel(ImDrawList* dl, const ImVec2& pMin, const ImVec2& pMax, const GlassPanelSpec& spec = GlassPanelSpec()) { if (IsBackdropActive() && !dragonx::ui::effects::isLowSpecMode()) { // --- Cached color lookups (invalidated on theme change) --- // These 3 resolveColor() calls do string parsing + map lookup // each time. Cache them per-frame using the schema generation // counter so they resolve at most once per theme load. static uint32_t s_gen = 0; static ImU32 s_glassFill = 0; static ImU32 s_glassNoiseTint = 0; static ImU32 s_glassBorder = 0; uint32_t curGen = schema::UI().generation(); if (curGen != s_gen) { s_gen = curGen; s_glassFill = schema::UI().resolveColor("var(--glass-fill)", IM_COL32(255, 255, 255, 18)); s_glassNoiseTint = schema::UI().resolveColor("var(--glass-noise-tint)", IM_COL32(255, 255, 255, 10)); s_glassBorder = schema::UI().resolveColor("var(--glass-border)", IM_COL32(255, 255, 255, 30)); } float uiOp = effects::ImGuiAcrylic::GetUIOpacity(); bool useAcrylic = effects::ImGuiAcrylic::IsEnabled() && effects::ImGuiAcrylic::IsAvailable(); // Glass / acrylic layer — only rendered when not fully opaque // (skip the blur pass at 100% for performance) if (uiOp < 0.99f) { if (useAcrylic) { const auto& acrylicTheme = GetCurrentAcrylicTheme(); effects::ImGuiAcrylic::DrawAcrylicRect(dl, pMin, pMax, acrylicTheme.card, spec.rounding); } else { // Lightweight fake-glass: translucent fill ImU32 fill = (spec.fillAlpha == 18) ? s_glassFill : ScaleAlpha(s_glassFill, spec.fillAlpha / 18.0f); dl->AddRectFilled(pMin, pMax, fill, spec.rounding); } } // Surface overlay — provides smooth transition from glass to opaque. // At uiOp=1.0 this fully covers the panel (opaque card). // As uiOp decreases, glass/blur progressively shows through. dl->AddRectFilled(pMin, pMax, WithAlphaF(GetElevatedSurface(GetCurrentColorTheme(), 1), uiOp), spec.rounding); // Noise grain overlay — drawn OVER the surface overlay so card // opacity doesn't hide it. Gives cards a tactile paper feel. { float noiseMul = dragonx::ui::effects::ImGuiAcrylic::GetNoiseOpacity(); if (noiseMul > 0.0f) { uint8_t origAlpha = (s_glassNoiseTint >> IM_COL32_A_SHIFT) & 0xFF; uint8_t scaledAlpha = static_cast(std::min(255.0f, origAlpha * noiseMul)); ImU32 noiseTint = (s_glassNoiseTint & ~(0xFFu << IM_COL32_A_SHIFT)) | (scaledAlpha << IM_COL32_A_SHIFT); float inset = spec.rounding * 0.3f; ImVec2 clipMin(pMin.x + inset, pMin.y + inset); ImVec2 clipMax(pMax.x - inset, pMax.y - inset); dl->PushClipRect(clipMin, clipMax, true); dragonx::util::DrawTiledNoiseRect(dl, clipMin, clipMax, noiseTint); dl->PopClipRect(); } } // Border — fades with UI opacity ImU32 border = (spec.borderAlpha == 30) ? s_glassBorder : ScaleAlpha(s_glassBorder, spec.borderAlpha / 30.0f); if (uiOp < 0.99f) border = ScaleAlpha(border, uiOp); dl->AddRect(pMin, pMax, border, spec.rounding, 0, spec.borderWidth); // Theme visual effects drawn on ForegroundDrawList so they // render above card content (text, values, etc.), not below. auto& fx = effects::ThemeEffects::instance(); ImDrawList* fxDl = ImGui::GetForegroundDrawList(); if (fx.hasRainbowBorder()) { fx.drawRainbowBorder(fxDl, pMin, pMax, spec.rounding, spec.borderWidth); } if (fx.hasShimmer()) { fx.drawShimmer(fxDl, pMin, pMax, spec.rounding); } if (fx.hasSpecularGlare()) { fx.drawSpecularGlare(fxDl, pMin, pMax, spec.rounding); } // Per-panel theme effects: edge trace + ember rise fx.drawPanelEffects(fxDl, pMin, pMax, spec.rounding); } else { // Low-spec opaque fallback dl->AddRectFilled(pMin, pMax, GetElevatedSurface(GetCurrentColorTheme(), 1), spec.rounding); } } // ============================================================================ // Stat Card — reusable card with overline / value / subtitle + accent stripe // ============================================================================ struct StatCardSpec { const char* overline = nullptr; // small label at top (e.g. "LOCAL HASHRATE") const char* value = nullptr; // main value text (e.g. "1.23 kH/s") const char* subtitle = nullptr; // optional small line (e.g. "3 blocks") ImU32 valueCol = 0; // value text colour (0 = OnSurface) ImU32 accentCol = 0; // left-stripe colour (0 = no stripe) bool centered = true; // centre text horizontally bool hovered = false; // draw hover glow border }; /// Compute a generous stat-card height with breathing room. inline float StatCardHeight(float vs, float minH = 56.0f) { float padV = Layout::spacingLg() * 2; // top + bottom float content = Type().overline()->LegacySize + Layout::spacingMd() + Type().subtitle1()->LegacySize; return std::max(minH, (content + padV) * std::max(vs, 0.85f)); } inline void DrawStatCard(ImDrawList* dl, const ImVec2& cMin, const ImVec2& cMax, const StatCardSpec& card, const GlassPanelSpec& glass = GlassPanelSpec()) { float cardW = cMax.x - cMin.x; float cardH = cMax.y - cMin.y; float rnd = glass.rounding; // 1. Glass background DrawGlassPanel(dl, cMin, cMax, glass); // 2. Accent stripe (left edge, clipped to card rounded corners). // Draw a full-height rounded rect with card rounding (left corners) // and clip to stripe width so the shape follows the corner radius. if ((card.accentCol & IM_COL32_A_MASK) != 0) { float stripeW = 4.0f; dl->PushClipRect(cMin, ImVec2(cMin.x + stripeW, cMax.y), true); dl->AddRectFilled(cMin, cMax, card.accentCol, rnd, ImDrawFlags_RoundCornersLeft); dl->PopClipRect(); } // 3. Compute content block height ImFont* ovFont = Type().overline(); ImFont* valFont = Type().subtitle1(); ImFont* subFont = Type().caption(); float blockH = 0; if (card.overline) blockH += ovFont->LegacySize; if (card.overline && card.value) blockH += Layout::spacingMd(); if (card.value) blockH += valFont->LegacySize; if (card.subtitle) blockH += Layout::spacingSm() + subFont->LegacySize; // 4. Vertically centre float topY = cMin.y + (cardH - blockH) * 0.5f; float padX = Layout::spacingLg(); float cy = topY; // 5. Overline if (card.overline) { ImVec2 sz = ovFont->CalcTextSizeA(ovFont->LegacySize, 10000, 0, card.overline); float tx = card.centered ? cMin.x + (cardW - sz.x) * 0.5f : cMin.x + padX; dl->AddText(ovFont, ovFont->LegacySize, ImVec2(tx, cy), OnSurfaceMedium(), card.overline); cy += ovFont->LegacySize + Layout::spacingMd(); } // 6. Value (with text shadow) if (card.value) { ImU32 col = card.valueCol ? card.valueCol : OnSurface(); ImVec2 sz = valFont->CalcTextSizeA(valFont->LegacySize, 10000, 0, card.value); float tx = card.centered ? cMin.x + (cardW - sz.x) * 0.5f : cMin.x + padX; DrawTextShadow(dl, valFont, valFont->LegacySize, ImVec2(tx, cy), col, card.value); cy += valFont->LegacySize; } // 7. Subtitle if (card.subtitle) { cy += Layout::spacingSm(); ImVec2 sz = subFont->CalcTextSizeA(subFont->LegacySize, 10000, 0, card.subtitle); float tx = card.centered ? cMin.x + (cardW - sz.x) * 0.5f : cMin.x + padX; dl->AddText(subFont, subFont->LegacySize, ImVec2(tx, cy), OnSurfaceDisabled(), card.subtitle); } // 8. Hover glow if (card.hovered) { dl->AddRect(cMin, cMax, schema::UI().resolveColor("var(--hover-overlay)", IM_COL32(255, 255, 255, 20)), rnd, 0, 1.5f); } } // ── Styled Button (font-only wrapper) ─────────────────────────────────── // Drop-in replacement for ImGui::Button that pushes the Button font style // (from JSON config) without adding tactile visual effects. inline bool StyledButton(const char* label, const ImVec2& size = ImVec2(0, 0), ImFont* font = nullptr) { ImGui::PushFont(font ? font : Type().button()); bool pressed = ImGui::Button(label, size); ImGui::PopFont(); return pressed; } inline bool StyledSmallButton(const char* label, ImFont* font = nullptr) { ImGui::PushFont(font ? font : Type().button()); bool pressed = ImGui::SmallButton(label); ImGui::PopFont(); return pressed; } // ============================================================================ // ButtonStyle-driven overloads (unified UI schema) // ============================================================================ // These accept a ButtonStyle + ColorVarResolver to push font, colors, // and size from the schema. Existing call sites are unaffected. inline bool StyledButton(const char* label, const schema::ButtonStyle& style, const schema::ColorVarResolver& colors) { // Resolve font ImFont* font = nullptr; if (!style.font.empty()) { font = Type().resolveByName(style.font); } ImGui::PushFont(font ? font : Type().button()); // Push color overrides int colorCount = 0; if (!style.colors.background.empty()) { ImGui::PushStyleColor(ImGuiCol_Button, colors.resolve(style.colors.background)); colorCount++; } if (!style.colors.backgroundHover.empty()) { ImGui::PushStyleColor(ImGuiCol_ButtonHovered, colors.resolve(style.colors.backgroundHover)); colorCount++; } if (!style.colors.backgroundActive.empty()) { ImGui::PushStyleColor(ImGuiCol_ButtonActive, colors.resolve(style.colors.backgroundActive)); colorCount++; } if (!style.colors.color.empty()) { ImGui::PushStyleColor(ImGuiCol_Text, colors.resolve(style.colors.color)); colorCount++; } // Push style overrides int styleCount = 0; if (style.borderRadius >= 0) { ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, style.borderRadius); styleCount++; } ImVec2 size(style.width > 0 ? style.width : 0, style.height > 0 ? style.height : 0); bool pressed = ImGui::Button(label, size); ImGui::PopStyleVar(styleCount); ImGui::PopStyleColor(colorCount); ImGui::PopFont(); return pressed; } inline bool TactileButton(const char* label, const schema::ButtonStyle& style, const schema::ColorVarResolver& colors) { // Resolve font ImFont* font = nullptr; if (!style.font.empty()) { font = Type().resolveByName(style.font); } ImGui::PushFont(font ? font : Type().button()); // Push color overrides int colorCount = 0; if (!style.colors.background.empty()) { ImGui::PushStyleColor(ImGuiCol_Button, colors.resolve(style.colors.background)); colorCount++; } if (!style.colors.backgroundHover.empty()) { ImGui::PushStyleColor(ImGuiCol_ButtonHovered, colors.resolve(style.colors.backgroundHover)); colorCount++; } if (!style.colors.backgroundActive.empty()) { ImGui::PushStyleColor(ImGuiCol_ButtonActive, colors.resolve(style.colors.backgroundActive)); colorCount++; } if (!style.colors.color.empty()) { ImGui::PushStyleColor(ImGuiCol_Text, colors.resolve(style.colors.color)); colorCount++; } // Push style overrides int styleCount = 0; if (style.borderRadius >= 0) { ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, style.borderRadius); styleCount++; } ImDrawList* dl = ImGui::GetWindowDrawList(); ImVec2 size(style.width > 0 ? style.width : 0, style.height > 0 ? style.height : 0); bool pressed = ImGui::Button(label, size); // Glass overlay on the button rect ImVec2 bMin = ImGui::GetItemRectMin(); ImVec2 bMax = ImGui::GetItemRectMax(); float rounding = ImGui::GetStyle().FrameRounding; bool active = ImGui::IsItemActive(); bool hovered = ImGui::IsItemHovered(); if (!active) { ImU32 col = hovered ? schema::UI().resolveColor("var(--hover-overlay)", IM_COL32(255, 255, 255, 12)) : schema::UI().resolveColor("var(--glass-fill)", IM_COL32(255, 255, 255, 6)); dl->AddRectFilled(bMin, bMax, col, rounding); } ImU32 rim = schema::UI().resolveColor("var(--rim-light)", IM_COL32(255, 255, 255, 25)); dl->AddRect(bMin, bMax, active ? ScaleAlpha(rim, 0.6f) : rim, rounding, 0, 1.0f); DrawTactileOverlay(dl, bMin, bMax, rounding, active); ImGui::PopStyleVar(styleCount); ImGui::PopStyleColor(colorCount); ImGui::PopFont(); return pressed; } } // namespace material } // namespace ui } // namespace dragonx namespace dragonx { namespace ui { namespace material { // ============================================================================ // Scroll-edge clipping mask — CSS mask-image style vertex alpha fade. // Call ApplyScrollEdgeMask after EndChild() to fade content at the // top/bottom edges of a scrollable panel. Works by walking vertices // added during rendering and scaling their alpha based on distance // to the panel edges. No opaque overlay rectangles — content becomes // truly transparent, revealing whatever is behind the panel. // // Usage pattern: // float scrollY = 0, scrollMaxY = 0; // int parentVtx = parentDL->VtxBuffer.Size; // ImGui::BeginChild(...); // ImDrawList* childDL = ImGui::GetWindowDrawList(); // int childVtx = childDL->VtxBuffer.Size; // { ... scrollY = GetScrollY(); scrollMaxY = GetScrollMaxY(); ... } // ImGui::EndChild(); // ApplyScrollEdgeMask(parentDL, parentVtx, childDL, childVtx, // panelMin.y, panelMax.y, fadeZone, scrollY, scrollMaxY); // ============================================================================ inline void ApplyScrollEdgeMask(ImDrawList* parentDL, int parentVtxStart, ImDrawList* childDL, int childVtxStart, float topEdge, float bottomEdge, float fadeZone, float scrollY, float scrollMaxY) { auto mask = [&](ImDrawList* dl, int startIdx) { for (int vi = startIdx; vi < dl->VtxBuffer.Size; vi++) { ImDrawVert& v = dl->VtxBuffer[vi]; float alpha = 1.0f; // Top fade — only when scrolled down if (scrollY > 1.0f) { float d = v.pos.y - topEdge; if (d < fadeZone) alpha = std::min(alpha, std::max(0.0f, d / fadeZone)); } // Bottom fade — only when not at scroll bottom if (scrollMaxY > 0 && scrollY < scrollMaxY - 1.0f) { float d = bottomEdge - v.pos.y; if (d < fadeZone) alpha = std::min(alpha, std::max(0.0f, d / fadeZone)); } if (alpha < 1.0f) { int a = (v.col >> IM_COL32_A_SHIFT) & 0xFF; a = static_cast(a * alpha); v.col = (v.col & ~IM_COL32_A_MASK) | (static_cast(a) << IM_COL32_A_SHIFT); } } }; if (parentDL) mask(parentDL, parentVtxStart); if (childDL) mask(childDL, childVtxStart); } // ============================================================================ // Smooth scrolling — exponential decay interpolation for mouse wheel. // Call immediately after BeginChild() on a child that was created with // ImGuiWindowFlags_NoScrollWithMouse. Captures wheel input and lerps // scroll position each frame for a smooth feel. // // Usage: // ImGui::BeginChild("##List", size, false, // ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollWithMouse); // ApplySmoothScroll(); // ... render content ... // ImGui::EndChild(); // ============================================================================ /// Returns true when any smooth-scroll child is still interpolating toward /// its target. Checked by the idle-rendering logic in main.cpp to keep /// rendering frames until the animation settles. inline bool& SmoothScrollAnimating() { static bool sAnimating = false; return sAnimating; } inline void ApplySmoothScroll(float speed = 12.0f) { struct ScrollState { float target = 0.0f; float current = 0.0f; bool init = false; }; static std::unordered_map sStates; ImGuiWindow* win = ImGui::GetCurrentWindow(); if (!win) return; ScrollState& s = sStates[win->ID]; float scrollMaxY = ImGui::GetScrollMaxY(); if (!s.init) { s.target = ImGui::GetScrollY(); s.current = s.target; s.init = true; } // If something external set scroll position (e.g. SetScrollHereY for // auto-scroll), sync our state so the next wheel-up starts from the // actual current position rather than an old remembered target. float actualY = ImGui::GetScrollY(); if (std::abs(s.current - actualY) > 1.0f) { s.target = actualY; s.current = actualY; } // Capture mouse wheel when hovered if (ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows)) { float wheel = ImGui::GetIO().MouseWheel; if (wheel != 0.0f) { float step = ImGui::GetTextLineHeightWithSpacing() * 3.0f; s.target -= wheel * step; s.target = ImClamp(s.target, 0.0f, scrollMaxY); } } // Clamp target if scrollMax changed (e.g., content resized) if (s.target > scrollMaxY) s.target = scrollMaxY; // Exponential decay lerp float dt = ImGui::GetIO().DeltaTime; s.current += (s.target - s.current) * (1.0f - expf(-speed * dt)); // Snap when close if (std::abs(s.current - s.target) < 0.5f) s.current = s.target; else SmoothScrollAnimating() = true; // still interpolating — keep rendering ImGui::SetScrollY(s.current); } } // namespace material } // namespace ui } // namespace dragonx