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)
This commit is contained in:
2026-02-26 02:31:52 -06:00
commit 3aee55b49c
306 changed files with 177789 additions and 0 deletions

View File

@@ -0,0 +1,501 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include "layout.h"
#include "colors.h"
#include "typography.h"
namespace dragonx {
namespace ui {
namespace material {
// ============================================================================
// App Layout Manager
// ============================================================================
// Manages the overall application layout following Material Design patterns.
//
// Usage:
// // In your main render loop:
// auto& layout = AppLayout::instance();
// layout.beginFrame();
//
// // Render app bar
// if (layout.beginAppBar("DragonX Wallet")) {
// // App bar content (menu items, etc.)
// layout.endAppBar();
// }
//
// // Render navigation
// if (layout.beginNavigation()) {
// layout.navItem("Balance", ICON_WALLET, currentTab == 0);
// layout.navItem("Send", ICON_SEND, currentTab == 1);
// layout.endNavigation();
// }
//
// // Render main content
// if (layout.beginContent()) {
// // Your content here
// layout.endContent();
// }
//
// layout.endFrame();
class AppLayout {
public:
static AppLayout& instance() {
static AppLayout s_instance;
return s_instance;
}
// ========================================================================
// Frame Management
// ========================================================================
/**
* @brief Begin a new frame layout
*
* Call this at the start of each frame before any layout calls.
* Updates responsive breakpoints and calculates regions.
*/
void beginFrame();
/**
* @brief End the frame layout
*/
void endFrame();
// ========================================================================
// Layout Regions
// ========================================================================
/**
* @brief Begin the app bar region
*
* @param title App title to display
* @param showBack Show back button (for sub-pages)
* @return true if app bar is visible
*/
bool beginAppBar(const char* title, bool showBack = false);
void endAppBar();
/**
* @brief Begin the navigation region (drawer/rail/bottom)
*
* @return true if navigation region is visible
*/
bool beginNavigation();
void endNavigation();
/**
* @brief Render a navigation item
*
* @param label Item label
* @param icon Icon glyph (can be nullptr)
* @param selected Whether this item is currently selected
* @return true if clicked
*/
bool navItem(const char* label, const char* icon, bool selected);
/**
* @brief Add a navigation section divider
*
* @param title Optional section title
*/
void navSection(const char* title = nullptr);
/**
* @brief Begin the main content region
*
* @return true if content region is visible
*/
bool beginContent();
void endContent();
// ========================================================================
// Card Helpers
// ========================================================================
/**
* @brief Begin a Material Design card
*
* @param id Unique ID for the card
* @param layout Card layout configuration
* @return true if card is visible
*/
bool beginCard(const char* id, const CardLayout& layout = CardLayout());
void endCard();
// ========================================================================
// Layout Queries
// ========================================================================
/**
* @brief Get current breakpoint category
*/
breakpoint::Category getBreakpoint() const { return breakpoint_; }
/**
* @brief Get current navigation style
*/
breakpoint::NavStyle getNavStyle() const { return navStyle_; }
/**
* @brief Get content region available width
*/
float getContentWidth() const { return contentWidth_; }
/**
* @brief Get content region available height
*/
float getContentHeight() const { return contentHeight_; }
/**
* @brief Check if navigation drawer is expanded
*/
bool isNavExpanded() const { return navExpanded_; }
/**
* @brief Toggle navigation drawer expansion
*/
void toggleNav() { navExpanded_ = !navExpanded_; }
/**
* @brief Set navigation drawer expansion state
*/
void setNavExpanded(bool expanded) { navExpanded_ = expanded; }
private:
AppLayout();
~AppLayout() = default;
AppLayout(const AppLayout&) = delete;
AppLayout& operator=(const AppLayout&) = delete;
// Layout state
breakpoint::Category breakpoint_ = breakpoint::Category::Md;
breakpoint::NavStyle navStyle_ = breakpoint::NavStyle::NavDrawer;
float windowWidth_ = 0;
float windowHeight_ = 0;
float contentWidth_ = 0;
float contentHeight_ = 0;
bool navExpanded_ = true;
// Region tracking
bool inAppBar_ = false;
bool inNav_ = false;
bool inContent_ = false;
// Calculated regions
ImVec2 appBarPos_;
ImVec2 appBarSize_;
ImVec2 navPos_;
ImVec2 navSize_;
ImVec2 contentPos_;
ImVec2 contentSize_;
void calculateRegions();
};
// ============================================================================
// Inline Implementation
// ============================================================================
inline AppLayout::AppLayout() {
// Initialize with reasonable defaults
navExpanded_ = true;
}
inline void AppLayout::beginFrame() {
// Get main viewport size
ImGuiViewport* viewport = ImGui::GetMainViewport();
windowWidth_ = viewport->WorkSize.x;
windowHeight_ = viewport->WorkSize.y;
// Update responsive state
breakpoint_ = breakpoint::GetCategory(windowWidth_);
navStyle_ = breakpoint::GetNavStyle(breakpoint_);
// Auto-collapse nav on small screens
if (breakpoint_ == breakpoint::Category::Xs) {
navExpanded_ = false;
}
calculateRegions();
}
inline void AppLayout::endFrame() {
// Reset state
inAppBar_ = false;
inNav_ = false;
inContent_ = false;
}
inline void AppLayout::calculateRegions() {
// App bar at top
appBarPos_ = ImVec2(0, 0);
appBarSize_ = ImVec2(windowWidth_, size::AppBarHeight);
float belowAppBar = size::AppBarHeight;
float contentAreaHeight = windowHeight_ - belowAppBar;
// Navigation region
switch (navStyle_) {
case breakpoint::NavStyle::NavDrawer:
if (navExpanded_) {
navSize_ = ImVec2(size::NavDrawerWidth, contentAreaHeight);
} else {
navSize_ = ImVec2(size::NavRailWidth, contentAreaHeight);
}
navPos_ = ImVec2(0, belowAppBar);
break;
case breakpoint::NavStyle::NavRail:
navSize_ = ImVec2(size::NavRailWidth, contentAreaHeight);
navPos_ = ImVec2(0, belowAppBar);
break;
case breakpoint::NavStyle::BottomNav:
// Bottom nav handled separately
navSize_ = ImVec2(windowWidth_, size::NavItemHeight);
navPos_ = ImVec2(0, windowHeight_ - size::NavItemHeight);
contentAreaHeight -= size::NavItemHeight;
break;
}
// Content region
if (navStyle_ == breakpoint::NavStyle::BottomNav) {
contentPos_ = ImVec2(0, belowAppBar);
contentSize_ = ImVec2(windowWidth_, contentAreaHeight);
} else {
contentPos_ = ImVec2(navSize_.x, belowAppBar);
contentSize_ = ImVec2(windowWidth_ - navSize_.x, contentAreaHeight);
}
contentWidth_ = contentSize_.x;
contentHeight_ = contentSize_.y;
}
inline bool AppLayout::beginAppBar(const char* title, bool showBack) {
ImGui::SetNextWindowPos(appBarPos_);
ImGui::SetNextWindowSize(appBarSize_);
ImGuiWindowFlags flags =
ImGuiWindowFlags_NoTitleBar |
ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoScrollbar |
ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoBringToFrontOnFocus;
// Use elevated surface color for app bar
ImGui::PushStyleColor(ImGuiCol_WindowBg, SurfaceVec4(4));
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(size::AppBarPadding, 0));
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0);
bool visible = ImGui::Begin("##AppBar", nullptr, flags);
if (visible) {
inAppBar_ = true;
// Center content vertically
float centerY = (size::AppBarHeight - Typography::instance().getFont(TypeStyle::H6)->FontSize) * 0.5f;
ImGui::SetCursorPosY(centerY);
// Menu/back button
if (showBack) {
if (ImGui::Button("<")) {
// Back action - handled by caller
}
ImGui::SameLine();
} else if (navStyle_ != breakpoint::NavStyle::BottomNav) {
// Menu button to toggle nav
if (ImGui::Button("=")) {
toggleNav();
}
ImGui::SameLine();
}
// Title
Typography::instance().text(TypeStyle::H6, title);
}
return visible;
}
inline void AppLayout::endAppBar() {
ImGui::End();
ImGui::PopStyleVar(2);
ImGui::PopStyleColor();
inAppBar_ = false;
}
inline bool AppLayout::beginNavigation() {
if (navStyle_ == breakpoint::NavStyle::BottomNav) {
ImGui::SetNextWindowPos(navPos_);
} else {
ImGui::SetNextWindowPos(navPos_);
}
ImGui::SetNextWindowSize(navSize_);
ImGuiWindowFlags flags =
ImGuiWindowFlags_NoTitleBar |
ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoBringToFrontOnFocus;
// Nav drawer has higher elevation
int elevation = (navStyle_ == breakpoint::NavStyle::NavDrawer) ? 16 : 0;
ImGui::PushStyleColor(ImGuiCol_WindowBg, SurfaceVec4(elevation));
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, size::NavSectionPadding));
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0);
bool visible = ImGui::Begin("##Navigation", nullptr, flags);
if (visible) {
inNav_ = true;
}
return visible;
}
inline void AppLayout::endNavigation() {
ImGui::End();
ImGui::PopStyleVar(2);
ImGui::PopStyleColor();
inNav_ = false;
}
inline bool AppLayout::navItem(const char* label, const char* icon, bool selected) {
bool compact = !navExpanded_ || navStyle_ == breakpoint::NavStyle::NavRail;
float itemWidth = navSize_.x;
float itemHeight = size::NavItemHeight;
ImGui::PushID(label);
// Selection highlight
if (selected) {
ImVec2 pos = ImGui::GetCursorScreenPos();
ImDrawList* drawList = ImGui::GetWindowDrawList();
drawList->AddRectFilled(
pos,
ImVec2(pos.x + itemWidth, pos.y + itemHeight),
StateSelected()
);
}
// Padding
ImGui::SetCursorPosX(size::NavItemPadding);
// Content
bool clicked = false;
ImGui::BeginGroup();
if (compact) {
// Rail/collapsed: Icon only, centered
CenterHorizontally(size::IconSize);
clicked = ImGui::Selectable(icon ? icon : "?", selected, 0, ImVec2(size::IconSize, itemHeight));
} else {
// Drawer: Icon + label
if (icon) {
ImGui::Text("%s", icon);
ImGui::SameLine();
}
float selectableWidth = itemWidth - size::NavItemPadding * 2 - (icon ? size::IconSize + spacing::Sm : 0);
clicked = ImGui::Selectable(label, selected, 0, ImVec2(selectableWidth, itemHeight));
}
ImGui::EndGroup();
ImGui::PopID();
return clicked;
}
inline void AppLayout::navSection(const char* title) {
VSpace(1);
if (title && navExpanded_) {
ImGui::SetCursorPosX(size::NavItemPadding);
Typography::instance().textColored(TypeStyle::Caption, OnSurfaceMedium(), title);
}
// Divider
ImGui::Separator();
VSpace(1);
}
inline bool AppLayout::beginContent() {
ImGui::SetNextWindowPos(contentPos_);
ImGui::SetNextWindowSize(contentSize_);
ImGuiWindowFlags flags =
ImGuiWindowFlags_NoTitleBar |
ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoScrollbar |
ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoBringToFrontOnFocus;
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImGui::ColorConvertU32ToFloat4(Background()));
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(spacing::Md, spacing::Md));
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0);
bool visible = ImGui::Begin("##Content", nullptr, flags);
if (visible) {
inContent_ = true;
}
return visible;
}
inline void AppLayout::endContent() {
ImGui::End();
ImGui::PopStyleVar(2);
ImGui::PopStyleColor();
inContent_ = false;
}
inline bool AppLayout::beginCard(const char* id, const CardLayout& layout) {
float width = layout.width > 0 ? layout.width : ImGui::GetContentRegionAvail().x;
ImGui::PushID(id);
// Card background
ImGui::PushStyleColor(ImGuiCol_ChildBg, SurfaceVec4(layout.elevation));
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, layout.cornerRadius);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(layout.padding, layout.padding));
ImVec2 size(width, layout.minHeight > 0 ? layout.minHeight : 0);
bool visible = ImGui::BeginChild(id, size, ImGuiChildFlags_AutoResizeY);
return visible;
}
inline void AppLayout::endCard() {
ImGui::EndChild();
ImGui::PopStyleVar(2);
ImGui::PopStyleColor();
ImGui::PopID();
// Add spacing after card
VSpace(2);
}
// ============================================================================
// Convenience Function
// ============================================================================
/**
* @brief Get the app layout instance
*/
inline AppLayout& Layout() {
return AppLayout::instance();
}
} // namespace material
} // namespace ui
} // namespace dragonx

View File

@@ -0,0 +1,516 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#include "color_theme.h"
#include "../schema/ui_schema.h"
#include <algorithm>
#include <cmath>
namespace dragonx {
namespace ui {
namespace material {
// ============================================================================
// Color Utility Implementations
// ============================================================================
ImU32 BlendOverlay(ImU32 base, ImU32 overlay, float overlayOpacity)
{
// Extract components
float baseR = ((base >> IM_COL32_R_SHIFT) & 0xFF) / 255.0f;
float baseG = ((base >> IM_COL32_G_SHIFT) & 0xFF) / 255.0f;
float baseB = ((base >> IM_COL32_B_SHIFT) & 0xFF) / 255.0f;
float baseA = ((base >> IM_COL32_A_SHIFT) & 0xFF) / 255.0f;
float overlayR = ((overlay >> IM_COL32_R_SHIFT) & 0xFF) / 255.0f;
float overlayG = ((overlay >> IM_COL32_G_SHIFT) & 0xFF) / 255.0f;
float overlayB = ((overlay >> IM_COL32_B_SHIFT) & 0xFF) / 255.0f;
// Blend
float resultR = baseR + (overlayR - baseR) * overlayOpacity;
float resultG = baseG + (overlayG - baseG) * overlayOpacity;
float resultB = baseB + (overlayB - baseB) * overlayOpacity;
return IM_COL32(
static_cast<uint8_t>(resultR * 255.0f),
static_cast<uint8_t>(resultG * 255.0f),
static_cast<uint8_t>(resultB * 255.0f),
static_cast<uint8_t>(baseA * 255.0f)
);
}
ImU32 Lighten(ImU32 color, float amount)
{
return BlendOverlay(color, IM_COL32(255, 255, 255, 255), amount);
}
ImU32 Darken(ImU32 color, float amount)
{
return BlendOverlay(color, IM_COL32(0, 0, 0, 255), amount);
}
// ============================================================================
// Elevation System
// ============================================================================
// Material Design elevation overlay values for dark theme
// These values lighten the surface to indicate elevation
static const float kElevationOverlays[] = {
0.00f, // 0dp
0.05f, // 1dp
0.07f, // 2dp
0.08f, // 3dp
0.09f, // 4dp
0.10f, // 5dp
0.11f, // 6dp
0.115f, // 7dp
0.12f, // 8dp
0.125f, // 9dp
0.13f, // 10dp
0.135f, // 11dp
0.14f, // 12dp
0.14f, // 13dp
0.14f, // 14dp
0.14f, // 15dp
0.15f, // 16dp
0.15f, // 17dp
0.15f, // 18dp
0.15f, // 19dp
0.15f, // 20dp
0.15f, // 21dp
0.15f, // 22dp
0.15f, // 23dp
0.16f // 24dp
};
float GetElevationOverlay(Elevation level)
{
return GetElevationOverlay(static_cast<int>(level));
}
float GetElevationOverlay(int dp)
{
if (dp < 0) return 0.0f;
if (dp > 24) dp = 24;
return kElevationOverlays[dp];
}
ImU32 GetElevatedSurface(const ColorTheme& theme, Elevation level)
{
return GetElevatedSurface(theme, static_cast<int>(level));
}
ImU32 GetElevatedSurface(const ColorTheme& theme, int dp)
{
float overlay = GetElevationOverlay(dp);
if (overlay <= 0.0f) {
return theme.surface;
}
return BlendOverlay(theme.surface, IM_COL32(255, 255, 255, 255), overlay);
}
// ============================================================================
// Theme State
// ============================================================================
static ColorTheme s_currentTheme;
static bool s_themeInitialized = false;
static bool s_isDarkTheme = true;
static bool s_backdropActive = false;
// ============================================================================
// DragonX Theme (Dark with Red Primary)
// ============================================================================
ColorTheme GetDragonXColorTheme()
{
ColorTheme theme;
// Primary: DragonX Red
theme.primary = Hex(0xF64740); // Bright red accent
theme.primaryVariant = Hex(0x6F1A07); // Dark maroon variant
theme.primaryLight = Hex(0xF87A75); // Lighter red for highlights
// Secondary: Blue-gray from text palette
theme.secondary = Hex(0xBFD1E5); // Light blue-gray
theme.secondaryVariant = Hex(0x8BA3BD); // Mid blue-gray
theme.secondaryLight = Hex(0xDAE5F0); // Pale blue-gray
// Surfaces (Dark navy palette)
theme.background = Hex(0x121420); // Deep navy background
theme.surface = Hex(0x1B2432); // Dark navy surface
theme.surfaceVariant = Hex(0x253345); // Lighter navy variant
// "On" colors — blue-gray text tones
theme.onPrimary = Hex(0xFFFFFF); // White on red
theme.onSecondary = Hex(0x121420); // Navy on blue-gray
theme.onBackground = Hex(0xE2EDF8); // Bright blue-gray on dark
theme.onSurface = Hex(0xE2EDF8); // Bright blue-gray on surface
theme.onSurfaceMedium = HexA(0xE2EDF8, 179); // 70%
theme.onSurfaceDisabled = HexA(0xE2EDF8, 97); // 38%
// Error
theme.error = Hex(0xF64740); // Primary red
theme.onError = Hex(0xFFFFFF); // White on error
// Success/Warning (follow DragonX palette)
theme.success = Hex(0xBFD1E5); // Blue-gray secondary
theme.onSuccess = Hex(0x121420); // Dark on success
theme.warning = Hex(0xF64740); // Primary red
theme.onWarning = Hex(0xFFFFFF); // White on warning
// State overlays (blue-gray tinted for dark navy theme)
theme.stateHover = HexA(0xBFD1E5, 10); // 4%
theme.stateFocus = HexA(0xBFD1E5, 31); // 12%
theme.statePressed = HexA(0xBFD1E5, 25); // 10%
theme.stateSelected = HexA(0xBFD1E5, 20); // 8%
theme.stateDragged = HexA(0xBFD1E5, 20); // 8%
// UI elements
theme.divider = HexA(0xBFD1E5, 31); // 12% blue-gray
theme.outline = HexA(0xBFD1E5, 31); // 12% blue-gray
theme.scrim = HexA(0x000000, 128); // 50% black
return theme;
}
// ============================================================================
// Material Dark Theme
// ============================================================================
ColorTheme GetMaterialDarkTheme()
{
ColorTheme theme;
// Primary: Deep Purple (Material baseline)
theme.primary = Hex(0xBB86FC); // Purple 200
theme.primaryVariant = Hex(0x3700B3); // Purple 700
theme.primaryLight = Hex(0xE1BEE7); // Purple 100
// Secondary: Teal
theme.secondary = Hex(0x03DAC6); // Teal 200
theme.secondaryVariant = Hex(0x018786); // Teal 700
theme.secondaryLight = Hex(0x64FFDA); // Teal A200
// Surfaces
theme.background = Hex(0x121212);
theme.surface = Hex(0x1E1E1E);
theme.surfaceVariant = Hex(0x2D2D2D);
// "On" colors
theme.onPrimary = Hex(0x000000);
theme.onSecondary = Hex(0x000000);
theme.onBackground = Hex(0xFFFFFF);
theme.onSurface = Hex(0xFFFFFF);
theme.onSurfaceMedium = HexA(0xFFFFFF, 179);
theme.onSurfaceDisabled = HexA(0xFFFFFF, 97);
// Error
theme.error = Hex(0xF64740);
theme.onError = Hex(0xFFFFFF);
// Success/Warning (follow DragonX palette)
theme.success = Hex(0xBFD1E5);
theme.onSuccess = Hex(0x121420);
theme.warning = Hex(0xF64740);
theme.onWarning = Hex(0xFFFFFF);
// State overlays
theme.stateHover = HexA(0xFFFFFF, 10);
theme.stateFocus = HexA(0xFFFFFF, 31);
theme.statePressed = HexA(0xFFFFFF, 25);
theme.stateSelected = HexA(0xFFFFFF, 20);
theme.stateDragged = HexA(0xFFFFFF, 20);
// UI elements
theme.divider = HexA(0xFFFFFF, 31);
theme.outline = HexA(0xFFFFFF, 31);
theme.scrim = HexA(0x000000, 128);
return theme;
}
// ============================================================================
// Material Light Theme
// ============================================================================
ColorTheme GetMaterialLightTheme()
{
ColorTheme theme;
// Primary: Deep Purple
theme.primary = Hex(0x6200EE); // Purple 500
theme.primaryVariant = Hex(0x3700B3); // Purple 700
theme.primaryLight = Hex(0xBB86FC); // Purple 200
// Secondary: Teal
theme.secondary = Hex(0x03DAC6); // Teal 200
theme.secondaryVariant = Hex(0x018786); // Teal 700
theme.secondaryLight = Hex(0x64FFDA);
// Surfaces (light)
theme.background = Hex(0xFFFFFF);
theme.surface = Hex(0xFFFFFF);
theme.surfaceVariant = Hex(0xF5F5F5);
// "On" colors
theme.onPrimary = Hex(0xFFFFFF);
theme.onSecondary = Hex(0x000000);
theme.onBackground = Hex(0x000000);
theme.onSurface = Hex(0x000000);
theme.onSurfaceMedium = HexA(0x000000, 153); // 60%
theme.onSurfaceDisabled = HexA(0x000000, 97); // 38%
// Error
theme.error = Hex(0xF64740);
theme.onError = Hex(0xFFFFFF);
// Success/Warning (follow DragonX palette)
theme.success = Hex(0xBFD1E5); // Blue-gray
theme.onSuccess = Hex(0x121420);
theme.warning = Hex(0xF64740); // Primary red
theme.onWarning = Hex(0xFFFFFF);
// State overlays (black for light theme)
theme.stateHover = HexA(0x000000, 10);
theme.stateFocus = HexA(0x000000, 31);
theme.statePressed = HexA(0x000000, 25);
theme.stateSelected = HexA(0x000000, 20);
theme.stateDragged = HexA(0x000000, 20);
// UI elements
theme.divider = HexA(0x000000, 31);
theme.outline = HexA(0x000000, 31);
theme.scrim = HexA(0x000000, 128);
return theme;
}
// ============================================================================
// Theme Management
// ============================================================================
const ColorTheme& GetCurrentColorTheme()
{
if (!s_themeInitialized) {
s_currentTheme = GetDragonXColorTheme();
s_themeInitialized = true;
s_isDarkTheme = true;
}
return s_currentTheme;
}
void SetCurrentColorTheme(const ColorTheme& theme)
{
s_currentTheme = theme;
s_themeInitialized = true;
// Detect if dark theme based on background luminance
float bgR = ((theme.background >> IM_COL32_R_SHIFT) & 0xFF) / 255.0f;
float bgG = ((theme.background >> IM_COL32_G_SHIFT) & 0xFF) / 255.0f;
float bgB = ((theme.background >> IM_COL32_B_SHIFT) & 0xFF) / 255.0f;
float luminance = 0.299f * bgR + 0.587f * bgG + 0.114f * bgB;
s_isDarkTheme = (luminance < 0.5f);
}
bool IsDarkTheme()
{
GetCurrentColorTheme(); // Ensure initialized
return s_isDarkTheme;
}
void SetBackdropActive(bool active)
{
s_backdropActive = active;
}
bool IsBackdropActive()
{
return s_backdropActive;
}
// ============================================================================
// Apply Theme to ImGui
// ============================================================================
void ApplyColorThemeToImGui(const ColorTheme& theme)
{
SetCurrentColorTheme(theme);
ImGuiStyle& style = ImGui::GetStyle();
ImVec4* colors = style.Colors;
const auto& S = schema::UI();
// Helper to convert ImU32 to ImVec4
auto toVec4 = [](ImU32 col) -> ImVec4 {
return ImGui::ColorConvertU32ToFloat4(col);
};
// Backdrop transparency: when DWM Acrylic is active, make backgrounds
// semi-transparent so the blurred + noisy Acrylic material shows through.
// A dark blue gradient is drawn on the background draw list (in app.cpp)
// for a stylish tinted look while retaining blur + noise.
auto bdElem = [&](const char* key, float fb) {
float v = S.drawElement("backdrop", key).size;
return v >= 0 ? v : fb;
};
const float bgAlpha = s_backdropActive ? bdElem("background-alpha", 0.40f) : 1.0f;
// When acrylic is active, ChildBg must be fully transparent so the
// sharp background doesn't bleed through behind acrylic cards.
// DrawGlassPanel draws the blurred card fill directly.
const float surfAlpha = s_backdropActive ? 0.0f : 1.0f;
const float tabAlpha = s_backdropActive ? 0.75f : 1.0f;
// Background colors
colors[ImGuiCol_WindowBg] = toVec4(WithAlphaF(theme.background, bgAlpha));
colors[ImGuiCol_ChildBg] = toVec4(WithAlphaF(GetElevatedSurface(theme, 1), surfAlpha));
colors[ImGuiCol_PopupBg] = toVec4(WithAlphaF(GetElevatedSurface(theme, 8), 0.95f));
// Borders — dark themes use white outlines; light themes use dark outlines
if (s_isDarkTheme) {
colors[ImGuiCol_Border] = ImVec4(1.0f, 1.0f, 1.0f, 0.15f);
colors[ImGuiCol_BorderShadow] = ImVec4(0, 0, 0, 0.08f);
} else {
colors[ImGuiCol_Border] = ImVec4(0, 0, 0, 0.12f);
colors[ImGuiCol_BorderShadow] = ImVec4(0, 0, 0, 0.04f);
}
// Frame backgrounds (inputs, checkboxes) — glass-card in dark, subtle grey in light
if (s_isDarkTheme) {
colors[ImGuiCol_FrameBg] = ImVec4(1.0f, 1.0f, 1.0f, 0.07f);
colors[ImGuiCol_FrameBgHovered] = ImVec4(1.0f, 1.0f, 1.0f, 0.10f);
colors[ImGuiCol_FrameBgActive] = ImVec4(1.0f, 1.0f, 1.0f, 0.15f);
} else {
colors[ImGuiCol_FrameBg] = ImVec4(0, 0, 0, 0.04f);
colors[ImGuiCol_FrameBgHovered] = ImVec4(0, 0, 0, 0.07f);
colors[ImGuiCol_FrameBgActive] = ImVec4(0, 0, 0, 0.10f);
}
// Title bar
colors[ImGuiCol_TitleBg] = toVec4(WithAlphaF(GetElevatedSurface(theme, 4), surfAlpha));
colors[ImGuiCol_TitleBgActive] = toVec4(theme.primary);
colors[ImGuiCol_TitleBgCollapsed] = toVec4(WithAlphaF(GetElevatedSurface(theme, 4), 0.75f));
// Menu bar — fully transparent, no background
colors[ImGuiCol_MenuBarBg] = ImVec4(0, 0, 0, 0);
// Scrollbar — minimal style (dark overlays for light, white overlays for dark)
colors[ImGuiCol_ScrollbarBg] = ImVec4(0, 0, 0, 0);
if (s_isDarkTheme) {
colors[ImGuiCol_ScrollbarGrab] = ImVec4(1.0f, 1.0f, 1.0f, 15.0f / 255.0f);
colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(1.0f, 1.0f, 1.0f, 30.0f / 255.0f);
colors[ImGuiCol_ScrollbarGrabActive] = ImVec4(1.0f, 1.0f, 1.0f, 45.0f / 255.0f);
} else {
colors[ImGuiCol_ScrollbarGrab] = ImVec4(0, 0, 0, 0.10f);
colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0, 0, 0, 0.18f);
colors[ImGuiCol_ScrollbarGrabActive] = ImVec4(0, 0, 0, 0.25f);
}
// Checkmarks, sliders
colors[ImGuiCol_CheckMark] = toVec4(theme.primary);
colors[ImGuiCol_SliderGrab] = toVec4(theme.primary);
colors[ImGuiCol_SliderGrabActive] = toVec4(theme.primaryLight);
// Buttons — glass-style overlays (white on dark, dark on light)
if (s_isDarkTheme) {
colors[ImGuiCol_Button] = ImVec4(1.0f, 1.0f, 1.0f, 15.0f / 255.0f);
colors[ImGuiCol_ButtonHovered] = ImVec4(1.0f, 1.0f, 1.0f, 30.0f / 255.0f);
colors[ImGuiCol_ButtonActive] = ImVec4(1.0f, 1.0f, 1.0f, 45.0f / 255.0f);
} else {
colors[ImGuiCol_Button] = ImVec4(0, 0, 0, 0.05f);
colors[ImGuiCol_ButtonHovered] = ImVec4(0, 0, 0, 0.08f);
colors[ImGuiCol_ButtonActive] = ImVec4(0, 0, 0, 0.12f);
}
// Headers (collapsing headers, tree nodes, selectable, menu items)
colors[ImGuiCol_Header] = toVec4(WithAlphaF(theme.primary, 0.6f));
colors[ImGuiCol_HeaderHovered] = toVec4(WithAlphaF(theme.primary, 0.8f));
colors[ImGuiCol_HeaderActive] = toVec4(theme.primary);
// Separator
colors[ImGuiCol_Separator] = toVec4(theme.divider);
colors[ImGuiCol_SeparatorHovered] = toVec4(theme.primaryLight);
colors[ImGuiCol_SeparatorActive] = toVec4(theme.primary);
// Resize grip
colors[ImGuiCol_ResizeGrip] = toVec4(WithAlphaF(theme.primary, 0.25f));
colors[ImGuiCol_ResizeGripHovered] = toVec4(WithAlphaF(theme.primary, 0.67f));
colors[ImGuiCol_ResizeGripActive] = toVec4(WithAlphaF(theme.primary, 0.95f));
// Tabs
ImU32 tabBg = GetElevatedSurface(theme, 4);
colors[ImGuiCol_Tab] = toVec4(WithAlphaF(tabBg, tabAlpha));
colors[ImGuiCol_TabHovered] = toVec4(WithAlphaF(Lighten(theme.primary, 0.15f), tabAlpha));
colors[ImGuiCol_TabSelected] = toVec4(WithAlphaF(theme.primary, tabAlpha));
colors[ImGuiCol_TabSelectedOverline] = toVec4(theme.secondary);
colors[ImGuiCol_TabDimmed] = toVec4(WithAlphaF(Darken(tabBg, 0.2f), tabAlpha));
colors[ImGuiCol_TabDimmedSelected] = toVec4(WithAlphaF(theme.primary, tabAlpha * 0.7f));
// Plot colors
colors[ImGuiCol_PlotLines] = toVec4(theme.primary);
colors[ImGuiCol_PlotLinesHovered] = toVec4(theme.secondary);
colors[ImGuiCol_PlotHistogram] = toVec4(theme.primary);
colors[ImGuiCol_PlotHistogramHovered] = toVec4(theme.secondary);
// Tables
colors[ImGuiCol_TableHeaderBg] = toVec4(WithAlphaF(GetElevatedSurface(theme, 2), surfAlpha));
colors[ImGuiCol_TableBorderStrong] = toVec4(theme.outline);
colors[ImGuiCol_TableBorderLight] = toVec4(theme.divider);
colors[ImGuiCol_TableRowBg] = ImVec4(0, 0, 0, 0);
colors[ImGuiCol_TableRowBgAlt] = toVec4(WithAlphaF(theme.onSurface, 0.03f));
// Text
colors[ImGuiCol_Text] = toVec4(theme.onSurface);
colors[ImGuiCol_TextDisabled] = toVec4(theme.onSurfaceDisabled);
colors[ImGuiCol_TextSelectedBg] = toVec4(WithAlphaF(theme.primary, 0.43f));
// Drag/drop
colors[ImGuiCol_DragDropTarget] = toVec4(WithAlphaF(theme.secondary, 0.9f));
// Navigation highlight
colors[ImGuiCol_NavCursor] = ImVec4(0.0f, 0.0f, 0.0f, 0.0f);
colors[ImGuiCol_NavWindowingHighlight]= ImVec4(1, 1, 1, 0.7f);
colors[ImGuiCol_NavWindowingDimBg] = ImVec4(0.8f, 0.8f, 0.8f, 0.2f);
// Modal window dim
colors[ImGuiCol_ModalWindowDimBg] = toVec4(theme.scrim);
// Style adjustments (from UISchema)
auto brElem = S.drawElement("style", "border-radius");
style.WindowRounding = brElem.extraFloats.count("window") ? brElem.extraFloats.at("window") : 6.0f;
style.ChildRounding = brElem.extraFloats.count("child") ? brElem.extraFloats.at("child") : 4.0f;
style.FrameRounding = brElem.extraFloats.count("frame") ? brElem.extraFloats.at("frame") : 4.0f;
style.PopupRounding = brElem.extraFloats.count("popup") ? brElem.extraFloats.at("popup") : 4.0f;
style.ScrollbarRounding = brElem.extraFloats.count("scrollbar") ? brElem.extraFloats.at("scrollbar") : 4.0f;
style.GrabRounding = brElem.extraFloats.count("grab") ? brElem.extraFloats.at("grab") : 4.0f;
style.TabRounding = brElem.extraFloats.count("tab") ? brElem.extraFloats.at("tab") : 4.0f;
style.WindowTitleAlign = ImVec2(0.0f, 0.5f); // Left-aligned titles (Material)
auto sElem = [&](const char* key, float fb) {
float v = S.drawElement("style", key).size;
return v >= 0 ? v : fb;
};
style.WindowPadding = ImVec2(sElem("window-padding-x", 10.0f), sElem("window-padding-y", 10.0f));
style.FramePadding = ImVec2(sElem("frame-padding-x", 8.0f), sElem("frame-padding-y", 4.0f));
style.ItemSpacing = ImVec2(sElem("item-spacing-x", 8.0f), sElem("item-spacing-y", 6.0f));
style.ItemInnerSpacing = ImVec2(sElem("item-inner-spacing-x", 6.0f), sElem("item-inner-spacing-y", 4.0f));
style.IndentSpacing = sElem("indent-spacing", 20.0f);
style.ScrollbarSize = sElem("scrollbar-size", 6.0f);
style.GrabMinSize = sElem("grab-min-size", 8.0f);
auto bwElem = S.drawElement("style", "border-width");
style.WindowBorderSize = bwElem.extraFloats.count("window") ? bwElem.extraFloats.at("window") : 1.0f;
style.ChildBorderSize = bwElem.extraFloats.count("child") ? bwElem.extraFloats.at("child") : 1.0f;
style.PopupBorderSize = bwElem.extraFloats.count("popup") ? bwElem.extraFloats.at("popup") : 1.0f;
style.FrameBorderSize = bwElem.extraFloats.count("frame") ? bwElem.extraFloats.at("frame") : 0.0f;
style.TabBorderSize = 0.0f;
style.AntiAliasedLines = true;
style.AntiAliasedFill = true;
}
} // namespace material
} // namespace ui
} // namespace dragonx

