Files
ObsidianDragon/src/ui/material/elevation.h
DanS 3aee55b49c ObsidianDragon - DragonX ImGui Wallet
Full-node GUI wallet for DragonX cryptocurrency.
Built with Dear ImGui, SDL3, and OpenGL3/DX11.

Features:
- Send/receive shielded and transparent transactions
- Autoshield with merged transaction display
- Built-in CPU mining (xmrig)
- Peer management and network monitoring
- Wallet encryption with PIN lock
- QR code generation for receive addresses
- Transaction history with pagination
- Console for direct RPC commands
- Cross-platform (Linux, Windows)
2026-02-27 00:26:01 -06:00

346 lines
12 KiB
C++

// 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 <cmath>
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<int>(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<float>(initialElevation))
, m_target(static_cast<float>(initialElevation))
{
}
inline void ElevationAnimator::setTarget(int targetElevation) {
m_target = static_cast<float>(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