// DragonX Wallet - ImGui Edition // Copyright 2024-2026 The Hush Developers // Released under the GPLv3 #pragma once #include "colors.h" #include "../effects/low_spec.h" #include "../schema/ui_schema.h" #include "imgui.h" #include "imgui_internal.h" #include namespace dragonx { namespace ui { namespace material { // ============================================================================ // Material Design Elevation and Shadow System // ============================================================================ // Based on https://m2.material.io/design/environment/elevation.html // // Material Design uses two light sources to create shadows: // - Key light: Creates sharper, directional shadows // - Ambient light: Creates softer, omnidirectional shadows // // In dark themes, elevation is primarily shown through surface color overlays // rather than shadows. However, shadows can still enhance depth perception. // ============================================================================ // Shadow Specifications // ============================================================================ /** * @brief Individual shadow layer specification * * Material shadows are composed of multiple layers with different * blur radii and offsets to simulate real-world lighting. */ struct ShadowLayer { float offsetX; // Horizontal offset (typically 0) float offsetY; // Vertical offset (key light from above) float blurRadius; // Blur spread float spreadRadius; // Size adjustment float opacity; // Alpha 0.0-1.0 }; /** * @brief Complete shadow specification for an elevation level */ struct ShadowSpec { ShadowLayer umbra; // Darkest part, sharp edge ShadowLayer penumbra; // Mid-tone, softer ShadowLayer ambient; // Lightest, most diffuse }; /** * @brief Get shadow specification for elevation level * * @param elevationDp Elevation in dp (0, 1, 2, 3, 4, 6, 8, 12, 16, 24) * @return ShadowSpec for the elevation */ ShadowSpec GetShadowSpec(int elevationDp); // ============================================================================ // Shadow Rendering // ============================================================================ /** * @brief Draw Material Design shadow for a rectangle * * Uses multi-layer soft shadow rendering to approximate Material shadows. * * @param drawList ImGui draw list * @param rect Rectangle bounds * @param elevationDp Elevation in dp * @param cornerRadius Corner radius for rounded rectangles */ void DrawShadow(ImDrawList* drawList, const ImRect& rect, int elevationDp, float cornerRadius = 0); /** * @brief Draw shadow with position/size parameters */ void DrawShadow(ImDrawList* drawList, const ImVec2& pos, const ImVec2& size, int elevationDp, float cornerRadius = 0); /** * @brief Draw soft shadow (single layer, for custom effects) * * @param drawList ImGui draw list * @param rect Rectangle bounds * @param color Shadow color with alpha * @param blurRadius Blur amount * @param offset Shadow offset * @param cornerRadius Corner radius */ void DrawSoftShadow(ImDrawList* drawList, const ImRect& rect, ImU32 color, float blurRadius, const ImVec2& offset = ImVec2(0, 0), float cornerRadius = 0); // ============================================================================ // Elevation Transition Helper // ============================================================================ /** * @brief Animated elevation value * * Use this to smoothly transition between elevation levels (e.g., card hover) */ class ElevationAnimator { public: ElevationAnimator(int initialElevation = 0); /** * @brief Set target elevation (will animate towards it) */ void setTarget(int targetElevation); /** * @brief Update animation (call each frame) * @param deltaTime Frame delta time */ void update(float deltaTime); /** * @brief Get current animated elevation value */ float getCurrent() const { return m_current; } /** * @brief Get current elevation as integer (for shadow lookup) */ int getCurrentInt() const { return static_cast(m_current + 0.5f); } /** * @brief Check if currently animating */ bool isAnimating() const { return m_current != m_target; } private: float m_current; float m_target; float m_animationSpeed = 16.0f; // dp per second }; // ============================================================================ // Implementation // ============================================================================ inline ShadowSpec GetShadowSpec(int elevationDp) { // Material Design shadow values adapted from the spec // These approximate the CSS box-shadow values from material.io switch (elevationDp) { case 0: return { {0, 0, 0, 0, 0}, // No shadow {0, 0, 0, 0, 0}, {0, 0, 0, 0, 0} }; case 1: return { {0, 2, 1, -1, 0.2f}, // Umbra {0, 1, 1, 0, 0.14f}, // Penumbra {0, 1, 3, 0, 0.12f} // Ambient }; case 2: return { {0, 3, 1, -2, 0.2f}, {0, 2, 2, 0, 0.14f}, {0, 1, 5, 0, 0.12f} }; case 3: return { {0, 3, 3, -2, 0.2f}, {0, 3, 4, 0, 0.14f}, {0, 1, 8, 0, 0.12f} }; case 4: return { {0, 2, 4, -1, 0.2f}, {0, 4, 5, 0, 0.14f}, {0, 1, 10, 0, 0.12f} }; case 6: return { {0, 3, 5, -1, 0.2f}, {0, 6, 10, 0, 0.14f}, {0, 1, 18, 0, 0.12f} }; case 8: return { {0, 5, 5, -3, 0.2f}, {0, 8, 10, 1, 0.14f}, {0, 3, 14, 2, 0.12f} }; case 12: return { {0, 7, 8, -4, 0.2f}, {0, 12, 17, 2, 0.14f}, {0, 5, 22, 4, 0.12f} }; case 16: return { {0, 8, 10, -5, 0.2f}, {0, 16, 24, 2, 0.14f}, {0, 6, 30, 5, 0.12f} }; case 24: return { {0, 11, 15, -7, 0.2f}, {0, 24, 38, 3, 0.14f}, {0, 9, 46, 8, 0.12f} }; default: // Interpolate for non-standard elevations if (elevationDp < 0) return GetShadowSpec(0); if (elevationDp > 24) return GetShadowSpec(24); // Find nearest standard elevation int lower = 0, upper = 1; int standards[] = {0, 1, 2, 3, 4, 6, 8, 12, 16, 24}; for (int i = 0; i < 9; i++) { if (standards[i] <= elevationDp && standards[i + 1] >= elevationDp) { lower = standards[i]; upper = standards[i + 1]; break; } } // Use nearest return GetShadowSpec((elevationDp - lower < upper - elevationDp) ? lower : upper); } } inline void DrawSoftShadow(ImDrawList* drawList, const ImRect& rect, ImU32 color, float blurRadius, const ImVec2& offset, float cornerRadius) { if (blurRadius <= 0 || (color & IM_COL32_A_MASK) == 0) return; // For ImGui, we'll simulate soft shadows using multiple semi-transparent layers // This is a performance-friendly approximation // In low-spec mode use only 1 layer instead of up to 8 const int numLayers = dragonx::ui::effects::isLowSpecMode() ? 1 : ImClamp((int)(blurRadius / 2), 2, 8); const float layerStep = blurRadius / numLayers; // Extract base alpha float baseAlpha = ((color >> IM_COL32_A_SHIFT) & 0xFF) / 255.0f; ImU32 baseColor = color & ~IM_COL32_A_MASK; for (int i = numLayers - 1; i >= 0; i--) { float expansion = layerStep * (i + 1); float alpha = baseAlpha * (1.0f - (float)i / numLayers) / numLayers; ImU32 layerColor = baseColor | (((ImU32)(alpha * 255)) << IM_COL32_A_SHIFT); ImRect expandedRect( rect.Min.x - expansion + offset.x, rect.Min.y - expansion + offset.y, rect.Max.x + expansion + offset.x, rect.Max.y + expansion + offset.y ); drawList->AddRectFilled(expandedRect.Min, expandedRect.Max, layerColor, cornerRadius + expansion * 0.5f); } } inline void DrawShadow(ImDrawList* drawList, const ImRect& rect, int elevationDp, float cornerRadius) { if (elevationDp <= 0) return; ShadowSpec spec = GetShadowSpec(elevationDp); // Shadow multiplier: light themes need stronger shadows for card depth, // dark themes rely more on surface color overlay for elevation. // Configurable via ui.toml [style] shadow-multiplier / shadow-multiplier-light. const float shadowMultiplier = schema::UI().isDarkTheme() ? schema::UI().drawElement("style", "shadow-multiplier").sizeOr(0.6f) : schema::UI().drawElement("style", "shadow-multiplier-light").sizeOr(1.0f); // Draw ambient shadow (largest, most diffuse) if (spec.ambient.opacity > 0) { ImU32 ambientColor = IM_COL32(0, 0, 0, (int)(spec.ambient.opacity * shadowMultiplier * 255)); ImRect ambientRect = rect; ambientRect.Expand(spec.ambient.spreadRadius); DrawSoftShadow(drawList, ambientRect, ambientColor, spec.ambient.blurRadius, ImVec2(spec.ambient.offsetX, spec.ambient.offsetY), cornerRadius); } // Draw penumbra (medium) if (spec.penumbra.opacity > 0) { ImU32 penumbraColor = IM_COL32(0, 0, 0, (int)(spec.penumbra.opacity * shadowMultiplier * 255)); ImRect penumbraRect = rect; penumbraRect.Expand(spec.penumbra.spreadRadius); DrawSoftShadow(drawList, penumbraRect, penumbraColor, spec.penumbra.blurRadius, ImVec2(spec.penumbra.offsetX, spec.penumbra.offsetY), cornerRadius); } // Draw umbra (sharpest, darkest) if (spec.umbra.opacity > 0) { ImU32 umbraColor = IM_COL32(0, 0, 0, (int)(spec.umbra.opacity * shadowMultiplier * 255)); ImRect umbraRect = rect; umbraRect.Expand(spec.umbra.spreadRadius); DrawSoftShadow(drawList, umbraRect, umbraColor, spec.umbra.blurRadius, ImVec2(spec.umbra.offsetX, spec.umbra.offsetY), cornerRadius); } } inline void DrawShadow(ImDrawList* drawList, const ImVec2& pos, const ImVec2& size, int elevationDp, float cornerRadius) { ImRect rect(pos, ImVec2(pos.x + size.x, pos.y + size.y)); DrawShadow(drawList, rect, elevationDp, cornerRadius); } inline ElevationAnimator::ElevationAnimator(int initialElevation) : m_current(static_cast(initialElevation)) , m_target(static_cast(initialElevation)) { } inline void ElevationAnimator::setTarget(int targetElevation) { m_target = static_cast(targetElevation); } inline void ElevationAnimator::update(float deltaTime) { if (m_current == m_target) return; float diff = m_target - m_current; float change = m_animationSpeed * deltaTime; if (std::abs(diff) <= change) { m_current = m_target; } else { m_current += (diff > 0 ? 1 : -1) * change; } } } // namespace material } // namespace ui } // namespace dragonx