View File

@@ -0,0 +1,284 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
// Material Design 2 Color Theme System
// Based on https://m2.material.io/design/color/the-color-system.html
#pragma once
#include "imgui.h"
#include <cstdint>
// Windows defines RGB as a macro - undefine it to avoid conflicts
#ifdef RGB
#undef RGB
#endif
namespace dragonx {
namespace ui {
namespace material {
// ============================================================================
// Color Utilities
// ============================================================================
/**
* @brief Convert ImU32 to ImVec4
*/
inline ImVec4 ColorU32ToVec4(ImU32 color) {
return ImGui::ColorConvertU32ToFloat4(color);
}
/**
* @brief Convert ImVec4 to ImU32
*/
inline ImU32 ColorVec4ToU32(const ImVec4& color) {
return ImGui::ColorConvertFloat4ToU32(color);
}
/**
* @brief Create color from RGB values (0-255)
*/
constexpr ImU32 MakeRGB(uint8_t r, uint8_t g, uint8_t b) {
return IM_COL32(r, g, b, 255);
}
/**
* @brief Create color from RGBA values (0-255)
*/
constexpr ImU32 MakeRGBA(uint8_t r, uint8_t g, uint8_t b, uint8_t a) {
return IM_COL32(r, g, b, a);
}
/**
* @brief Create color from hex value (0xRRGGBB)
*/
constexpr ImU32 Hex(uint32_t hex) {
return IM_COL32(
(hex >> 16) & 0xFF,
(hex >> 8) & 0xFF,
hex & 0xFF,
255
);
}
/**
* @brief Create color from hex value with alpha (0xRRGGBB, alpha 0-255)
*/
constexpr ImU32 HexA(uint32_t hex, uint8_t alpha) {
return IM_COL32(
(hex >> 16) & 0xFF,
(hex >> 8) & 0xFF,
hex & 0xFF,
alpha
);
}
/**
* @brief Apply alpha to existing color
*/
inline ImU32 WithAlpha(ImU32 color, uint8_t alpha) {
return (color & 0x00FFFFFF) | (static_cast<ImU32>(alpha) << 24);
}
/**
* @brief Apply alpha percentage (0.0 - 1.0) to existing color
*/
inline ImU32 WithAlphaF(ImU32 color, float alpha) {
return WithAlpha(color, static_cast<uint8_t>(alpha * 255.0f));
}
/**
* @brief Blend two colors with overlay opacity
*/
ImU32 BlendOverlay(ImU32 base, ImU32 overlay, float overlayOpacity);
/**
* @brief Lighten a color by percentage (0.0 - 1.0)
*/
ImU32 Lighten(ImU32 color, float amount);
/**
* @brief Darken a color by percentage (0.0 - 1.0)
*/
ImU32 Darken(ImU32 color, float amount);
// ============================================================================
// Material Design Color Theme
// ============================================================================
/**
* @brief Complete Material Design 2 color theme
*
* Based on the Material Design color system with:
* - Primary and secondary brand colors
* - Surface and background colors
* - "On" colors for content on surfaces
* - State overlay colors
* - Error colors
*/
struct ColorTheme {
// ========================================================================
// Brand Colors - Primary
// ========================================================================
ImU32 primary; // Main brand color (displayed most frequently)
ImU32 primaryVariant; // Darker variant for contrast
ImU32 primaryLight; // Lighter variant for highlights
// ========================================================================
// Brand Colors - Secondary (Accent)
// ========================================================================
ImU32 secondary; // Accent color for emphasis
ImU32 secondaryVariant; // Darker variant
ImU32 secondaryLight; // Lighter variant
// ========================================================================
// Surface Colors
// ========================================================================
ImU32 background; // App background
ImU32 surface; // Card/dialog surfaces (elevation 0dp)
ImU32 surfaceVariant; // Alternative surface
// ========================================================================
// "On" Colors - Content colors for surfaces
// ========================================================================
ImU32 onPrimary; // Text/icons on primary color
ImU32 onSecondary; // Text/icons on secondary color
ImU32 onBackground; // Text/icons on background
ImU32 onSurface; // Text/icons on surface (high emphasis)
ImU32 onSurfaceMedium; // Medium emphasis text (70% opacity)
ImU32 onSurfaceDisabled; // Disabled text (38% opacity)
// ========================================================================
// Error Colors
// ========================================================================
ImU32 error; // Error indication
ImU32 onError; // Text on error color
// ========================================================================
// Success/Warning Colors (Material extension)
// ========================================================================
ImU32 success; // Success indication
ImU32 onSuccess; // Text on success
ImU32 warning; // Warning indication
ImU32 onWarning; // Text on warning
// ========================================================================
// State Overlay Colors (applied to surfaces)
// ========================================================================
ImU32 stateHover; // Hover overlay (4% white/black)
ImU32 stateFocus; // Focus overlay (12%)
ImU32 statePressed; // Pressed overlay (10%)
ImU32 stateSelected; // Selected overlay (8%)
ImU32 stateDragged; // Dragged overlay (8%)
// ========================================================================
// Additional UI Colors
// ========================================================================
ImU32 divider; // Divider lines
ImU32 outline; // Outline/border color
ImU32 scrim; // Modal overlay background
};
// ============================================================================
// Elevation System (Dark Theme)
// ============================================================================
/**
* @brief Material Design elevation levels
*
* In dark theme, elevation is shown through surface color lightening
* rather than shadows.
*/
enum class Elevation {
Dp0 = 0, // 0dp - Background
Dp1 = 1, // 1dp - Cards at rest, Search bar
Dp2 = 2, // 2dp - Buttons at rest
Dp3 = 3, // 3dp - Refresh indicator
Dp4 = 4, // 4dp - App bar
Dp6 = 6, // 6dp - FAB, Snackbar
Dp8 = 8, // 8dp - Bottom sheet, Menu, Cards picked up
Dp12 = 12, // 12dp - FAB pressed
Dp16 = 16, // 16dp - Navigation drawer, Modal sheets
Dp24 = 24 // 24dp - Dialog
};
/**
* @brief Get overlay opacity for elevation level (dark theme)
*
* Material Design uses white overlay on dark surfaces to indicate elevation.
* Higher elevation = more white overlay = lighter surface.
*/
float GetElevationOverlay(Elevation level);
float GetElevationOverlay(int dp);
/**
* @brief Calculate surface color for given elevation
*
* Applies white overlay to base surface color based on elevation.
*/
ImU32 GetElevatedSurface(const ColorTheme& theme, Elevation level);
ImU32 GetElevatedSurface(const ColorTheme& theme, int dp);
// ============================================================================
// Predefined Themes
// ============================================================================
/**
* @brief DragonX branded dark theme with red primary color
*/
ColorTheme GetDragonXColorTheme();
/**
* @brief Standard Material dark theme
*/
ColorTheme GetMaterialDarkTheme();
/**
* @brief Standard Material light theme
*/
ColorTheme GetMaterialLightTheme();
// ============================================================================
// Theme Management
// ============================================================================
/**
* @brief Get current active color theme
*/
const ColorTheme& GetCurrentColorTheme();
/**
* @brief Set current color theme
*/
void SetCurrentColorTheme(const ColorTheme& theme);
/**
* @brief Check if current theme is dark mode
*/
bool IsDarkTheme();
/**
* @brief Set whether OS-level backdrop (DWM Mica/Acrylic) is active
*
* When active, background and surface colors become semi-transparent
* so the compositor's blur effect shows through.
*/
void SetBackdropActive(bool active);
/**
* @brief Check if OS-level backdrop is active
*/
bool IsBackdropActive();
/**
* @brief Apply Material Design color theme to ImGui
*
* This updates ImGui's color palette to use the Material theme colors.
*/
void ApplyColorThemeToImGui(const ColorTheme& theme);
} // namespace material
} // namespace ui
} // namespace dragonx

201
src/ui/material/colors.h Normal file
View File

@@ -0,0 +1,201 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
// ============================================================================
// Material Design Color Tokens
// ============================================================================
//
// This header provides convenient access to Material Design color tokens.
// Use these instead of hardcoded colors to maintain theme consistency.
//
// Usage:
// #include "ui/material/colors.h"
//
// // Get surface colors at different elevations
// ImU32 cardBg = dragonx::ui::material::Surface(4); // 4dp elevation
//
// // Get state colors
// ImU32 hovered = dragonx::ui::material::Primary() | dragonx::ui::material::StateHover();
//
// // Use in ImGui
// ImGui::PushStyleColor(ImGuiCol_ChildBg, ImGui::ColorConvertU32ToFloat4(cardBg));
#pragma once
#include "color_theme.h"
#include "../schema/ui_schema.h"
namespace dragonx {
namespace ui {
namespace material {
// ============================================================================
// Color Token Accessors (Inline for performance)
// ============================================================================
// Primary colors
inline ImU32 Primary() { return GetCurrentColorTheme().primary; }
inline ImU32 PrimaryVariant() { return GetCurrentColorTheme().primaryVariant; }
inline ImU32 PrimaryLight() { return GetCurrentColorTheme().primaryLight; }
// Secondary colors
inline ImU32 Secondary() { return GetCurrentColorTheme().secondary; }
inline ImU32 SecondaryVariant() { return GetCurrentColorTheme().secondaryVariant; }
inline ImU32 SecondaryLight() { return GetCurrentColorTheme().secondaryLight; }
// Surface colors (semi-transparent when OS backdrop is active)
inline ImU32 Background() {
ImU32 bg = GetCurrentColorTheme().background;
if (!IsBackdropActive()) return bg;
float alpha = dragonx::ui::schema::UI().drawElement("backdrop", "background-inline-alpha").size;
if (alpha < 0) alpha = 0.30f;
return WithAlphaF(bg, alpha);
}
inline ImU32 Surface() {
ImU32 s = GetCurrentColorTheme().surface;
if (!IsBackdropActive()) return s;
float alpha = dragonx::ui::schema::UI().drawElement("backdrop", "surface-inline-alpha").size;
if (alpha < 0) alpha = 0.55f;
return WithAlphaF(s, alpha);
}
inline ImU32 SurfaceVariant() { return GetCurrentColorTheme().surfaceVariant; }
// Elevated surfaces - use for cards, dialogs, menus
inline ImU32 Surface(int dp) {
ImU32 s = GetElevatedSurface(GetCurrentColorTheme(), dp);
if (!IsBackdropActive()) return s;
float alpha = dragonx::ui::schema::UI().drawElement("backdrop", "surface-inline-alpha").size;
if (alpha < 0) alpha = 0.55f;
return WithAlphaF(s, alpha);
}
inline ImU32 SurfaceAt(Elevation level) { return GetElevatedSurface(GetCurrentColorTheme(), level); }
// "On" colors (text on various backgrounds)
inline ImU32 OnPrimary() { return GetCurrentColorTheme().onPrimary; }
inline ImU32 OnSecondary() { return GetCurrentColorTheme().onSecondary; }
inline ImU32 OnBackground() { return GetCurrentColorTheme().onBackground; }
inline ImU32 OnSurface() { return GetCurrentColorTheme().onSurface; }
inline ImU32 OnSurfaceMedium() { return GetCurrentColorTheme().onSurfaceMedium; }
inline ImU32 OnSurfaceDisabled() { return GetCurrentColorTheme().onSurfaceDisabled; }
// Semantic colors
inline ImU32 Error() { return GetCurrentColorTheme().error; }
inline ImU32 OnError() { return GetCurrentColorTheme().onError; }
inline ImU32 Success() { return GetCurrentColorTheme().success; }
inline ImU32 OnSuccess() { return GetCurrentColorTheme().onSuccess; }
inline ImU32 Warning() { return GetCurrentColorTheme().warning; }
inline ImU32 OnWarning() { return GetCurrentColorTheme().onWarning; }
// State overlay colors (use with BlendOverlay)
inline ImU32 StateHover() { return GetCurrentColorTheme().stateHover; }
inline ImU32 StateFocus() { return GetCurrentColorTheme().stateFocus; }
inline ImU32 StatePressed() { return GetCurrentColorTheme().statePressed; }
inline ImU32 StateSelected() { return GetCurrentColorTheme().stateSelected; }
inline ImU32 StateDragged() { return GetCurrentColorTheme().stateDragged; }
// UI structure
inline ImU32 Divider() { return GetCurrentColorTheme().divider; }
inline ImU32 Outline() { return GetCurrentColorTheme().outline; }
inline ImU32 Scrim() { return GetCurrentColorTheme().scrim; }
// ============================================================================
// ImVec4 Variants (for direct use with ImGui style colors)
// ============================================================================
inline ImVec4 PrimaryVec4() { return ImGui::ColorConvertU32ToFloat4(Primary()); }
inline ImVec4 SecondaryVec4() { return ImGui::ColorConvertU32ToFloat4(Secondary()); }
inline ImVec4 SurfaceVec4() { return ImGui::ColorConvertU32ToFloat4(Surface()); }
inline ImVec4 SurfaceVec4(int dp) { return ImGui::ColorConvertU32ToFloat4(Surface(dp)); }
inline ImVec4 OnSurfaceVec4() { return ImGui::ColorConvertU32ToFloat4(OnSurface()); }
inline ImVec4 ErrorVec4() { return ImGui::ColorConvertU32ToFloat4(Error()); }
inline ImVec4 SuccessVec4() { return ImGui::ColorConvertU32ToFloat4(Success()); }
inline ImVec4 WarningVec4() { return ImGui::ColorConvertU32ToFloat4(Warning()); }
// ============================================================================
// Convenience Functions for Common Patterns
// ============================================================================
/**
* @brief Get color with applied state overlay
*
* @param base Base color
* @param state State overlay (StateHover, StateFocus, etc.)
* @return Color with state applied
*
* Usage:
* ImU32 hoveredButton = WithState(Primary(), StateHover());
*/
inline ImU32 WithState(ImU32 base, ImU32 state)
{
return BlendOverlay(base, state, 1.0f);
}
/**
* @brief Get button color for current state
*
* @param isHovered Button is hovered
* @param isActive Button is pressed
* @return Appropriate button color
*/
inline ImU32 ButtonColor(bool isHovered, bool isActive)
{
if (isActive) return WithState(Primary(), StatePressed());
if (isHovered) return WithState(Primary(), StateHover());
return Primary();
}
/**
* @brief Get surface color for card at specified elevation
*
* @param elevation Elevation in dp (0-24)
* @return Surface color with elevation overlay
*/
inline ImU32 CardSurface(int elevation = 2)
{
return Surface(elevation);
}
/**
* @brief Get surface color for dialog/modal
*
* @return Surface color at 24dp elevation
*/
inline ImU32 DialogSurface()
{
return SurfaceAt(Elevation::Dp24);
}
/**
* @brief Get surface color for menu/popup
*
* @return Surface color at 8dp elevation
*/
inline ImU32 MenuSurface()
{
return SurfaceAt(Elevation::Dp8);
}
/**
* @brief Get surface color for app bar/toolbar
*
* @return Surface color at 4dp elevation
*/
inline ImU32 AppBarSurface()
{
return SurfaceAt(Elevation::Dp4);
}
/**
* @brief Get surface color for nav drawer
*
* @return Surface color at 16dp elevation
*/
inline ImU32 NavDrawerSurface()
{
return SurfaceAt(Elevation::Dp16);
}
} // namespace material
} // namespace ui
} // namespace dragonx

View File

@@ -0,0 +1,330 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include "../colors.h"
#include "../typography.h"
#include "../layout.h"
#include "../../schema/ui_schema.h"
#include "buttons.h"
#include "imgui.h"
#include "imgui_internal.h"
namespace dragonx {
namespace ui {
namespace material {
// ============================================================================
// Material Design App Bar Component
// ============================================================================
// Based on https://m2.material.io/components/app-bars-top
//
// The top app bar displays information and actions relating to the current
// screen.
enum class AppBarType {
Regular, // Standard height (56/64dp)
Prominent, // Extended height for larger titles
Dense // Smaller height for desktop
};
/**
* @brief App bar configuration
*/
struct AppBarSpec {
AppBarType type = AppBarType::Regular;
ImU32 backgroundColor = 0; // 0 = use elevated surface
bool elevated = true; // Show elevation
bool centerTitle = false; // Center title (default: left)
float elevation = 4.0f; // Elevation in dp
};
/**
* @brief Begin a top app bar
*
* @param id Unique identifier
* @param spec App bar configuration
* @return true if app bar is visible
*/
bool BeginAppBar(const char* id, const AppBarSpec& spec = AppBarSpec());
/**
* @brief End app bar
*/
void EndAppBar();
/**
* @brief Set app bar navigation icon (left side)
*
* @param icon Icon text (e.g., "☰" for menu)
* @param tooltip Optional tooltip
* @return true if clicked
*/
bool AppBarNavIcon(const char* icon, const char* tooltip = nullptr);
/**
* @brief Set app bar title
*/
void AppBarTitle(const char* title);
/**
* @brief Add app bar action button (right side)
*
* @param icon Icon text
* @param tooltip Optional tooltip
* @return true if clicked
*/
bool AppBarAction(const char* icon, const char* tooltip = nullptr);
/**
* @brief Begin app bar action menu (for overflow)
*/
bool BeginAppBarMenu(const char* icon);
/**
* @brief End app bar action menu
*/
void EndAppBarMenu();
/**
* @brief Add menu item to app bar menu
*/
bool AppBarMenuItem(const char* label, const char* icon = nullptr);
// ============================================================================
// Implementation
// ============================================================================
struct AppBarState {
ImVec2 barMin;
ImVec2 barMax;
float height;
float navIconWidth;
float actionsStartX;
float titleX;
bool hasNavIcon;
bool centerTitle;
ImU32 backgroundColor;
};
static AppBarState g_appBarState;
inline bool BeginAppBar(const char* id, const AppBarSpec& spec) {
ImGuiWindow* window = ImGui::GetCurrentWindow();
if (window->SkipItems)
return false;
ImGui::PushID(id);
ImGuiIO& io = ImGui::GetIO();
// Calculate height based on type
float barHeight;
switch (spec.type) {
case AppBarType::Prominent:
barHeight = 128.0f;
break;
case AppBarType::Dense:
barHeight = 48.0f;
break;
default:
barHeight = size::AppBarHeight; // 56dp
break;
}
g_appBarState.height = barHeight;
g_appBarState.hasNavIcon = false;
g_appBarState.centerTitle = spec.centerTitle;
g_appBarState.navIconWidth = 0;
g_appBarState.actionsStartX = io.DisplaySize.x; // Will be adjusted as actions added
// Bar position (always at top)
g_appBarState.barMin = ImVec2(0, 0);
g_appBarState.barMax = ImVec2(io.DisplaySize.x, barHeight);
// Background color
if (spec.backgroundColor != 0) {
g_appBarState.backgroundColor = spec.backgroundColor;
} else {
g_appBarState.backgroundColor = Surface(Elevation::Dp4);
}
// Draw app bar background
ImDrawList* drawList = window->DrawList;
drawList->AddRectFilled(g_appBarState.barMin, g_appBarState.barMax, g_appBarState.backgroundColor);
// Bottom divider/shadow
if (spec.elevated) {
drawList->AddLine(
ImVec2(g_appBarState.barMin.x, g_appBarState.barMax.y),
ImVec2(g_appBarState.barMax.x, g_appBarState.barMax.y),
schema::UI().resolveColor("var(--app-bar-shadow)", IM_COL32(0, 0, 0, 50))
);
}
// Set up layout
g_appBarState.titleX = spacing::dp(2); // Default title position
return true;
}
inline void EndAppBar() {
ImGui::PopID();
}
inline bool AppBarNavIcon(const char* icon, const char* tooltip) {
ImGuiWindow* window = ImGui::GetCurrentWindow();
if (window->SkipItems)
return false;
g_appBarState.hasNavIcon = true;
g_appBarState.navIconWidth = size::TouchTarget;
// Position nav icon on left
float iconX = spacing::dp(0.5f); // 4dp left margin
float centerY = g_appBarState.barMin.y + g_appBarState.height * 0.5f;
ImVec2 buttonPos(iconX, centerY - size::TouchTarget * 0.5f);
// Draw icon button
ImGui::SetCursorScreenPos(buttonPos);
bool clicked = IconButton(icon, tooltip);
// Update title position
g_appBarState.titleX = iconX + size::TouchTarget + spacing::dp(1);
return clicked;
}
inline void AppBarTitle(const char* title) {
ImGuiWindow* window = ImGui::GetCurrentWindow();
if (window->SkipItems)
return;
float centerY = g_appBarState.barMin.y + g_appBarState.height * 0.5f;
// Calculate title position
float titleX;
if (g_appBarState.centerTitle) {
// Center title between nav icon and actions
float availableWidth = g_appBarState.actionsStartX - g_appBarState.titleX;
Typography::instance().pushFont(TypeStyle::H6);
float titleWidth = ImGui::CalcTextSize(title).x;
Typography::instance().popFont();
titleX = g_appBarState.titleX + (availableWidth - titleWidth) * 0.5f;
} else {
titleX = g_appBarState.titleX;
}
// Render title
Typography::instance().pushFont(TypeStyle::H6);
float titleY = centerY - ImGui::GetFontSize() * 0.5f;
ImDrawList* drawList = window->DrawList;
drawList->AddText(ImVec2(titleX, titleY), OnSurface(), title);
Typography::instance().popFont();
}
inline bool AppBarAction(const char* icon, const char* tooltip) {
ImGuiWindow* window = ImGui::GetCurrentWindow();
if (window->SkipItems)
return false;
// Actions are positioned from right edge
g_appBarState.actionsStartX -= size::TouchTarget;
float centerY = g_appBarState.barMin.y + g_appBarState.height * 0.5f;
ImVec2 buttonPos(g_appBarState.actionsStartX, centerY - size::TouchTarget * 0.5f);
ImGui::SetCursorScreenPos(buttonPos);
return IconButton(icon, tooltip);
}
inline bool BeginAppBarMenu(const char* icon) {
ImGuiWindow* window = ImGui::GetCurrentWindow();
if (window->SkipItems)
return false;
// Position menu button
g_appBarState.actionsStartX -= size::TouchTarget;
float centerY = g_appBarState.barMin.y + g_appBarState.height * 0.5f;
ImVec2 buttonPos(g_appBarState.actionsStartX, centerY - size::TouchTarget * 0.5f);
ImGui::SetCursorScreenPos(buttonPos);
bool menuOpen = false;
if (IconButton(icon, "More options")) {
ImGui::OpenPopup("##appbar_menu");
}
// Style the popup menu
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, spacing::dp(1)));
ImGui::PushStyleVar(ImGuiStyleVar_PopupRounding, size::MenuCornerRadius);
ImGui::PushStyleColor(ImGuiCol_PopupBg, ImGui::ColorConvertU32ToFloat4(Surface(Elevation::Dp8)));
if (ImGui::BeginPopup("##appbar_menu")) {
menuOpen = true;
}
return menuOpen;
}
inline void EndAppBarMenu() {
ImGui::EndPopup();
ImGui::PopStyleColor();
ImGui::PopStyleVar(2);
}
inline bool AppBarMenuItem(const char* label, const char* icon) {
ImGuiWindow* window = ImGui::GetCurrentWindow();
const float itemHeight = size::ListItemHeight;
const float itemWidth = 200.0f; // Menu min width
ImVec2 pos = window->DC.CursorPos;
ImRect itemBB(pos, ImVec2(pos.x + itemWidth, pos.y + itemHeight));
ImGuiID id = window->GetID(label);
ImGui::ItemSize(itemBB);
if (!ImGui::ItemAdd(itemBB, id))
return false;
bool hovered, held;
bool pressed = ImGui::ButtonBehavior(itemBB, id, &hovered, &held);
// Draw
ImDrawList* drawList = window->DrawList;
if (hovered) {
drawList->AddRectFilled(itemBB.Min, itemBB.Max, schema::UI().resolveColor("var(--hover-overlay)", IM_COL32(255, 255, 255, 10)));
}
float contentX = pos.x + spacing::dp(2);
float centerY = pos.y + itemHeight * 0.5f;
// Icon
if (icon) {
drawList->AddText(ImVec2(contentX, centerY - 12.0f), OnSurfaceMedium(), icon);
contentX += 24.0f + spacing::dp(2);
}
// Label
Typography::instance().pushFont(TypeStyle::Body1);
float labelY = centerY - ImGui::GetFontSize() * 0.5f;
drawList->AddText(ImVec2(contentX, labelY), OnSurface(), label);
Typography::instance().popFont();
if (pressed) {
ImGui::CloseCurrentPopup();
}
return pressed;
}
} // namespace material
} // namespace ui
} // namespace dragonx

View File

@@ -0,0 +1,351 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include "../colors.h"
#include "../typography.h"
#include "../layout.h"
#include "imgui.h"
#include "imgui_internal.h"
namespace dragonx {
namespace ui {
namespace material {
// ============================================================================
// Material Design Button Components
// ============================================================================
// Based on https://m2.material.io/components/buttons
//
// Three button variants:
// - Text Button: Low emphasis, no container
// - Outlined Button: Medium emphasis, border only
// - Contained Button: High emphasis, filled background
enum class ButtonStyle {
Text, // Low emphasis - text only
Outlined, // Medium emphasis - border
Contained // High emphasis - filled (default)
};
/**
* @brief Button configuration
*/
struct ButtonSpec {
ButtonStyle style = ButtonStyle::Contained;
bool enabled = true;
bool fullWidth = false;
const char* icon = nullptr; // Leading icon (text glyph)
ImU32 color = 0; // 0 = use primary color
};
/**
* @brief Render a Material Design button
*
* @param label Button text (will be uppercased per Material spec)
* @param spec Button configuration
* @return true if clicked
*/
bool Button(const char* label, const ButtonSpec& spec = ButtonSpec());
/**
* @brief Render a text-only button (low emphasis)
*/
inline bool TextButton(const char* label, bool enabled = true) {
ButtonSpec spec;
spec.style = ButtonStyle::Text;
spec.enabled = enabled;
return Button(label, spec);
}
/**
* @brief Render an outlined button (medium emphasis)
*/
inline bool OutlinedButton(const char* label, bool enabled = true) {
ButtonSpec spec;
spec.style = ButtonStyle::Outlined;
spec.enabled = enabled;
return Button(label, spec);
}
/**
* @brief Render a contained/filled button (high emphasis)
*/
inline bool ContainedButton(const char* label, bool enabled = true) {
ButtonSpec spec;
spec.style = ButtonStyle::Contained;
spec.enabled = enabled;
return Button(label, spec);
}
/**
* @brief Render an icon button (circular touch target)
*
* @param icon Icon glyph/character
* @param tooltip Hover tooltip text
* @param enabled Whether button is enabled
* @return true if clicked
*/
bool IconButton(const char* icon, const char* tooltip = nullptr, bool enabled = true);
/**
* @brief Render a Floating Action Button (FAB)
*
* @param icon Icon glyph
* @param label Optional label for extended FAB
* @param mini Use mini size (40dp instead of 56dp)
* @return true if clicked
*/
bool FAB(const char* icon, const char* label = nullptr, bool mini = false);
// ============================================================================
// Implementation
// ============================================================================
inline bool Button(const char* label, const ButtonSpec& spec) {
ImGuiWindow* window = ImGui::GetCurrentWindow();
if (window->SkipItems)
return false;
ImGuiContext& g = *GImGui;
const ImGuiStyle& style = g.Style;
const ImGuiID id = window->GetID(label);
// Convert label to uppercase for Material Design
char upperLabel[256];
{
const char* src = label;
char* dst = upperLabel;
char* end = upperLabel + sizeof(upperLabel) - 1;
while (*src && dst < end) {
*dst++ = (char)toupper((unsigned char)*src++);
}
*dst = '\0';
}
// Get button font
ImFont* buttonFont = Typography::instance().button();
ImGui::PushFont(buttonFont);
// Calculate button size
ImVec2 labelSize = ImGui::CalcTextSize(upperLabel);
float iconWidth = spec.icon ? (size::IconSize + size::ButtonIconGap) : 0.0f;
ImVec2 buttonSize;
buttonSize.x = spec.fullWidth ? ImGui::GetContentRegionAvail().x
: ImMax(labelSize.x + iconWidth + size::ButtonPaddingH * 2, size::ButtonMinWidth);
buttonSize.y = size::ButtonHeight;
// Get position
ImVec2 pos = window->DC.CursorPos;
ImRect bb(pos, ImVec2(pos.x + buttonSize.x, pos.y + buttonSize.y));
ImGui::ItemSize(buttonSize, style.FramePadding.y);
if (!ImGui::ItemAdd(bb, id)) {
ImGui::PopFont();
return false;
}
// Handle interaction
bool hovered, held;
bool pressed = ImGui::ButtonBehavior(bb, id, &hovered, &held, 0);
if (!spec.enabled) {
hovered = held = false;
}
if (hovered) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
// Get colors
ImU32 primaryColor = spec.color ? spec.color : Primary();
ImU32 textColor;
ImU32 bgColor;
ImU32 borderColor = 0;
switch (spec.style) {
case ButtonStyle::Text:
bgColor = 0; // No background
textColor = spec.enabled ? primaryColor : OnSurfaceDisabled();
if (hovered) bgColor = BlendOverlay(Surface(), StateHover(), 1.0f);
if (held) bgColor = BlendOverlay(Surface(), StatePressed(), 1.0f);
break;
case ButtonStyle::Outlined:
bgColor = 0; // No background
textColor = spec.enabled ? primaryColor : OnSurfaceDisabled();
borderColor = spec.enabled ? primaryColor : OnSurfaceDisabled();
if (hovered) bgColor = BlendOverlay(Surface(), StateHover(), 1.0f);
if (held) bgColor = BlendOverlay(Surface(), StatePressed(), 1.0f);
break;
case ButtonStyle::Contained:
default:
if (spec.enabled) {
bgColor = primaryColor;
textColor = OnPrimary();
if (hovered) bgColor = BlendOverlay(primaryColor, StateHover(), 1.0f);
if (held) bgColor = BlendOverlay(primaryColor, StatePressed(), 1.0f);
} else {
bgColor = WithAlphaF(OnSurface(), 0.12f);
textColor = OnSurfaceDisabled();
}
break;
}
// Render
ImDrawList* drawList = window->DrawList;
// Background
if (bgColor) {
drawList->AddRectFilled(bb.Min, bb.Max, bgColor, size::ButtonCornerRadius);
}
// Border (for outlined)
if (borderColor) {
drawList->AddRect(bb.Min, bb.Max, borderColor, size::ButtonCornerRadius, 0, 1.0f);
}
// Icon
float contentX = bb.Min.x + size::ButtonPaddingH;
float contentY = bb.Min.y + (buttonSize.y - labelSize.y) * 0.5f;
if (spec.icon) {
ImFont* iconFont = Type().iconMed();
ImVec2 iconSize = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, spec.icon);
ImVec2 iconPos(contentX, bb.Min.y + (buttonSize.y - iconSize.y) * 0.5f);
drawList->AddText(iconFont, iconFont->LegacySize, iconPos, textColor, spec.icon);
contentX += iconSize.x + size::ButtonIconGap;
}
// Label
drawList->AddText(ImVec2(contentX, contentY), textColor, upperLabel);
ImGui::PopFont();
return pressed && spec.enabled;
}
inline bool IconButton(const char* icon, const char* tooltip, bool enabled) {
ImGuiWindow* window = ImGui::GetCurrentWindow();
if (window->SkipItems)
return false;
ImGuiContext& g = *GImGui;
const ImGuiID id = window->GetID(icon);
ImVec2 pos = window->DC.CursorPos;
ImVec2 size(size::IconButtonSize, size::IconButtonSize);
ImRect bb(pos, ImVec2(pos.x + size.x, pos.y + size.y));
ImGui::ItemSize(size);
if (!ImGui::ItemAdd(bb, id))
return false;
bool hovered, held;
bool pressed = ImGui::ButtonBehavior(bb, id, &hovered, &held, 0);
if (!enabled) {
hovered = held = false;
}
if (hovered) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
// Render ripple/hover circle
ImDrawList* drawList = window->DrawList;
ImVec2 center = bb.GetCenter();
float radius = size::IconButtonSize * 0.5f;
if (hovered || held) {
ImU32 overlayColor = held ? StatePressed() : StateHover();
drawList->AddCircleFilled(center, radius, overlayColor);
}
// Render icon (use icon font)
ImFont* iconFont = Type().iconMed();
ImU32 iconColor = enabled ? OnSurface() : OnSurfaceDisabled();
ImVec2 iconSize = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, icon);
ImVec2 iconPos(center.x - iconSize.x * 0.5f, center.y - iconSize.y * 0.5f);
drawList->AddText(iconFont, iconFont->LegacySize, iconPos, iconColor, icon);
// Tooltip
if (tooltip && hovered) {
ImGui::SetTooltip("%s", tooltip);
}
return pressed && enabled;
}
inline bool FAB(const char* icon, const char* label, bool mini) {
ImGuiWindow* window = ImGui::GetCurrentWindow();
if (window->SkipItems)
return false;
ImGuiContext& g = *GImGui;
const ImGuiID id = window->GetID(icon);
bool extended = (label != nullptr);
float fabSize = mini ? size::FabMiniSize : size::FabSize;
ImVec2 buttonSize;
if (extended) {
ImVec2 labelSize = ImGui::CalcTextSize(label);
buttonSize.x = size::FabExtendedPadding * 2 + size::IconSize + spacing::Sm + labelSize.x;
buttonSize.y = size::FabExtendedHeight;
} else {
buttonSize.x = fabSize;
buttonSize.y = fabSize;
}
ImVec2 pos = window->DC.CursorPos;
ImRect bb(pos, ImVec2(pos.x + buttonSize.x, pos.y + buttonSize.y));
ImGui::ItemSize(buttonSize);
if (!ImGui::ItemAdd(bb, id))
return false;
bool hovered, held;
bool pressed = ImGui::ButtonBehavior(bb, id, &hovered, &held, 0);
if (hovered) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
// Colors
ImU32 bgColor = Secondary();
ImU32 textColor = OnSecondary();
if (hovered) bgColor = BlendOverlay(bgColor, StateHover(), 1.0f);
if (held) bgColor = BlendOverlay(bgColor, StatePressed(), 1.0f);
// Render
ImDrawList* drawList = window->DrawList;
float radius = extended ? size::FabCornerRadius : (fabSize * 0.5f);
drawList->AddRectFilled(bb.Min, bb.Max, bgColor, radius);
// Icon
ImVec2 center = bb.GetCenter();
ImFont* iconFont = Type().iconMed();
if (extended) {
ImVec2 iconSize = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, icon);
float iconX = bb.Min.x + size::FabExtendedPadding;
float iconY = center.y - iconSize.y * 0.5f;
drawList->AddText(iconFont, iconFont->LegacySize, ImVec2(iconX, iconY), textColor, icon);
// Label
Typography::instance().pushFont(TypeStyle::Button);
float labelX = iconX + iconSize.x + spacing::Sm;
float labelY = center.y - ImGui::GetFontSize() * 0.5f;
drawList->AddText(ImVec2(labelX, labelY), textColor, label);
Typography::instance().popFont();
} else {
ImVec2 iconSize = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, icon);
ImVec2 iconPos(center.x - iconSize.x * 0.5f, center.y - iconSize.y * 0.5f);
drawList->AddText(iconFont, iconFont->LegacySize, iconPos, textColor, icon);
}
return pressed;
}
} // namespace material
} // namespace ui
} // namespace dragonx

View File

@@ -0,0 +1,214 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include "../colors.h"
#include "../typography.h"
#include "../layout.h"
#include "../draw_helpers.h"
#include "imgui.h"
namespace dragonx {
namespace ui {
namespace material {
// ============================================================================
// Material Design Card Component
// ============================================================================
// Based on https://m2.material.io/components/cards
//
// Cards contain content and actions about a single subject.
// They can be elevated (with shadow) or outlined (with border).
/**
* @brief Card configuration
*/
struct CardSpec {
int elevation = 1; // Elevation in dp (0-24)
bool outlined = false; // Use outline instead of elevation
float cornerRadius = 4.0f; // Corner radius in dp
bool clickable = false; // Make entire card clickable
float padding = 16.0f; // Content padding
float minHeight = 0.0f; // Minimum height (0 = auto)
};
/**
* @brief Begin a Material Design card
*
* @param id Unique identifier for the card
* @param spec Card configuration
* @return true if card is visible and content should be rendered
*/
bool BeginCard(const char* id, const CardSpec& spec = CardSpec());
/**
* @brief End the card
*/
void EndCard();
/**
* @brief Begin a clickable card that returns click state
*
* @param id Unique identifier
* @param spec Card configuration
* @param clicked Output: true if card was clicked
* @return true if card is visible
*/
bool BeginClickableCard(const char* id, const CardSpec& spec, bool* clicked);
/**
* @brief Card header with title and optional subtitle
*
* @param title Primary title text
* @param subtitle Optional secondary text
* @param avatar Optional avatar texture (rendered as circle)
*/
void CardHeader(const char* title, const char* subtitle = nullptr);
/**
* @brief Card supporting text/content
*
* @param text Body text content
*/
void CardContent(const char* text);
/**
* @brief Begin card action area (for buttons)
*
* Actions are typically placed at the bottom of the card.
*/
void CardActions();
/**
* @brief End card action area
*/
void CardActionsEnd();
/**
* @brief Add divider within card
*/
void CardDivider();
// ============================================================================
// Implementation
// ============================================================================
inline bool BeginCard(const char* id, const CardSpec& spec) {
ImGui::PushID(id);
// Calculate surface color based on elevation
ImU32 bgColor = spec.outlined ? Surface() : GetElevatedSurface(GetCurrentColorTheme(), spec.elevation);
// When acrylic backdrop is active, scale card bg alpha by UI opacity
// so cards smoothly transition from opaque (1.0) to see-through.
bool opaqueCards = dragonx::ui::effects::isLowSpecMode();
if (IsBackdropActive() && !opaqueCards) {
ImVec4 c = ImGui::ColorConvertU32ToFloat4(bgColor);
float uiOp = dragonx::ui::effects::ImGuiAcrylic::GetUIOpacity();
c.w *= uiOp;
ImGui::PushStyleColor(ImGuiCol_ChildBg, c);
} else {
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImGui::ColorConvertU32ToFloat4(bgColor));
}
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, spec.cornerRadius);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(spec.padding, spec.padding));
// Border for outlined variant
if (spec.outlined) {
ImGui::PushStyleColor(ImGuiCol_Border, ImGui::ColorConvertU32ToFloat4(Outline()));
ImGui::PushStyleVar(ImGuiStyleVar_ChildBorderSize, 1.0f);
} else {
ImGui::PushStyleVar(ImGuiStyleVar_ChildBorderSize, 0.0f);
}
ImVec2 size(0, spec.minHeight); // 0 width = use available width
ImGuiChildFlags flags = ImGuiChildFlags_AutoResizeY;
if (spec.outlined) {
flags |= ImGuiChildFlags_Borders;
}
bool visible = ImGui::BeginChild(id, size, flags);
return visible;
}
inline void EndCard() {
ImGui::EndChild();
ImGui::PopStyleVar(3); // ChildRounding, WindowPadding, ChildBorderSize
ImGui::PopStyleColor(1); // ChildBg
// Check if we used outline style (need to pop extra color)
// Note: We always push the border size var, handle outline color in BeginCard
ImGui::PopID();
// Add spacing after card
VSpace(2);
}
inline bool BeginClickableCard(const char* id, const CardSpec& spec, bool* clicked) {
*clicked = false;
ImGui::PushID(id);
ImVec2 startPos = ImGui::GetCursorScreenPos();
// Render card background
ImU32 bgColor = spec.outlined ? Surface() : GetElevatedSurface(GetCurrentColorTheme(), spec.elevation);
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImGui::ColorConvertU32ToFloat4(bgColor));
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, spec.cornerRadius);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(spec.padding, spec.padding));
ImGui::PushStyleVar(ImGuiStyleVar_ChildBorderSize, spec.outlined ? 1.0f : 0.0f);
if (spec.outlined) {
ImGui::PushStyleColor(ImGuiCol_Border, ImGui::ColorConvertU32ToFloat4(Outline()));
}
ImVec2 size(0, spec.minHeight);
ImGuiChildFlags flags = ImGuiChildFlags_AutoResizeY;
if (spec.outlined) {
flags |= ImGuiChildFlags_Borders;
}
bool visible = ImGui::BeginChild(id, size, flags);
return visible;
}
inline void CardHeader(const char* title, const char* subtitle) {
Typography::instance().text(TypeStyle::H6, title);
if (subtitle) {
Typography::instance().textColored(TypeStyle::Body2, OnSurfaceMedium(), subtitle);
}
VSpace(1);
}
inline void CardContent(const char* text) {
Typography::instance().textWrapped(TypeStyle::Body2, text);
VSpace(1);
}
inline void CardActions() {
ImGui::Separator();
VSpace(1);
ImGui::BeginGroup();
}
inline void CardActionsEnd() {
ImGui::EndGroup();
}
inline void CardDivider() {
ImGui::Separator();
VSpace(1);
}
} // namespace material
} // namespace ui
} // namespace dragonx

View File

@@ -0,0 +1,296 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include "../colors.h"
#include "../typography.h"
#include "../layout.h"
#include "../../schema/ui_schema.h"
#include "../../embedded/IconsMaterialDesign.h"
#include "imgui.h"
#include "imgui_internal.h"
namespace dragonx {
namespace ui {
namespace material {
// ============================================================================
// Material Design Chips Component
// ============================================================================
// Based on https://m2.material.io/components/chips
//
// Chips are compact elements that represent an input, attribute, or action.
enum class ChipType {
Input, // User input (deletable)
Choice, // Single selection from set
Filter, // Filter/checkbox style
Action // Triggers action
};
/**
* @brief Chip configuration
*/
struct ChipSpec {
ChipType type = ChipType::Action;
const char* label = nullptr;
const char* icon = nullptr; // Leading icon
const char* avatar = nullptr; // Avatar text (overrides icon)
ImU32 avatarColor = 0; // Avatar background color
bool selected = false; // For choice/filter chips
bool disabled = false;
bool outlined = false; // Outlined vs filled style
};
/**
* @brief Render a chip
*
* @param spec Chip configuration
* @return For filter/choice: true if clicked (toggle selection)
* For input: true if delete clicked
* For action: true if clicked
*/
bool Chip(const ChipSpec& spec);
/**
* @brief Simple action chip
*/
bool Chip(const char* label);
/**
* @brief Filter chip (toggleable)
*/
bool FilterChip(const char* label, bool* selected);
/**
* @brief Choice chip (radio-style)
*/
bool ChoiceChip(const char* label, bool selected);
/**
* @brief Input chip with delete
*/
bool InputChip(const char* label, const char* avatar = nullptr);
/**
* @brief Begin a chip group for layout
*/
void BeginChipGroup();
/**
* @brief End a chip group
*/
void EndChipGroup();
// ============================================================================
// Implementation
// ============================================================================
inline bool Chip(const ChipSpec& spec) {
ImGuiWindow* window = ImGui::GetCurrentWindow();
if (window->SkipItems)
return false;
ImGui::PushID(spec.label);
// Chip dimensions
const float chipHeight = 32.0f;
const float cornerRadius = chipHeight * 0.5f;
const float horizontalPadding = 12.0f;
const float iconSize = 18.0f;
const float avatarSize = 24.0f;
const float deleteIconSize = 18.0f;
// Calculate content width
float contentWidth = horizontalPadding * 2;
bool hasLeading = spec.icon || spec.avatar;
bool hasDelete = (spec.type == ChipType::Input);
bool hasCheckmark = (spec.type == ChipType::Filter && spec.selected);
if (spec.avatar) {
contentWidth += avatarSize + 8.0f;
} else if (spec.icon || hasCheckmark) {
contentWidth += iconSize + 8.0f;
}
contentWidth += ImGui::CalcTextSize(spec.label).x;
if (hasDelete) {
contentWidth += deleteIconSize + 8.0f;
}
// Layout
ImVec2 pos = window->DC.CursorPos;
ImRect chipBB(pos, ImVec2(pos.x + contentWidth, pos.y + chipHeight));
// Interaction
ImGuiID id = window->GetID("##chip");
ImGui::ItemSize(chipBB);
if (!ImGui::ItemAdd(chipBB, id))
return false;
bool hovered, held;
bool clicked = ImGui::ButtonBehavior(chipBB, id, &hovered, &held) && !spec.disabled;
// Delete button hit test (for input chips)
bool deleteClicked = false;
if (hasDelete) {
float deleteX = chipBB.Max.x - horizontalPadding - deleteIconSize;
ImRect deleteBB(
ImVec2(deleteX, pos.y + (chipHeight - deleteIconSize) * 0.5f),
ImVec2(deleteX + deleteIconSize, pos.y + (chipHeight + deleteIconSize) * 0.5f)
);
ImGuiID deleteId = window->GetID("##delete");
bool deleteHovered, deleteHeld;
deleteClicked = ImGui::ButtonBehavior(deleteBB, deleteId, &deleteHovered, &deleteHeld);
}
// Draw
ImDrawList* drawList = window->DrawList;
// Background
ImU32 bgColor;
ImU32 borderColor = 0;
if (spec.disabled) {
bgColor = schema::UI().resolveColor("var(--surface-hover)", IM_COL32(255, 255, 255, 30));
} else if (spec.selected) {
bgColor = WithAlpha(Primary(), 51); // Primary at 20%
} else if (spec.outlined) {
bgColor = 0; // Transparent
borderColor = OnSurfaceMedium();
} else {
bgColor = schema::UI().resolveColor("var(--surface-hover)", IM_COL32(255, 255, 255, 30));
}
// Hover/press overlay
if (!spec.disabled) {
if (held) {
bgColor = IM_COL32_ADD(bgColor, schema::UI().resolveColor("var(--hover-overlay)", IM_COL32(255, 255, 255, 25)));
} else if (hovered) {
bgColor = IM_COL32_ADD(bgColor, schema::UI().resolveColor("var(--active-overlay)", IM_COL32(255, 255, 255, 10)));
}
}
// Draw background
if (bgColor) {
drawList->AddRectFilled(chipBB.Min, chipBB.Max, bgColor, cornerRadius);
}
// Draw border for outlined
if (borderColor) {
drawList->AddRect(chipBB.Min, chipBB.Max, borderColor, cornerRadius, 0, 1.0f);
}
// Content
float currentX = pos.x + horizontalPadding;
float centerY = pos.y + chipHeight * 0.5f;
ImU32 contentColor = spec.disabled ? OnSurfaceDisabled() : OnSurface();
ImU32 iconColor = spec.disabled ? OnSurfaceDisabled() :
spec.selected ? Primary() : OnSurfaceMedium();
// Avatar or icon
if (spec.avatar) {
// Avatar circle
ImVec2 avatarCenter(currentX + avatarSize * 0.5f - 4.0f, centerY);
ImU32 avatarBg = spec.avatarColor ? spec.avatarColor : Primary();
drawList->AddCircleFilled(avatarCenter, avatarSize * 0.5f, avatarBg);
// Avatar text
ImVec2 textSize = ImGui::CalcTextSize(spec.avatar);
ImVec2 textPos(avatarCenter.x - textSize.x * 0.5f, avatarCenter.y - textSize.y * 0.5f);
drawList->AddText(textPos, OnPrimary(), spec.avatar);
currentX += avatarSize + 4.0f;
} else if (hasCheckmark) {
// Checkmark for selected filter chips
ImFont* iconFont = Typography::instance().iconSmall();
ImVec2 chkSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, ICON_MD_CHECK);
drawList->AddText(iconFont, iconFont->LegacySize,
ImVec2(currentX, centerY - chkSz.y * 0.5f), Primary(), ICON_MD_CHECK);
currentX += iconSize + 4.0f;
} else if (spec.icon) {
drawList->AddText(ImVec2(currentX, centerY - iconSize * 0.5f), iconColor, spec.icon);
currentX += iconSize + 4.0f;
}
// Label
Typography::instance().pushFont(TypeStyle::Body2);
float labelY = centerY - ImGui::GetFontSize() * 0.5f;
drawList->AddText(ImVec2(currentX, labelY), contentColor, spec.label);
Typography::instance().popFont();
// Delete icon (for input chips)
if (hasDelete) {
float deleteX = chipBB.Max.x - horizontalPadding - deleteIconSize;
ImFont* iconFont = Typography::instance().iconSmall();
ImVec2 delSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, ICON_MD_CLOSE);
drawList->AddText(iconFont, iconFont->LegacySize,
ImVec2(deleteX, centerY - delSz.y * 0.5f),
OnSurfaceMedium(), ICON_MD_CLOSE
);
}
ImGui::PopID();
// Return value depends on chip type
if (spec.type == ChipType::Input) {
return deleteClicked;
}
return clicked;
}
inline bool Chip(const char* label) {
ChipSpec spec;
spec.label = label;
spec.type = ChipType::Action;
return Chip(spec);
}
inline bool FilterChip(const char* label, bool* selected) {
ChipSpec spec;
spec.label = label;
spec.type = ChipType::Filter;
spec.selected = *selected;
bool clicked = Chip(spec);
if (clicked) {
*selected = !*selected;
}
return clicked;
}
inline bool ChoiceChip(const char* label, bool selected) {
ChipSpec spec;
spec.label = label;
spec.type = ChipType::Choice;
spec.selected = selected;
return Chip(spec);
}
inline bool InputChip(const char* label, const char* avatar) {
ChipSpec spec;
spec.label = label;
spec.type = ChipType::Input;
spec.avatar = avatar;
return Chip(spec);
}
inline void BeginChipGroup() {
ImGui::BeginGroup();
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(spacing::dp(1), spacing::dp(1))); // 8dp spacing
}
inline void EndChipGroup() {
ImGui::PopStyleVar();
ImGui::EndGroup();
}
} // namespace material
} // namespace ui
} // namespace dragonx

View File

@@ -0,0 +1,122 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
// ============================================================================
// Material Design Components - Unified Header
// ============================================================================
// Include this single header to get all Material Design components.
//
// Based on Material Design 2 (m2.material.io)
//
// All components are in the namespace: dragonx::ui::material
// Core dependencies
#include "../colors.h"
#include "../typography.h"
#include "../layout.h"
// Components
#include "buttons.h" // Button, IconButton, FAB
#include "cards.h" // Card, CardHeader, CardContent, CardActions
#include "text_fields.h" // TextField
#include "lists.h" // ListItem, ListDivider, ListSubheader
#include "dialogs.h" // Dialog, ConfirmDialog, AlertDialog
#include "inputs.h" // Switch, Checkbox, RadioButton
#include "progress.h" // LinearProgress, CircularProgress
#include "snackbar.h" // Snackbar, ShowSnackbar
#include "slider.h" // Slider, SliderDiscrete, SliderRange
#include "tabs.h" // TabBar, Tab
#include "chips.h" // Chip, FilterChip, ChoiceChip, InputChip
#include "nav_drawer.h" // NavDrawer, NavItem
#include "app_bar.h" // AppBar, AppBarTitle, AppBarAction
// ============================================================================
// Quick Reference
// ============================================================================
//
// BUTTONS:
// Button(label, spec) - Generic button with style config
// TextButton(label) - Text-only button
// OutlinedButton(label) - Button with outline
// ContainedButton(label) - Filled button (primary)
// IconButton(icon, tooltip) - Circular icon button
// FAB(icon) - Floating action button
//
// CARDS:
// BeginCard(spec)/EndCard() - Card container
// CardHeader(title, subtitle) - Card header section
// CardContent(text) - Card body text
// CardActions()/EndCardActions()- Card button area
//
// TEXT FIELDS:
// TextField(label, buf, size) - Text input field
// TextField(id, buf, size, spec)- Configurable text field
//
// LISTS:
// BeginList(id)/EndList() - List container
// ListItem(text) - Simple list item
// ListItem(primary, secondary) - Two-line item
// ListItem(spec) - Full config item
// ListDivider(inset) - Divider line
// ListSubheader(text) - Section header
//
// DIALOGS:
// BeginDialog(id, &open, spec) - Modal dialog
// EndDialog()
// ConfirmDialog(...) - Confirm/cancel dialog
// AlertDialog(...) - Single-action alert
//
// SELECTION CONTROLS:
// Switch(label, &value) - Toggle switch
// Checkbox(label, &value) - Checkbox
// RadioButton(label, active) - Radio button
// RadioButton(label, &sel, val) - Radio with int selection
//
// PROGRESS:
// LinearProgress(fraction) - Determinate progress bar
// LinearProgressIndeterminate() - Indeterminate progress bar
// CircularProgress(fraction) - Circular progress
// CircularProgressIndeterminate()- Spinner
//
// SNACKBAR:
// ShowSnackbar(msg, action) - Show notification
// DismissSnackbar() - Dismiss current snackbar
// RenderSnackbar() - Call each frame to render
//
// SLIDER:
// Slider(label, &val, min, max) - Continuous slider
// SliderInt(label, &val, ...) - Integer slider
// SliderDiscrete(...) - Stepped slider
// SliderRange(...) - Two-thumb range slider
//
// TABS:
// BeginTabBar(id, &idx) - Tab bar container
// Tab(label) - Tab item
// EndTabBar()
// TabBar(id, labels, count, &idx) - Simple tab bar
//
// CHIPS:
// Chip(label) - Action chip
// FilterChip(label, &selected) - Toggleable filter chip
// ChoiceChip(label, selected) - Choice chip
// InputChip(label, avatar) - Deletable input chip
// BeginChipGroup()/EndChipGroup()- Chip layout helper
//
// NAVIGATION DRAWER:
// BeginNavDrawer(id, &open, spec) - Navigation drawer
// EndNavDrawer()
// NavItem(icon, label, selected) - Navigation item
// NavDivider() - Drawer divider
// NavSubheader(text) - Section header
//
// APP BAR:
// BeginAppBar(id, spec) - Top app bar
// EndAppBar()
// AppBarNavIcon(icon) - Navigation icon (left)
// AppBarTitle(title) - App bar title
// AppBarAction(icon) - Action button (right)
// BeginAppBarMenu(icon) - Overflow menu
// AppBarMenuItem(label) - Menu item

View File

@@ -0,0 +1,293 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include "../colors.h"
#include "../typography.h"
#include "../layout.h"
#include "../../schema/ui_schema.h"
#include "buttons.h"
#include "imgui.h"
#include "imgui_internal.h"
namespace dragonx {
namespace ui {
namespace material {
// ============================================================================
// Material Design Dialog Component
// ============================================================================
// Based on https://m2.material.io/components/dialogs
//
// Dialogs inform users about a task and can contain critical information,
// require decisions, or involve multiple tasks.
/**
* @brief Dialog configuration
*/
struct DialogSpec {
const char* title = nullptr; // Dialog title
float width = 560.0f; // Dialog width (280-560dp typical)
float maxHeight = 0; // Max height (0 = auto)
bool scrollableContent = false; // Enable content scrolling
bool fullWidth = false; // Actions span full width
};
/**
* @brief Begin a modal dialog
*
* @param id Unique identifier
* @param open Pointer to open state (will be set false when closed)
* @param spec Dialog configuration
* @return true if dialog is open
*/
bool BeginDialog(const char* id, bool* open, const DialogSpec& spec = DialogSpec());
/**
* @brief End a dialog
*/
void EndDialog();
/**
* @brief Simple dialog with just text content
*/
bool BeginDialog(const char* id, bool* open, const char* title);
/**
* @brief Dialog content area (scrollable if configured)
*/
void BeginDialogContent();
/**
* @brief End dialog content area
*/
void EndDialogContent();
/**
* @brief Dialog actions area (buttons)
*/
void BeginDialogActions();
/**
* @brief End dialog actions area
*/
void EndDialogActions();
/**
* @brief Standard confirm dialog
*
* @param id Unique identifier
* @param open Pointer to open state
* @param title Dialog title
* @param message Dialog message
* @param confirmText Confirm button text
* @param cancelText Cancel button text (nullptr for no cancel)
* @return 0 = still open, 1 = confirmed, -1 = cancelled
*/
int ConfirmDialog(const char* id, bool* open, const char* title, const char* message,
const char* confirmText = "Confirm", const char* cancelText = "Cancel");
/**
* @brief Alert dialog (single action)
*
* @param id Unique identifier
* @param open Pointer to open state
* @param title Dialog title
* @param message Dialog message
* @param buttonText Button text
* @return true when dismissed
*/
bool AlertDialog(const char* id, bool* open, const char* title, const char* message,
const char* buttonText = "OK");
// ============================================================================
// Implementation
// ============================================================================
// Internal state for dialog rendering
struct DialogState {
ImVec2 contentMin;
ImVec2 contentMax;
float contentScrollY;
bool scrollable;
float width;
};
static DialogState g_currentDialog;
inline bool BeginDialog(const char* id, bool* open, const DialogSpec& spec) {
if (!*open)
return false;
// Center dialog on screen
ImGuiIO& io = ImGui::GetIO();
ImVec2 center = ImVec2(io.DisplaySize.x * 0.5f, io.DisplaySize.y * 0.5f);
ImGui::SetNextWindowPos(center, ImGuiCond_Always, ImVec2(0.5f, 0.5f));
// Set dialog size
float dialogWidth = spec.width;
ImGui::SetNextWindowSizeConstraints(
ImVec2(280.0f, 0), // Min size
ImVec2(dialogWidth, spec.maxHeight > 0 ? spec.maxHeight : io.DisplaySize.y * 0.9f)
);
// Style dialog
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, size::DialogCornerRadius);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImGui::ColorConvertU32ToFloat4(Surface(Elevation::Dp24)));
ImGui::PushStyleColor(ImGuiCol_PopupBg, ImGui::ColorConvertU32ToFloat4(Surface(Elevation::Dp24)));
// Modal background (scrim)
ImDrawList* bgDrawList = ImGui::GetBackgroundDrawList();
bgDrawList->AddRectFilled(
ImVec2(0, 0), io.DisplaySize,
schema::UI().resolveColor("var(--scrim)", IM_COL32(0, 0, 0, (int)(0.32f * 255)))
);
// Open popup
ImGui::OpenPopup(id);
bool isOpen = ImGui::BeginPopupModal(id, open,
ImGuiWindowFlags_AlwaysAutoResize |
ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoTitleBar);
if (isOpen) {
g_currentDialog.scrollable = spec.scrollableContent;
g_currentDialog.width = dialogWidth;
// Title
if (spec.title) {
ImGui::Dummy(ImVec2(0, spacing::dp(3))); // 24dp top padding
ImGui::SetCursorPosX(spacing::dp(3)); // 24dp left padding
Typography::instance().text(TypeStyle::H6, spec.title);
ImGui::Dummy(ImVec2(0, spacing::dp(2))); // 16dp below title
} else {
ImGui::Dummy(ImVec2(0, spacing::dp(2))); // 16dp top padding without title
}
}
ImGui::PopStyleColor(2);
ImGui::PopStyleVar(2);
return isOpen;
}
inline void EndDialog() {
ImGui::EndPopup();
}
inline bool BeginDialog(const char* id, bool* open, const char* title) {
DialogSpec spec;
spec.title = title;
return BeginDialog(id, open, spec);
}
inline void BeginDialogContent() {
ImGui::SetCursorPosX(spacing::dp(3)); // 24dp left padding
// Start content region
float maxWidth = g_currentDialog.width - spacing::dp(6); // 24dp padding each side
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + maxWidth);
if (g_currentDialog.scrollable) {
ImGui::BeginChild("##dialogContent", ImVec2(maxWidth, 200), false);
}
}
inline void EndDialogContent() {
if (g_currentDialog.scrollable) {
ImGui::EndChild();
}
ImGui::PopTextWrapPos();
ImGui::Dummy(ImVec2(0, spacing::dp(3))); // 24dp after content
}
inline void BeginDialogActions() {
// Actions area - right-aligned buttons with 8dp spacing
float contentWidth = ImGui::GetContentRegionAvail().x;
ImGui::SetCursorPosX(spacing::dp(1)); // 8dp left padding for actions
// Push style for action buttons
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(spacing::dp(1), 0)); // 8dp between buttons
// Right-align: use a dummy to push buttons right
// Buttons will be added inline with SameLine
}
inline void EndDialogActions() {
ImGui::PopStyleVar();
ImGui::Dummy(ImVec2(0, spacing::dp(1))); // 8dp bottom padding
}
inline int ConfirmDialog(const char* id, bool* open, const char* title, const char* message,
const char* confirmText, const char* cancelText) {
int result = 0;
if (BeginDialog(id, open, title)) {
BeginDialogContent();
Typography::instance().textWrapped(TypeStyle::Body1, message);
EndDialogContent();
BeginDialogActions();
// Calculate button positions for right alignment
float cancelWidth = cancelText ? ImGui::CalcTextSize(cancelText).x + spacing::dp(2) : 0;
float confirmWidth = ImGui::CalcTextSize(confirmText).x + spacing::dp(2);
float totalButtonWidth = cancelWidth + confirmWidth + (cancelText ? spacing::dp(1) : 0);
float startX = ImGui::GetContentRegionAvail().x - totalButtonWidth - spacing::dp(2);
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + startX);
if (cancelText) {
if (TextButton(cancelText)) {
*open = false;
result = -1;
}
ImGui::SameLine();
}
if (ContainedButton(confirmText)) {
*open = false;
result = 1;
}
EndDialogActions();
EndDialog();
}
return result;
}
inline bool AlertDialog(const char* id, bool* open, const char* title, const char* message,
const char* buttonText) {
bool dismissed = false;
if (BeginDialog(id, open, title)) {
BeginDialogContent();
Typography::instance().textWrapped(TypeStyle::Body1, message);
EndDialogContent();
BeginDialogActions();
// Right-align single button
float buttonWidth = ImGui::CalcTextSize(buttonText).x + spacing::dp(2);
float startX = ImGui::GetContentRegionAvail().x - buttonWidth - spacing::dp(2);
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + startX);
if (ContainedButton(buttonText)) {
*open = false;
dismissed = true;
}
EndDialogActions();
EndDialog();
}
return dismissed;
}
} // namespace material
} // namespace ui
} // namespace dragonx

View File

@@ -0,0 +1,414 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include "../colors.h"
#include "../typography.h"
#include "../layout.h"
#include "../../schema/ui_schema.h"
#include "imgui.h"
#include "imgui_internal.h"
namespace dragonx {
namespace ui {
namespace material {
// ============================================================================
// Material Design Input Controls
// ============================================================================
// Based on https://m2.material.io/components/selection-controls
//
// Selection controls allow users to complete tasks that involve making choices:
// - Switch: Toggle single option on/off
// - Checkbox: Select multiple options
// - Radio: Select one option from a set
// ============================================================================
// Switch
// ============================================================================
/**
* @brief Material Design switch (toggle)
*
* @param label Text label
* @param value Pointer to boolean value
* @param disabled If true, switch is non-interactive
* @return true if value changed
*/
bool Switch(const char* label, bool* value, bool disabled = false);
// ============================================================================
// Checkbox
// ============================================================================
/**
* @brief Checkbox state
*/
enum class CheckboxState {
Unchecked,
Checked,
Indeterminate // For parent with mixed children
};
/**
* @brief Material Design checkbox
*
* @param label Text label
* @param value Pointer to boolean value
* @param disabled If true, checkbox is non-interactive
* @return true if value changed
*/
bool Checkbox(const char* label, bool* value, bool disabled = false);
/**
* @brief Tri-state checkbox
*/
bool Checkbox(const char* label, CheckboxState* state, bool disabled = false);
// ============================================================================
// Radio Button
// ============================================================================
/**
* @brief Material Design radio button
*
* @param label Text label
* @param active true if this option is selected
* @param disabled If true, radio is non-interactive
* @return true if clicked (caller should update selection)
*/
bool RadioButton(const char* label, bool active, bool disabled = false);
/**
* @brief Radio button with int selection
*
* @param label Text label
* @param selection Pointer to current selection
* @param value Value this radio represents
* @return true if clicked
*/
bool RadioButton(const char* label, int* selection, int value, bool disabled = false);
// ============================================================================
// Implementation
// ============================================================================
inline bool Switch(const char* label, bool* value, bool disabled) {
ImGuiWindow* window = ImGui::GetCurrentWindow();
if (window->SkipItems)
return false;
ImGui::PushID(label);
// Switch dimensions (Material spec: 36x14 track, 20dp thumb)
const float trackWidth = 36.0f;
const float trackHeight = 14.0f;
const float thumbRadius = 10.0f; // 20dp diameter
const float thumbTravel = trackWidth - thumbRadius * 2;
// Calculate layout
ImVec2 pos = window->DC.CursorPos;
float labelWidth = ImGui::CalcTextSize(label).x;
float totalWidth = trackWidth + spacing::dp(2) + labelWidth;
float totalHeight = ImMax(trackHeight + 6.0f, size::TouchTarget); // Min 48dp touch target
ImRect bb(pos, ImVec2(pos.x + totalWidth, pos.y + totalHeight));
// Interaction
ImGuiID id = window->GetID("##switch");
ImGui::ItemSize(bb);
if (!ImGui::ItemAdd(bb, id))
return false;
bool hovered, held;
bool pressed = ImGui::ButtonBehavior(bb, id, &hovered, &held) && !disabled;
bool changed = false;
if (pressed) {
*value = !*value;
changed = true;
}
// Animation (simple snap for now)
float thumbX = *value ? (thumbTravel) : 0;
// Draw track
ImDrawList* drawList = window->DrawList;
float trackY = pos.y + totalHeight * 0.5f;
ImVec2 trackMin(pos.x, trackY - trackHeight * 0.5f);
ImVec2 trackMax(pos.x + trackWidth, trackY + trackHeight * 0.5f);
ImU32 trackColor;
if (disabled) {
trackColor = schema::UI().resolveColor("var(--switch-track-off)", IM_COL32(255, 255, 255, 30));
} else if (*value) {
trackColor = PrimaryVariant(); // Primary at 50% opacity
} else {
trackColor = schema::UI().resolveColor("var(--switch-track-on)", IM_COL32(255, 255, 255, 97));
}
drawList->AddRectFilled(trackMin, trackMax, trackColor, trackHeight * 0.5f);
// Draw thumb
ImVec2 thumbCenter(pos.x + thumbRadius + thumbX, trackY);
ImU32 thumbColor;
if (disabled) {
thumbColor = schema::UI().resolveColor("var(--switch-thumb-off)", IM_COL32(189, 189, 189, 255));
} else if (*value) {
thumbColor = Primary();
} else {
thumbColor = schema::UI().resolveColor("var(--switch-thumb-on)", IM_COL32(250, 250, 250, 255));
}
// Thumb shadow
drawList->AddCircleFilled(ImVec2(thumbCenter.x + 1, thumbCenter.y + 2), thumbRadius, schema::UI().resolveColor("var(--control-shadow)", IM_COL32(0, 0, 0, 60)));
drawList->AddCircleFilled(thumbCenter, thumbRadius, thumbColor);
// Hover ripple effect
if (hovered && !disabled) {
ImU32 ripple = *value ? WithAlpha(Primary(), 25) : schema::UI().resolveColor("var(--hover-overlay)", IM_COL32(255, 255, 255, 25));
drawList->AddCircleFilled(thumbCenter, thumbRadius + 12.0f, ripple);
}
// Draw label
ImVec2 labelPos(pos.x + trackWidth + spacing::dp(2), pos.y + (totalHeight - ImGui::GetFontSize()) * 0.5f);
ImU32 labelColor = disabled ? OnSurfaceDisabled() : OnSurface();
drawList->AddText(labelPos, labelColor, label);
ImGui::PopID();
return changed;
}
inline bool Checkbox(const char* label, bool* value, bool disabled) {
ImGuiWindow* window = ImGui::GetCurrentWindow();
if (window->SkipItems)
return false;
ImGui::PushID(label);
// Checkbox dimensions (18dp box, 48dp touch target)
const float boxSize = 18.0f;
// Calculate layout
ImVec2 pos = window->DC.CursorPos;
float labelWidth = ImGui::CalcTextSize(label).x;
float totalWidth = boxSize + spacing::dp(2) + labelWidth;
float totalHeight = size::TouchTarget;
ImRect bb(pos, ImVec2(pos.x + totalWidth, pos.y + totalHeight));
// Interaction
ImGuiID id = window->GetID("##checkbox");
ImGui::ItemSize(bb);
if (!ImGui::ItemAdd(bb, id))
return false;
bool hovered, held;
bool pressed = ImGui::ButtonBehavior(bb, id, &hovered, &held) && !disabled;
bool changed = false;
if (pressed) {
*value = !*value;
changed = true;
}
// Draw checkbox
ImDrawList* drawList = window->DrawList;
float centerY = pos.y + totalHeight * 0.5f;
ImVec2 boxMin(pos.x, centerY - boxSize * 0.5f);
ImVec2 boxMax(pos.x + boxSize, centerY + boxSize * 0.5f);
ImU32 boxColor, checkColor;
if (disabled) {
boxColor = OnSurfaceDisabled();
checkColor = schema::UI().resolveColor("var(--checkbox-check)", IM_COL32(0, 0, 0, 255));
} else if (*value) {
boxColor = Primary();
checkColor = OnPrimary();
} else {
boxColor = OnSurfaceMedium();
checkColor = OnPrimary();
}
if (*value) {
// Filled checkbox with checkmark
drawList->AddRectFilled(boxMin, boxMax, boxColor, 2.0f);
// Draw checkmark
ImVec2 checkStart(boxMin.x + 4, centerY);
ImVec2 checkMid(boxMin.x + 7, centerY + 3);
ImVec2 checkEnd(boxMin.x + 14, centerY - 4);
drawList->AddLine(checkStart, checkMid, checkColor, 2.0f);
drawList->AddLine(checkMid, checkEnd, checkColor, 2.0f);
} else {
// Empty checkbox border
drawList->AddRect(boxMin, boxMax, boxColor, 2.0f, 0, 2.0f);
}
// Hover ripple
if (hovered && !disabled) {
ImVec2 boxCenter((boxMin.x + boxMax.x) * 0.5f, centerY);
drawList->AddCircleFilled(boxCenter, boxSize,
*value ? WithAlpha(Primary(), 25) : schema::UI().resolveColor("var(--hover-overlay)", IM_COL32(255, 255, 255, 25)));
}
// Draw label
ImVec2 labelPos(pos.x + boxSize + spacing::dp(2), pos.y + (totalHeight - ImGui::GetFontSize()) * 0.5f);
ImU32 labelColor = disabled ? OnSurfaceDisabled() : OnSurface();
drawList->AddText(labelPos, labelColor, label);
ImGui::PopID();
return changed;
}
inline bool Checkbox(const char* label, CheckboxState* state, bool disabled) {
bool checked = (*state == CheckboxState::Checked);
bool indeterminate = (*state == CheckboxState::Indeterminate);
ImGuiWindow* window = ImGui::GetCurrentWindow();
if (window->SkipItems)
return false;
ImGui::PushID(label);
const float boxSize = 18.0f;
ImVec2 pos = window->DC.CursorPos;
float labelWidth = ImGui::CalcTextSize(label).x;
float totalWidth = boxSize + spacing::dp(2) + labelWidth;
float totalHeight = size::TouchTarget;
ImRect bb(pos, ImVec2(pos.x + totalWidth, pos.y + totalHeight));
ImGuiID id = window->GetID("##checkbox");
ImGui::ItemSize(bb);
if (!ImGui::ItemAdd(bb, id))
return false;
bool hovered, held;
bool pressed = ImGui::ButtonBehavior(bb, id, &hovered, &held) && !disabled;
bool changed = false;
if (pressed) {
// Cycle: Unchecked -> Checked -> Unchecked (indeterminate only set programmatically)
*state = (*state == CheckboxState::Checked) ? CheckboxState::Unchecked : CheckboxState::Checked;
changed = true;
}
ImDrawList* drawList = window->DrawList;
float centerY = pos.y + totalHeight * 0.5f;
ImVec2 boxMin(pos.x, centerY - boxSize * 0.5f);
ImVec2 boxMax(pos.x + boxSize, centerY + boxSize * 0.5f);
ImU32 boxColor = disabled ? OnSurfaceDisabled() : (checked || indeterminate) ? Primary() : OnSurfaceMedium();
if (checked || indeterminate) {
drawList->AddRectFilled(boxMin, boxMax, boxColor, 2.0f);
if (indeterminate) {
// Horizontal line for indeterminate
drawList->AddLine(
ImVec2(boxMin.x + 4, centerY),
ImVec2(boxMax.x - 4, centerY),
OnPrimary(), 2.0f
);
} else {
// Checkmark
ImVec2 checkStart(boxMin.x + 4, centerY);
ImVec2 checkMid(boxMin.x + 7, centerY + 3);
ImVec2 checkEnd(boxMin.x + 14, centerY - 4);
drawList->AddLine(checkStart, checkMid, OnPrimary(), 2.0f);
drawList->AddLine(checkMid, checkEnd, OnPrimary(), 2.0f);
}
} else {
drawList->AddRect(boxMin, boxMax, boxColor, 2.0f, 0, 2.0f);
}
if (hovered && !disabled) {
ImVec2 boxCenter((boxMin.x + boxMax.x) * 0.5f, centerY);
drawList->AddCircleFilled(boxCenter, boxSize, schema::UI().resolveColor("var(--hover-overlay)", IM_COL32(255, 255, 255, 25)));
}
ImVec2 labelPos(pos.x + boxSize + spacing::dp(2), pos.y + (totalHeight - ImGui::GetFontSize()) * 0.5f);
ImU32 labelColor = disabled ? OnSurfaceDisabled() : OnSurface();
drawList->AddText(labelPos, labelColor, label);
ImGui::PopID();
return changed;
}
inline bool RadioButton(const char* label, bool active, bool disabled) {
ImGuiWindow* window = ImGui::GetCurrentWindow();
if (window->SkipItems)
return false;
ImGui::PushID(label);
// Radio button dimensions (20dp outer, 10dp inner when selected)
const float outerRadius = 10.0f;
const float innerRadius = 5.0f;
ImVec2 pos = window->DC.CursorPos;
float labelWidth = ImGui::CalcTextSize(label).x;
float totalWidth = outerRadius * 2 + spacing::dp(2) + labelWidth;
float totalHeight = size::TouchTarget;
ImRect bb(pos, ImVec2(pos.x + totalWidth, pos.y + totalHeight));
ImGuiID id = window->GetID("##radio");
ImGui::ItemSize(bb);
if (!ImGui::ItemAdd(bb, id))
return false;
bool hovered, held;
bool pressed = ImGui::ButtonBehavior(bb, id, &hovered, &held) && !disabled;
ImDrawList* drawList = window->DrawList;
float centerY = pos.y + totalHeight * 0.5f;
ImVec2 center(pos.x + outerRadius, centerY);
ImU32 ringColor = disabled ? OnSurfaceDisabled() : active ? Primary() : OnSurfaceMedium();
// Outer ring
drawList->AddCircle(center, outerRadius, ringColor, 0, 2.0f);
// Inner dot when active
if (active) {
drawList->AddCircleFilled(center, innerRadius, ringColor);
}
// Hover ripple
if (hovered && !disabled) {
drawList->AddCircleFilled(center, outerRadius + 12.0f,
active ? WithAlpha(Primary(), 25) : schema::UI().resolveColor("var(--hover-overlay)", IM_COL32(255, 255, 255, 25)));
}
// Label
ImVec2 labelPos(pos.x + outerRadius * 2 + spacing::dp(2), pos.y + (totalHeight - ImGui::GetFontSize()) * 0.5f);
ImU32 labelColor = disabled ? OnSurfaceDisabled() : OnSurface();
drawList->AddText(labelPos, labelColor, label);
ImGui::PopID();
return pressed;
}
inline bool RadioButton(const char* label, int* selection, int value, bool disabled) {
bool active = (*selection == value);
if (RadioButton(label, active, disabled)) {
*selection = value;
return true;
}
return false;
}
} // namespace material
} // namespace ui
} // namespace dragonx

View File

@@ -0,0 +1,306 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include "../colors.h"
#include "../typography.h"
#include "../layout.h"
#include "../../schema/ui_schema.h"
#include "../../embedded/IconsMaterialDesign.h"
#include "imgui.h"
#include "imgui_internal.h"
namespace dragonx {
namespace ui {
namespace material {
// ============================================================================
// Material Design List Component
// ============================================================================
// Based on https://m2.material.io/components/lists
//
// Lists present content in a way that makes it easy to identify a specific
// item in a collection and act on it.
/**
* @brief List item configuration
*/
struct ListItemSpec {
const char* leadingIcon = nullptr; // Optional leading icon (text representation)
const char* leadingAvatar = nullptr; // Optional avatar text (for initials)
ImU32 leadingAvatarColor = 0; // Avatar background color (0 = primary)
bool leadingCheckbox = false; // Show checkbox instead of icon
bool checkboxChecked = false; // Checkbox state
const char* primaryText = nullptr; // Main text (required)
const char* secondaryText = nullptr; // Secondary text (optional)
const char* trailingIcon = nullptr; // Optional trailing icon
const char* trailingText = nullptr; // Optional trailing metadata text
bool selected = false; // Selected state (highlight)
bool disabled = false; // Disabled state
bool dividerBelow = false; // Draw divider below item
};
/**
* @brief Begin a list container
*
* @param id Unique identifier
* @param withPadding Add top/bottom padding
*/
void BeginList(const char* id, bool withPadding = true);
/**
* @brief End a list container
*/
void EndList();
/**
* @brief Render a list item
*
* @param spec Item configuration
* @return true if clicked
*/
bool ListItem(const ListItemSpec& spec);
/**
* @brief Simple single-line list item
*/
bool ListItem(const char* text);
/**
* @brief Two-line list item with primary and secondary text
*/
bool ListItem(const char* primary, const char* secondary);
/**
* @brief List divider (full width or inset)
*
* @param inset If true, indented to align with text
*/
void ListDivider(bool inset = false);
/**
* @brief List subheader text
*/
void ListSubheader(const char* text);
// ============================================================================
// Implementation
// ============================================================================
inline void BeginList(const char* id, bool withPadding) {
ImGui::PushID(id);
if (withPadding) {
ImGui::Dummy(ImVec2(0, spacing::dp(1))); // 8dp top padding
}
}
inline void EndList() {
ImGui::PopID();
}
inline bool ListItem(const ListItemSpec& spec) {
ImGuiWindow* window = ImGui::GetCurrentWindow();
if (window->SkipItems)
return false;
// Calculate item height based on content
bool hasSecondary = (spec.secondaryText != nullptr);
bool hasLeadingElement = (spec.leadingIcon || spec.leadingAvatar || spec.leadingCheckbox);
float itemHeight;
if (hasSecondary) {
itemHeight = size::ListItemTwoLineHeight; // 72dp for two-line
} else if (hasLeadingElement) {
itemHeight = size::ListItemHeight; // 56dp with leading element
} else {
itemHeight = 48.0f; // 48dp simple one-line
}
// Item dimensions
ImVec2 pos = window->DC.CursorPos;
float itemWidth = ImGui::GetContentRegionAvail().x;
ImRect bb(pos, ImVec2(pos.x + itemWidth, pos.y + itemHeight));
// Interaction
ImGuiID itemId = window->GetID(spec.primaryText);
ImGui::ItemSize(bb);
if (!ImGui::ItemAdd(bb, itemId))
return false;
bool hovered, held;
bool pressed = ImGui::ButtonBehavior(bb, itemId, &hovered, &held) && !spec.disabled;
// Draw background
ImDrawList* drawList = window->DrawList;
ImU32 bgColor = 0;
if (spec.selected) {
bgColor = PrimaryContainer();
} else if (held && !spec.disabled) {
bgColor = schema::UI().resolveColor("var(--hover-overlay)", IM_COL32(255, 255, 255, 25));
} else if (hovered && !spec.disabled) {
bgColor = schema::UI().resolveColor("var(--active-overlay)", IM_COL32(255, 255, 255, 10));
}
if (bgColor) {
drawList->AddRectFilled(bb.Min, bb.Max, bgColor);
}
// Layout positions
float leftPadding = spacing::dp(2); // 16dp left padding
float currentX = bb.Min.x + leftPadding;
float centerY = bb.Min.y + itemHeight * 0.5f;
// Draw leading element
if (spec.leadingAvatar) {
// Avatar circle with text
float avatarRadius = 20.0f; // 40dp diameter
ImVec2 avatarCenter(currentX + avatarRadius, centerY);
ImU32 avatarBg = spec.leadingAvatarColor ? spec.leadingAvatarColor : Primary();
drawList->AddCircleFilled(avatarCenter, avatarRadius, avatarBg);
// Avatar text (centered)
ImVec2 textSize = ImGui::CalcTextSize(spec.leadingAvatar);
ImVec2 textPos(avatarCenter.x - textSize.x * 0.5f, avatarCenter.y - textSize.y * 0.5f);
drawList->AddText(textPos, OnPrimary(), spec.leadingAvatar);
currentX += 40.0f + spacing::dp(2); // 40dp avatar + 16dp gap
} else if (spec.leadingIcon) {
// Icon
ImVec2 iconSize = ImGui::CalcTextSize(spec.leadingIcon);
float iconY = centerY - iconSize.y * 0.5f;
ImU32 iconColor = spec.disabled ? OnSurfaceDisabled() : OnSurfaceMedium();
drawList->AddText(ImVec2(currentX, iconY), iconColor, spec.leadingIcon);
currentX += 24.0f + spacing::dp(2); // 24dp icon + 16dp gap
} else if (spec.leadingCheckbox) {
// Checkbox (simplified visual)
float checkboxSize = 18.0f;
ImVec2 checkMin(currentX, centerY - checkboxSize * 0.5f);
ImVec2 checkMax(currentX + checkboxSize, centerY + checkboxSize * 0.5f);
if (spec.checkboxChecked) {
drawList->AddRectFilled(checkMin, checkMax, Primary(), 2.0f);
// Checkmark
ImFont* iconFont = Typography::instance().iconSmall();
ImVec2 chkSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, ICON_MD_CHECK);
drawList->AddText(iconFont, iconFont->LegacySize,
ImVec2(checkMin.x + (checkboxSize - chkSz.x) * 0.5f, checkMin.y + (checkboxSize - chkSz.y) * 0.5f),
OnPrimary(), ICON_MD_CHECK);
} else {
drawList->AddRect(checkMin, checkMax, OnSurfaceMedium(), 2.0f, 0, 2.0f);
}
currentX += checkboxSize + spacing::dp(2);
}
// Calculate text area
float rightPadding = spacing::dp(2); // 16dp right padding
float trailingSpace = 0;
if (spec.trailingIcon) trailingSpace += 24.0f + spacing::dp(1);
if (spec.trailingText) trailingSpace += ImGui::CalcTextSize(spec.trailingText).x + spacing::dp(1);
float textMaxX = bb.Max.x - rightPadding - trailingSpace;
// Draw text
ImU32 primaryColor = spec.disabled ? OnSurfaceDisabled() : OnSurface();
ImU32 secondaryColor = spec.disabled ? OnSurfaceDisabled() : OnSurfaceMedium();
if (hasSecondary) {
// Two-line layout
float primaryY = bb.Min.y + 16.0f;
float secondaryY = primaryY + 20.0f;
Typography::instance().pushFont(TypeStyle::Body1);
drawList->AddText(ImVec2(currentX, primaryY), primaryColor, spec.primaryText);
Typography::instance().popFont();
Typography::instance().pushFont(TypeStyle::Body2);
drawList->AddText(ImVec2(currentX, secondaryY), secondaryColor, spec.secondaryText);
Typography::instance().popFont();
} else {
// Single-line layout
Typography::instance().pushFont(TypeStyle::Body1);
float textY = centerY - Typography::instance().getFont(TypeStyle::Body1)->FontSize * 0.5f;
drawList->AddText(ImVec2(currentX, textY), primaryColor, spec.primaryText);
Typography::instance().popFont();
}
// Draw trailing elements
float trailingX = bb.Max.x - rightPadding;
if (spec.trailingText) {
ImVec2 textSize = ImGui::CalcTextSize(spec.trailingText);
trailingX -= textSize.x;
float textY = centerY - textSize.y * 0.5f;
drawList->AddText(ImVec2(trailingX, textY), secondaryColor, spec.trailingText);
trailingX -= spacing::dp(1);
}
if (spec.trailingIcon) {
ImVec2 iconSize = ImGui::CalcTextSize(spec.trailingIcon);
trailingX -= 24.0f;
float iconY = centerY - iconSize.y * 0.5f;
drawList->AddText(ImVec2(trailingX, iconY), OnSurfaceMedium(), spec.trailingIcon);
}
// Draw divider
if (spec.dividerBelow) {
float dividerY = bb.Max.y - 0.5f;
drawList->AddLine(
ImVec2(bb.Min.x + leftPadding, dividerY),
ImVec2(bb.Max.x, dividerY),
OnSurfaceDisabled()
);
}
return pressed;
}
inline bool ListItem(const char* text) {
ListItemSpec spec;
spec.primaryText = text;
return ListItem(spec);
}
inline bool ListItem(const char* primary, const char* secondary) {
ListItemSpec spec;
spec.primaryText = primary;
spec.secondaryText = secondary;
return ListItem(spec);
}
inline void ListDivider(bool inset) {
ImGuiWindow* window = ImGui::GetCurrentWindow();
if (window->SkipItems)
return;
float width = ImGui::GetContentRegionAvail().x;
float leftOffset = inset ? 72.0f : 0; // Align with text after avatar/icon
ImVec2 pos = window->DC.CursorPos;
ImDrawList* drawList = window->DrawList;
drawList->AddLine(
ImVec2(pos.x + leftOffset, pos.y),
ImVec2(pos.x + width, pos.y),
OnSurfaceDisabled()
);
ImGui::Dummy(ImVec2(width, 1.0f));
}
inline void ListSubheader(const char* text) {
ImGui::Dummy(ImVec2(0, spacing::dp(1))); // 8dp top padding
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + spacing::dp(2)); // 16dp left padding
Typography::instance().textColored(TypeStyle::Caption, Primary(), text);
ImGui::Dummy(ImVec2(0, spacing::dp(1))); // 8dp bottom padding
}
} // namespace material
} // namespace ui
} // namespace dragonx

View File

@@ -0,0 +1,379 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include "../colors.h"
#include "../typography.h"
#include "../layout.h"
#include "../../schema/ui_schema.h"
#include "imgui.h"
#include "imgui_internal.h"
namespace dragonx {
namespace ui {
namespace material {
// ============================================================================
// Material Design Navigation Drawer Component
// ============================================================================
// Based on https://m2.material.io/components/navigation-drawer
//
// Navigation drawers provide access to destinations in your app.
enum class NavDrawerType {
Standard, // Permanent, always visible
Modal, // Overlay with scrim, can be dismissed
Dismissible // Can be shown/hidden, no scrim
};
/**
* @brief Navigation drawer configuration
*/
struct NavDrawerSpec {
NavDrawerType type = NavDrawerType::Standard;
float width = 256.0f; // 256dp standard width
bool showDividerBottom = true; // Divider at bottom
const char* headerTitle = nullptr; // Optional header title
const char* headerSubtitle = nullptr;
};
/**
* @brief Navigation item configuration
*/
struct NavItemSpec {
const char* icon = nullptr; // Leading icon
const char* label = nullptr; // Item label (required)
bool selected = false; // Selected state
bool disabled = false;
int badgeCount = 0; // Badge (0 = no badge)
};
/**
* @brief Begin a navigation drawer
*
* @param id Unique identifier
* @param open Pointer to open state (for modal/dismissible)
* @param spec Drawer configuration
* @return true if drawer is visible
*/
bool BeginNavDrawer(const char* id, bool* open, const NavDrawerSpec& spec = NavDrawerSpec());
/**
* @brief Begin standard (always visible) navigation drawer
*/
bool BeginNavDrawer(const char* id, const NavDrawerSpec& spec = NavDrawerSpec());
/**
* @brief End navigation drawer
*/
void EndNavDrawer();
/**
* @brief Render a navigation item
*
* @param spec Item configuration
* @return true if clicked
*/
bool NavItem(const NavItemSpec& spec);
/**
* @brief Simple navigation item
*/
bool NavItem(const char* icon, const char* label, bool selected = false);
/**
* @brief Navigation divider
*/
void NavDivider();
/**
* @brief Navigation subheader
*/
void NavSubheader(const char* text);
// ============================================================================
// Implementation
// ============================================================================
struct NavDrawerState {
float width;
ImVec2 contentMin;
ImVec2 contentMax;
bool isModal;
};
static NavDrawerState g_navDrawerState;
inline bool BeginNavDrawer(const char* id, bool* open, const NavDrawerSpec& spec) {
// For modal drawers, check open state
if (spec.type == NavDrawerType::Modal && !*open) {
return false;
}
ImGuiWindow* window = ImGui::GetCurrentWindow();
if (window->SkipItems)
return false;
ImGui::PushID(id);
g_navDrawerState.width = spec.width;
g_navDrawerState.isModal = (spec.type == NavDrawerType::Modal);
ImGuiIO& io = ImGui::GetIO();
ImDrawList* drawList = window->DrawList;
// For modal, draw scrim and handle dismiss
if (spec.type == NavDrawerType::Modal) {
ImDrawList* bgDrawList = ImGui::GetBackgroundDrawList();
bgDrawList->AddRectFilled(
ImVec2(0, 0), io.DisplaySize,
schema::UI().resolveColor("var(--scrim)", IM_COL32(0, 0, 0, (int)(0.32f * 255)))
);
// Click outside to dismiss
if (ImGui::IsMouseClicked(0)) {
ImVec2 mousePos = io.MousePos;
if (mousePos.x > spec.width) {
*open = false;
}
}
}
// Drawer position and size
ImVec2 drawerPos(0, 0);
ImVec2 drawerSize(spec.width, io.DisplaySize.y);
// If not modal, account for app bar
if (spec.type != NavDrawerType::Modal) {
drawerPos.y = size::AppBarHeight;
drawerSize.y = io.DisplaySize.y - size::AppBarHeight;
}
ImRect drawerBB(drawerPos, ImVec2(drawerPos.x + drawerSize.x, drawerPos.y + drawerSize.y));
// Draw drawer background
ImU32 bgColor = Surface(Elevation::Dp16);
drawList->AddRectFilled(drawerBB.Min, drawerBB.Max, bgColor);
// Store content region
g_navDrawerState.contentMin = ImVec2(drawerBB.Min.x, drawerBB.Min.y);
g_navDrawerState.contentMax = drawerBB.Max;
// Header
float currentY = drawerBB.Min.y;
if (spec.headerTitle || spec.headerSubtitle) {
// Header area (optional)
float headerHeight = 64.0f;
ImVec2 headerMin(drawerBB.Min.x, currentY);
ImVec2 headerMax(drawerBB.Max.x, currentY + headerHeight);
// Header background (slightly elevated)
drawList->AddRectFilled(headerMin, headerMax, Surface(Elevation::Dp16));
// Header title
if (spec.headerTitle) {
ImGui::SetCursorScreenPos(ImVec2(drawerBB.Min.x + spacing::dp(2), currentY + 20.0f));
Typography::instance().text(TypeStyle::H6, spec.headerTitle);
}
if (spec.headerSubtitle) {
ImGui::SetCursorScreenPos(ImVec2(drawerBB.Min.x + spacing::dp(2), currentY + 42.0f));
Typography::instance().textColored(TypeStyle::Body2, OnSurfaceMedium(), spec.headerSubtitle);
}
currentY += headerHeight;
// Divider under header
drawList->AddLine(
ImVec2(drawerBB.Min.x, currentY),
ImVec2(drawerBB.Max.x, currentY),
OnSurfaceDisabled()
);
}
// Set cursor for nav items
ImGui::SetCursorScreenPos(ImVec2(drawerBB.Min.x, currentY + spacing::dp(1)));
ImGui::BeginGroup();
return true;
}
inline bool BeginNavDrawer(const char* id, const NavDrawerSpec& spec) {
static bool alwaysOpen = true;
NavDrawerSpec standardSpec = spec;
standardSpec.type = NavDrawerType::Standard;
return BeginNavDrawer(id, &alwaysOpen, standardSpec);
}
inline void EndNavDrawer() {
ImGui::EndGroup();
// Divider at bottom if configured
ImGuiWindow* window = ImGui::GetCurrentWindow();
ImDrawList* drawList = window->DrawList;
// Right edge divider
drawList->AddLine(
ImVec2(g_navDrawerState.contentMax.x - 1, g_navDrawerState.contentMin.y),
ImVec2(g_navDrawerState.contentMax.x - 1, g_navDrawerState.contentMax.y),
OnSurfaceDisabled()
);
ImGui::PopID();
}
inline bool NavItem(const NavItemSpec& spec) {
ImGuiWindow* window = ImGui::GetCurrentWindow();
if (window->SkipItems)
return false;
ImGui::PushID(spec.label);
// Item dimensions
const float itemHeight = 48.0f;
const float iconSize = 24.0f;
const float horizontalPadding = spacing::dp(2); // 16dp
const float iconLabelGap = spacing::dp(4); // 32dp from left edge to label
float itemWidth = g_navDrawerState.width - spacing::dp(1); // 8dp margin right
ImVec2 pos = window->DC.CursorPos;
pos.x += spacing::dp(1); // 8dp margin left
ImRect itemBB(pos, ImVec2(pos.x + itemWidth, pos.y + itemHeight));
// Interaction
ImGuiID id = window->GetID("##navitem");
ImGui::ItemSize(ImRect(window->DC.CursorPos, ImVec2(window->DC.CursorPos.x + g_navDrawerState.width, window->DC.CursorPos.y + itemHeight)));
if (!ImGui::ItemAdd(itemBB, id))
return false;
bool hovered, held;
bool pressed = ImGui::ButtonBehavior(itemBB, id, &hovered, &held) && !spec.disabled;
// Draw background
ImDrawList* drawList = window->DrawList;
ImU32 bgColor = 0;
if (spec.selected) {
bgColor = WithAlpha(Primary(), 30); // Primary at ~12%
} else if (held && !spec.disabled) {
bgColor = schema::UI().resolveColor("var(--sidebar-hover)", IM_COL32(255, 255, 255, 25));
} else if (hovered && !spec.disabled) {
bgColor = schema::UI().resolveColor("var(--active-overlay)", IM_COL32(255, 255, 255, 10));
}
if (bgColor) {
drawList->AddRectFilled(itemBB.Min, itemBB.Max, bgColor, size::ButtonCornerRadius);
}
// Selected indicator (left edge)
if (spec.selected) {
drawList->AddRectFilled(
ImVec2(itemBB.Min.x, itemBB.Min.y + 8.0f),
ImVec2(itemBB.Min.x + 4.0f, itemBB.Max.y - 8.0f),
Primary(), 2.0f
);
}
// Content
float contentX = pos.x + horizontalPadding;
float centerY = pos.y + itemHeight * 0.5f;
ImU32 iconColor = spec.disabled ? OnSurfaceDisabled() :
spec.selected ? Primary() : OnSurfaceMedium();
ImU32 labelColor = spec.disabled ? OnSurfaceDisabled() :
spec.selected ? Primary() : OnSurface();
// Icon
if (spec.icon) {
drawList->AddText(
ImVec2(contentX, centerY - iconSize * 0.5f),
iconColor, spec.icon
);
contentX += iconSize + spacing::dp(2); // 16dp gap after icon
}
// Label
Typography::instance().pushFont(TypeStyle::Body1);
float labelY = centerY - ImGui::GetFontSize() * 0.5f;
drawList->AddText(ImVec2(contentX, labelY), labelColor, spec.label);
Typography::instance().popFont();
// Badge
if (spec.badgeCount > 0) {
char badgeText[8];
if (spec.badgeCount > 999) {
snprintf(badgeText, sizeof(badgeText), "999+");
} else {
snprintf(badgeText, sizeof(badgeText), "%d", spec.badgeCount);
}
ImVec2 badgeSize = ImGui::CalcTextSize(badgeText);
float badgeWidth = ImMax(24.0f, badgeSize.x + 12.0f);
float badgeHeight = 20.0f;
float badgeX = itemBB.Max.x - horizontalPadding - badgeWidth;
float badgeY = centerY - badgeHeight * 0.5f;
drawList->AddRectFilled(
ImVec2(badgeX, badgeY),
ImVec2(badgeX + badgeWidth, badgeY + badgeHeight),
Primary(), badgeHeight * 0.5f
);
Typography::instance().pushFont(TypeStyle::Caption);
ImVec2 textPos(badgeX + (badgeWidth - badgeSize.x) * 0.5f, badgeY + (badgeHeight - badgeSize.y) * 0.5f);
drawList->AddText(textPos, OnPrimary(), badgeText);
Typography::instance().popFont();
}
ImGui::PopID();
return pressed;
}
inline bool NavItem(const char* icon, const char* label, bool selected) {
NavItemSpec spec;
spec.icon = icon;
spec.label = label;
spec.selected = selected;
return NavItem(spec);
}
inline void NavDivider() {
ImGuiWindow* window = ImGui::GetCurrentWindow();
if (window->SkipItems)
return;
ImVec2 pos = window->DC.CursorPos;
ImDrawList* drawList = window->DrawList;
ImGui::Dummy(ImVec2(0, spacing::dp(1))); // 8dp spacing above
drawList->AddLine(
ImVec2(pos.x + spacing::dp(2), pos.y + spacing::dp(1)),
ImVec2(pos.x + g_navDrawerState.width - spacing::dp(2), pos.y + spacing::dp(1)),
OnSurfaceDisabled()
);
ImGui::Dummy(ImVec2(0, spacing::dp(1))); // 8dp spacing below
}
inline void NavSubheader(const char* text) {
ImGui::Dummy(ImVec2(0, spacing::dp(1))); // 8dp above
ImVec2 pos = ImGui::GetCursorScreenPos();
ImGui::SetCursorScreenPos(ImVec2(pos.x + spacing::dp(2), pos.y));
Typography::instance().textColored(TypeStyle::Caption, OnSurfaceMedium(), text);
ImGui::Dummy(ImVec2(0, spacing::dp(1))); // 8dp below
}
} // namespace material
} // namespace ui
} // namespace dragonx

View File

@@ -0,0 +1,303 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include "../colors.h"
#include "../typography.h"
#include "../layout.h"
#include "imgui.h"
#include "imgui_internal.h"
namespace dragonx {
namespace ui {
namespace material {
// ============================================================================
// Material Design Progress Indicators
// ============================================================================
// Based on https://m2.material.io/components/progress-indicators
//
// Progress indicators express an unspecified wait time or display the length
// of a process.
// ============================================================================
// Linear Progress
// ============================================================================
/**
* @brief Determinate linear progress bar
*
* @param fraction Progress value 0.0 to 1.0
* @param width Width of bar (0 = full available width)
*/
void LinearProgress(float fraction, float width = 0);
/**
* @brief Indeterminate linear progress bar (animated)
*
* @param width Width of bar (0 = full available width)
*/
void LinearProgressIndeterminate(float width = 0);
/**
* @brief Buffer linear progress bar
*
* @param fraction Primary progress 0.0 to 1.0
* @param buffer Buffer progress 0.0 to 1.0
* @param width Width of bar (0 = full available width)
*/
void LinearProgressBuffer(float fraction, float buffer, float width = 0);
// ============================================================================
// Circular Progress
// ============================================================================
/**
* @brief Determinate circular progress indicator
*
* @param fraction Progress value 0.0 to 1.0
* @param radius Radius of circle (default 20dp)
*/
void CircularProgress(float fraction, float radius = 20.0f);
/**
* @brief Indeterminate circular progress (spinner)
*
* @param radius Radius of circle (default 20dp)
*/
void CircularProgressIndeterminate(float radius = 20.0f);
// ============================================================================
// Implementation
// ============================================================================
inline void LinearProgress(float fraction, float width) {
ImGuiWindow* window = ImGui::GetCurrentWindow();
if (window->SkipItems)
return;
const float barHeight = 4.0f; // Material spec: 4dp height
float barWidth = width > 0 ? width : ImGui::GetContentRegionAvail().x;
ImVec2 pos = window->DC.CursorPos;
ImRect bb(pos, ImVec2(pos.x + barWidth, pos.y + barHeight));
ImGui::ItemSize(bb);
if (!ImGui::ItemAdd(bb, 0))
return;
ImDrawList* drawList = window->DrawList;
// Track (background)
ImU32 trackColor = WithAlpha(Primary(), 64); // Primary at 25%
drawList->AddRectFilled(bb.Min, bb.Max, trackColor, 0);
// Progress indicator
float progressWidth = barWidth * ImClamp(fraction, 0.0f, 1.0f);
if (progressWidth > 0) {
drawList->AddRectFilled(
bb.Min,
ImVec2(bb.Min.x + progressWidth, bb.Max.y),
Primary(), 0
);
}
}
inline void LinearProgressIndeterminate(float width) {
ImGuiWindow* window = ImGui::GetCurrentWindow();
if (window->SkipItems)
return;
const float barHeight = 4.0f;
float barWidth = width > 0 ? width : ImGui::GetContentRegionAvail().x;
ImVec2 pos = window->DC.CursorPos;
ImRect bb(pos, ImVec2(pos.x + barWidth, pos.y + barHeight));
ImGui::ItemSize(bb);
if (!ImGui::ItemAdd(bb, 0))
return;
ImDrawList* drawList = window->DrawList;
// Track
ImU32 trackColor = WithAlpha(Primary(), 64);
drawList->AddRectFilled(bb.Min, bb.Max, trackColor, 0);
// Animated indicator - sliding back and forth
float time = (float)ImGui::GetTime();
float cycleTime = fmodf(time, 2.0f); // 2 second cycle
// Two bars: primary and secondary with different phases
float indicatorWidth = barWidth * 0.3f; // 30% of track
// Primary indicator
float primaryPhase = fmodf(time * 1.2f, 2.0f);
float primaryPos;
if (primaryPhase < 1.0f) {
// Accelerating from left
primaryPos = primaryPhase * primaryPhase * (barWidth + indicatorWidth) - indicatorWidth;
} else {
// Continue off right (reset happens at 2.0)
primaryPos = (2.0f - primaryPhase) * (2.0f - primaryPhase) * -(barWidth + indicatorWidth) + barWidth;
}
float primaryStart = ImMax(bb.Min.x, bb.Min.x + primaryPos);
float primaryEnd = ImMin(bb.Max.x, bb.Min.x + primaryPos + indicatorWidth);
if (primaryEnd > primaryStart) {
drawList->AddRectFilled(
ImVec2(primaryStart, bb.Min.y),
ImVec2(primaryEnd, bb.Max.y),
Primary(), 0
);
}
}
inline void LinearProgressBuffer(float fraction, float buffer, float width) {
ImGuiWindow* window = ImGui::GetCurrentWindow();
if (window->SkipItems)
return;
const float barHeight = 4.0f;
float barWidth = width > 0 ? width : ImGui::GetContentRegionAvail().x;
ImVec2 pos = window->DC.CursorPos;
ImRect bb(pos, ImVec2(pos.x + barWidth, pos.y + barHeight));
ImGui::ItemSize(bb);
if (!ImGui::ItemAdd(bb, 0))
return;
ImDrawList* drawList = window->DrawList;
// Track
ImU32 trackColor = WithAlpha(Primary(), 38); // Primary at 15%
drawList->AddRectFilled(bb.Min, bb.Max, trackColor, 0);
// Buffer (lighter than progress)
float bufferWidth = barWidth * ImClamp(buffer, 0.0f, 1.0f);
if (bufferWidth > 0) {
drawList->AddRectFilled(
bb.Min,
ImVec2(bb.Min.x + bufferWidth, bb.Max.y),
WithAlpha(Primary(), 102), 0 // Primary at 40%
);
}
// Progress
float progressWidth = barWidth * ImClamp(fraction, 0.0f, 1.0f);
if (progressWidth > 0) {
drawList->AddRectFilled(
bb.Min,
ImVec2(bb.Min.x + progressWidth, bb.Max.y),
Primary(), 0
);
}
}
inline void CircularProgress(float fraction, float radius) {
ImGuiWindow* window = ImGui::GetCurrentWindow();
if (window->SkipItems)
return;
const float thickness = 4.0f; // Stroke width
float diameter = radius * 2;
ImVec2 pos = window->DC.CursorPos;
ImVec2 center(pos.x + radius, pos.y + radius);
ImRect bb(pos, ImVec2(pos.x + diameter, pos.y + diameter));
ImGui::ItemSize(bb);
if (!ImGui::ItemAdd(bb, 0))
return;
ImDrawList* drawList = window->DrawList;
// Track circle
ImU32 trackColor = WithAlpha(Primary(), 64);
drawList->AddCircle(center, radius - thickness * 0.5f, trackColor, 0, thickness);
// Progress arc
float clampedFraction = ImClamp(fraction, 0.0f, 1.0f);
if (clampedFraction > 0) {
float startAngle = -IM_PI * 0.5f; // Start at top (12 o'clock)
float endAngle = startAngle + IM_PI * 2.0f * clampedFraction;
// Draw arc as line segments
const int segments = (int)(32 * clampedFraction) + 1;
float angleStep = (endAngle - startAngle) / segments;
for (int i = 0; i < segments; i++) {
float a1 = startAngle + angleStep * i;
float a2 = startAngle + angleStep * (i + 1);
ImVec2 p1(center.x + cosf(a1) * (radius - thickness * 0.5f),
center.y + sinf(a1) * (radius - thickness * 0.5f));
ImVec2 p2(center.x + cosf(a2) * (radius - thickness * 0.5f),
center.y + sinf(a2) * (radius - thickness * 0.5f));
drawList->AddLine(p1, p2, Primary(), thickness);
}
}
}
inline void CircularProgressIndeterminate(float radius) {
ImGuiWindow* window = ImGui::GetCurrentWindow();
if (window->SkipItems)
return;
const float thickness = 4.0f;
float diameter = radius * 2;
ImVec2 pos = window->DC.CursorPos;
ImVec2 center(pos.x + radius, pos.y + radius);
ImRect bb(pos, ImVec2(pos.x + diameter, pos.y + diameter));
ImGui::ItemSize(bb);
if (!ImGui::ItemAdd(bb, 0))
return;
ImDrawList* drawList = window->DrawList;
float time = (float)ImGui::GetTime();
// Rotation animation
float rotation = fmodf(time * 2.0f * IM_PI / 1.4f, IM_PI * 2.0f); // ~1.4s rotation
// Arc length animation (grows and shrinks)
float cycleTime = fmodf(time, 1.333f); // ~1.333s cycle
float arcLength;
if (cycleTime < 0.666f) {
// Growing phase
arcLength = (cycleTime / 0.666f) * 0.75f + 0.1f; // 10% to 85%
} else {
// Shrinking phase
arcLength = ((1.333f - cycleTime) / 0.666f) * 0.75f + 0.1f;
}
float startAngle = rotation - IM_PI * 0.5f;
float endAngle = startAngle + IM_PI * 2.0f * arcLength;
// Draw arc
const int segments = (int)(32 * arcLength) + 1;
float angleStep = (endAngle - startAngle) / segments;
for (int i = 0; i < segments; i++) {
float a1 = startAngle + angleStep * i;
float a2 = startAngle + angleStep * (i + 1);
ImVec2 p1(center.x + cosf(a1) * (radius - thickness * 0.5f),
center.y + sinf(a1) * (radius - thickness * 0.5f));
ImVec2 p2(center.x + cosf(a2) * (radius - thickness * 0.5f),
center.y + sinf(a2) * (radius - thickness * 0.5f));
drawList->AddLine(p1, p2, Primary(), thickness);
}
}
} // namespace material
} // namespace ui
} // namespace dragonx

View File

@@ -0,0 +1,402 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include "../colors.h"
#include "../typography.h"
#include "../layout.h"
#include "../../schema/ui_schema.h"
#include "imgui.h"
#include "imgui_internal.h"
namespace dragonx {
namespace ui {
namespace material {
// ============================================================================
// Material Design Slider Component
// ============================================================================
// Based on https://m2.material.io/components/sliders
//
// Sliders allow users to make selections from a range of values.
/**
* @brief Continuous slider
*
* @param label Label for the slider (hidden, used for ID)
* @param value Pointer to current value
* @param minValue Minimum value
* @param maxValue Maximum value
* @param format Printf format for value display (nullptr = no display)
* @param width Slider width (0 = full available)
* @return true if value changed
*/
bool Slider(const char* label, float* value, float minValue, float maxValue,
const char* format = nullptr, float width = 0);
/**
* @brief Integer slider
*/
bool SliderInt(const char* label, int* value, int minValue, int maxValue,
const char* format = nullptr, float width = 0);
/**
* @brief Discrete slider with steps
*
* @param label Label for the slider
* @param value Pointer to current value
* @param minValue Minimum value
* @param maxValue Maximum value
* @param step Step size
* @param showTicks Show tick marks
* @return true if value changed
*/
bool SliderDiscrete(const char* label, float* value, float minValue, float maxValue,
float step, bool showTicks = true, float width = 0);
/**
* @brief Range slider (two thumbs)
*
* @param label Label for the slider
* @param minVal Pointer to range minimum
* @param maxVal Pointer to range maximum
* @param rangeMin Allowed minimum
* @param rangeMax Allowed maximum
* @return true if either value changed
*/
bool SliderRange(const char* label, float* minVal, float* maxVal,
float rangeMin, float rangeMax, float width = 0);
// ============================================================================
// Implementation
// ============================================================================
inline bool Slider(const char* label, float* value, float minValue, float maxValue,
const char* format, float width) {
ImGuiWindow* window = ImGui::GetCurrentWindow();
if (window->SkipItems)
return false;
ImGui::PushID(label);
// Slider dimensions
const float trackHeight = 4.0f;
const float thumbRadius = 10.0f; // 20dp diameter
float sliderWidth = width > 0 ? width : ImGui::GetContentRegionAvail().x;
float totalHeight = size::TouchTarget; // 48dp touch target
ImVec2 pos = window->DC.CursorPos;
ImRect bb(pos, ImVec2(pos.x + sliderWidth, pos.y + totalHeight));
// Item interaction
ImGuiID id = window->GetID("##slider");
ImGui::ItemSize(bb);
if (!ImGui::ItemAdd(bb, id))
return false;
bool hovered, held;
bool pressed = ImGui::ButtonBehavior(bb, id, &hovered, &held);
// Calculate thumb position
float trackLeft = pos.x + thumbRadius;
float trackRight = pos.x + sliderWidth - thumbRadius;
float trackWidth = trackRight - trackLeft;
float centerY = pos.y + totalHeight * 0.5f;
float fraction = (*value - minValue) / (maxValue - minValue);
fraction = ImClamp(fraction, 0.0f, 1.0f);
float thumbX = trackLeft + trackWidth * fraction;
// Handle dragging
bool changed = false;
if (held) {
float mouseX = ImGui::GetIO().MousePos.x;
float newFraction = (mouseX - trackLeft) / trackWidth;
newFraction = ImClamp(newFraction, 0.0f, 1.0f);
float newValue = minValue + newFraction * (maxValue - minValue);
if (newValue != *value) {
*value = newValue;
changed = true;
}
thumbX = trackLeft + trackWidth * newFraction;
}
// Draw
ImDrawList* drawList = window->DrawList;
// Track (inactive part)
ImU32 trackInactiveColor = WithAlpha(Primary(), 64); // Primary at 25%
drawList->AddRectFilled(
ImVec2(trackLeft, centerY - trackHeight * 0.5f),
ImVec2(trackRight, centerY + trackHeight * 0.5f),
trackInactiveColor, trackHeight * 0.5f
);
// Track (active part)
drawList->AddRectFilled(
ImVec2(trackLeft, centerY - trackHeight * 0.5f),
ImVec2(thumbX, centerY + trackHeight * 0.5f),
Primary(), trackHeight * 0.5f
);
// Thumb shadow
drawList->AddCircleFilled(ImVec2(thumbX + 1, centerY + 2), thumbRadius, schema::UI().resolveColor("var(--control-shadow)", IM_COL32(0, 0, 0, 60)));
// Thumb
drawList->AddCircleFilled(ImVec2(thumbX, centerY), thumbRadius, Primary());
// Hover/pressed ripple
if (hovered || held) {
ImU32 rippleColor = WithAlpha(Primary(), held ? 51 : 25);
drawList->AddCircleFilled(ImVec2(thumbX, centerY), thumbRadius + 12.0f, rippleColor);
}
// Value label (when held)
if (held && format) {
char valueText[64];
snprintf(valueText, sizeof(valueText), format, *value);
ImVec2 textSize = ImGui::CalcTextSize(valueText);
float labelY = centerY - thumbRadius - 32.0f;
float labelX = thumbX - textSize.x * 0.5f;
// Label background (rounded rectangle)
float labelPadX = 8.0f;
float labelPadY = 4.0f;
ImVec2 labelMin(labelX - labelPadX, labelY - labelPadY);
ImVec2 labelMax(labelX + textSize.x + labelPadX, labelY + textSize.y + labelPadY);
drawList->AddRectFilled(labelMin, labelMax, Primary(), 4.0f);
drawList->AddText(ImVec2(labelX, labelY), OnPrimary(), valueText);
}
ImGui::PopID();
return changed;
}
inline bool SliderInt(const char* label, int* value, int minValue, int maxValue,
const char* format, float width) {
float floatVal = (float)*value;
bool changed = Slider(label, &floatVal, (float)minValue, (float)maxValue, format, width);
if (changed) {
*value = (int)roundf(floatVal);
}
return changed;
}
inline bool SliderDiscrete(const char* label, float* value, float minValue, float maxValue,
float step, bool showTicks, float width) {
ImGuiWindow* window = ImGui::GetCurrentWindow();
if (window->SkipItems)
return false;
ImGui::PushID(label);
const float trackHeight = 4.0f;
const float thumbRadius = 10.0f;
const float tickRadius = 2.0f;
float sliderWidth = width > 0 ? width : ImGui::GetContentRegionAvail().x;
float totalHeight = size::TouchTarget;
ImVec2 pos = window->DC.CursorPos;
ImRect bb(pos, ImVec2(pos.x + sliderWidth, pos.y + totalHeight));
ImGuiID id = window->GetID("##slider");
ImGui::ItemSize(bb);
if (!ImGui::ItemAdd(bb, id))
return false;
bool hovered, held;
ImGui::ButtonBehavior(bb, id, &hovered, &held);
float trackLeft = pos.x + thumbRadius;
float trackRight = pos.x + sliderWidth - thumbRadius;
float trackWidth = trackRight - trackLeft;
float centerY = pos.y + totalHeight * 0.5f;
// Snap to step
float snappedValue = roundf((*value - minValue) / step) * step + minValue;
snappedValue = ImClamp(snappedValue, minValue, maxValue);
float fraction = (snappedValue - minValue) / (maxValue - minValue);
float thumbX = trackLeft + trackWidth * fraction;
bool changed = false;
if (held) {
float mouseX = ImGui::GetIO().MousePos.x;
float newFraction = (mouseX - trackLeft) / trackWidth;
newFraction = ImClamp(newFraction, 0.0f, 1.0f);
float rawValue = minValue + newFraction * (maxValue - minValue);
float newValue = roundf((rawValue - minValue) / step) * step + minValue;
newValue = ImClamp(newValue, minValue, maxValue);
if (newValue != *value) {
*value = newValue;
changed = true;
}
fraction = (newValue - minValue) / (maxValue - minValue);
thumbX = trackLeft + trackWidth * fraction;
}
ImDrawList* drawList = window->DrawList;
// Track
drawList->AddRectFilled(
ImVec2(trackLeft, centerY - trackHeight * 0.5f),
ImVec2(trackRight, centerY + trackHeight * 0.5f),
WithAlpha(Primary(), 64), trackHeight * 0.5f
);
drawList->AddRectFilled(
ImVec2(trackLeft, centerY - trackHeight * 0.5f),
ImVec2(thumbX, centerY + trackHeight * 0.5f),
Primary(), trackHeight * 0.5f
);
// Tick marks
if (showTicks) {
int numSteps = (int)((maxValue - minValue) / step);
for (int i = 0; i <= numSteps; i++) {
float tickFraction = (float)i / numSteps;
float tickX = trackLeft + trackWidth * tickFraction;
ImU32 tickColor = (tickX <= thumbX) ? OnPrimary() : WithAlpha(Primary(), 128);
drawList->AddCircleFilled(ImVec2(tickX, centerY), tickRadius, tickColor);
}
}
// Thumb
drawList->AddCircleFilled(ImVec2(thumbX + 1, centerY + 2), thumbRadius, schema::UI().resolveColor("var(--control-shadow)", IM_COL32(0, 0, 0, 60)));
drawList->AddCircleFilled(ImVec2(thumbX, centerY), thumbRadius, Primary());
if (hovered || held) {
ImU32 rippleColor = WithAlpha(Primary(), held ? 51 : 25);
drawList->AddCircleFilled(ImVec2(thumbX, centerY), thumbRadius + 12.0f, rippleColor);
}
ImGui::PopID();
return changed;
}
inline bool SliderRange(const char* label, float* minVal, float* maxVal,
float rangeMin, float rangeMax, float width) {
ImGuiWindow* window = ImGui::GetCurrentWindow();
if (window->SkipItems)
return false;
ImGui::PushID(label);
const float trackHeight = 4.0f;
const float thumbRadius = 10.0f;
float sliderWidth = width > 0 ? width : ImGui::GetContentRegionAvail().x;
float totalHeight = size::TouchTarget;
ImVec2 pos = window->DC.CursorPos;
ImRect bb(pos, ImVec2(pos.x + sliderWidth, pos.y + totalHeight));
ImGuiID id = window->GetID("##slider");
ImGui::ItemSize(bb);
if (!ImGui::ItemAdd(bb, id))
return false;
float trackLeft = pos.x + thumbRadius;
float trackRight = pos.x + sliderWidth - thumbRadius;
float trackWidth = trackRight - trackLeft;
float centerY = pos.y + totalHeight * 0.5f;
float minFraction = (*minVal - rangeMin) / (rangeMax - rangeMin);
float maxFraction = (*maxVal - rangeMin) / (rangeMax - rangeMin);
float minThumbX = trackLeft + trackWidth * minFraction;
float maxThumbX = trackLeft + trackWidth * maxFraction;
// Hit test both thumbs
ImVec2 mousePos = ImGui::GetIO().MousePos;
float distToMin = fabsf(mousePos.x - minThumbX);
float distToMax = fabsf(mousePos.x - maxThumbX);
bool nearMin = distToMin < distToMax;
ImGuiID minId = window->GetID("##min");
ImGuiID maxId = window->GetID("##max");
bool minHovered, minHeld;
bool maxHovered, maxHeld;
ImRect minHitBox(ImVec2(minThumbX - thumbRadius - 8, centerY - thumbRadius - 8),
ImVec2(minThumbX + thumbRadius + 8, centerY + thumbRadius + 8));
ImRect maxHitBox(ImVec2(maxThumbX - thumbRadius - 8, centerY - thumbRadius - 8),
ImVec2(maxThumbX + thumbRadius + 8, centerY + thumbRadius + 8));
ImGui::ButtonBehavior(nearMin ? minHitBox : maxHitBox, nearMin ? minId : maxId,
nearMin ? &minHovered : &maxHovered,
nearMin ? &minHeld : &maxHeld);
bool changed = false;
if (minHeld) {
float newFraction = (mousePos.x - trackLeft) / trackWidth;
newFraction = ImClamp(newFraction, 0.0f, maxFraction - 0.01f);
float newValue = rangeMin + newFraction * (rangeMax - rangeMin);
if (newValue != *minVal) {
*minVal = newValue;
changed = true;
}
minThumbX = trackLeft + trackWidth * newFraction;
}
if (maxHeld) {
float newFraction = (mousePos.x - trackLeft) / trackWidth;
newFraction = ImClamp(newFraction, minFraction + 0.01f, 1.0f);
float newValue = rangeMin + newFraction * (rangeMax - rangeMin);
if (newValue != *maxVal) {
*maxVal = newValue;
changed = true;
}
maxThumbX = trackLeft + trackWidth * newFraction;
}
ImDrawList* drawList = window->DrawList;
// Inactive track
drawList->AddRectFilled(
ImVec2(trackLeft, centerY - trackHeight * 0.5f),
ImVec2(trackRight, centerY + trackHeight * 0.5f),
WithAlpha(Primary(), 64), trackHeight * 0.5f
);
// Active track (between thumbs)
drawList->AddRectFilled(
ImVec2(minThumbX, centerY - trackHeight * 0.5f),
ImVec2(maxThumbX, centerY + trackHeight * 0.5f),
Primary(), trackHeight * 0.5f
);
// Min thumb
drawList->AddCircleFilled(ImVec2(minThumbX + 1, centerY + 2), thumbRadius, schema::UI().resolveColor("var(--control-shadow)", IM_COL32(0, 0, 0, 60)));
drawList->AddCircleFilled(ImVec2(minThumbX, centerY), thumbRadius, Primary());
if (minHovered || minHeld) {
ImU32 rippleColor = WithAlpha(Primary(), minHeld ? 51 : 25);
drawList->AddCircleFilled(ImVec2(minThumbX, centerY), thumbRadius + 12.0f, rippleColor);
}
// Max thumb
drawList->AddCircleFilled(ImVec2(maxThumbX + 1, centerY + 2), thumbRadius, schema::UI().resolveColor("var(--control-shadow)", IM_COL32(0, 0, 0, 60)));
drawList->AddCircleFilled(ImVec2(maxThumbX, centerY), thumbRadius, Primary());
if (maxHovered || maxHeld) {
ImU32 rippleColor = WithAlpha(Primary(), maxHeld ? 51 : 25);
drawList->AddCircleFilled(ImVec2(maxThumbX, centerY), thumbRadius + 12.0f, rippleColor);
}
ImGui::PopID();
return changed;
}
} // namespace material
} // namespace ui
} // namespace dragonx

View File

@@ -0,0 +1,242 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include "../colors.h"
#include "../typography.h"
#include "../layout.h"
#include "../../schema/ui_schema.h"
#include "../draw_helpers.h"
#include "imgui.h"
#include "imgui_internal.h"
namespace dragonx {
namespace ui {
namespace material {
// ============================================================================
// Material Design Snackbar Component
// ============================================================================
// Based on https://m2.material.io/components/snackbars
//
// Snackbars provide brief messages about app processes at the bottom of the
// screen. They can include a single action.
/**
* @brief Snackbar configuration
*/
struct SnackbarSpec {
const char* message = nullptr; // Message text
const char* actionText = nullptr; // Optional action button text
float duration = 4.0f; // Duration in seconds (0 = indefinite)
bool multiLine = false; // Allow multi-line message
};
/**
* @brief Snackbar manager for showing notifications
*/
class Snackbar {
public:
static Snackbar& instance();
/**
* @brief Show a snackbar message
*
* @param message Message text
* @param actionText Optional action text
* @param duration Display duration (0 = until dismissed)
*/
void show(const char* message, const char* actionText = nullptr, float duration = 4.0f);
/**
* @brief Show a snackbar with full configuration
*/
void show(const SnackbarSpec& spec);
/**
* @brief Dismiss current snackbar
*/
void dismiss();
/**
* @brief Render snackbar (call each frame)
*
* @return true if action was clicked
*/
bool render();
/**
* @brief Check if snackbar is visible
*/
bool isVisible() const { return m_visible; }
private:
Snackbar() = default;
bool m_visible = false;
SnackbarSpec m_currentSpec;
float m_showTime = 0;
float m_animProgress = 0; // 0 = hidden, 1 = fully shown
};
// ============================================================================
// Convenience Functions
// ============================================================================
/**
* @brief Show a snackbar message
*/
inline void ShowSnackbar(const char* message, const char* action = nullptr, float duration = 4.0f) {
Snackbar::instance().show(message, action, duration);
}
/**
* @brief Dismiss current snackbar
*/
inline void DismissSnackbar() {
Snackbar::instance().dismiss();
}
/**
* @brief Render snackbar system (call once per frame in main render loop)
*
* @return true if action was clicked
*/
inline bool RenderSnackbar() {
return Snackbar::instance().render();
}
// ============================================================================
// Implementation
// ============================================================================
inline Snackbar& Snackbar::instance() {
static Snackbar s_instance;
return s_instance;
}
inline void Snackbar::show(const char* message, const char* actionText, float duration) {
SnackbarSpec spec;
spec.message = message;
spec.actionText = actionText;
spec.duration = duration;
show(spec);
}
inline void Snackbar::show(const SnackbarSpec& spec) {
m_currentSpec = spec;
m_visible = true;
m_showTime = (float)ImGui::GetTime();
m_animProgress = 0;
}
inline void Snackbar::dismiss() {
m_visible = false;
}
inline bool Snackbar::render() {
if (!m_visible && m_animProgress <= 0)
return false;
bool actionClicked = false;
float currentTime = (float)ImGui::GetTime();
// Check auto-dismiss
if (m_visible && m_currentSpec.duration > 0) {
if (currentTime - m_showTime > m_currentSpec.duration) {
m_visible = false;
}
}
// Animate in/out
float animTarget = m_visible ? 1.0f : 0.0f;
float animSpeed = 8.0f; // Animation speed
if (m_animProgress < animTarget) {
m_animProgress = ImMin(m_animProgress + ImGui::GetIO().DeltaTime * animSpeed, animTarget);
} else if (m_animProgress > animTarget) {
m_animProgress = ImMax(m_animProgress - ImGui::GetIO().DeltaTime * animSpeed, animTarget);
}
if (m_animProgress <= 0)
return false;
// Snackbar dimensions
const float snackbarHeight = m_currentSpec.multiLine ? 68.0f : 48.0f;
const float snackbarMinWidth = 344.0f;
const float snackbarMaxWidth = 672.0f;
const float margin = spacing::dp(3); // 24dp from edges
ImGuiIO& io = ImGui::GetIO();
// Calculate width based on content
float messageWidth = ImGui::CalcTextSize(m_currentSpec.message).x;
float actionWidth = m_currentSpec.actionText ?
ImGui::CalcTextSize(m_currentSpec.actionText).x + spacing::dp(2) : 0;
float contentWidth = messageWidth + actionWidth + spacing::dp(4); // 32dp padding
float snackbarWidth = ImClamp(contentWidth, snackbarMinWidth, snackbarMaxWidth);
// Position at bottom center
float bottomY = io.DisplaySize.y - margin - snackbarHeight;
float slideOffset = (1.0f - m_animProgress) * (snackbarHeight + margin);
ImVec2 snackbarPos(
(io.DisplaySize.x - snackbarWidth) * 0.5f,
bottomY + slideOffset
);
// Draw snackbar
ImDrawList* drawList = ImGui::GetForegroundDrawList();
// Background (elevation dp6 equivalent)
ImU32 snackBg = schema::UI().resolveColor("var(--snackbar-bg)", IM_COL32(50, 50, 50, 255));
ImU32 bgColor = ScaleAlpha(snackBg, m_animProgress);
ImVec2 snackbarMin = snackbarPos;
ImVec2 snackbarMax(snackbarPos.x + snackbarWidth, snackbarPos.y + snackbarHeight);
drawList->AddRectFilled(snackbarMin, snackbarMax, bgColor, 4.0f);
// Message text
float textY = snackbarPos.y + (snackbarHeight - ImGui::GetFontSize()) * 0.5f;
float textX = snackbarPos.x + spacing::dp(2); // 16dp left padding
ImU32 snackText = schema::UI().resolveColor("var(--snackbar-text)", IM_COL32(255, 255, 255, 222));
ImU32 textColor = ScaleAlpha(snackText, m_animProgress);
drawList->AddText(ImVec2(textX, textY), textColor, m_currentSpec.message);
// Action button
if (m_currentSpec.actionText) {
float actionX = snackbarMax.x - spacing::dp(2) - actionWidth;
// Hit test for action
ImVec2 actionMin(actionX, snackbarPos.y);
ImVec2 actionMax(snackbarMax.x, snackbarMax.y);
ImVec2 mousePos = io.MousePos;
bool hovered = (mousePos.x >= actionMin.x && mousePos.x < actionMax.x &&
mousePos.y >= actionMin.y && mousePos.y < actionMax.y);
// Action text color
ImU32 actionColor;
if (hovered) {
actionColor = ScaleAlpha(schema::UI().resolveColor("var(--snackbar-action-hover)", IM_COL32(255, 213, 79, 255)), m_animProgress);
} else {
actionColor = ScaleAlpha(schema::UI().resolveColor("var(--snackbar-action)", IM_COL32(255, 193, 7, 255)), m_animProgress);
}
drawList->AddText(ImVec2(actionX, textY), actionColor, m_currentSpec.actionText);
// Check click
if (hovered && io.MouseClicked[0]) {
actionClicked = true;
dismiss();
}
}
return actionClicked;
}
} // namespace material
} // namespace ui
} // namespace dragonx

View File

@@ -0,0 +1,319 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include "../colors.h"
#include "../typography.h"
#include "../layout.h"
#include "../../schema/ui_schema.h"
#include "imgui.h"
#include "imgui_internal.h"
namespace dragonx {
namespace ui {
namespace material {
// ============================================================================
// Material Design Tabs Component
// ============================================================================
// Based on https://m2.material.io/components/tabs
//
// Tabs organize content across different screens, data sets, and other
// interactions.
/**
* @brief Tab bar configuration
*/
struct TabBarSpec {
bool scrollable = false; // Enable horizontal scrolling
bool fullWidth = true; // Tabs fill available width
bool showIndicator = true; // Show selection indicator
bool centered = false; // Center tabs (when not full width)
};
/**
* @brief Individual tab configuration
*/
struct TabSpec {
const char* label = nullptr;
const char* icon = nullptr; // Optional icon (text representation)
bool disabled = false;
int badgeCount = 0; // Badge count (0 = no badge)
};
/**
* @brief Begin a tab bar
*
* @param id Unique identifier
* @param selectedIndex Pointer to selected tab index
* @param spec Tab bar configuration
* @return true if tab bar is visible
*/
bool BeginTabBar(const char* id, int* selectedIndex, const TabBarSpec& spec = TabBarSpec());
/**
* @brief End a tab bar
*/
void EndTabBar();
/**
* @brief Add a tab to current tab bar
*
* @param spec Tab configuration
* @return true if this tab is selected
*/
bool Tab(const TabSpec& spec);
/**
* @brief Simple tab with just label
*/
bool Tab(const char* label);
/**
* @brief Simple tab bar - returns selected index
*
* @param id Unique identifier
* @param labels Array of tab labels
* @param count Number of tabs
* @param selectedIndex Current selected index (will be updated)
* @return true if selection changed
*/
bool TabBar(const char* id, const char** labels, int count, int* selectedIndex);
// ============================================================================
// Implementation
// ============================================================================
// Internal state for tab rendering
struct TabBarState {
int* selectedIndex;
int currentTabIndex;
TabBarSpec spec;
float tabBarWidth;
float tabWidth;
float indicatorX;
float indicatorWidth;
ImVec2 barPos;
};
static TabBarState g_tabBarState;
inline bool BeginTabBar(const char* id, int* selectedIndex, const TabBarSpec& spec) {
ImGuiWindow* window = ImGui::GetCurrentWindow();
if (window->SkipItems)
return false;
ImGui::PushID(id);
g_tabBarState.selectedIndex = selectedIndex;
g_tabBarState.currentTabIndex = 0;
g_tabBarState.spec = spec;
g_tabBarState.tabBarWidth = ImGui::GetContentRegionAvail().x;
g_tabBarState.tabWidth = 0; // Will be calculated if fullWidth
g_tabBarState.barPos = window->DC.CursorPos;
g_tabBarState.indicatorX = 0;
g_tabBarState.indicatorWidth = 0;
// Reserve space for tab bar
float barHeight = size::TabBarHeight;
ImRect bb(g_tabBarState.barPos,
ImVec2(g_tabBarState.barPos.x + g_tabBarState.tabBarWidth,
g_tabBarState.barPos.y + barHeight));
ImGui::ItemSize(bb);
// Draw tab bar background
ImDrawList* drawList = window->DrawList;
drawList->AddRectFilled(bb.Min, bb.Max, Surface(Elevation::Dp4));
// Begin horizontal layout for tabs
ImGui::SetCursorScreenPos(g_tabBarState.barPos);
ImGui::BeginGroup();
return true;
}
inline void EndTabBar() {
ImGui::EndGroup();
// Draw indicator line
if (g_tabBarState.spec.showIndicator && g_tabBarState.indicatorWidth > 0) {
ImGuiWindow* window = ImGui::GetCurrentWindow();
ImDrawList* drawList = window->DrawList;
float indicatorY = g_tabBarState.barPos.y + size::TabBarHeight - 2.0f;
drawList->AddRectFilled(
ImVec2(g_tabBarState.indicatorX, indicatorY),
ImVec2(g_tabBarState.indicatorX + g_tabBarState.indicatorWidth, indicatorY + 2.0f),
Primary()
);
}
// Add bottom divider
ImGuiWindow* window = ImGui::GetCurrentWindow();
ImDrawList* drawList = window->DrawList;
float dividerY = g_tabBarState.barPos.y + size::TabBarHeight;
drawList->AddLine(
ImVec2(g_tabBarState.barPos.x, dividerY),
ImVec2(g_tabBarState.barPos.x + g_tabBarState.tabBarWidth, dividerY),
OnSurfaceDisabled()
);
ImGui::PopID();
}
inline bool Tab(const TabSpec& spec) {
ImGuiWindow* window = ImGui::GetCurrentWindow();
if (window->SkipItems)
return false;
int tabIndex = g_tabBarState.currentTabIndex++;
bool isSelected = (*g_tabBarState.selectedIndex == tabIndex);
// Calculate tab dimensions
float minTabWidth = spec.icon ? 72.0f : 90.0f; // Material min widths
float maxTabWidth = 360.0f;
float labelWidth = ImGui::CalcTextSize(spec.label).x;
float iconWidth = spec.icon ? 24.0f + spacing::dp(1) : 0;
float contentWidth = labelWidth + iconWidth + spacing::dp(4); // 32dp padding
float tabWidth;
if (g_tabBarState.spec.fullWidth) {
// Divide evenly (assuming we don't know total count here - simplified)
tabWidth = ImMax(minTabWidth, contentWidth);
} else {
tabWidth = ImClamp(contentWidth, minTabWidth, maxTabWidth);
}
float tabHeight = size::TabBarHeight;
ImVec2 tabPos = window->DC.CursorPos;
ImRect tabBB(tabPos, ImVec2(tabPos.x + tabWidth, tabPos.y + tabHeight));
// Interaction
ImGuiID id = window->GetID(spec.label);
bool hovered, held;
bool pressed = ImGui::ButtonBehavior(tabBB, id, &hovered, &held) && !spec.disabled;
if (pressed && !isSelected) {
*g_tabBarState.selectedIndex = tabIndex;
}
// Update indicator position for selected tab
if (isSelected) {
g_tabBarState.indicatorX = tabPos.x;
g_tabBarState.indicatorWidth = tabWidth;
}
// Draw
ImDrawList* drawList = window->DrawList;
// Hover/press state overlay
if (!spec.disabled) {
if (held) {
drawList->AddRectFilled(tabBB.Min, tabBB.Max, schema::UI().resolveColor("var(--hover-overlay)", IM_COL32(255, 255, 255, 25)));
} else if (hovered) {
drawList->AddRectFilled(tabBB.Min, tabBB.Max, schema::UI().resolveColor("var(--active-overlay)", IM_COL32(255, 255, 255, 10)));
}
}
// Content color
ImU32 contentColor;
if (spec.disabled) {
contentColor = OnSurfaceDisabled();
} else if (isSelected) {
contentColor = Primary();
} else {
contentColor = OnSurfaceMedium();
}
// Draw content (icon and/or label)
float contentX = tabPos.x + (tabWidth - labelWidth - iconWidth) * 0.5f;
float centerY = tabPos.y + tabHeight * 0.5f;
if (spec.icon) {
ImFont* iconFont = Type().iconMed();
ImVec2 iconSize = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, spec.icon);
ImVec2 iconPos(contentX, centerY - iconSize.y * 0.5f);
drawList->AddText(iconFont, iconFont->LegacySize, iconPos, contentColor, spec.icon);
contentX += iconSize.x + spacing::Xs;
}
// Label (uppercase)
Typography::instance().pushFont(TypeStyle::Button);
float labelY = centerY - ImGui::GetFontSize() * 0.5f;
// Convert to uppercase
char upperLabel[128];
size_t i = 0;
for (const char* p = spec.label; *p && i < sizeof(upperLabel) - 1; p++, i++) {
upperLabel[i] = (*p >= 'a' && *p <= 'z') ? (*p - 32) : *p;
}
upperLabel[i] = '\0';
drawList->AddText(ImVec2(contentX, labelY), contentColor, upperLabel);
Typography::instance().popFont();
// Badge
if (spec.badgeCount > 0) {
float badgeX = tabPos.x + tabWidth - 16.0f;
float badgeY = tabPos.y + 8.0f;
float badgeRadius = 8.0f;
drawList->AddCircleFilled(ImVec2(badgeX, badgeY), badgeRadius, Error());
char badgeText[8];
if (spec.badgeCount > 99) {
snprintf(badgeText, sizeof(badgeText), "99+");
} else {
snprintf(badgeText, sizeof(badgeText), "%d", spec.badgeCount);
}
ImVec2 badgeTextSize = ImGui::CalcTextSize(badgeText);
ImVec2 badgeTextPos(badgeX - badgeTextSize.x * 0.5f, badgeY - badgeTextSize.y * 0.5f);
Typography::instance().pushFont(TypeStyle::Caption);
drawList->AddText(badgeTextPos, OnError(), badgeText);
Typography::instance().popFont();
}
// Advance cursor
ImGui::SameLine(0, 0);
ImGui::SetCursorScreenPos(ImVec2(tabPos.x + tabWidth, tabPos.y));
return isSelected;
}
inline bool Tab(const char* label) {
TabSpec spec;
spec.label = label;
return Tab(spec);
}
inline bool TabBar(const char* id, const char** labels, int count, int* selectedIndex) {
int oldIndex = *selectedIndex;
TabBarSpec spec;
spec.fullWidth = true;
if (BeginTabBar(id, selectedIndex, spec)) {
// Calculate tab width for full-width mode
float tabWidth = ImGui::GetContentRegionAvail().x / count;
for (int i = 0; i < count; i++) {
TabSpec tabSpec;
tabSpec.label = labels[i];
Tab(tabSpec);
}
EndTabBar();
}
return (*selectedIndex != oldIndex);
}
} // namespace material
} // namespace ui
} // namespace dragonx

View File

@@ -0,0 +1,227 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include "../colors.h"
#include "../typography.h"
#include "../layout.h"
#include "imgui.h"
#include "imgui_internal.h"
namespace dragonx {
namespace ui {
namespace material {
// ============================================================================
// Material Design Text Field Component
// ============================================================================
// Based on https://m2.material.io/components/text-fields
//
// Two variants:
// - Filled: Background fill with bottom line indicator
// - Outlined: Border around entire field
enum class TextFieldStyle {
Filled, // Background fill
Outlined // Border only
};
/**
* @brief Text field configuration
*/
struct TextFieldSpec {
TextFieldStyle style = TextFieldStyle::Outlined;
const char* label = nullptr; // Floating label text
const char* hint = nullptr; // Placeholder when empty
const char* helperText = nullptr; // Helper text below field
const char* errorText = nullptr; // Error message (shows in error state)
const char* prefix = nullptr; // Prefix text (e.g., "$")
const char* suffix = nullptr; // Suffix text (e.g., "DRGX")
bool password = false; // Mask input
bool readOnly = false; // Read-only field
bool multiline = false; // Multi-line text area
int multilineRows = 3; // Number of rows for multiline
float width = 0; // Width (0 = full available)
};
/**
* @brief Render a Material Design text field
*
* @param id Unique identifier
* @param buf Text buffer
* @param bufSize Buffer size
* @param spec Field configuration
* @return true if value changed
*/
bool TextField(const char* id, char* buf, size_t bufSize, const TextFieldSpec& spec = TextFieldSpec());
/**
* @brief Render a simple text field with label
*/
inline bool TextField(const char* label, char* buf, size_t bufSize) {
TextFieldSpec spec;
spec.label = label;
return TextField(label, buf, bufSize, spec);
}
// ============================================================================
// Implementation
// ============================================================================
inline bool TextField(const char* id, char* buf, size_t bufSize, const TextFieldSpec& spec) {
ImGuiWindow* window = ImGui::GetCurrentWindow();
if (window->SkipItems)
return false;
ImGui::PushID(id);
bool hasError = (spec.errorText != nullptr);
bool hasValue = (buf[0] != '\0');
// Calculate dimensions
float fieldWidth = spec.width > 0 ? spec.width : ImGui::GetContentRegionAvail().x;
float fieldHeight = spec.multiline ?
(size::TextFieldHeight + (spec.multilineRows - 1) * Typography::instance().getFont(TypeStyle::Body1)->FontSize * 1.5f) :
size::TextFieldHeight;
ImVec2 pos = window->DC.CursorPos;
ImRect bb(pos, ImVec2(pos.x + fieldWidth, pos.y + fieldHeight));
// Interaction
ImGuiID inputId = window->GetID("##input");
bool focused = (ImGui::GetFocusID() == inputId);
// Colors
ImU32 bgColor, borderColor, labelColor;
if (hasError) {
borderColor = Error();
labelColor = Error();
} else if (focused) {
borderColor = Primary();
labelColor = Primary();
} else {
borderColor = OnSurfaceMedium();
labelColor = OnSurfaceMedium();
}
if (spec.style == TextFieldStyle::Filled) {
bgColor = GetElevatedSurface(GetCurrentColorTheme(), 1);
} else {
bgColor = 0; // Transparent for outlined
}
// Draw background/border
ImDrawList* drawList = window->DrawList;
if (spec.style == TextFieldStyle::Filled) {
// Filled style: background with bottom line
drawList->AddRectFilled(bb.Min, bb.Max, bgColor,
size::TextFieldCornerRadius, ImDrawFlags_RoundCornersTop);
// Bottom indicator line
float lineThickness = focused ? 2.0f : 1.0f;
drawList->AddLine(
ImVec2(bb.Min.x, bb.Max.y - lineThickness),
ImVec2(bb.Max.x, bb.Max.y - lineThickness),
borderColor, lineThickness
);
} else {
// Outlined style: border around entire field
float lineThickness = focused ? 2.0f : 1.0f;
drawList->AddRect(bb.Min, bb.Max, borderColor,
size::TextFieldCornerRadius, 0, lineThickness);
}
// Label (floating or inline)
bool labelFloating = focused || hasValue;
if (spec.label) {
ImVec2 labelPos;
TypeStyle labelStyle;
if (labelFloating) {
// Floating label (smaller, at top)
labelPos.x = bb.Min.x + size::TextFieldPadding;
labelPos.y = bb.Min.y + 4.0f;
labelStyle = TypeStyle::Caption;
} else {
// Inline label (body size, centered)
labelPos.x = bb.Min.x + size::TextFieldPadding;
labelPos.y = bb.Min.y + (fieldHeight - Typography::instance().getFont(TypeStyle::Body1)->FontSize) * 0.5f;
labelStyle = TypeStyle::Body1;
}
// For outlined style, need to clear background behind floating label
if (spec.style == TextFieldStyle::Outlined && labelFloating) {
ImVec2 labelSize = ImGui::CalcTextSize(spec.label);
ImVec2 clearMin(labelPos.x - 4.0f, bb.Min.y - 1.0f);
ImVec2 clearMax(labelPos.x + labelSize.x + 4.0f, bb.Min.y + Typography::instance().getFont(TypeStyle::Caption)->FontSize);
drawList->AddRectFilled(clearMin, clearMax, Background());
}
Typography::instance().pushFont(labelStyle);
drawList->AddText(labelPos, labelColor, spec.label);
Typography::instance().popFont();
}
// Input field
float inputY = spec.label && labelFloating ? bb.Min.y + 20.0f : bb.Min.y + 12.0f;
float inputHeight = bb.Max.y - inputY - 8.0f;
ImGui::SetCursorScreenPos(ImVec2(bb.Min.x + size::TextFieldPadding, inputY));
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0, 0));
ImGui::PushStyleColor(ImGuiCol_FrameBg, IM_COL32(0, 0, 0, 0));
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(OnSurface()));
ImGuiInputTextFlags flags = 0;
if (spec.password) flags |= ImGuiInputTextFlags_Password;
if (spec.readOnly) flags |= ImGuiInputTextFlags_ReadOnly;
float inputWidth = fieldWidth - size::TextFieldPadding * 2;
if (spec.prefix) {
ImGui::TextUnformatted(spec.prefix);
ImGui::SameLine();
inputWidth -= ImGui::CalcTextSize(spec.prefix).x + 4.0f;
}
ImGui::PushItemWidth(inputWidth);
bool changed;
if (spec.multiline) {
changed = ImGui::InputTextMultiline("##input", buf, bufSize,
ImVec2(inputWidth, inputHeight), flags);
} else {
changed = ImGui::InputText("##input", buf, bufSize, flags);
}
ImGui::PopItemWidth();
if (spec.suffix) {
ImGui::SameLine();
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium()), "%s", spec.suffix);
}
ImGui::PopStyleColor(2);
ImGui::PopStyleVar();
// Helper/Error text below field
ImGui::SetCursorScreenPos(ImVec2(bb.Min.x, bb.Max.y + 4.0f));
if (spec.errorText) {
Typography::instance().textColored(TypeStyle::Caption, Error(), spec.errorText);
ImGui::SetCursorScreenPos(ImVec2(bb.Min.x, bb.Max.y + 4.0f + Typography::instance().getFont(TypeStyle::Caption)->FontSize + 4.0f));
} else if (spec.helperText) {
Typography::instance().textColored(TypeStyle::Caption, OnSurfaceMedium(), spec.helperText);
ImGui::SetCursorScreenPos(ImVec2(bb.Min.x, bb.Max.y + 4.0f + Typography::instance().getFont(TypeStyle::Caption)->FontSize + 4.0f));
}
// Advance cursor
ImGui::SetCursorScreenPos(ImVec2(pos.x, bb.Max.y + (spec.errorText || spec.helperText ? 24.0f : 8.0f)));
ImGui::PopID();
return changed;
}
} // namespace material
} // namespace ui
} // namespace dragonx

View File

@@ -0,0 +1,779 @@
// 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 <algorithm>
#include <unordered_map>
#include <cmath>
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<int>(((col >> IM_COL32_A_SHIFT) & 0xFF) * scale);
if (a > 255) a = 255;
return (col & ~IM_COL32_A_MASK) | (static_cast<ImU32>(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<uint8_t>(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<int>(a * alpha);
v.col = (v.col & ~IM_COL32_A_MASK) | (static_cast<ImU32>(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<ImGuiID, ScrollState> 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

345
src/ui/material/elevation.h Normal file
View File

@@ -0,0 +1,345 @@
// 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

190
src/ui/material/gpu_mask.h Normal file
View File

@@ -0,0 +1,190 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
//
// GPU alpha mask — the ImGui equivalent of CSS mask-image: linear-gradient().
// Uses AddCallback to switch the GPU blend mode so that gradient quads
// multiply the framebuffer's alpha (and RGB) by the source alpha, producing
// a smooth per-pixel fade without vertex-spacing artefacts.
#pragma once
#include "imgui.h"
#ifdef DRAGONX_USE_DX11
#include <d3d11.h>
#else
#ifdef DRAGONX_HAS_GLAD
#include <glad/gl.h>
#else
#include <SDL3/SDL_opengl.h>
#endif
#include <SDL3/SDL.h> // for SDL_GL_GetProcAddress
#endif
namespace dragonx {
namespace ui {
namespace material {
// ============================================================================
// Blend-mode callbacks — called by ImGui's backend during draw list rendering
// ============================================================================
#ifdef DRAGONX_USE_DX11
// Cached DX11 blend state for the mask pass
inline ID3D11BlendState* GetMaskBlendState() {
static ID3D11BlendState* s_maskBlend = nullptr;
if (!s_maskBlend) {
ImGuiIO& io = ImGui::GetIO();
if (!io.BackendRendererUserData) return nullptr;
ID3D11Device* dev = *reinterpret_cast<ID3D11Device**>(io.BackendRendererUserData);
if (!dev) return nullptr;
D3D11_BLEND_DESC desc = {};
desc.RenderTarget[0].BlendEnable = TRUE;
desc.RenderTarget[0].SrcBlend = D3D11_BLEND_ZERO;
desc.RenderTarget[0].DestBlend = D3D11_BLEND_SRC_ALPHA;
desc.RenderTarget[0].BlendOp = D3D11_BLEND_OP_ADD;
desc.RenderTarget[0].SrcBlendAlpha = D3D11_BLEND_ZERO;
desc.RenderTarget[0].DestBlendAlpha = D3D11_BLEND_SRC_ALPHA;
desc.RenderTarget[0].BlendOpAlpha = D3D11_BLEND_OP_ADD;
desc.RenderTarget[0].RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL;
dev->CreateBlendState(&desc, &s_maskBlend);
}
return s_maskBlend;
}
// Switch to mask blend: dst *= srcAlpha (both RGB and A)
inline void MaskBlendCallback(const ImDrawList*, const ImDrawCmd*) {
ImGuiIO& io = ImGui::GetIO();
if (!io.BackendRendererUserData) return;
// The ImGui DX11 backend stores the device as the first pointer
ID3D11Device* dev = *reinterpret_cast<ID3D11Device**>(io.BackendRendererUserData);
if (!dev) return;
ID3D11DeviceContext* ctx = nullptr;
dev->GetImmediateContext(&ctx);
if (!ctx) return;
ID3D11BlendState* bs = GetMaskBlendState();
if (bs) {
float blendFactor[4] = {0, 0, 0, 0};
ctx->OMSetBlendState(bs, blendFactor, 0xFFFFFFFF);
}
ctx->Release();
}
// Restore normal ImGui blend: src*srcA + dst*(1-srcA)
inline void RestoreBlendCallback(const ImDrawList*, const ImDrawCmd*) {
ImGuiIO& io = ImGui::GetIO();
if (!io.BackendRendererUserData) return;
ID3D11Device* dev = *reinterpret_cast<ID3D11Device**>(io.BackendRendererUserData);
if (!dev) return;
ID3D11DeviceContext* ctx = nullptr;
dev->GetImmediateContext(&ctx);
if (!ctx) return;
// Setting nullptr restores the default blend state that ImGui's DX11
// backend configures at the start of each frame.
ctx->OMSetBlendState(nullptr, nullptr, 0xFFFFFFFF);
ctx->Release();
}
#else // OpenGL
// glBlendFuncSeparate may not be in the GLAD profile — load it once via SDL.
typedef void (*PFN_glBlendFuncSeparate)(GLenum, GLenum, GLenum, GLenum);
inline PFN_glBlendFuncSeparate GetBlendFuncSeparate() {
static PFN_glBlendFuncSeparate fn = nullptr;
static bool resolved = false;
if (!resolved) {
resolved = true;
fn = (PFN_glBlendFuncSeparate)(void*)SDL_GL_GetProcAddress("glBlendFuncSeparate");
}
return fn;
}
inline void MaskBlendCallback(const ImDrawList*, const ImDrawCmd*) {
// dst.rgb = dst.rgb * srcAlpha (erase content where mask alpha < 1)
// dst.a = dst.a * srcAlpha (match alpha channel)
auto fn = GetBlendFuncSeparate();
if (fn)
fn(GL_ZERO, GL_SRC_ALPHA, GL_ZERO, GL_SRC_ALPHA);
else
glBlendFunc(GL_ZERO, GL_SRC_ALPHA);
}
inline void RestoreBlendCallback(const ImDrawList*, const ImDrawCmd*) {
// Restore ImGui's exact blend state:
// RGB: src*srcA + dst*(1-srcA)
// Alpha: src*1 + dst*(1-srcA)
auto fn = GetBlendFuncSeparate();
if (fn)
fn(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
else
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
}
#endif
// ============================================================================
// DrawScrollFadeMask — draw gradient quads that mask the top/bottom edges
// of a scrollable child region, producing a smooth per-pixel fade.
//
// Call this on the child's draw list BEFORE EndChild().
// The gradient quads use the mask blend mode to multiply the existing
// framebuffer content by their alpha, so alpha=1 means "keep" and
// alpha=0 means "erase to transparent/black".
//
// Parameters:
// dl — the child window's draw list (ImGui::GetWindowDrawList())
// clipMin/Max — the child window's visible area (screen coords)
// fadeH — the height of the fade zone in pixels
// scrollY — current scroll offset (ImGui::GetScrollY())
// scrollMaxY — maximum scroll offset (ImGui::GetScrollMaxY())
// ============================================================================
inline void DrawScrollFadeMask(ImDrawList* dl,
const ImVec2& clipMin, const ImVec2& clipMax,
float fadeH,
float scrollY, float scrollMaxY)
{
if (fadeH <= 0.0f) return;
bool needTop = scrollY > 1.0f;
bool needBottom = scrollMaxY > 0 && scrollY < scrollMaxY - 1.0f;
if (!needTop && !needBottom) return;
float left = clipMin.x;
float right = clipMax.x;
// Switch to mask blend mode
dl->AddCallback(MaskBlendCallback, nullptr);
if (needTop) {
// Top gradient: alpha=0 at top edge (erase) → alpha=1 at top+fadeH (keep)
ImVec2 tMin(left, clipMin.y);
ImVec2 tMax(right, clipMin.y + fadeH);
ImU32 transparent = IM_COL32(0, 0, 0, 0);
ImU32 opaque = IM_COL32(0, 0, 0, 255);
dl->AddRectFilledMultiColor(tMin, tMax,
transparent, transparent, // top-left, top-right
opaque, opaque); // bottom-left, bottom-right
}
if (needBottom) {
// Bottom gradient: alpha=1 at bottom-fadeH (keep) → alpha=0 at bottom (erase)
ImVec2 bMin(left, clipMax.y - fadeH);
ImVec2 bMax(right, clipMax.y);
ImU32 opaque = IM_COL32(0, 0, 0, 255);
ImU32 transparent = IM_COL32(0, 0, 0, 0);
dl->AddRectFilledMultiColor(bMin, bMax,
opaque, opaque, // top-left, top-right
transparent, transparent); // bottom-left, bottom-right
}
// Restore normal blend mode
dl->AddCallback(RestoreBlendCallback, nullptr);
}
} // namespace material
} // namespace ui
} // namespace dragonx

351
src/ui/material/layout.h Normal file
View File

@@ -0,0 +1,351 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include "imgui.h"
namespace dragonx {
namespace ui {
namespace material {
// ============================================================================
// Material Design 8dp Grid System
// ============================================================================
// All spacing in Material Design is based on an 8dp grid.
// https://m2.material.io/design/layout/spacing-methods.html
constexpr float kGridUnit = 8.0f; // Base unit in dp (device-independent pixels)
// ============================================================================
// Spacing Scale
// ============================================================================
// Use these values for all margins, padding, and gaps
namespace spacing {
constexpr float Xxs = 2.0f; // 0.25x - Very tight spacing
constexpr float Xs = 4.0f; // 0.5x - Extra small
constexpr float Sm = 8.0f; // 1x - Small (default minimum)
constexpr float Md = 16.0f; // 2x - Medium (default standard)
constexpr float Lg = 24.0f; // 3x - Large
constexpr float Xl = 32.0f; // 4x - Extra large
constexpr float Xxl = 40.0f; // 5x - Double extra large
constexpr float Xxxl = 48.0f; // 6x - Triple extra large
// Multiplier helpers
constexpr float Grid(float multiplier) { return kGridUnit * multiplier; }
} // namespace spacing
// ============================================================================
// Component Dimensions
// ============================================================================
// Standard sizes for Material Design components
namespace size {
// Buttons
constexpr float ButtonHeight = 36.0f;
constexpr float ButtonMinWidth = 64.0f;
constexpr float ButtonPaddingH = 16.0f;
constexpr float ButtonPaddingV = 8.0f;
constexpr float ButtonIconGap = 8.0f; // Gap between icon and label
constexpr float ButtonCornerRadius = 4.0f;
// FAB (Floating Action Button)
constexpr float FabSize = 56.0f;
constexpr float FabMiniSize = 40.0f;
constexpr float FabExtendedHeight = 48.0f;
constexpr float FabExtendedPadding = 16.0f;
constexpr float FabCornerRadius = 16.0f; // Half of mini size for circle
// Icons
constexpr float IconSize = 24.0f;
constexpr float IconSizeSm = 18.0f;
constexpr float IconSizeLg = 36.0f;
constexpr float IconButtonSize = 48.0f; // Touch target for icon buttons
// App Bar / Toolbar
constexpr float AppBarHeight = 56.0f; // Mobile
constexpr float AppBarHeightLarge = 64.0f; // Desktop
constexpr float AppBarPadding = 16.0f;
// Navigation
constexpr float NavDrawerWidth = 256.0f;
constexpr float NavRailWidth = 72.0f;
constexpr float NavItemHeight = 48.0f;
constexpr float NavItemPadding = 12.0f;
constexpr float NavSectionPadding = 8.0f;
// Cards
constexpr float CardCornerRadius = 4.0f;
constexpr float CardPadding = 16.0f;
constexpr float CardElevation = 1.0f; // Default elevation in dp
// Dialogs
constexpr float DialogMinWidth = 280.0f;
constexpr float DialogMaxWidth = 560.0f;
constexpr float DialogPadding = 24.0f;
constexpr float DialogTitlePadding = 20.0f;
constexpr float DialogActionGap = 8.0f;
constexpr float DialogCornerRadius = 4.0f;
// Lists
constexpr float ListItemHeight = 48.0f; // Single line
constexpr float ListItemHeightTwoLine = 64.0f;
constexpr float ListItemHeightThreeLine = 88.0f;
constexpr float ListItemPaddingH = 16.0f;
constexpr float ListAvatarSize = 40.0f;
constexpr float ListIconMargin = 32.0f;
// Text Fields
constexpr float TextFieldHeight = 56.0f;
constexpr float TextFieldPadding = 16.0f;
constexpr float TextFieldDenseHeight = 40.0f;
constexpr float TextFieldCornerRadius = 4.0f;
// Chips
constexpr float ChipHeight = 32.0f;
constexpr float ChipPaddingH = 12.0f;
constexpr float ChipCornerRadius = 16.0f;
// Tabs
constexpr float TabHeight = 48.0f;
constexpr float TabMinWidth = 90.0f;
constexpr float TabMaxWidth = 360.0f;
constexpr float TabPaddingH = 16.0f;
constexpr float TabIndicatorHeight = 2.0f;
// Snackbar
constexpr float SnackbarMinWidth = 288.0f;
constexpr float SnackbarMaxWidth = 568.0f;
constexpr float SnackbarHeight = 48.0f;
constexpr float SnackbarPadding = 8.0f;
constexpr float SnackbarMargin = 8.0f;
// Dividers
constexpr float DividerThickness = 1.0f;
constexpr float DividerInset = 16.0f; // For inset dividers
} // namespace size
// ============================================================================
// Responsive Breakpoints
// ============================================================================
// Based on Material Design responsive layout grid
namespace breakpoint {
constexpr float Xs = 0.0f; // Extra small: 0-599dp
constexpr float Sm = 600.0f; // Small: 600-904dp
constexpr float Md = 905.0f; // Medium: 905-1239dp
constexpr float Lg = 1240.0f; // Large: 1240-1439dp
constexpr float Xl = 1440.0f; // Extra large: 1440dp+
// Returns the current breakpoint category
enum class Category { Xs, Sm, Md, Lg, Xl };
inline Category GetCategory(float windowWidth) {
if (windowWidth >= Xl) return Category::Xl;
if (windowWidth >= Lg) return Category::Lg;
if (windowWidth >= Md) return Category::Md;
if (windowWidth >= Sm) return Category::Sm;
return Category::Xs;
}
// Margin values for each breakpoint
inline float GetMargin(Category cat) {
switch (cat) {
case Category::Xs: return 16.0f;
case Category::Sm: return 32.0f;
case Category::Md: return 32.0f; // Or fluid centering
case Category::Lg: return 200.0f;
case Category::Xl: return 200.0f; // Or fluid centering
}
return 16.0f;
}
// Column count for each breakpoint
inline int GetColumns(Category cat) {
switch (cat) {
case Category::Xs: return 4;
case Category::Sm: return 8;
default: return 12;
}
}
// Recommended navigation style
enum class NavStyle { BottomNav, NavRail, NavDrawer };
inline NavStyle GetNavStyle(Category cat) {
switch (cat) {
case Category::Xs: return NavStyle::BottomNav;
case Category::Sm:
case Category::Md: return NavStyle::NavRail;
default: return NavStyle::NavDrawer;
}
}
} // namespace breakpoint
// ============================================================================
// Layout Helpers
// ============================================================================
/**
* @brief Get current window's breakpoint category
*/
inline breakpoint::Category GetCurrentBreakpoint() {
ImVec2 windowSize = ImGui::GetWindowSize();
return breakpoint::GetCategory(windowSize.x);
}
/**
* @brief Get recommended margin for current window
*/
inline float GetCurrentMargin() {
return breakpoint::GetMargin(GetCurrentBreakpoint());
}
/**
* @brief Add horizontal spacing (uses 8dp grid)
* @param units Number of 8dp units (default 1)
*/
inline void HSpace(float units = 1.0f) {
ImGui::SameLine(0, kGridUnit * units);
}
/**
* @brief Add vertical spacing (uses 8dp grid)
* @param units Number of 8dp units (default 1)
*/
inline void VSpace(float units = 1.0f) {
ImGui::Dummy(ImVec2(0, kGridUnit * units));
}
/**
* @brief Begin a padded region
* @param padding Padding in pixels (use spacing:: constants)
*/
inline void BeginPadding(float padding) {
ImGui::BeginGroup();
ImGui::Dummy(ImVec2(0, padding));
ImGui::Indent(padding);
}
/**
* @brief End a padded region
* @param padding Same padding used in BeginPadding
*/
inline void EndPadding(float padding) {
ImGui::Unindent(padding);
ImGui::Dummy(ImVec2(0, padding));
ImGui::EndGroup();
}
/**
* @brief Center the next item horizontally within available width
* @param itemWidth Width of the item to center
*/
inline void CenterHorizontally(float itemWidth) {
float availWidth = ImGui::GetContentRegionAvail().x;
float offset = (availWidth - itemWidth) * 0.5f;
if (offset > 0) {
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + offset);
}
}
/**
* @brief Calculate column width for responsive grid
* @param numColumns Number of columns to span
* @param gutterWidth Gap between columns (default 16dp)
*/
inline float GetColumnWidth(int numColumns = 1, float gutterWidth = spacing::Md) {
float availWidth = ImGui::GetContentRegionAvail().x;
auto cat = GetCurrentBreakpoint();
int totalColumns = breakpoint::GetColumns(cat);
float totalGutter = gutterWidth * (totalColumns - 1);
float columnWidth = (availWidth - totalGutter) / totalColumns;
return columnWidth * numColumns + gutterWidth * (numColumns - 1);
}
// ============================================================================
// Card Layout Helper
// ============================================================================
struct CardLayout {
float width;
float minHeight;
float padding;
float cornerRadius;
int elevation;
CardLayout()
: width(0) // 0 = auto width
, minHeight(0)
, padding(size::CardPadding)
, cornerRadius(size::CardCornerRadius)
, elevation(1)
{}
CardLayout& Width(float w) { width = w; return *this; }
CardLayout& MinHeight(float h) { minHeight = h; return *this; }
CardLayout& Padding(float p) { padding = p; return *this; }
CardLayout& CornerRadius(float r) { cornerRadius = r; return *this; }
CardLayout& Elevation(int e) { elevation = e; return *this; }
};
// ============================================================================
// Navigation Drawer Item
// ============================================================================
struct NavItem {
const char* label;
const char* icon; // Icon font glyph or nullptr
bool selected;
bool enabled;
NavItem(const char* lbl, const char* ico = nullptr)
: label(lbl), icon(ico), selected(false), enabled(true)
{}
NavItem& Selected(bool s) { selected = s; return *this; }
NavItem& Enabled(bool e) { enabled = e; return *this; }
};
// ============================================================================
// Alignment Helpers
// ============================================================================
/**
* @brief Align item to right edge of available space
* @param itemWidth Width of the item
*/
inline void AlignRight(float itemWidth) {
float availWidth = ImGui::GetContentRegionAvail().x;
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + availWidth - itemWidth);
}
/**
* @brief Push items to start from right side (call before group of items)
*/
inline void BeginRightAlign() {
ImGui::BeginGroup();
}
/**
* @brief End right-aligned group and position it
*/
inline void EndRightAlign() {
ImGui::EndGroup();
float groupWidth = ImGui::GetItemRectSize().x;
AlignRight(groupWidth);
}
} // namespace material
} // namespace ui
} // namespace dragonx

160
src/ui/material/material.h Normal file
View File

@@ -0,0 +1,160 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
// ============================================================================
// Material Design 2 - Complete UI System
// ============================================================================
// Based on https://m2.material.io/design/foundation-overview
//
// This header provides the complete Material Design 2 implementation for
// the DragonX Wallet ImGui interface.
//
// Namespace: dragonx::ui::material
// Foundation
#include "color_theme.h" // ColorTheme struct, theme presets
#include "colors.h" // Color accessor functions
#include "typography.h" // Typography system, type scale
#include "layout.h" // Spacing grid, breakpoints, sizes
// Effects
#include "elevation.h" // Shadow rendering, elevation animation
#include "ripple.h" // Touch ripple effect
#include "draw_helpers.h" // DrawTextShadow, DrawGlassPanel
// Motion
#include "motion.h" // Easing curves, AnimatedValue, StaggerAnimation
#include "transitions.h" // View transitions, FadeTransition, ExpandableSection
// Layout
#include "app_layout.h" // Application layout manager
// Components
#include "components/components.h" // All Material components
// ============================================================================
// Quick Start Guide
// ============================================================================
//
// 1. INITIALIZATION
// In your app startup, initialize the material system:
//
// ```cpp
// using namespace dragonx::ui::material;
//
// // Initialize color theme (creates global theme)
// SetDragonXTheme(); // or SetHushTheme() for HUSH variant
//
// // Initialize typography (load fonts)
// Typography::instance().initialize(io);
// ```
//
// 2. FRAME SETUP
// At the start of each frame:
//
// ```cpp
// // Update ripple animations
// UpdateRipples();
// ```
//
// 3. USING COLORS
// Access theme colors with helper functions:
//
// ```cpp
// ImU32 bg = Background(); // App background
// ImU32 primary = Primary(); // Brand color
// ImU32 cardBg = Surface(Elevation::Dp4); // Elevated surface
// ImU32 text = OnSurface(); // Text on surfaces
// ```
//
// 4. USING TYPOGRAPHY
// Render text with the type scale:
//
// ```cpp
// Typography::instance().text(TypeStyle::H6, "Section Title");
// Typography::instance().text(TypeStyle::Body1, "Body text here...");
// Typography::instance().textColored(TypeStyle::Caption, OnSurfaceMedium(), "Hint");
// ```
//
// 5. USING COMPONENTS
// Components follow Material Design patterns:
//
// ```cpp
// // Buttons
// if (ContainedButton("Send")) { ... }
// if (OutlinedButton("Cancel")) { ... }
// if (TextButton("Learn More")) { ... }
//
// // Cards
// BeginCard(myCardSpec);
// CardHeader("Card Title", "Subtitle");
// CardContent("Card body content...");
// CardActions();
// TextButton("Action 1");
// TextButton("Action 2");
// CardActionsEnd();
// EndCard();
//
// // Lists
// BeginList("myList");
// if (ListItem("Item 1")) { ... }
// if (ListItem("Item 2", "Secondary text")) { ... }
// ListDivider();
// if (ListItem("Item 3")) { ... }
// EndList();
//
// // Dialogs
// static bool showDialog = false;
// if (ContainedButton("Open Dialog")) showDialog = true;
// int result = ConfirmDialog("confirm", &showDialog, "Confirm",
// "Are you sure?", "Yes", "No");
// ```
//
// 6. LAYOUT
// Use the spacing system for consistent layouts:
//
// ```cpp
// ImGui::Dummy(ImVec2(0, spacing::dp(2))); // 16dp vertical space
// ImGui::SetCursorPosX(spacing::dp(3)); // 24dp indent
// ```
//
// ============================================================================
// Module Reference
// ============================================================================
//
// COLORS (colors.h)
// Primary(), PrimaryVariant(), PrimaryContainer()
// Secondary(), SecondaryVariant()
// Background(), Surface(elevation), SurfaceVariant()
// OnPrimary(), OnSecondary(), OnBackground(), OnSurface()
// OnSurfaceMedium(), OnSurfaceDisabled()
// Error(), OnError()
// StateHover(), StateFocus(), StatePressed(), StateSelected()
//
// TYPOGRAPHY (typography.h)
// TypeStyle: H1-H6, Subtitle1-2, Body1-2, Button, Caption, Overline
// Typography::text(style, text)
// Typography::textColored(style, color, text)
// Typography::textWrapped(style, text)
// Typography::pushFont(style) / popFont()
//
// LAYOUT (layout.h)
// spacing::dp(n) - n * 8dp
// spacing::Unit - 8dp
// size::TouchTarget - 48dp
// size::ButtonHeight - 36dp
// breakpoint::current() - Get current breakpoint
//
// ELEVATION (elevation.h)
// DrawShadow(drawList, rect, elevationDp, cornerRadius)
// ElevationAnimator - Smooth elevation transitions
//
// RIPPLE (ripple.h)
// DrawRippleEffect(drawList, rect, id, cornerRadius, hovered, held)
// UpdateRipples() - Call each frame
//
// COMPONENTS (components/components.h)
// See components.h for full component reference

452
src/ui/material/motion.h Normal file
View File

@@ -0,0 +1,452 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include "imgui.h"
#include <cmath>
#include <functional>
namespace dragonx {
namespace ui {
namespace material {
// ============================================================================
// Material Design Motion System
// ============================================================================
// Based on https://m2.material.io/design/motion/speed.html
// and https://m2.material.io/design/motion/customization.html
//
// Material motion uses specific easing curves and durations to create
// natural, responsive animations that feel connected to user input.
// ============================================================================
// Standard Durations (in seconds)
// ============================================================================
namespace duration {
// Simple transitions (toggle, fade)
constexpr float Instant = 0.0f;
constexpr float VeryFast = 0.05f; // 50ms
constexpr float Fast = 0.1f; // 100ms - simple toggles
constexpr float Short = 0.15f; // 150ms
// Standard transitions
constexpr float Medium = 0.2f; // 200ms - collapse, simple move
constexpr float Standard = 0.25f; // 250ms - expand, standard
constexpr float Long = 0.3f; // 300ms - large transforms
// Complex transitions
constexpr float Complex = 0.375f; // 375ms
constexpr float VeryLong = 0.5f; // 500ms - elaborate sequences
// Screen transitions
constexpr float EnterScreen = 0.225f; // Entering screen
constexpr float ExitScreen = 0.195f; // Leaving screen
constexpr float ScreenChange = 0.3f; // Full screen transition
}
// ============================================================================
// Easing Curves
// ============================================================================
/**
* @brief Cubic bezier curve evaluation
*
* Evaluates a cubic bezier curve defined by control points (0,0), (x1,y1), (x2,y2), (1,1)
*
* @param t Progress 0.0-1.0
* @param x1 First control point X
* @param y1 First control point Y
* @param x2 Second control point X
* @param y2 Second control point Y
* @return Eased value
*/
float CubicBezier(float t, float x1, float y1, float x2, float y2);
/**
* @brief Standard easing - for objects moving between on-screen positions
*
* CSS: cubic-bezier(0.4, 0.0, 0.2, 1.0)
* Starts quickly, slows down to rest
*/
float EaseStandard(float t);
/**
* @brief Deceleration easing - for objects entering the screen
*
* CSS: cubic-bezier(0.0, 0.0, 0.2, 1.0)
* Starts at full velocity, decelerates to rest
*/
float EaseDecelerate(float t);
/**
* @brief Acceleration easing - for objects leaving the screen
*
* CSS: cubic-bezier(0.4, 0.0, 1.0, 1.0)
* Accelerates from rest, exits at full speed
*/
float EaseAccelerate(float t);
/**
* @brief Sharp easing - for objects that may return to screen
*
* CSS: cubic-bezier(0.4, 0.0, 0.6, 1.0)
* Quicker than standard, maintains connection
*/
float EaseSharp(float t);
/**
* @brief Linear interpolation (no easing)
*/
float EaseLinear(float t);
/**
* @brief Overshoot easing - goes past target then settles
*
* Good for bouncy, playful animations
*/
float EaseOvershoot(float t, float overshoot = 1.70158f);
/**
* @brief Elastic easing - springy oscillation
*/
float EaseElastic(float t);
// ============================================================================
// Easing Function Type
// ============================================================================
using EasingFunction = float(*)(float);
// ============================================================================
// Animated Value
// ============================================================================
/**
* @brief Animated value with automatic interpolation
*
* Template class for smooth value transitions.
*/
template<typename T>
class AnimatedValue {
public:
AnimatedValue(const T& initialValue = T())
: m_current(initialValue)
, m_target(initialValue)
, m_start(initialValue)
, m_duration(duration::Standard)
, m_elapsed(0)
, m_easingFunc(EaseStandard)
, m_animating(false)
{}
/**
* @brief Set target value with animation
*/
void animateTo(const T& target, float dur = duration::Standard,
EasingFunction easing = EaseStandard) {
if (target == m_target && m_animating)
return; // Already animating to this target
m_start = m_current;
m_target = target;
m_duration = dur;
m_elapsed = 0;
m_easingFunc = easing;
m_animating = true;
}
/**
* @brief Set value immediately (no animation)
*/
void set(const T& value) {
m_current = value;
m_target = value;
m_start = value;
m_animating = false;
}
/**
* @brief Update animation (call each frame)
* @param deltaTime Frame delta time in seconds
*/
void update(float deltaTime) {
if (!m_animating)
return;
m_elapsed += deltaTime;
if (m_elapsed >= m_duration) {
m_current = m_target;
m_animating = false;
} else {
float t = m_elapsed / m_duration;
float eased = m_easingFunc(t);
m_current = lerp(m_start, m_target, eased);
}
}
/**
* @brief Get current value
*/
const T& get() const { return m_current; }
/**
* @brief Get target value
*/
const T& getTarget() const { return m_target; }
/**
* @brief Check if currently animating
*/
bool isAnimating() const { return m_animating; }
/**
* @brief Get animation progress (0-1)
*/
float getProgress() const {
if (!m_animating) return 1.0f;
return m_elapsed / m_duration;
}
/**
* @brief Implicit conversion to value type
*/
operator const T&() const { return m_current; }
private:
T m_current;
T m_target;
T m_start;
float m_duration;
float m_elapsed;
EasingFunction m_easingFunc;
bool m_animating;
// Lerp specializations
static T lerp(const T& a, const T& b, float t) {
return a + (b - a) * t;
}
};
// Specialization for ImVec2
template<>
inline ImVec2 AnimatedValue<ImVec2>::lerp(const ImVec2& a, const ImVec2& b, float t) {
return ImVec2(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t);
}
// Specialization for ImVec4/color
template<>
inline ImVec4 AnimatedValue<ImVec4>::lerp(const ImVec4& a, const ImVec4& b, float t) {
return ImVec4(
a.x + (b.x - a.x) * t,
a.y + (b.y - a.y) * t,
a.z + (b.z - a.z) * t,
a.w + (b.w - a.w) * t
);
}
// ============================================================================
// Animation Sequencer
// ============================================================================
/**
* @brief Staggered animation for lists
*
* Creates staggered entrance animations for list items.
*/
class StaggerAnimation {
public:
StaggerAnimation(int itemCount, float staggerDelay = 0.05f,
float itemDuration = duration::EnterScreen)
: m_itemCount(itemCount)
, m_staggerDelay(staggerDelay)
, m_itemDuration(itemDuration)
, m_elapsed(0)
, m_running(false)
{}
/**
* @brief Start the stagger animation
*/
void start() {
m_elapsed = 0;
m_running = true;
}
/**
* @brief Update animation
*/
void update(float deltaTime) {
if (!m_running) return;
m_elapsed += deltaTime;
// Check if all items have finished
float totalDuration = m_staggerDelay * (m_itemCount - 1) + m_itemDuration;
if (m_elapsed >= totalDuration) {
m_running = false;
}
}
/**
* @brief Get animation progress for a specific item
*
* @param itemIndex Item index (0-based)
* @return Progress 0.0-1.0 (clamped)
*/
float getItemProgress(int itemIndex) const {
if (!m_running && m_elapsed > 0)
return 1.0f; // Animation complete
if (itemIndex < 0 || itemIndex >= m_itemCount)
return 0.0f;
float itemStart = m_staggerDelay * itemIndex;
float itemElapsed = m_elapsed - itemStart;
if (itemElapsed <= 0) return 0.0f;
if (itemElapsed >= m_itemDuration) return 1.0f;
return EaseDecelerate(itemElapsed / m_itemDuration);
}
/**
* @brief Get eased alpha for item (for fade-in)
*/
float getItemAlpha(int itemIndex) const {
return getItemProgress(itemIndex);
}
/**
* @brief Get Y offset for item (for slide-in from bottom)
*/
float getItemYOffset(int itemIndex, float maxOffset = 20.0f) const {
float progress = getItemProgress(itemIndex);
return maxOffset * (1.0f - progress);
}
bool isRunning() const { return m_running; }
private:
int m_itemCount;
float m_staggerDelay;
float m_itemDuration;
float m_elapsed;
bool m_running;
};
// ============================================================================
// Container Transform
// ============================================================================
/**
* @brief Container transform animation state
*
* For hero-style transitions where a card expands into a full dialog/page.
*/
struct ContainerTransform {
ImRect startRect; // Starting bounds (e.g., card)
ImRect endRect; // Ending bounds (e.g., dialog)
float progress; // 0 = start, 1 = end
bool expanding; // Direction
ContainerTransform()
: progress(0)
, expanding(true)
{}
/**
* @brief Get interpolated bounds at current progress
*/
ImRect getCurrentRect() const {
float t = expanding ? progress : (1.0f - progress);
float eased = EaseStandard(t);
return ImRect(
ImLerp(startRect.Min, endRect.Min, eased),
ImLerp(startRect.Max, endRect.Max, eased)
);
}
/**
* @brief Get corner radius (shrinks as container expands)
*/
float getCornerRadius(float startRadius, float endRadius) const {
float t = expanding ? progress : (1.0f - progress);
float eased = EaseStandard(t);
return startRadius + (endRadius - startRadius) * eased;
}
};
// ============================================================================
// Implementation
// ============================================================================
inline float CubicBezier(float t, float x1, float y1, float x2, float y2) {
// Attempt to find t value for given x (Newton-Raphson approximation)
// This is needed because CSS bezier curves are defined in terms of x
// For simplicity, we'll use a direct parametric approach
// which is accurate enough for UI animations
float cx = 3.0f * x1;
float bx = 3.0f * (x2 - x1) - cx;
float ax = 1.0f - cx - bx;
float cy = 3.0f * y1;
float by = 3.0f * (y2 - y1) - cy;
float ay = 1.0f - cy - by;
// Sample y at parameter t
// Note: This assumes t directly maps to time, which is an approximation
// For more accuracy, we'd need to solve for the bezier parameter given x=t
float t2 = t * t;
float t3 = t2 * t;
return ay * t3 + by * t2 + cy * t;
}
inline float EaseStandard(float t) {
// cubic-bezier(0.4, 0.0, 0.2, 1.0)
return CubicBezier(t, 0.4f, 0.0f, 0.2f, 1.0f);
}
inline float EaseDecelerate(float t) {
// cubic-bezier(0.0, 0.0, 0.2, 1.0)
return CubicBezier(t, 0.0f, 0.0f, 0.2f, 1.0f);
}
inline float EaseAccelerate(float t) {
// cubic-bezier(0.4, 0.0, 1.0, 1.0)
return CubicBezier(t, 0.4f, 0.0f, 1.0f, 1.0f);
}
inline float EaseSharp(float t) {
// cubic-bezier(0.4, 0.0, 0.6, 1.0)
return CubicBezier(t, 0.4f, 0.0f, 0.6f, 1.0f);
}
inline float EaseLinear(float t) {
return t;
}
inline float EaseOvershoot(float t, float overshoot) {
// Back ease out
t = t - 1.0f;
return t * t * ((overshoot + 1.0f) * t + overshoot) + 1.0f;
}
inline float EaseElastic(float t) {
if (t == 0.0f || t == 1.0f) return t;
float p = 0.3f;
float s = p / 4.0f;
return std::pow(2.0f, -10.0f * t) * std::sin((t - s) * (2.0f * IM_PI) / p) + 1.0f;
}
} // namespace material
} // namespace ui
} // namespace dragonx

290
src/ui/material/ripple.h Normal file
View File

@@ -0,0 +1,290 @@
// 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 "imgui.h"
#include "imgui_internal.h"
#include <vector>
#include <cmath>
namespace dragonx {
namespace ui {
namespace material {
// ============================================================================
// Material Design Ripple Effect
// ============================================================================
// Based on https://m2.material.io/design/motion/customization.html
//
// Ripple effects provide visual feedback when users touch interactive elements.
// The ripple emanates from the touch point and expands outward.
/**
* @brief Individual ripple instance
*/
struct Ripple {
ImVec2 center; // Ripple center point
float startTime; // When ripple started
float maxRadius; // Maximum expansion radius
ImU32 color; // Ripple color
bool releasing; // True when finger lifted
float releaseTime; // When release started
Ripple(const ImVec2& center, float maxRadius, ImU32 color, float currentTime)
: center(center)
, startTime(currentTime)
, maxRadius(maxRadius)
, color(color)
, releasing(false)
, releaseTime(0)
{}
};
/**
* @brief Ripple effect manager
*
* Manages ripple animations for interactive elements.
*/
class RippleManager {
public:
static RippleManager& instance();
/**
* @brief Start a new ripple
*
* @param id Widget ID
* @param center Ripple center (touch point)
* @param maxRadius Maximum ripple radius
* @param color Ripple color (typically primary with low alpha)
*/
void startRipple(ImGuiID id, const ImVec2& center, float maxRadius, ImU32 color);
/**
* @brief Release current ripple (finger lifted)
*/
void releaseRipple(ImGuiID id);
/**
* @brief Draw ripple effect for a widget
*
* @param drawList Draw list to use
* @param id Widget ID
* @param clipRect Clip rectangle for the ripple
* @param cornerRadius Corner radius for clip shape
*/
void drawRipple(ImDrawList* drawList, ImGuiID id, const ImRect& clipRect,
float cornerRadius = 0);
/**
* @brief Update all ripples (call once per frame)
*/
void update();
/**
* @brief Clear all ripples
*/
void clear();
private:
RippleManager() = default;
struct RippleEntry {
ImGuiID id;
Ripple ripple;
};
std::vector<RippleEntry> m_ripples;
static constexpr float kExpandDuration = 0.3f; // 300ms to full size
static constexpr float kFadeDuration = 0.2f; // 200ms fade out
static constexpr float kMaxOpacity = 0.12f; // 12% max opacity
};
// ============================================================================
// Convenience Functions
// ============================================================================
/**
* @brief Draw ripple effect for a button/interactive element
*
* Call this after drawing the element background but before text/content.
*
* @param drawList Draw list
* @param rect Element bounds
* @param id Element ID
* @param cornerRadius Corner radius
* @param hovered Is element hovered
* @param held Is element being pressed
* @param color Optional ripple color (default: white for dark theme)
*/
void DrawRippleEffect(ImDrawList* drawList, const ImRect& rect, ImGuiID id,
float cornerRadius = 0, bool hovered = false, bool held = false,
ImU32 color = IM_COL32(255, 255, 255, 255));
/**
* @brief Update ripple system (call once per frame in main loop)
*/
void UpdateRipples();
// ============================================================================
// Implementation
// ============================================================================
inline RippleManager& RippleManager::instance() {
static RippleManager s_instance;
return s_instance;
}
inline void RippleManager::startRipple(ImGuiID id, const ImVec2& center,
float maxRadius, ImU32 color) {
float currentTime = (float)ImGui::GetTime();
// Check if ripple already exists for this ID
for (auto& entry : m_ripples) {
if (entry.id == id) {
// Reset existing ripple
entry.ripple = Ripple(center, maxRadius, color, currentTime);
return;
}
}
// Add new ripple
m_ripples.push_back({id, Ripple(center, maxRadius, color, currentTime)});
}
inline void RippleManager::releaseRipple(ImGuiID id) {
float currentTime = (float)ImGui::GetTime();
for (auto& entry : m_ripples) {
if (entry.id == id && !entry.ripple.releasing) {
entry.ripple.releasing = true;
entry.ripple.releaseTime = currentTime;
}
}
}
inline void RippleManager::drawRipple(ImDrawList* drawList, ImGuiID id,
const ImRect& clipRect, float cornerRadius) {
float currentTime = (float)ImGui::GetTime();
for (const auto& entry : m_ripples) {
if (entry.id != id)
continue;
const Ripple& ripple = entry.ripple;
// Calculate expansion progress
float expandProgress = (currentTime - ripple.startTime) / kExpandDuration;
expandProgress = ImClamp(expandProgress, 0.0f, 1.0f);
// Deceleration easing for expansion
expandProgress = 1.0f - (1.0f - expandProgress) * (1.0f - expandProgress);
float currentRadius = ripple.maxRadius * expandProgress;
// Calculate opacity
float opacity = kMaxOpacity;
if (ripple.releasing) {
float fadeProgress = (currentTime - ripple.releaseTime) / kFadeDuration;
fadeProgress = ImClamp(fadeProgress, 0.0f, 1.0f);
opacity *= (1.0f - fadeProgress);
}
if (opacity <= 0.001f)
continue;
// Extract color components
float r = ((ripple.color >> IM_COL32_R_SHIFT) & 0xFF) / 255.0f;
float g = ((ripple.color >> IM_COL32_G_SHIFT) & 0xFF) / 255.0f;
float b = ((ripple.color >> IM_COL32_B_SHIFT) & 0xFF) / 255.0f;
ImU32 rippleColor = ImGui::ColorConvertFloat4ToU32(ImVec4(r, g, b, opacity));
// Push clip rect for rounded corners
if (cornerRadius > 0) {
// For rounded rectangles, we approximate with the clip rect
// A perfect solution would require custom clipping
drawList->PushClipRect(clipRect.Min, clipRect.Max, true);
}
// Draw ripple circle (skip expanding animation in low-spec mode)
if (!dragonx::ui::effects::isLowSpecMode())
drawList->AddCircleFilled(ripple.center, currentRadius, rippleColor, 32);
if (cornerRadius > 0) {
drawList->PopClipRect();
}
break; // Only one ripple per ID
}
}
inline void RippleManager::update() {
float currentTime = (float)ImGui::GetTime();
// Remove completed ripples
m_ripples.erase(
std::remove_if(m_ripples.begin(), m_ripples.end(),
[currentTime](const RippleEntry& entry) {
if (!entry.ripple.releasing)
return false;
float fadeProgress = (currentTime - entry.ripple.releaseTime) / kFadeDuration;
return fadeProgress >= 1.0f;
}
),
m_ripples.end()
);
}
inline void RippleManager::clear() {
m_ripples.clear();
}
inline void DrawRippleEffect(ImDrawList* drawList, const ImRect& rect, ImGuiID id,
float cornerRadius, bool hovered, bool held, ImU32 color) {
RippleManager& manager = RippleManager::instance();
// Start ripple on press
if (held && ImGui::IsMouseClicked(0)) {
ImVec2 mousePos = ImGui::GetIO().MousePos;
// Calculate max radius (distance from click to farthest corner)
float dx1 = mousePos.x - rect.Min.x;
float dy1 = mousePos.y - rect.Min.y;
float dx2 = rect.Max.x - mousePos.x;
float dy2 = rect.Max.y - mousePos.y;
float maxDx = ImMax(dx1, dx2);
float maxDy = ImMax(dy1, dy2);
float maxRadius = std::sqrt(maxDx * maxDx + maxDy * maxDy) * 1.2f;
manager.startRipple(id, mousePos, maxRadius, color);
}
// Release ripple when mouse released
if (!held && !ImGui::IsMouseDown(0)) {
manager.releaseRipple(id);
}
// Draw the ripple
manager.drawRipple(drawList, id, rect, cornerRadius);
// Also draw hover state overlay
if (hovered && !held) {
float r = ((color >> IM_COL32_R_SHIFT) & 0xFF) / 255.0f;
float g = ((color >> IM_COL32_G_SHIFT) & 0xFF) / 255.0f;
float b = ((color >> IM_COL32_B_SHIFT) & 0xFF) / 255.0f;
ImU32 hoverColor = ImGui::ColorConvertFloat4ToU32(ImVec4(r, g, b, 0.04f)); // 4% overlay
drawList->AddRectFilled(rect.Min, rect.Max, hoverColor, cornerRadius);
}
}
inline void UpdateRipples() {
RippleManager::instance().update();
}
} // namespace material
} // namespace ui
} // namespace dragonx

View File

@@ -0,0 +1,467 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include "motion.h"
#include "colors.h"
#include "imgui.h"
#include "imgui_internal.h"
#include <functional>
#include <string>
namespace dragonx {
namespace ui {
namespace material {
// ============================================================================
// Material Design Transitions
// ============================================================================
// Based on https://m2.material.io/design/motion/the-motion-system.html
//
// Transition patterns for navigating between views and states.
// ============================================================================
// Transition Types
// ============================================================================
enum class TransitionType {
None, // Instant switch
Fade, // Cross-fade
SlideLeft, // Slide from right to left (forward navigation)
SlideRight, // Slide from left to right (back navigation)
SlideUp, // Slide from bottom (modal entry)
SlideDown, // Slide to bottom (modal exit)
Scale, // Scale up/down
SharedAxis, // Material shared axis transition
ContainerTransform // Card to full-screen
};
// ============================================================================
// View Transition Manager
// ============================================================================
/**
* @brief Manages transitions between views/screens
*/
class TransitionManager {
public:
static TransitionManager& instance();
/**
* @brief Start a transition to a new view
*
* @param newViewId Identifier for the new view
* @param type Transition type
* @param duration Transition duration (default from motion.h)
*/
void transitionTo(const std::string& newViewId,
TransitionType type = TransitionType::Fade,
float duration = duration::ScreenChange);
/**
* @brief Go back with reverse transition
*/
void goBack(TransitionType type = TransitionType::SlideRight,
float duration = duration::ExitScreen);
/**
* @brief Update transition state (call each frame)
*/
void update(float deltaTime);
/**
* @brief Get current view ID
*/
const std::string& getCurrentView() const { return m_currentView; }
/**
* @brief Get previous view ID (during transition)
*/
const std::string& getPreviousView() const { return m_previousView; }
/**
* @brief Check if transition is in progress
*/
bool isTransitioning() const { return m_transitioning; }
/**
* @brief Get transition progress (0-1)
*/
float getProgress() const { return m_progress; }
/**
* @brief Get current transition type
*/
TransitionType getType() const { return m_type; }
/**
* @brief Apply transition effect to outgoing content
*
* Call before rendering the previous view content.
* Returns false if outgoing content should be skipped.
*/
bool beginOutgoingTransition();
/**
* @brief End outgoing content transition
*/
void endOutgoingTransition();
/**
* @brief Apply transition effect to incoming content
*
* Call before rendering the new view content.
* Returns false if incoming content should be skipped.
*/
bool beginIncomingTransition();
/**
* @brief End incoming content transition
*/
void endIncomingTransition();
/**
* @brief Get alpha for fading effects
*/
float getOutgoingAlpha() const;
float getIncomingAlpha() const;
/**
* @brief Get offset for sliding effects
*/
ImVec2 getOutgoingOffset() const;
ImVec2 getIncomingOffset() const;
private:
TransitionManager() = default;
std::string m_currentView;
std::string m_previousView;
TransitionType m_type = TransitionType::None;
float m_duration = 0;
float m_elapsed = 0;
float m_progress = 0;
bool m_transitioning = false;
};
// ============================================================================
// Fade Transition Helper
// ============================================================================
/**
* @brief Simple fade transition between two states
*/
class FadeTransition {
public:
FadeTransition() : m_alpha(1.0f) {}
/**
* @brief Fade out
*/
void fadeOut(float duration = duration::Fast) {
m_alpha.animateTo(0.0f, duration, EaseAccelerate);
}
/**
* @brief Fade in
*/
void fadeIn(float duration = duration::Fast) {
m_alpha.animateTo(1.0f, duration, EaseDecelerate);
}
/**
* @brief Update (call each frame)
*/
void update(float deltaTime) {
m_alpha.update(deltaTime);
}
/**
* @brief Get current alpha
*/
float getAlpha() const { return m_alpha.get(); }
/**
* @brief Check if visible (alpha > 0)
*/
bool isVisible() const { return m_alpha.get() > 0.001f; }
/**
* @brief Check if animating
*/
bool isAnimating() const { return m_alpha.isAnimating(); }
/**
* @brief Push alpha to ImGui
*/
void pushAlpha() const {
ImGui::PushStyleVar(ImGuiStyleVar_Alpha, m_alpha.get());
}
/**
* @brief Pop alpha from ImGui
*/
void popAlpha() const {
ImGui::PopStyleVar();
}
private:
AnimatedValue<float> m_alpha;
};
// ============================================================================
// Expandable/Collapsible Section
// ============================================================================
/**
* @brief Animated expandable section
*/
class ExpandableSection {
public:
ExpandableSection(bool initiallyExpanded = false)
: m_expanded(initiallyExpanded)
, m_height(initiallyExpanded ? 1.0f : 0.0f)
{}
/**
* @brief Toggle expansion state
*/
void toggle() {
setExpanded(!m_expanded);
}
/**
* @brief Set expansion state
*/
void setExpanded(bool expanded) {
m_expanded = expanded;
m_height.animateTo(expanded ? 1.0f : 0.0f,
expanded ? duration::Standard : duration::Medium,
EaseStandard);
}
/**
* @brief Update animation
*/
void update(float deltaTime) {
m_height.update(deltaTime);
}
/**
* @brief Check if expanded
*/
bool isExpanded() const { return m_expanded; }
/**
* @brief Get height multiplier (0-1)
*/
float getHeightMultiplier() const { return m_height.get(); }
/**
* @brief Check if animating
*/
bool isAnimating() const { return m_height.isAnimating(); }
/**
* @brief Begin expandable content (applies clipping)
*
* @param maxHeight Maximum content height when fully expanded
* @return true if content should be rendered
*/
bool beginContent(float maxHeight) {
float currentHeight = maxHeight * m_height.get();
if (currentHeight < 1.0f)
return false;
ImGui::BeginChild("##expandable", ImVec2(0, currentHeight), false,
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
return true;
}
/**
* @brief End expandable content
*/
void endContent() {
ImGui::EndChild();
}
private:
bool m_expanded;
AnimatedValue<float> m_height;
};
// ============================================================================
// Implementation
// ============================================================================
inline TransitionManager& TransitionManager::instance() {
static TransitionManager s_instance;
return s_instance;
}
inline void TransitionManager::transitionTo(const std::string& newViewId,
TransitionType type, float duration) {
if (m_transitioning)
return; // Don't interrupt ongoing transition
m_previousView = m_currentView;
m_currentView = newViewId;
m_type = type;
m_duration = duration;
m_elapsed = 0;
m_progress = 0;
m_transitioning = true;
}
inline void TransitionManager::goBack(TransitionType type, float duration) {
if (m_transitioning || m_previousView.empty())
return;
std::string temp = m_currentView;
m_currentView = m_previousView;
m_previousView = temp;
m_type = type;
m_duration = duration;
m_elapsed = 0;
m_progress = 0;
m_transitioning = true;
}
inline void TransitionManager::update(float deltaTime) {
if (!m_transitioning)
return;
m_elapsed += deltaTime;
m_progress = m_elapsed / m_duration;
if (m_progress >= 1.0f) {
m_progress = 1.0f;
m_transitioning = false;
m_previousView.clear();
}
}
inline bool TransitionManager::beginOutgoingTransition() {
if (!m_transitioning || m_type == TransitionType::None)
return false;
float alpha = getOutgoingAlpha();
if (alpha < 0.001f)
return false;
ImGui::PushStyleVar(ImGuiStyleVar_Alpha, alpha);
ImVec2 offset = getOutgoingOffset();
if (offset.x != 0 || offset.y != 0) {
ImVec2 cursor = ImGui::GetCursorPos();
ImGui::SetCursorPos(ImVec2(cursor.x + offset.x, cursor.y + offset.y));
}
return true;
}
inline void TransitionManager::endOutgoingTransition() {
ImGui::PopStyleVar();
}
inline bool TransitionManager::beginIncomingTransition() {
if (!m_transitioning && m_progress >= 1.0f)
return true; // No transition, just render normally
if (m_type == TransitionType::None)
return true;
float alpha = getIncomingAlpha();
ImGui::PushStyleVar(ImGuiStyleVar_Alpha, alpha);
ImVec2 offset = getIncomingOffset();
if (offset.x != 0 || offset.y != 0) {
ImVec2 cursor = ImGui::GetCursorPos();
ImGui::SetCursorPos(ImVec2(cursor.x + offset.x, cursor.y + offset.y));
}
return true;
}
inline void TransitionManager::endIncomingTransition() {
if (m_transitioning || m_type != TransitionType::None) {
ImGui::PopStyleVar();
}
}
inline float TransitionManager::getOutgoingAlpha() const {
float eased = EaseAccelerate(m_progress);
switch (m_type) {
case TransitionType::Fade:
return 1.0f - eased;
case TransitionType::Scale:
return 1.0f - eased;
default:
// For slides, fade quickly in first half
return m_progress < 0.5f ? 1.0f - eased * 2.0f : 0.0f;
}
}
inline float TransitionManager::getIncomingAlpha() const {
float eased = EaseDecelerate(m_progress);
switch (m_type) {
case TransitionType::Fade:
return eased;
case TransitionType::Scale:
return eased;
default:
// For slides, fade in during second half
return m_progress > 0.5f ? (eased - 0.5f) * 2.0f : 0.0f;
}
}
inline ImVec2 TransitionManager::getOutgoingOffset() const {
ImGuiIO& io = ImGui::GetIO();
float eased = EaseAccelerate(m_progress);
float slideDistance = 100.0f; // Pixels to slide
switch (m_type) {
case TransitionType::SlideLeft:
return ImVec2(-slideDistance * eased, 0);
case TransitionType::SlideRight:
return ImVec2(slideDistance * eased, 0);
case TransitionType::SlideUp:
return ImVec2(0, -slideDistance * eased);
case TransitionType::SlideDown:
return ImVec2(0, slideDistance * eased);
case TransitionType::SharedAxis:
return ImVec2(-slideDistance * 0.3f * eased, 0);
default:
return ImVec2(0, 0);
}
}
inline ImVec2 TransitionManager::getIncomingOffset() const {
float eased = EaseDecelerate(m_progress);
float slideDistance = 100.0f;
float remaining = 1.0f - eased;
switch (m_type) {
case TransitionType::SlideLeft:
return ImVec2(slideDistance * remaining, 0);
case TransitionType::SlideRight:
return ImVec2(-slideDistance * remaining, 0);
case TransitionType::SlideUp:
return ImVec2(0, slideDistance * remaining);
case TransitionType::SlideDown:
return ImVec2(0, -slideDistance * remaining);
case TransitionType::SharedAxis:
return ImVec2(slideDistance * 0.3f * remaining, 0);
default:
return ImVec2(0, 0);
}
}
} // namespace material
} // namespace ui
} // namespace dragonx

144
src/ui/material/type.h Normal file
View File

@@ -0,0 +1,144 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
// ============================================================================
// Material Design Typography Quick Reference
// ============================================================================
//
// Include this header for convenient access to the typography system.
//
// Usage:
// #include "ui/material/type.h"
//
// // Access fonts directly
// ImGui::PushFont(Type().h5());
// ImGui::Text("Card Title");
// ImGui::PopFont();
//
// // Use text helpers
// Type().text(TypeStyle::H5, "Card Title");
// Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), "Timestamp");
//
// // Scoped font (RAII)
// MATERIAL_FONT(Body1) {
// ImGui::TextWrapped("Body text goes here...");
// }
#pragma once
#include "typography.h"
#include "colors.h"
namespace dragonx {
namespace ui {
namespace material {
// ============================================================================
// Convenience Accessor
// ============================================================================
/**
* @brief Quick access to Typography singleton
*
* Usage:
* ImFont* title = Type().h5();
* Type().text(TypeStyle::Body1, "Hello");
*/
inline Typography& Type() {
return Typography::instance();
}
// ============================================================================
// Text Style Constants for Quick Reference
// ============================================================================
// Headers (Large display text)
// H1: 96px Light - Rare, used for massive display numbers
// H2: 60px Light - Section banners
// H3: 48px Regular - Major section titles
// H4: 34px Regular - Page titles
// H5: 24px Regular - Card titles, dialog titles
// H6: 20px Medium - Section headers, sidebar items
// Body (Primary content)
// Subtitle1: 16px Regular - Secondary information, list item primary text
// Subtitle2: 14px Medium - List item secondary text, menu items
// Body1: 16px Regular - Primary body text (default for content)
// Body2: 14px Regular - Secondary body text
// UI Elements
// Button: 14px Medium, UPPERCASE - Button labels
// Caption: 12px Regular - Timestamps, hints, helper text
// Overline: 10px Regular, UPPERCASE - Section labels, tabs
// ============================================================================
// Common Typography Patterns
// ============================================================================
/**
* @brief Render a card header with title and optional subtitle
*/
inline void CardHeader(const char* title, const char* subtitle = nullptr)
{
Type().text(TypeStyle::H5, title);
if (subtitle) {
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), subtitle);
}
}
/**
* @brief Render an overline label (uppercase section marker)
*/
inline void OverlineLabel(const char* text)
{
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), text);
}
/**
* @brief Render body text
*/
inline void BodyText(const char* text, bool secondary = false)
{
Type().text(secondary ? TypeStyle::Body2 : TypeStyle::Body1, text);
}
/**
* @brief Render caption/helper text
*/
inline void Caption(const char* text)
{
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), text);
}
/**
* @brief Render error text
*/
inline void ErrorText(const char* text)
{
Type().textColored(TypeStyle::Body2, Error(), text);
}
/**
* @brief Render success text
*/
inline void SuccessText(const char* text)
{
Type().textColored(TypeStyle::Body2, Success(), text);
}
/**
* @brief Render a balance/amount display (large number)
*/
inline void AmountDisplay(const char* amount, const char* unit = nullptr)
{
Type().text(TypeStyle::H4, amount);
if (unit) {
ImGui::SameLine();
Type().textColored(TypeStyle::H6, OnSurfaceMedium(), unit);
}
}
} // namespace material
} // namespace ui
} // namespace dragonx

View File

@@ -0,0 +1,401 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#include "typography.h"
#include "../layout.h"
#include "../schema/ui_schema.h"
#include <cstring>
#include <cstdio>
#include <cctype>
#include <string>
// Embedded font data (via INCBIN assembler .incbin directive)
#include "../embedded/embedded_fonts.h" // g_ubuntu_*_data/size, g_material_icons_*
#include "../embedded/IconsMaterialDesign.h" // Icon codepoint defines
#include "../../util/logger.h"
namespace dragonx {
namespace ui {
namespace material {
// ============================================================================
// Type Scale Specifications
// ============================================================================
// Font sizes are configured in ui/layout.h for easy global adjustment.
// Letter spacing, line height and text-transform are loaded from ui.toml
// via the UISchema typography section.
// Font sizes come from UISchema (JSON-configurable) via Layout:: accessors.
const TypeSpec* Typography::getTypeSpecs()
{
// Rebuild each call to pick up hot-reloaded values
static TypeSpec specs[kNumStyles];
// Helper: read typography entry from UISchema
auto& S = schema::UI();
auto tspec = [&](const char* key, float defLS, float defLH, bool defUpper) -> TypeSpec {
auto e = S.drawElement("typography", key);
float ls = e.getFloat("letter-spacing", defLS);
float lh = e.getFloat("line-height", defLH);
// text-transform stored as extraColors (string map)
bool upper = defUpper;
auto it = e.extraColors.find("text-transform");
if (it != e.extraColors.end()) {
upper = (it->second == "uppercase");
}
return TypeSpec{0.0f, ls, lh, upper};
};
// Font size comes from Layout:: accessors; typography from UISchema
auto ts0 = tspec("h1", -1.0f, 1.167f, false);
auto ts1 = tspec("h2", -0.5f, 1.2f, false);
auto ts2 = tspec("h3", 0.0f, 1.167f, false);
auto ts3 = tspec("h4", 0.25f, 1.235f, false);
auto ts4 = tspec("h5", 0.0f, 1.334f, false);
auto ts5 = tspec("h6", 0.15f, 1.6f, false);
auto ts6 = tspec("subtitle1", 0.15f, 1.75f, false);
auto ts7 = tspec("subtitle2", 0.1f, 1.57f, false);
auto ts8 = tspec("body1", 0.5f, 1.5f, false);
auto ts9 = tspec("body2", 0.25f, 1.43f, false);
auto ts10 = tspec("button", 1.25f, 1.75f, true);
auto ts11 = tspec("caption", 0.4f, 1.66f, false);
auto ts12 = tspec("overline", 1.5f, 2.66f, true);
specs[0] = { Layout::kFontH1(), ts0.letterSpacing, ts0.lineHeight, ts0.uppercase };
specs[1] = { Layout::kFontH2(), ts1.letterSpacing, ts1.lineHeight, ts1.uppercase };
specs[2] = { Layout::kFontH3(), ts2.letterSpacing, ts2.lineHeight, ts2.uppercase };
specs[3] = { Layout::kFontH4(), ts3.letterSpacing, ts3.lineHeight, ts3.uppercase };
specs[4] = { Layout::kFontH5(), ts4.letterSpacing, ts4.lineHeight, ts4.uppercase };
specs[5] = { Layout::kFontH6(), ts5.letterSpacing, ts5.lineHeight, ts5.uppercase };
specs[6] = { Layout::kFontSubtitle1(), ts6.letterSpacing, ts6.lineHeight, ts6.uppercase };
specs[7] = { Layout::kFontSubtitle2(), ts7.letterSpacing, ts7.lineHeight, ts7.uppercase };
specs[8] = { Layout::kFontBody1(), ts8.letterSpacing, ts8.lineHeight, ts8.uppercase };
specs[9] = { Layout::kFontBody2(), ts9.letterSpacing, ts9.lineHeight, ts9.uppercase };
specs[10] = { Layout::kFontButton(), ts10.letterSpacing, ts10.lineHeight, ts10.uppercase };
specs[11] = { Layout::kFontCaption(), ts11.letterSpacing, ts11.lineHeight, ts11.uppercase };
specs[12] = { Layout::kFontOverline(), ts12.letterSpacing, ts12.lineHeight, ts12.uppercase };
specs[13] = { Layout::kFontButtonSm(), ts10.letterSpacing, ts10.lineHeight, ts10.uppercase };
specs[14] = { Layout::kFontButtonLg(), ts10.letterSpacing, ts10.lineHeight, ts10.uppercase };
return specs;
}
// ============================================================================
// Typography Implementation
// ============================================================================
Typography& Typography::instance()
{
static Typography s_instance;
return s_instance;
}
bool Typography::reload(ImGuiIO& io, float dpiScale)
{
if (!loaded_) {
return load(io, dpiScale);
}
DEBUG_LOGF("Typography: Reloading fonts for DPI scale %.2f\n", dpiScale);
// Clear existing fonts and reload
io.Fonts->Clear();
loaded_ = false;
for (int i = 0; i < kNumStyles; ++i) fonts_[i] = nullptr;
for (int i = 0; i < kNumIconSizes; ++i) iconFonts_[i] = nullptr;
return load(io, dpiScale);
}
bool Typography::load(ImGuiIO& io, float dpiScale)
{
if (loaded_) {
DEBUG_LOGF("Typography: Already loaded\n");
return true;
}
dpiScale_ = dpiScale;
// Multiply by dpiScale so fonts are the correct physical size.
// On Windows Per-Monitor DPI v2, SDL3 coordinates are physical pixels
// and DisplayFramebufferScale is 1.0 (no automatic upscaling).
// The window is resized by dpiScale in main.cpp so that fonts at
// size*dpiScale fit proportionally (no overflow).
float scale = dpiScale * Layout::kFontScale();
DEBUG_LOGF("Typography: Loading Material Design type scale (DPI: %.2f, fontScale: %.2f, combined: %.2f)\n", dpiScale, Layout::kFontScale(), scale);
// For ImGui, we need to load fonts at specific pixel sizes.
// Font sizes come from Layout:: accessors (backed by UISchema JSON)
// Load fonts in order of TypeStyle enum
// H1: Light
fonts_[0] = loadFont(io, kWeightLight, Layout::kFontH1() * scale, "H1");
// H2: Light
fonts_[1] = loadFont(io, kWeightLight, Layout::kFontH2() * scale, "H2");
// H3: Regular
fonts_[2] = loadFont(io, kWeightRegular, Layout::kFontH3() * scale, "H3");
// H4: Regular
fonts_[3] = loadFont(io, kWeightRegular, Layout::kFontH4() * scale, "H4");
// H5: Regular
fonts_[4] = loadFont(io, kWeightRegular, Layout::kFontH5() * scale, "H5");
// H6: Medium
fonts_[5] = loadFont(io, kWeightMedium, Layout::kFontH6() * scale, "H6");
// Subtitle1: Regular
fonts_[6] = loadFont(io, kWeightRegular, Layout::kFontSubtitle1() * scale, "Subtitle1");
// Subtitle2: Medium
fonts_[7] = loadFont(io, kWeightMedium, Layout::kFontSubtitle2() * scale, "Subtitle2");
// Body1: Regular (shares with Subtitle1 if same size)
if (Layout::kFontBody1() == Layout::kFontSubtitle1()) {
fonts_[8] = fonts_[6]; // Reuse Subtitle1 font
} else {
fonts_[8] = loadFont(io, kWeightRegular, Layout::kFontBody1() * scale, "Body1");
}
// Body2: Regular
fonts_[9] = loadFont(io, kWeightRegular, Layout::kFontBody2() * scale, "Body2");
// Button: Medium (shares with Subtitle2 if same size)
if (Layout::kFontButton() == Layout::kFontSubtitle2()) {
fonts_[10] = fonts_[7]; // Reuse Subtitle2 font
} else {
fonts_[10] = loadFont(io, kWeightMedium, Layout::kFontButton() * scale, "Button");
}
// Caption: Regular
fonts_[11] = loadFont(io, kWeightRegular, Layout::kFontCaption() * scale, "Caption");
// Overline: Regular
fonts_[12] = loadFont(io, kWeightRegular, Layout::kFontOverline() * scale, "Overline");
// ButtonSm: Medium
fonts_[13] = loadFont(io, kWeightMedium, Layout::kFontButtonSm() * scale, "ButtonSm");
// ButtonLg: Medium
fonts_[14] = loadFont(io, kWeightMedium, Layout::kFontButtonLg() * scale, "ButtonLg");
// --- Icon fonts at multiple sizes ---
// These are standalone fonts (not merged) so we can PushFont(iconXxx) for icon rendering.
iconFonts_[0] = loadIconFont(io, 14.0f * scale, "IconSmall");
iconFonts_[1] = loadIconFont(io, 18.0f * scale, "IconMed");
iconFonts_[2] = loadIconFont(io, 24.0f * scale, "IconLarge");
iconFonts_[3] = loadIconFont(io, 40.0f * scale, "IconXL");
// Verify all fonts loaded
bool allLoaded = true;
for (int i = 0; i < kNumStyles; ++i) {
if (!fonts_[i]) {
DEBUG_LOGF("Typography: Warning - Font for style %d not loaded\n", i);
fonts_[i] = io.Fonts->Fonts.Size > 0 ? io.Fonts->Fonts[0] : nullptr;
allLoaded = false;
}
}
// Set Body1 as the default font so that bare ImGui::Text() calls
// (status bar, dialogs, notifications, etc.) render at Body1 size
// instead of H1 (the first font loaded).
io.FontDefault = fonts_[static_cast<int>(TypeStyle::Body1)];
loaded_ = true;
DEBUG_LOGF("Typography: Loaded %d font styles (default=Body1)\n", allLoaded ? kNumStyles : -1);
return allLoaded;
}
ImFont* Typography::loadFont(ImGuiIO& io, int weight, float size, const char* name)
{
// Select font data based on weight
const unsigned char* fontData = nullptr;
unsigned int fontDataLen = 0;
switch (weight) {
case kWeightLight:
fontData = g_ubuntu_light_data;
fontDataLen = g_ubuntu_light_size;
break;
case kWeightMedium:
fontData = g_ubuntu_medium_data;
fontDataLen = g_ubuntu_medium_size;
break;
case kWeightRegular:
default:
fontData = g_ubuntu_regular_data;
fontDataLen = g_ubuntu_regular_size;
break;
}
// ImGui needs to own a copy of the font data
void* fontDataCopy = IM_ALLOC(fontDataLen);
memcpy(fontDataCopy, fontData, fontDataLen);
ImFontConfig cfg;
cfg.FontDataOwnedByAtlas = true; // ImGui will free this
cfg.OversampleH = 2;
cfg.OversampleV = 1;
cfg.PixelSnapH = true;
// Include default ASCII + Latin, Latin Extended (for Spanish/multilingual),
// plus arrows (⇄ U+21C4), math (≈ U+2248),
// general punctuation (— U+2014, … U+2026, etc.)
static const ImWchar glyphRanges[] = {
0x0020, 0x00FF, // Basic Latin + Latin-1 Supplement
0x0100, 0x024F, // Latin Extended-A + Latin Extended-B (Spanish, etc.)
0x2000, 0x206F, // General Punctuation (em dash, ellipsis, etc.)
0x2190, 0x21FF, // Arrows (includes ⇄ U+21C4, ↻ U+21BB)
0x2200, 0x22FF, // Mathematical Operators (includes ≈ U+2248)
0x2600, 0x26FF, // Miscellaneous Symbols (includes ⛏ U+26CF)
0,
};
cfg.GlyphRanges = glyphRanges;
// Create a unique name for this font variant
snprintf(cfg.Name, sizeof(cfg.Name), "Ubuntu %s %.0fpx",
weight == kWeightLight ? "Light" :
weight == kWeightMedium ? "Medium" : "Regular",
size);
ImFont* font = io.Fonts->AddFontFromMemoryTTF(fontDataCopy, fontDataLen, size, &cfg);
if (font) {
DEBUG_LOGF("Typography: Loaded %s (%.0fpx) as '%s'\n", name, size, cfg.Name);
} else {
DEBUG_LOGF("Typography: Failed to load %s\n", name);
IM_FREE(fontDataCopy);
}
return font;
}
ImFont* Typography::loadIconFont(ImGuiIO& io, float size, const char* name)
{
// ImGui needs to own a copy of the font data
void* fontDataCopy = IM_ALLOC(g_material_icons_size);
memcpy(fontDataCopy, g_material_icons_data, g_material_icons_size);
ImFontConfig cfg;
cfg.FontDataOwnedByAtlas = true;
cfg.OversampleH = 2;
cfg.OversampleV = 1;
cfg.PixelSnapH = true;
cfg.MergeMode = false; // standalone icon font
cfg.GlyphMinAdvanceX = size; // monospace icons
static const ImWchar iconRanges[] = { ICON_MIN_MD, ICON_MAX_16_MD, 0 };
cfg.GlyphRanges = iconRanges;
snprintf(cfg.Name, sizeof(cfg.Name), "MDIcons %.0fpx", size);
ImFont* font = io.Fonts->AddFontFromMemoryTTF(fontDataCopy, g_material_icons_size, size, &cfg);
if (font) {
DEBUG_LOGF("Typography: Loaded icon font %s (%.0fpx)\n", name, size);
} else {
DEBUG_LOGF("Typography: Failed to load icon font %s\n", name);
IM_FREE(fontDataCopy);
}
return font;
}
ImFont* Typography::getFont(TypeStyle style) const
{
int index = static_cast<int>(style);
if (index >= 0 && index < kNumStyles && fonts_[index]) {
return fonts_[index];
}
// Return default font if not found
return ImGui::GetIO().Fonts->Fonts.Size > 0 ?
ImGui::GetIO().Fonts->Fonts[0] : nullptr;
}
const TypeSpec& Typography::getSpec(TypeStyle style) const
{
const TypeSpec* specs = getTypeSpecs();
int index = static_cast<int>(style);
if (index >= 0 && index < kNumStyles) {
return specs[index];
}
// Return Body1 as default
return specs[static_cast<int>(TypeStyle::Body1)];
}
void Typography::pushFont(TypeStyle style) const
{
ImGui::PushFont(getFont(style));
}
void Typography::popFont() const
{
ImGui::PopFont();
}
// ============================================================================
// Text Rendering Helpers
// ============================================================================
void Typography::text(TypeStyle style, const char* text) const
{
const TypeSpec& spec = getSpec(style);
pushFont(style);
if (spec.uppercase) {
// Convert to uppercase
std::string upper;
upper.reserve(strlen(text));
for (const char* p = text; *p; ++p) {
upper += static_cast<char>(std::toupper(static_cast<unsigned char>(*p)));
}
ImGui::TextUnformatted(upper.c_str());
} else {
ImGui::TextUnformatted(text);
}
popFont();
}
void Typography::textWrapped(TypeStyle style, const char* text) const
{
const TypeSpec& spec = getSpec(style);
pushFont(style);
if (spec.uppercase) {
std::string upper;
upper.reserve(strlen(text));
for (const char* p = text; *p; ++p) {
upper += static_cast<char>(std::toupper(static_cast<unsigned char>(*p)));
}
ImGui::TextWrapped("%s", upper.c_str());
} else {
ImGui::TextWrapped("%s", text);
}
popFont();
}
void Typography::textColored(TypeStyle style, ImU32 color, const char* text) const
{
const TypeSpec& spec = getSpec(style);
pushFont(style);
ImVec4 colorVec = ImGui::ColorConvertU32ToFloat4(color);
if (spec.uppercase) {
std::string upper;
upper.reserve(strlen(text));
for (const char* p = text; *p; ++p) {
upper += static_cast<char>(std::toupper(static_cast<unsigned char>(*p)));
}
ImGui::TextColored(colorVec, "%s", upper.c_str());
} else {
ImGui::TextColored(colorVec, "%s", text);
}
popFont();
}
} // namespace material
} // namespace ui
} // namespace dragonx

View File

@@ -0,0 +1,288 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include "imgui.h"
#include <string>
namespace dragonx {
namespace ui {
namespace material {
// ============================================================================
// Material Design Type Scale
// ============================================================================
// Based on https://m2.material.io/design/typography/the-type-system.html
//
// The type scale is a combination of 13 styles that are supported by the
// type system. It contains reusable categories of text, each with an
// intended application and meaning.
/**
* @brief Material Design typography style identifiers
*/
enum class TypeStyle {
H1, // 96sp Light - Large display headers
H2, // 60sp Light - Display headers
H3, // 48sp Regular - Section titles
H4, // 34sp Regular - Section titles
H5, // 24sp Regular - Card titles, dialog titles
H6, // 20sp Medium - Section headers, subtitles
Subtitle1, // 16sp Regular - Secondary information
Subtitle2, // 14sp Medium - Secondary information
Body1, // 16sp Regular - Primary body text
Body2, // 14sp Regular - Secondary body text
Button, // 14sp Medium - Button labels (uppercase)
Caption, // 12sp Regular - Timestamps, labels
Overline, // 10sp Regular - Overline labels (uppercase)
ButtonSm, // Small button labels
ButtonLg // Large button labels
};
/**
* @brief Typography specification for a type style
*/
struct TypeSpec {
float size; // Font size in sp (scaled pixels)
float letterSpacing; // Letter spacing in px (applied after scaling)
float lineHeight; // Line height multiplier
bool uppercase; // Whether text should be uppercase
// Font weight is handled by which font is used (Light/Regular/Medium)
};
/**
* @brief Typography system managing Material Design fonts
*
* Loads Ubuntu fonts in Light, Regular, and Medium weights at multiple
* sizes to implement the Material Design type scale.
*
* Usage:
* // Initialize during startup (after ImGui context)
* dragonx::ui::material::Typography::instance().load(io);
*
* // Use fonts throughout the application
* ImGui::PushFont(Typography::instance().getFont(TypeStyle::H5));
* ImGui::Text("Card Title");
* ImGui::PopFont();
*
* // Or use convenience functions
* Typography::instance().pushFont(TypeStyle::Body1);
* ImGui::TextWrapped("Body text here...");
* Typography::instance().popFont();
*/
class Typography {
public:
/**
* @brief Get the singleton instance
*/
static Typography& instance();
/**
* @brief Load all fonts for the type scale
*
* Must be called after ImGui context is created but before first frame.
*
* @param io ImGui IO reference
* @param dpiScale DPI scale factor (default 1.0)
* @return true if fonts loaded successfully
*/
bool load(ImGuiIO& io, float dpiScale = 1.0f);
/**
* @brief Reload fonts at a new DPI scale
*
* Call when the display scale changes (e.g., window moved to a different
* DPI monitor). Clears the existing font atlas and reloads all fonts.
*
* @param io ImGui IO reference
* @param dpiScale New DPI scale factor
* @return true if fonts reloaded successfully
*/
bool reload(ImGuiIO& io, float dpiScale);
/**
* @brief Check if typography system is loaded
*/
bool isLoaded() const { return loaded_; }
/**
* @brief Get the current DPI scale
*/
float getDpiScale() const { return dpiScale_; }
/**
* @brief Get font for a type style
*
* @param style The Material Design type style
* @return ImFont pointer (never null, returns default if not loaded)
*/
ImFont* getFont(TypeStyle style) const;
/**
* @brief Get the spec for a type style
*
* @param style The Material Design type style
* @return TypeSpec with size, spacing, and line height
*/
const TypeSpec& getSpec(TypeStyle style) const;
/**
* @brief Push font for a type style
*
* Convenience wrapper around ImGui::PushFont(getFont(style))
*/
void pushFont(TypeStyle style) const;
/**
* @brief Pop the current font
*
* Convenience wrapper around ImGui::PopFont()
*/
void popFont() const;
// ========================================================================
// Font Accessors (for common cases)
// ========================================================================
// Headers
ImFont* h1() const { return getFont(TypeStyle::H1); }
ImFont* h2() const { return getFont(TypeStyle::H2); }
ImFont* h3() const { return getFont(TypeStyle::H3); }
ImFont* h4() const { return getFont(TypeStyle::H4); }
ImFont* h5() const { return getFont(TypeStyle::H5); }
ImFont* h6() const { return getFont(TypeStyle::H6); }
// Body text
ImFont* subtitle1() const { return getFont(TypeStyle::Subtitle1); }
ImFont* subtitle2() const { return getFont(TypeStyle::Subtitle2); }
ImFont* body1() const { return getFont(TypeStyle::Body1); }
ImFont* body2() const { return getFont(TypeStyle::Body2); }
// UI elements
ImFont* button() const { return getFont(TypeStyle::Button); }
ImFont* buttonSm() const { return getFont(TypeStyle::ButtonSm); }
ImFont* buttonLg() const { return getFont(TypeStyle::ButtonLg); }
ImFont* caption() const { return getFont(TypeStyle::Caption); }
ImFont* overline() const { return getFont(TypeStyle::Overline); }
// Icon fonts — Material Design Icons merged at specific pixel sizes
// Use these when you need to render an icon at a specific size via AddText/ImGui::Text.
// Common sizes: iconSmall (14px), iconMed (18px), iconLarge (24px).
ImFont* iconSmall() const { return iconFonts_[0] ? iconFonts_[0] : getFont(TypeStyle::Body2); }
ImFont* iconMed() const { return iconFonts_[1] ? iconFonts_[1] : getFont(TypeStyle::Body1); }
ImFont* iconLarge() const { return iconFonts_[2] ? iconFonts_[2] : getFont(TypeStyle::H5); }
ImFont* iconXL() const { return iconFonts_[3] ? iconFonts_[3] : getFont(TypeStyle::H3); }
/**
* @brief Resolve a font name string to ImFont*
* @param name Font name like "button", "button-sm", "button-lg", "h4", "body1"
* @return ImFont* pointer, or nullptr if name is empty/unknown
*/
ImFont* resolveByName(const std::string& name) const {
if (name.empty()) return nullptr;
if (name == "h1") return h1();
if (name == "h2") return h2();
if (name == "h3") return h3();
if (name == "h4") return h4();
if (name == "h5") return h5();
if (name == "h6") return h6();
if (name == "subtitle1") return subtitle1();
if (name == "subtitle2") return subtitle2();
if (name == "body1") return body1();
if (name == "body2") return body2();
if (name == "button") return button();
if (name == "button-sm") return buttonSm();
if (name == "button-lg") return buttonLg();
if (name == "caption") return caption();
if (name == "overline") return overline();
return nullptr;
}
// ========================================================================
// Text Rendering Helpers
// ========================================================================
/**
* @brief Render text with a specific type style
*
* Handles font push/pop and optional uppercase transformation.
*
* @param style The type style to use
* @param text The text to render
*/
void text(TypeStyle style, const char* text) const;
/**
* @brief Render wrapped text with a specific type style
*
* @param style The type style to use
* @param text The text to render
*/
void textWrapped(TypeStyle style, const char* text) const;
/**
* @brief Render colored text with a specific type style
*
* @param style The type style to use
* @param color Text color
* @param text The text to render
*/
void textColored(TypeStyle style, ImU32 color, const char* text) const;
private:
Typography() = default;
~Typography() = default;
Typography(const Typography&) = delete;
Typography& operator=(const Typography&) = delete;
// Load fonts at a specific size with a specific weight
ImFont* loadFont(ImGuiIO& io, int weight, float size, const char* name);
// Font weight constants
static constexpr int kWeightLight = 300;
static constexpr int kWeightRegular = 400;
static constexpr int kWeightMedium = 500;
bool loaded_ = false;
float dpiScale_ = 1.0f;
// Fonts for each type style
ImFont* fonts_[15] = {};
// Icon fonts at different sizes: [0]=small(14), [1]=med(18), [2]=large(24), [3]=xl(40)
ImFont* iconFonts_[4] = {};
static constexpr int kNumIconSizes = 4;
// Load an icon-only font at a specific pixel size
ImFont* loadIconFont(ImGuiIO& io, float size, const char* name);
// Type specifications
static const TypeSpec* getTypeSpecs();
static constexpr int kNumStyles = 15;
};
// ============================================================================
// Convenience Macros (Optional)
// ============================================================================
// Scoped font push/pop using RAII
class ScopedFont {
public:
explicit ScopedFont(TypeStyle style) {
Typography::instance().pushFont(style);
}
~ScopedFont() {
Typography::instance().popFont();
}
};
// Usage: MATERIAL_FONT(H5) { ImGui::Text("Title"); }
#define MATERIAL_FONT(style) \
if (dragonx::ui::material::ScopedFont _font{dragonx::ui::material::TypeStyle::style}; true)
} // namespace material
} // namespace ui
} // namespace dragonx