refactor(ui): remove abandoned Material-Design component library + screens layer

~9,988 lines of header-only UI code that no compiled translation unit reached,
verified by transitive include-reachability from every .cpp plus a symbol sweep
(all 28 component classes — Snackbar, Ripple, NavDrawerSpec, TabBarSpec,
TransitionManager, … — had zero references in live code):

- src/ui/material/ component library: the material.h umbrella, components/*
  (app_bar, cards, chips, dialogs, inputs, lists, nav_drawer, progress, slider,
  snackbar, tabs, text_fields), and the animation system (elevation, motion,
  ripple, transitions, app_layout) — 19 headers. Kept the live helpers the app
  actually uses directly: color_theme, colors, type/typography, draw_helpers,
  layout, project_icons, and components/buttons (included by mining_tab).
- src/ui/screens/ layer: main_layout, home_screen, send_screen, etc. — the
  original screen stack and the only consumer of the dead component library.
  The live UI runs through ui/windows/ (34 .cpp) + ui/pages/.
- src/embedded/resources.h: a superseded dragonx::embedded::Resources duplicate;
  the app uses src/resources/embedded_resources.h.

None were in CMakeLists or included by live code, so the build is unaffected.
Both variants build; full test suite passes; source-hygiene check clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-06 11:31:06 -05:00
parent a5da5562cf
commit ca14aaddc7
31 changed files with 1 additions and 9989 deletions

View File

@@ -49,7 +49,7 @@ There is no per-test filtering — it is one binary that runs every assertion. T
**Data model** (`src/data/`): `WalletState`, `address_book`, `transaction_history_cache`, `exchange_info`. UI reads from these.
**UI** (`src/ui/`): `windows/` are the tabs and dialogs (one pair per screen, e.g. `send_tab`, `mining_tab`, `console_tab`), `pages/` are multi-section screens (Settings), `screens/` are layout headers, `material/` is the design-system layer, `schema/` loads the TOML UI schema/skins, `effects/` is GL post-processing (blur/acrylic).
**UI** (`src/ui/`): `windows/` are the tabs and dialogs (one pair per screen, e.g. `send_tab`, `mining_tab`, `console_tab`), `pages/` are multi-section screens (Settings), `material/` is the design-system layer (the live helpers `color_theme`, `colors`, `type`/`typography`, `draw_helpers`, `layout`, `project_icons`, `components/buttons`), `schema/` loads the TOML UI schema/skins, `effects/` is GL post-processing (blur/acrylic).
**Lite wallet** (`src/wallet/`): the bridge to an external `litelib_*` C-ABI backend. `lite_client_bridge` loads the backend (via direct `litelib_*` externs in `linkedSdxl()`) and owns each Rust string through `lite_owned_string` (copy-before-free / free-once). On top sit `lite_connection_service`, `lite_sync_service`, `lite_result_parsers`, `lite_wallet_gateway`, `lite_wallet_state_mapper`, and `lite_wallet_lifecycle_service`, all driven by `lite_wallet_controller`. The real frontend entry points are `lite_wallet_lifecycle_ui_adapter` and `lite_wallet_server_selection_adapter` (used by `src/ui/pages/settings_page.cpp`); everything else is reachable through them. (The prebuilt-backend symbol check for `DRAGONX_ENABLE_LITE_BACKEND` is done in CMake against the symbols inventory — see below — not in C++.)

View File

@@ -1,65 +0,0 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
// Embedded Resources Header
// This provides access to resources embedded in the binary
#pragma once
#include <cstddef>
#include <cstdint>
#include <string>
#include <unordered_map>
namespace dragonx {
namespace embedded {
// Forward declarations for embedded data (generated at build time)
struct EmbeddedResource {
const unsigned char* data;
size_t size;
};
// Resource registry
class Resources {
public:
static Resources& instance() {
static Resources inst;
return inst;
}
// Get embedded resource by name
// Returns nullptr if not found
const EmbeddedResource* get(const std::string& name) const {
auto it = resources_.find(name);
if (it != resources_.end()) {
return &it->second;
}
return nullptr;
}
// Check if resource exists
bool has(const std::string& name) const {
return resources_.find(name) != resources_.end();
}
// Register a resource (called during static init)
void registerResource(const std::string& name, const unsigned char* data, size_t size) {
resources_[name] = {data, size};
}
private:
Resources() = default;
std::unordered_map<std::string, EmbeddedResource> resources_;
};
// Helper macro for registering resources
#define REGISTER_EMBEDDED_RESOURCE(name, data, size) \
static struct _EmbeddedResourceRegister_##name { \
_EmbeddedResourceRegister_##name() { \
dragonx::embedded::Resources::instance().registerResource(#name, data, size); \
} \
} _embedded_resource_register_##name
} // namespace embedded
} // namespace dragonx

View File

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

View File

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

View File

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

View File

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

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

View File

@@ -1,352 +0,0 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include "../material/material.h"
#include "../../embedded/IconsMaterialDesign.h"
#include "imgui.h"
#include <string>
#include <functional>
namespace dragonx {
namespace ui {
namespace screens {
using namespace material;
// ============================================================================
// Confirmation Dialog
// ============================================================================
// Material Design confirmation dialog for critical actions
/**
* @brief Confirmation dialog type
*/
enum class ConfirmationType {
Info,
Warning,
Danger,
Transaction
};
/**
* @brief Transaction confirmation details
*/
struct TransactionConfirmation {
std::string toAddress;
double amount;
double fee;
std::string memo;
bool isShielded;
};
/**
* @brief Confirmation dialog manager
*/
class ConfirmationDialog {
public:
static ConfirmationDialog& instance() {
static ConfirmationDialog s_instance;
return s_instance;
}
/**
* @brief Show a simple confirmation dialog
*/
void show(const std::string& title,
const std::string& message,
ConfirmationType type,
std::function<void(bool)> callback) {
m_title = title;
m_message = message;
m_type = type;
m_callback = callback;
m_isOpen = true;
m_isTransaction = false;
}
/**
* @brief Show transaction confirmation dialog
*/
void showTransaction(const TransactionConfirmation& tx,
std::function<void(bool)> callback) {
m_title = "Confirm Transaction";
m_type = ConfirmationType::Transaction;
m_transaction = tx;
m_callback = callback;
m_isOpen = true;
m_isTransaction = true;
}
/**
* @brief Render the dialog (call every frame)
*/
void render();
/**
* @brief Check if dialog is currently open
*/
bool isOpen() const { return m_isOpen; }
private:
ConfirmationDialog() = default;
void renderSimpleDialog();
void renderTransactionDialog();
std::string m_title;
std::string m_message;
ConfirmationType m_type = ConfirmationType::Info;
TransactionConfirmation m_transaction;
std::function<void(bool)> m_callback;
bool m_isOpen = false;
bool m_isTransaction = false;
};
// ============================================================================
// Implementation
// ============================================================================
inline void ConfirmationDialog::render() {
if (!m_isOpen) return;
if (m_isTransaction) {
renderTransactionDialog();
} else {
renderSimpleDialog();
}
}
inline void ConfirmationDialog::renderSimpleDialog() {
DialogSpec dialogSpec;
dialogSpec.title = m_title.c_str();
dialogSpec.maxWidth = 400.0f;
DialogResult result = BeginDialog("confirm_dialog", dialogSpec);
if (result.isOpen) {
// Icon based on type
ImGui::BeginGroup();
{
ImVec4 iconColor;
const char* icon;
switch (m_type) {
case ConfirmationType::Info:
iconColor = colors::Blue500;
icon = ICON_MD_INFO;
break;
case ConfirmationType::Warning:
iconColor = Secondary();
icon = ICON_MD_WARNING;
break;
case ConfirmationType::Danger:
iconColor = Error();
icon = ICON_MD_DANGEROUS;
break;
default:
iconColor = Primary();
icon = ICON_MD_HELP;
break;
}
Typography::instance().textColored(TypeStyle::H4, iconColor, icon);
ImGui::SameLine();
ImGui::BeginGroup();
ImGui::TextWrapped("%s", m_message.c_str());
ImGui::EndGroup();
}
ImGui::EndGroup();
ImGui::Dummy(ImVec2(0, spacing::dp(3)));
// Buttons
float availWidth = ImGui::GetContentRegionAvail().x;
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + availWidth - 200);
if (TextButton("CANCEL")) {
m_isOpen = false;
if (m_callback) m_callback(false);
}
ImGui::SameLine(0, spacing::dp(2));
// Confirm button color based on type
if (m_type == ConfirmationType::Danger) {
ImGui::PushStyleColor(ImGuiCol_Button, Error());
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, colors::Red400);
}
if (ContainedButton("CONFIRM")) {
m_isOpen = false;
if (m_callback) m_callback(true);
}
if (m_type == ConfirmationType::Danger) {
ImGui::PopStyleColor(2);
}
EndDialog();
}
if (result.dismissed) {
m_isOpen = false;
if (m_callback) m_callback(false);
}
}
inline void ConfirmationDialog::renderTransactionDialog() {
DialogSpec dialogSpec;
dialogSpec.title = "Confirm Transaction";
dialogSpec.maxWidth = 500.0f;
DialogResult result = BeginDialog("tx_confirm_dialog", dialogSpec);
if (result.isOpen) {
// Transaction type badge
ChipSpec chipSpec;
chipSpec.variant = ChipVariant::Filled;
if (m_transaction.isShielded) {
chipSpec.color = Primary();
Chip(ICON_MD_SHIELD " Shielded Transaction", chipSpec);
} else {
chipSpec.color = Secondary();
Chip(ICON_MD_DESCRIPTION " Transparent Transaction", chipSpec);
}
ImGui::Dummy(ImVec2(0, spacing::dp(3)));
// Amount (large, prominent)
Typography::instance().textColored(TypeStyle::Overline, OnSurfaceMedium(), "AMOUNT");
char amountStr[64];
snprintf(amountStr, sizeof(amountStr), "%.8f DRGX", m_transaction.amount);
Typography::instance().textColored(TypeStyle::H4, Primary(), amountStr);
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
// Recipient
Typography::instance().textColored(TypeStyle::Overline, OnSurfaceMedium(), "TO ADDRESS");
ImGui::TextWrapped("%s", m_transaction.toAddress.c_str());
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
// Fee
Typography::instance().textColored(TypeStyle::Overline, OnSurfaceMedium(), "NETWORK FEE");
char feeStr[64];
snprintf(feeStr, sizeof(feeStr), "%.8f DRGX", m_transaction.fee);
Typography::instance().text(TypeStyle::Body1, feeStr);
// Memo if present
if (!m_transaction.memo.empty()) {
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
Typography::instance().textColored(TypeStyle::Overline, OnSurfaceMedium(), "MEMO (ENCRYPTED)");
ImGui::TextWrapped("%s", m_transaction.memo.c_str());
}
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
// Total
ImVec2 divPos = ImGui::GetCursorScreenPos();
float availWidth = ImGui::GetContentRegionAvail().x;
ImGui::GetWindowDrawList()->AddLine(
divPos,
ImVec2(divPos.x + availWidth, divPos.y),
ImGui::GetColorU32(Divider())
);
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
Typography::instance().textColored(TypeStyle::Overline, OnSurfaceMedium(), "TOTAL");
char totalStr[64];
snprintf(totalStr, sizeof(totalStr), "%.8f DRGX", m_transaction.amount + m_transaction.fee);
Typography::instance().text(TypeStyle::H5, totalStr);
ImGui::Dummy(ImVec2(0, spacing::dp(3)));
// Warning for shielded transactions
if (m_transaction.isShielded) {
CardSpec warningCard;
warningCard.elevation = 0;
warningCard.padding = spacing::dp(2);
warningCard.outlined = true;
ImGui::PushStyleColor(ImGuiCol_Border, colors::withAlpha(Secondary(), 0.5f));
if (BeginCard(warningCard)) {
Typography::instance().textColored(TypeStyle::Body2, Secondary(),
ICON_MD_WARNING " Shielded transactions are private but take longer to process.");
EndCard();
}
ImGui::PopStyleColor();
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
}
// Buttons
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + availWidth - 220);
if (OutlinedButton("CANCEL")) {
m_isOpen = false;
if (m_callback) m_callback(false);
}
ImGui::SameLine(0, spacing::dp(2));
if (ContainedButton("SEND NOW")) {
m_isOpen = false;
if (m_callback) m_callback(true);
}
EndDialog();
}
if (result.dismissed) {
m_isOpen = false;
if (m_callback) m_callback(false);
}
}
// ============================================================================
// Quick Confirmation Helpers
// ============================================================================
/**
* @brief Show a simple confirmation dialog
*/
inline void ShowConfirmation(const std::string& title,
const std::string& message,
std::function<void(bool)> callback) {
ConfirmationDialog::instance().show(title, message, ConfirmationType::Info, callback);
}
/**
* @brief Show a warning confirmation dialog
*/
inline void ShowWarningConfirmation(const std::string& title,
const std::string& message,
std::function<void(bool)> callback) {
ConfirmationDialog::instance().show(title, message, ConfirmationType::Warning, callback);
}
/**
* @brief Show a danger confirmation dialog
*/
inline void ShowDangerConfirmation(const std::string& title,
const std::string& message,
std::function<void(bool)> callback) {
ConfirmationDialog::instance().show(title, message, ConfirmationType::Danger, callback);
}
/**
* @brief Show a transaction confirmation dialog
*/
inline void ShowTransactionConfirmation(const TransactionConfirmation& tx,
std::function<void(bool)> callback) {
ConfirmationDialog::instance().showTransaction(tx, callback);
}
} // namespace screens
} // namespace ui
} // namespace dragonx

View File

@@ -1,304 +0,0 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include "../material/material.h"
#include "../../embedded/IconsMaterialDesign.h"
#include "imgui.h"
#include <string>
#include <vector>
#include <functional>
namespace dragonx {
namespace ui {
namespace screens {
using namespace material;
// ============================================================================
// Balance/Home Screen
// ============================================================================
// Main dashboard showing wallet balances and recent activity
/**
* @brief Balance information for display
*/
struct BalanceInfo {
double total = 0.0;
double shielded = 0.0;
double transparent = 0.0;
double pending = 0.0;
double fiatValue = 0.0;
std::string fiatCurrency = "USD";
};
/**
* @brief Recent transaction summary
*/
struct RecentTransaction {
std::string txid;
std::string type; // "sent", "received", "mined"
std::string address; // Truncated address
double amount;
std::string time; // Relative time "2 hours ago"
bool confirmed;
};
/**
* @brief Render the home/balance screen
*/
class HomeScreen {
public:
void setBalance(const BalanceInfo& balance) { m_balance = balance; }
void setRecentTransactions(const std::vector<RecentTransaction>& txns) { m_recentTxns = txns; }
void setOnSendClick(std::function<void()> callback) { m_onSendClick = callback; }
void setOnReceiveClick(std::function<void()> callback) { m_onReceiveClick = callback; }
void setOnTransactionClick(std::function<void(const std::string&)> callback) { m_onTxClick = callback; }
void render();
private:
void renderBalanceCard();
void renderQuickActions();
void renderShieldedCard();
void renderTransparentCard();
void renderRecentTransactions();
BalanceInfo m_balance;
std::vector<RecentTransaction> m_recentTxns;
std::function<void()> m_onSendClick;
std::function<void()> m_onReceiveClick;
std::function<void(const std::string&)> m_onTxClick;
};
// ============================================================================
// Implementation
// ============================================================================
inline void HomeScreen::render() {
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(spacing::dp(2), spacing::dp(2)));
// Two-column layout
float availWidth = ImGui::GetContentRegionAvail().x;
float cardWidth = (availWidth - spacing::dp(2)) * 0.5f;
// Row 1: Total Balance + Recent Transactions
ImGui::BeginGroup();
{
// Total Balance Card
ImGui::BeginGroup();
renderBalanceCard();
ImGui::EndGroup();
ImGui::SameLine();
// Recent Transactions Card
ImGui::BeginGroup();
renderRecentTransactions();
ImGui::EndGroup();
}
ImGui::EndGroup();
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
// Row 2: Shielded + Transparent
ImGui::BeginGroup();
{
ImGui::BeginGroup();
renderShieldedCard();
ImGui::EndGroup();
ImGui::SameLine();
ImGui::BeginGroup();
renderTransparentCard();
ImGui::EndGroup();
}
ImGui::EndGroup();
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
// Quick Actions
renderQuickActions();
ImGui::PopStyleVar();
}
inline void HomeScreen::renderBalanceCard() {
float cardWidth = (ImGui::GetContentRegionAvail().x - spacing::dp(2)) * 0.5f;
CardSpec spec;
spec.elevation = 2;
spec.padding = spacing::dp(3);
ImGui::PushID("balance_card");
if (BeginCard(spec)) {
// Overline
Typography::instance().textColored(TypeStyle::Overline, OnSurfaceMedium(), "TOTAL BALANCE");
ImGui::Dummy(ImVec2(0, spacing::dp(1)));
// Balance amount
char balanceStr[64];
snprintf(balanceStr, sizeof(balanceStr), "%.8f", m_balance.total);
Typography::instance().text(TypeStyle::H4, balanceStr);
ImGui::SameLine();
Typography::instance().textColored(TypeStyle::H6, OnSurfaceMedium(), "DRGX");
// Fiat value
if (m_balance.fiatValue > 0) {
ImGui::Dummy(ImVec2(0, spacing::dp(0.5f)));
char fiatStr[64];
snprintf(fiatStr, sizeof(fiatStr), "≈ $%.2f %s", m_balance.fiatValue, m_balance.fiatCurrency.c_str());
Typography::instance().textColored(TypeStyle::Body2, OnSurfaceMedium(), fiatStr);
}
// Pending indicator
if (m_balance.pending > 0) {
ImGui::Dummy(ImVec2(0, spacing::dp(1)));
char pendingStr[64];
snprintf(pendingStr, sizeof(pendingStr), ICON_MD_HOURGLASS_EMPTY " %.8f pending", m_balance.pending);
Typography::instance().textColored(TypeStyle::Caption, Secondary(), pendingStr);
}
EndCard();
}
ImGui::PopID();
}
inline void HomeScreen::renderQuickActions() {
ImGui::Dummy(ImVec2(0, spacing::dp(1)));
float buttonWidth = 120.0f;
float availWidth = ImGui::GetContentRegionAvail().x;
float startX = (availWidth - buttonWidth * 2 - spacing::dp(2)) * 0.5f;
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + startX);
if (ContainedButton(ICON_MD_CALL_MADE " SEND")) {
if (m_onSendClick) m_onSendClick();
}
ImGui::SameLine(0, spacing::dp(2));
if (OutlinedButton(ICON_MD_CALL_RECEIVED " RECEIVE")) {
if (m_onReceiveClick) m_onReceiveClick();
}
}
inline void HomeScreen::renderShieldedCard() {
CardSpec spec;
spec.elevation = 1;
spec.padding = spacing::dp(2);
ImGui::PushID("shielded_card");
if (BeginCard(spec)) {
// Icon and label
ImGui::BeginGroup();
Typography::instance().textColored(TypeStyle::Body2, Primary(), ICON_MD_SHIELD);
ImGui::SameLine();
Typography::instance().textColored(TypeStyle::Overline, OnSurfaceMedium(), "SHIELDED (PRIVATE)");
ImGui::EndGroup();
ImGui::Dummy(ImVec2(0, spacing::dp(1)));
// Amount
char amountStr[64];
snprintf(amountStr, sizeof(amountStr), "%.8f", m_balance.shielded);
Typography::instance().text(TypeStyle::H5, amountStr);
ImGui::SameLine();
Typography::instance().textColored(TypeStyle::Body1, OnSurfaceMedium(), "DRGX");
EndCard();
}
ImGui::PopID();
}
inline void HomeScreen::renderTransparentCard() {
CardSpec spec;
spec.elevation = 1;
spec.padding = spacing::dp(2);
ImGui::PushID("transparent_card");
if (BeginCard(spec)) {
// Icon and label
ImGui::BeginGroup();
Typography::instance().textColored(TypeStyle::Body2, Secondary(), ICON_MD_CONTENT_COPY);
ImGui::SameLine();
Typography::instance().textColored(TypeStyle::Overline, OnSurfaceMedium(), "TRANSPARENT");
ImGui::EndGroup();
ImGui::Dummy(ImVec2(0, spacing::dp(1)));
// Amount
char amountStr[64];
snprintf(amountStr, sizeof(amountStr), "%.8f", m_balance.transparent);
Typography::instance().text(TypeStyle::H5, amountStr);
ImGui::SameLine();
Typography::instance().textColored(TypeStyle::Body1, OnSurfaceMedium(), "DRGX");
EndCard();
}
ImGui::PopID();
}
inline void HomeScreen::renderRecentTransactions() {
CardSpec spec;
spec.elevation = 1;
spec.padding = spacing::dp(2);
ImGui::PushID("recent_txns");
if (BeginCard(spec)) {
Typography::instance().textColored(TypeStyle::Overline, OnSurfaceMedium(), "RECENT TRANSACTIONS");
ImGui::Dummy(ImVec2(0, spacing::dp(1)));
if (m_recentTxns.empty()) {
Typography::instance().textColored(TypeStyle::Body2, OnSurfaceMedium(), "No recent transactions");
} else {
BeginList("recent_list", false);
int count = 0;
for (const auto& tx : m_recentTxns) {
if (count >= 5) break; // Show max 5
ListItemSpec itemSpec;
// Icon based on type
if (tx.type == "received") {
itemSpec.leadingIcon = ICON_MD_CALL_RECEIVED;
} else if (tx.type == "sent") {
itemSpec.leadingIcon = ICON_MD_CALL_MADE;
} else {
itemSpec.leadingIcon = ICON_MD_CONSTRUCTION;
}
// Amount as primary text
char amountStr[32];
snprintf(amountStr, sizeof(amountStr), "%+.4f DRGX",
tx.type == "sent" ? -tx.amount : tx.amount);
itemSpec.primaryText = amountStr;
itemSpec.secondaryText = tx.time.c_str();
itemSpec.dividerBelow = (count < 4 && count < (int)m_recentTxns.size() - 1);
if (ListItem(itemSpec)) {
if (m_onTxClick) m_onTxClick(tx.txid);
}
count++;
}
EndList();
}
EndCard();
}
ImGui::PopID();
}
} // namespace screens
} // namespace ui
} // namespace dragonx

View File

@@ -1,553 +0,0 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include "../material/material.h"
#include "../../embedded/IconsMaterialDesign.h"
#include "../schema/ui_schema.h"
#include "home_screen.h"
#include "send_screen.h"
#include "receive_screen.h"
#include "transactions_screen.h"
#include "mining_screen.h"
#include "peers_screen.h"
#include "settings_screen.h"
#include "imgui.h"
#include <functional>
#include <string>
namespace dragonx {
namespace ui {
namespace screens {
using namespace material;
// ============================================================================
// Main Application Layout
// ============================================================================
// Combines app bar, navigation drawer, content area, and status bar
/**
* @brief Navigation destinations
*/
enum class NavDestination {
Home,
Send,
Receive,
Transactions,
Mining,
Peers,
Settings
};
/**
* @brief Connection status for status bar
*/
struct ConnectionStatus {
bool connected = false;
int blockHeight = 0;
int peers = 0;
double networkHashrate = 0.0;
bool syncing = false;
float syncProgress = 0.0f;
std::string statusMessage;
};
/**
* @brief Main application layout with Material Design structure
*/
class MainLayout {
public:
MainLayout() = default;
void setConnectionStatus(const ConnectionStatus& status) { m_status = status; }
void setOnNavigation(std::function<void(NavDestination)> callback) {
m_onNavigation = callback;
}
void setOnSearch(std::function<void(const std::string&)> callback) {
m_onSearch = callback;
}
void setOnSettingsClick(std::function<void()> callback) {
m_onSettingsClick = callback;
}
void navigateTo(NavDestination dest) {
m_currentDestination = dest;
m_drawerOpen = false; // Close drawer on navigation
}
NavDestination getCurrentDestination() const { return m_currentDestination; }
// Screen access
HomeScreen& homeScreen() { return m_homeScreen; }
SendScreen& sendScreen() { return m_sendScreen; }
ReceiveScreen& receiveScreen() { return m_receiveScreen; }
TransactionsScreen& transactionsScreen() { return m_transactionsScreen; }
MiningScreen& miningScreen() { return m_miningScreen; }
PeersScreen& peersScreen() { return m_peersScreen; }
SettingsScreen& settingsScreen() { return m_settingsScreen; }
void render();
private:
void renderAppBar();
void renderNavigationDrawer();
void renderContent();
void renderStatusBar();
void renderSearchOverlay();
NavDestination m_currentDestination = NavDestination::Home;
ConnectionStatus m_status;
std::function<void(NavDestination)> m_onNavigation;
std::function<void(const std::string&)> m_onSearch;
std::function<void()> m_onSettingsClick;
// Navigation drawer state
bool m_drawerOpen = false;
AnimatedValue<float> m_drawerAnimation{0.0f};
// Search state
bool m_searchOpen = false;
char m_searchBuffer[256] = {0};
// Screens
HomeScreen m_homeScreen;
SendScreen m_sendScreen;
ReceiveScreen m_receiveScreen;
TransactionsScreen m_transactionsScreen;
MiningScreen m_miningScreen;
PeersScreen m_peersScreen;
SettingsScreen m_settingsScreen;
// Layout values — read from ui.toml schema at runtime
static float APP_BAR_HEIGHT;
static float STATUS_BAR_HEIGHT;
static float DRAWER_WIDTH;
static void initLayoutConstants() {
APP_BAR_HEIGHT = schema::UI().drawElement("components.main-layout", "app-bar-height").size;
STATUS_BAR_HEIGHT = schema::UI().drawElement("components.main-layout", "status-bar-height").size;
DRAWER_WIDTH = schema::UI().drawElement("components.main-layout", "drawer-width").size;
if (APP_BAR_HEIGHT <= 0) APP_BAR_HEIGHT = 64.0f;
if (STATUS_BAR_HEIGHT <= 0) STATUS_BAR_HEIGHT = 32.0f;
if (DRAWER_WIDTH <= 0) DRAWER_WIDTH = 256.0f;
}
};
// Static member definitions
inline float MainLayout::APP_BAR_HEIGHT = 64.0f;
inline float MainLayout::STATUS_BAR_HEIGHT = 32.0f;
inline float MainLayout::DRAWER_WIDTH = 256.0f;
// ============================================================================
// Implementation
// ============================================================================
inline void MainLayout::render() {
static bool layoutInitialized = false;
if (!layoutInitialized) {
initLayoutConstants();
layoutInitialized = true;
}
ImGuiIO& io = ImGui::GetIO();
float windowWidth = io.DisplaySize.x;
float windowHeight = io.DisplaySize.y;
// Full-screen window
ImGui::SetNextWindowPos(ImVec2(0, 0));
ImGui::SetNextWindowSize(ImVec2(windowWidth, windowHeight));
ImGuiWindowFlags windowFlags = ImGuiWindowFlags_NoTitleBar |
ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoBringToFrontOnFocus;
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
ImGui::PushStyleColor(ImGuiCol_WindowBg, Background());
if (ImGui::Begin("MainLayout", nullptr, windowFlags)) {
// App bar
renderAppBar();
// Content area with optional drawer
float contentY = APP_BAR_HEIGHT;
float contentHeight = windowHeight - APP_BAR_HEIGHT - STATUS_BAR_HEIGHT;
// Update drawer animation
float targetDrawer = m_drawerOpen ? 1.0f : 0.0f;
m_drawerAnimation.animateTo(targetDrawer, duration::Medium);
m_drawerAnimation.update(io.DeltaTime);
float drawerProgress = m_drawerAnimation.value();
// Navigation drawer (slides in from left)
if (drawerProgress > 0.01f) {
renderNavigationDrawer();
}
// Content offset based on drawer
float contentX = drawerProgress * DRAWER_WIDTH;
// Content area
ImGui::SetCursorPos(ImVec2(contentX, contentY));
ImGui::BeginChild("Content", ImVec2(windowWidth - contentX, contentHeight), false,
ImGuiWindowFlags_NoScrollbar);
{
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(spacing::dp(3), spacing::dp(3)));
renderContent();
ImGui::PopStyleVar();
}
ImGui::EndChild();
// Status bar
ImGui::SetCursorPos(ImVec2(0, windowHeight - STATUS_BAR_HEIGHT));
renderStatusBar();
// Search overlay
if (m_searchOpen) {
renderSearchOverlay();
}
// Click outside drawer to close
if (m_drawerOpen && ImGui::IsMouseClicked(0)) {
ImVec2 mousePos = ImGui::GetMousePos();
if (mousePos.x > DRAWER_WIDTH) {
m_drawerOpen = false;
}
}
}
ImGui::End();
ImGui::PopStyleColor();
ImGui::PopStyleVar(2);
}
inline void MainLayout::renderAppBar() {
ImGuiIO& io = ImGui::GetIO();
float windowWidth = io.DisplaySize.x;
// App bar background
ImVec2 appBarPos = ImGui::GetCursorScreenPos();
ImGui::GetWindowDrawList()->AddRectFilled(
appBarPos,
ImVec2(appBarPos.x + windowWidth, appBarPos.y + APP_BAR_HEIGHT),
ImGui::GetColorU32(Surface())
);
// Elevation shadow
DrawShadow(ImVec2(appBarPos.x, appBarPos.y + APP_BAR_HEIGHT - 4),
ImVec2(windowWidth, 4), 4);
ImGui::SetCursorPos(ImVec2(spacing::dp(1), (APP_BAR_HEIGHT - schema::UI().drawElement("components.main-layout", "app-bar-button-size").size) / 2));
// Menu button
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, colors::withAlpha(OnSurface(), 0.08f));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, schema::UI().drawElement("components.main-layout", "app-bar-btn-rounding").size);
float appBarBtnSz = schema::UI().drawElement("components.main-layout", "app-bar-button-size").size;
if (ImGui::Button(ICON_MD_MENU, ImVec2(appBarBtnSz, appBarBtnSz))) {
m_drawerOpen = !m_drawerOpen;
}
ImGui::PopStyleVar();
ImGui::PopStyleColor(2);
// Title
ImGui::SameLine(0, spacing::dp(2));
ImGui::SetCursorPosY((APP_BAR_HEIGHT - schema::UI().drawElement("components.main-layout", "title-font-height").size) / 2);
Typography::instance().text(TypeStyle::H6, "DragonX Wallet");
// Right actions
float rightOffset = windowWidth - spacing::dp(1) - appBarBtnSz;
// Settings button
ImGui::SameLine(rightOffset);
ImGui::SetCursorPosY((APP_BAR_HEIGHT - appBarBtnSz) / 2);
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, colors::withAlpha(OnSurface(), 0.08f));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, schema::UI().drawElement("components.main-layout", "app-bar-btn-rounding").size);
if (ImGui::Button(ICON_MD_SETTINGS, ImVec2(appBarBtnSz, appBarBtnSz))) {
navigateTo(NavDestination::Settings);
}
// Search button
ImGui::SameLine(rightOffset - 48);
ImGui::SetCursorPosY((APP_BAR_HEIGHT - appBarBtnSz) / 2);
if (ImGui::Button(ICON_MD_SEARCH, ImVec2(appBarBtnSz, appBarBtnSz))) {
m_searchOpen = !m_searchOpen;
}
ImGui::PopStyleVar();
ImGui::PopStyleColor(2);
}
inline void MainLayout::renderNavigationDrawer() {
ImGuiIO& io = ImGui::GetIO();
float windowHeight = io.DisplaySize.y;
float drawerProgress = m_drawerAnimation.value();
// Scrim (semi-transparent overlay)
if (drawerProgress > 0.01f) {
ImVec2 scrimStart = ImVec2(DRAWER_WIDTH, APP_BAR_HEIGHT);
ImVec2 scrimEnd = ImVec2(io.DisplaySize.x, windowHeight - STATUS_BAR_HEIGHT);
ImGui::GetWindowDrawList()->AddRectFilled(
scrimStart, scrimEnd,
ImGui::GetColorU32(ImVec4(0, 0, 0, 0.5f * drawerProgress))
);
}
// Drawer background
float drawerX = (drawerProgress - 1.0f) * DRAWER_WIDTH;
ImVec2 drawerStart = ImVec2(0, APP_BAR_HEIGHT);
ImVec2 drawerEnd = ImVec2(DRAWER_WIDTH, windowHeight - STATUS_BAR_HEIGHT);
ImGui::SetCursorPos(ImVec2(drawerX, APP_BAR_HEIGHT));
ImGui::BeginChild("NavDrawer", ImVec2(DRAWER_WIDTH, windowHeight - APP_BAR_HEIGHT - STATUS_BAR_HEIGHT),
false, ImGuiWindowFlags_NoScrollbar);
{
// Drawer surface
ImVec2 pos = ImGui::GetCursorScreenPos();
ImGui::GetWindowDrawList()->AddRectFilled(
pos,
ImVec2(pos.x + DRAWER_WIDTH, pos.y + windowHeight),
ImGui::GetColorU32(Surface())
);
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
// Navigation items
auto navItem = [this](const char* icon, const char* label, NavDestination dest) {
bool selected = (m_currentDestination == dest);
NavItemSpec spec;
spec.icon = icon;
spec.label = label;
spec.selected = selected;
if (NavItem(spec)) {
navigateTo(dest);
if (m_onNavigation) m_onNavigation(dest);
}
};
navItem(ICON_MD_HOME, "Home", NavDestination::Home);
navItem(ICON_MD_CALL_MADE, "Send", NavDestination::Send);
navItem(ICON_MD_CALL_RECEIVED, "Receive", NavDestination::Receive);
navItem(ICON_MD_RECEIPT, "Transactions", NavDestination::Transactions);
ImGui::Dummy(ImVec2(0, spacing::dp(1)));
// Divider
ImVec2 divStart = ImGui::GetCursorScreenPos();
ImGui::GetWindowDrawList()->AddLine(
ImVec2(divStart.x + spacing::dp(2), divStart.y),
ImVec2(divStart.x + DRAWER_WIDTH - spacing::dp(2), divStart.y),
ImGui::GetColorU32(Divider())
);
ImGui::Dummy(ImVec2(0, spacing::dp(1)));
navItem(ICON_MD_CONSTRUCTION, "Mining", NavDestination::Mining);
navItem(ICON_MD_HUB, "Network", NavDestination::Peers);
ImGui::Dummy(ImVec2(0, spacing::dp(1)));
// Another divider
divStart = ImGui::GetCursorScreenPos();
ImGui::GetWindowDrawList()->AddLine(
ImVec2(divStart.x + spacing::dp(2), divStart.y),
ImVec2(divStart.x + DRAWER_WIDTH - spacing::dp(2), divStart.y),
ImGui::GetColorU32(Divider())
);
ImGui::Dummy(ImVec2(0, spacing::dp(1)));
navItem(ICON_MD_SETTINGS, "Settings", NavDestination::Settings);
}
ImGui::EndChild();
}
inline void MainLayout::renderContent() {
// Padding
ImGui::Dummy(ImVec2(0, spacing::dp(1)));
switch (m_currentDestination) {
case NavDestination::Home:
m_homeScreen.render();
break;
case NavDestination::Send:
m_sendScreen.render();
break;
case NavDestination::Receive:
m_receiveScreen.render();
break;
case NavDestination::Transactions:
m_transactionsScreen.render();
break;
case NavDestination::Mining:
m_miningScreen.render();
break;
case NavDestination::Peers:
m_peersScreen.render();
break;
case NavDestination::Settings:
m_settingsScreen.render();
break;
}
}
inline void MainLayout::renderStatusBar() {
ImGuiIO& io = ImGui::GetIO();
float windowWidth = io.DisplaySize.x;
// Status bar background
ImVec2 statusPos = ImGui::GetCursorScreenPos();
ImGui::GetWindowDrawList()->AddRectFilled(
statusPos,
ImVec2(statusPos.x + windowWidth, statusPos.y + STATUS_BAR_HEIGHT),
ImGui::GetColorU32(SurfaceVariant())
);
// Top border
ImGui::GetWindowDrawList()->AddLine(
statusPos,
ImVec2(statusPos.x + windowWidth, statusPos.y),
ImGui::GetColorU32(Divider())
);
ImGui::SetCursorPos(ImVec2(spacing::dp(2), ImGui::GetCursorPosY() + 6));
// Connection status
if (m_status.connected) {
Typography::instance().textColored(TypeStyle::Caption, colors::Green500, ICON_MD_FIBER_MANUAL_RECORD " Connected");
} else {
Typography::instance().textColored(TypeStyle::Caption, colors::Red500, ICON_MD_FIBER_MANUAL_RECORD " Disconnected");
}
ImGui::SameLine(0, spacing::dp(3));
// Block height
char heightStr[64];
snprintf(heightStr, sizeof(heightStr), "Block: %d", m_status.blockHeight);
Typography::instance().textColored(TypeStyle::Caption, OnSurfaceMedium(), heightStr);
ImGui::SameLine(0, spacing::dp(3));
// Peer count
char peerStr[32];
snprintf(peerStr, sizeof(peerStr), "Peers: %d", m_status.peers);
Typography::instance().textColored(TypeStyle::Caption, OnSurfaceMedium(), peerStr);
// Sync progress (if syncing)
if (m_status.syncing) {
ImGui::SameLine(0, spacing::dp(3));
// Mini progress bar
ImVec2 progressPos = ImGui::GetCursorScreenPos();
float progressWidth = schema::UI().drawElement("components.main-layout", "sync-bar-width").size;
float progressHeight = schema::UI().drawElement("components.main-layout", "sync-bar-height").size;
progressPos.y += 6;
// Background
ImGui::GetWindowDrawList()->AddRectFilled(
progressPos,
ImVec2(progressPos.x + progressWidth, progressPos.y + progressHeight),
ImGui::GetColorU32(Divider()),
2.0f
);
// Progress
ImGui::GetWindowDrawList()->AddRectFilled(
progressPos,
ImVec2(progressPos.x + progressWidth * m_status.syncProgress, progressPos.y + progressHeight),
ImGui::GetColorU32(Primary()),
2.0f
);
ImGui::Dummy(ImVec2(progressWidth, 0));
ImGui::SameLine();
char syncStr[32];
snprintf(syncStr, sizeof(syncStr), "%.1f%%", m_status.syncProgress * 100.0f);
ImU32 syncTextColor = IsDarkTheme() ? Primary() : PrimaryVariant();
Typography::instance().textColored(TypeStyle::Caption, syncTextColor, syncStr);
}
// Network hashrate (right aligned)
if (m_status.networkHashrate > 0) {
char hashrateStr[64];
if (m_status.networkHashrate > 1e9) {
snprintf(hashrateStr, sizeof(hashrateStr), "Network: %.2f GH/s",
m_status.networkHashrate / 1e9);
} else if (m_status.networkHashrate > 1e6) {
snprintf(hashrateStr, sizeof(hashrateStr), "Network: %.2f MH/s",
m_status.networkHashrate / 1e6);
} else {
snprintf(hashrateStr, sizeof(hashrateStr), "Network: %.2f KH/s",
m_status.networkHashrate / 1e3);
}
ImVec2 textSize = ImGui::CalcTextSize(hashrateStr);
ImGui::SameLine(windowWidth - textSize.x - spacing::dp(2));
Typography::instance().textColored(TypeStyle::Caption, OnSurfaceMedium(), hashrateStr);
}
}
inline void MainLayout::renderSearchOverlay() {
ImGuiIO& io = ImGui::GetIO();
float windowWidth = io.DisplaySize.x;
// Search bar at top (below app bar)
ImGui::SetCursorPos(ImVec2(0, APP_BAR_HEIGHT));
CardSpec cardSpec;
cardSpec.elevation = 8;
cardSpec.padding = spacing::dp(2);
cardSpec.width = windowWidth;
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(spacing::dp(2), spacing::dp(2)));
if (BeginCard(cardSpec)) {
ImGui::SetNextItemWidth(windowWidth - spacing::dp(8));
TextFieldSpec textSpec;
textSpec.placeholder = "Search transactions, addresses...";
textSpec.variant = TextFieldVariant::Filled;
textSpec.leadingIcon = ICON_MD_SEARCH;
textSpec.width = windowWidth - spacing::dp(12);
TextFieldResult result = TextField("global_search", m_searchBuffer,
sizeof(m_searchBuffer), textSpec);
if (result.submitted && strlen(m_searchBuffer) > 0) {
if (m_onSearch) m_onSearch(m_searchBuffer);
m_searchOpen = false;
}
// Close on escape
if (ImGui::IsKeyPressed(ImGuiKey_Escape)) {
m_searchOpen = false;
memset(m_searchBuffer, 0, sizeof(m_searchBuffer));
}
EndCard();
}
ImGui::PopStyleVar();
}
} // namespace screens
} // namespace ui
} // namespace dragonx

View File

@@ -1,371 +0,0 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include "../material/material.h"
#include "../../embedded/IconsMaterialDesign.h"
#include "imgui.h"
#include <string>
#include <vector>
#include <functional>
namespace dragonx {
namespace ui {
namespace screens {
using namespace material;
// ============================================================================
// Mining Screen
// ============================================================================
// Mining controls and statistics display
/**
* @brief Mining statistics
*/
struct MiningStats {
bool isMining = false;
int threads = 0;
int maxThreads = 8;
double hashrate = 0.0; // Hashes per second
double networkHashrate = 0.0; // Network hashrate
int blocksFound = 0;
double totalMined = 0.0;
std::string miningAddress;
int currentHeight = 0;
double networkDifficulty = 0.0;
std::string estimatedTimeToBlock;
};
/**
* @brief Recent mined block
*/
struct MinedBlock {
int height;
double reward;
std::string time;
std::string txid;
};
/**
* @brief Mining screen with controls and stats
*/
class MiningScreen {
public:
void setStats(const MiningStats& stats) { m_stats = stats; }
void setMinedBlocks(const std::vector<MinedBlock>& blocks) { m_minedBlocks = blocks; }
void setOnStartMining(std::function<void(int threads)> callback) {
m_onStartMining = callback;
}
void setOnStopMining(std::function<void()> callback) {
m_onStopMining = callback;
}
void setOnSetAddress(std::function<void(const std::string&)> callback) {
m_onSetAddress = callback;
}
void render();
private:
void renderMiningControls();
void renderStatsCards();
void renderMinedBlocksList();
MiningStats m_stats;
std::vector<MinedBlock> m_minedBlocks;
std::function<void(int)> m_onStartMining;
std::function<void()> m_onStopMining;
std::function<void(const std::string&)> m_onSetAddress;
int m_selectedThreads = 4;
};
// ============================================================================
// Implementation
// ============================================================================
inline void MiningScreen::render() {
// Title
ImGui::BeginGroup();
{
Typography::instance().text(TypeStyle::H5, "Mining");
ImGui::SameLine();
// Mining status indicator
if (m_stats.isMining) {
ChipSpec chipSpec;
chipSpec.variant = ChipVariant::Filled;
chipSpec.color = colors::Green500;
Chip(ICON_MD_HARDWARE " Mining Active", chipSpec);
}
}
ImGui::EndGroup();
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
// Mining controls card
renderMiningControls();
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
// Stats cards row
renderStatsCards();
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
// Mined blocks history
renderMinedBlocksList();
}
inline void MiningScreen::renderMiningControls() {
CardSpec cardSpec;
cardSpec.elevation = 2;
cardSpec.padding = spacing::dp(3);
ImGui::PushID("mining_controls");
if (BeginCard(cardSpec)) {
Typography::instance().textColored(TypeStyle::Overline, OnSurfaceMedium(), "MINING CONTROLS");
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
// Thread selector
ImGui::BeginGroup();
{
Typography::instance().text(TypeStyle::Body1, "Mining Threads:");
ImGui::SameLine();
ImGui::SetNextItemWidth(150.0f);
ImGui::SliderInt("##threads", &m_selectedThreads, 1, m_stats.maxThreads);
ImGui::SameLine();
char threadInfo[64];
snprintf(threadInfo, sizeof(threadInfo), "(%d available)", m_stats.maxThreads);
Typography::instance().textColored(TypeStyle::Caption, OnSurfaceMedium(), threadInfo);
}
ImGui::EndGroup();
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
// Mining address display
if (!m_stats.miningAddress.empty()) {
Typography::instance().textColored(TypeStyle::Caption, OnSurfaceMedium(), "Mining to:");
std::string displayAddr = m_stats.miningAddress;
if (displayAddr.length() > 50) {
displayAddr = displayAddr.substr(0, 25) + "..." +
displayAddr.substr(displayAddr.length() - 20);
}
Typography::instance().text(TypeStyle::Body2, displayAddr.c_str());
}
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
// Start/Stop buttons
if (m_stats.isMining) {
// Stop button (red)
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(colors::Red700.x, colors::Red700.y, colors::Red700.z, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(colors::Red500.x, colors::Red500.y, colors::Red500.z, 1.0f));
if (ImGui::Button("⏹ STOP MINING", ImVec2(150, 40))) {
if (m_onStopMining) m_onStopMining();
}
ImGui::PopStyleColor(2);
ImGui::SameLine();
// Live hashrate display
char hashrateStr[64];
if (m_stats.hashrate > 1000000) {
snprintf(hashrateStr, sizeof(hashrateStr), "%.2f MH/s", m_stats.hashrate / 1000000.0);
} else if (m_stats.hashrate > 1000) {
snprintf(hashrateStr, sizeof(hashrateStr), "%.2f KH/s", m_stats.hashrate / 1000.0);
} else {
snprintf(hashrateStr, sizeof(hashrateStr), "%.0f H/s", m_stats.hashrate);
}
Typography::instance().textColored(TypeStyle::H6, Primary(), hashrateStr);
} else {
// Start button (green)
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(colors::Green700.x, colors::Green700.y, colors::Green700.z, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(colors::Green500.x, colors::Green500.y, colors::Green500.z, 1.0f));
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1, 1, 1, 1));
if (ImGui::Button(ICON_MD_PLAY_ARROW " START MINING", ImVec2(150, 40))) {
if (m_onStartMining) m_onStartMining(m_selectedThreads);
}
ImGui::PopStyleColor(3);
}
EndCard();
}
ImGui::PopID();
}
inline void MiningScreen::renderStatsCards() {
float availWidth = ImGui::GetContentRegionAvail().x;
float cardWidth = (availWidth - spacing::dp(2) * 2) / 3.0f;
// Hashrate card
ImGui::BeginGroup();
{
CardSpec cardSpec;
cardSpec.elevation = 1;
cardSpec.padding = spacing::dp(2);
cardSpec.width = cardWidth;
ImGui::PushID("hashrate_card");
if (BeginCard(cardSpec)) {
Typography::instance().textColored(TypeStyle::Overline, OnSurfaceMedium(), "YOUR HASHRATE");
ImGui::Dummy(ImVec2(0, spacing::dp(1)));
char hashrateStr[64];
if (m_stats.hashrate > 1000000) {
snprintf(hashrateStr, sizeof(hashrateStr), "%.2f", m_stats.hashrate / 1000000.0);
Typography::instance().text(TypeStyle::H4, hashrateStr);
ImGui::SameLine();
Typography::instance().textColored(TypeStyle::Body1, OnSurfaceMedium(), "MH/s");
} else if (m_stats.hashrate > 1000) {
snprintf(hashrateStr, sizeof(hashrateStr), "%.2f", m_stats.hashrate / 1000.0);
Typography::instance().text(TypeStyle::H4, hashrateStr);
ImGui::SameLine();
Typography::instance().textColored(TypeStyle::Body1, OnSurfaceMedium(), "KH/s");
} else {
snprintf(hashrateStr, sizeof(hashrateStr), "%.0f", m_stats.hashrate);
Typography::instance().text(TypeStyle::H4, hashrateStr);
ImGui::SameLine();
Typography::instance().textColored(TypeStyle::Body1, OnSurfaceMedium(), "H/s");
}
EndCard();
}
ImGui::PopID();
}
ImGui::EndGroup();
ImGui::SameLine(0, spacing::dp(2));
// Network hashrate card
ImGui::BeginGroup();
{
CardSpec cardSpec;
cardSpec.elevation = 1;
cardSpec.padding = spacing::dp(2);
cardSpec.width = cardWidth;
ImGui::PushID("network_card");
if (BeginCard(cardSpec)) {
Typography::instance().textColored(TypeStyle::Overline, OnSurfaceMedium(), "NETWORK HASHRATE");
ImGui::Dummy(ImVec2(0, spacing::dp(1)));
char hashrateStr[64];
if (m_stats.networkHashrate > 1000000000) {
snprintf(hashrateStr, sizeof(hashrateStr), "%.2f", m_stats.networkHashrate / 1000000000.0);
Typography::instance().text(TypeStyle::H4, hashrateStr);
ImGui::SameLine();
Typography::instance().textColored(TypeStyle::Body1, OnSurfaceMedium(), "GH/s");
} else if (m_stats.networkHashrate > 1000000) {
snprintf(hashrateStr, sizeof(hashrateStr), "%.2f", m_stats.networkHashrate / 1000000.0);
Typography::instance().text(TypeStyle::H4, hashrateStr);
ImGui::SameLine();
Typography::instance().textColored(TypeStyle::Body1, OnSurfaceMedium(), "MH/s");
} else {
snprintf(hashrateStr, sizeof(hashrateStr), "%.2f", m_stats.networkHashrate / 1000.0);
Typography::instance().text(TypeStyle::H4, hashrateStr);
ImGui::SameLine();
Typography::instance().textColored(TypeStyle::Body1, OnSurfaceMedium(), "KH/s");
}
EndCard();
}
ImGui::PopID();
}
ImGui::EndGroup();
ImGui::SameLine(0, spacing::dp(2));
// Total mined card
ImGui::BeginGroup();
{
CardSpec cardSpec;
cardSpec.elevation = 1;
cardSpec.padding = spacing::dp(2);
cardSpec.width = cardWidth;
ImGui::PushID("mined_card");
if (BeginCard(cardSpec)) {
Typography::instance().textColored(TypeStyle::Overline, OnSurfaceMedium(), "TOTAL MINED");
ImGui::Dummy(ImVec2(0, spacing::dp(1)));
char minedStr[64];
snprintf(minedStr, sizeof(minedStr), "%.4f", m_stats.totalMined);
Typography::instance().text(TypeStyle::H4, minedStr);
ImGui::SameLine();
Typography::instance().textColored(TypeStyle::Body1, OnSurfaceMedium(), "DRGX");
ImGui::Dummy(ImVec2(0, spacing::dp(0.5f)));
char blocksStr[32];
snprintf(blocksStr, sizeof(blocksStr), "%d blocks found", m_stats.blocksFound);
Typography::instance().textColored(TypeStyle::Caption, OnSurfaceMedium(), blocksStr);
EndCard();
}
ImGui::PopID();
}
ImGui::EndGroup();
}
inline void MiningScreen::renderMinedBlocksList() {
CardSpec cardSpec;
cardSpec.elevation = 1;
cardSpec.padding = spacing::dp(2);
ImGui::PushID("mined_blocks");
if (BeginCard(cardSpec)) {
Typography::instance().textColored(TypeStyle::Overline, OnSurfaceMedium(), "BLOCKS YOU MINED");
ImGui::Dummy(ImVec2(0, spacing::dp(1)));
if (m_minedBlocks.empty()) {
Typography::instance().textColored(TypeStyle::Body2, OnSurfaceMedium(),
"No blocks mined yet. Keep mining!");
} else {
BeginList("mined_list", false);
for (size_t i = 0; i < m_minedBlocks.size() && i < 10; i++) {
const auto& block = m_minedBlocks[i];
ListItemSpec itemSpec;
itemSpec.leadingIcon = "🏆";
char primaryStr[64];
snprintf(primaryStr, sizeof(primaryStr), "Block #%d", block.height);
itemSpec.primaryText = primaryStr;
char secondaryStr[64];
snprintf(secondaryStr, sizeof(secondaryStr), "+%.4f DRGX • %s",
block.reward, block.time.c_str());
itemSpec.secondaryText = secondaryStr;
itemSpec.dividerBelow = (i < m_minedBlocks.size() - 1 && i < 9);
ListItem(itemSpec);
}
EndList();
}
EndCard();
}
ImGui::PopID();
}
} // namespace screens
} // namespace ui
} // namespace dragonx

View File

@@ -1,462 +0,0 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include "../material/material.h"
#include "../../embedded/IconsMaterialDesign.h"
#include "imgui.h"
#include <string>
#include <vector>
#include <functional>
namespace dragonx {
namespace ui {
namespace screens {
using namespace material;
// ============================================================================
// Peers Screen
// ============================================================================
// Display connected peers with Material list
/**
* @brief Peer connection info
*/
struct PeerInfo {
int id;
std::string address;
std::string subversion;
int startingHeight;
int currentHeight;
int latency; // ms
int64_t bytesSent;
int64_t bytesReceived;
std::string connTime; // Connection duration
bool isInbound;
};
/**
* @brief Network statistics
*/
struct NetworkStats {
int totalConnections;
int inboundCount;
int outboundCount;
int64_t totalBytesSent;
int64_t totalBytesReceived;
int currentHeight;
std::string networkName;
};
/**
* @brief Peers screen with network info
*/
class PeersScreen {
public:
void setPeers(const std::vector<PeerInfo>& peers) { m_peers = peers; }
void setNetworkStats(const NetworkStats& stats) { m_networkStats = stats; }
void setOnDisconnectPeer(std::function<void(int peerId)> callback) {
m_onDisconnectPeer = callback;
}
void setOnAddNode(std::function<void(const std::string&)> callback) {
m_onAddNode = callback;
}
void render();
private:
void renderNetworkStats();
void renderPeersList();
void renderPeerDetails();
void renderAddNodeDialog();
std::vector<PeerInfo> m_peers;
NetworkStats m_networkStats;
std::function<void(int)> m_onDisconnectPeer;
std::function<void(const std::string&)> m_onAddNode;
int m_selectedPeerId = -1;
bool m_showDetails = false;
bool m_showAddNode = false;
char m_addNodeBuffer[256] = {0};
};
// ============================================================================
// Implementation
// ============================================================================
inline void PeersScreen::render() {
// Title
ImGui::BeginGroup();
{
Typography::instance().text(TypeStyle::H5, "Network Peers");
ImGui::SameLine(ImGui::GetContentRegionAvail().x - 120);
if (OutlinedButton("+ ADD NODE")) {
m_showAddNode = true;
}
}
ImGui::EndGroup();
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
// Network stats cards
renderNetworkStats();
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
// Peers list
renderPeersList();
// Dialogs
if (m_showDetails) {
renderPeerDetails();
}
if (m_showAddNode) {
renderAddNodeDialog();
}
}
inline void PeersScreen::renderNetworkStats() {
float availWidth = ImGui::GetContentRegionAvail().x;
float cardWidth = (availWidth - spacing::dp(2) * 3) / 4.0f;
// Connections card
ImGui::BeginGroup();
{
CardSpec cardSpec;
cardSpec.elevation = 1;
cardSpec.padding = spacing::dp(2);
cardSpec.width = cardWidth;
ImGui::PushID("conn_card");
if (BeginCard(cardSpec)) {
Typography::instance().textColored(TypeStyle::Overline, OnSurfaceMedium(), "CONNECTIONS");
ImGui::Dummy(ImVec2(0, spacing::dp(1)));
char connStr[32];
snprintf(connStr, sizeof(connStr), "%d", m_networkStats.totalConnections);
Typography::instance().text(TypeStyle::H4, connStr);
char detailStr[64];
snprintf(detailStr, sizeof(detailStr), ICON_MD_ARROW_DOWNWARD "%d in " ICON_MD_ARROW_UPWARD "%d out",
m_networkStats.inboundCount, m_networkStats.outboundCount);
Typography::instance().textColored(TypeStyle::Caption, OnSurfaceMedium(), detailStr);
EndCard();
}
ImGui::PopID();
}
ImGui::EndGroup();
ImGui::SameLine(0, spacing::dp(2));
// Block height card
ImGui::BeginGroup();
{
CardSpec cardSpec;
cardSpec.elevation = 1;
cardSpec.padding = spacing::dp(2);
cardSpec.width = cardWidth;
ImGui::PushID("height_card");
if (BeginCard(cardSpec)) {
Typography::instance().textColored(TypeStyle::Overline, OnSurfaceMedium(), "BLOCK HEIGHT");
ImGui::Dummy(ImVec2(0, spacing::dp(1)));
char heightStr[32];
snprintf(heightStr, sizeof(heightStr), "%d", m_networkStats.currentHeight);
Typography::instance().text(TypeStyle::H4, heightStr);
Typography::instance().textColored(TypeStyle::Caption, OnSurfaceMedium(),
m_networkStats.networkName.c_str());
EndCard();
}
ImGui::PopID();
}
ImGui::EndGroup();
ImGui::SameLine(0, spacing::dp(2));
// Data sent card
ImGui::BeginGroup();
{
CardSpec cardSpec;
cardSpec.elevation = 1;
cardSpec.padding = spacing::dp(2);
cardSpec.width = cardWidth;
ImGui::PushID("sent_card");
if (BeginCard(cardSpec)) {
Typography::instance().textColored(TypeStyle::Overline, OnSurfaceMedium(), "DATA SENT");
ImGui::Dummy(ImVec2(0, spacing::dp(1)));
char dataStr[32];
double mb = m_networkStats.totalBytesSent / (1024.0 * 1024.0);
if (mb > 1024) {
snprintf(dataStr, sizeof(dataStr), "%.1f GB", mb / 1024.0);
} else {
snprintf(dataStr, sizeof(dataStr), "%.1f MB", mb);
}
Typography::instance().text(TypeStyle::H5, dataStr);
EndCard();
}
ImGui::PopID();
}
ImGui::EndGroup();
ImGui::SameLine(0, spacing::dp(2));
// Data received card
ImGui::BeginGroup();
{
CardSpec cardSpec;
cardSpec.elevation = 1;
cardSpec.padding = spacing::dp(2);
cardSpec.width = cardWidth;
ImGui::PushID("recv_card");
if (BeginCard(cardSpec)) {
Typography::instance().textColored(TypeStyle::Overline, OnSurfaceMedium(), "DATA RECEIVED");
ImGui::Dummy(ImVec2(0, spacing::dp(1)));
char dataStr[32];
double mb = m_networkStats.totalBytesReceived / (1024.0 * 1024.0);
if (mb > 1024) {
snprintf(dataStr, sizeof(dataStr), "%.1f GB", mb / 1024.0);
} else {
snprintf(dataStr, sizeof(dataStr), "%.1f MB", mb);
}
Typography::instance().text(TypeStyle::H5, dataStr);
EndCard();
}
ImGui::PopID();
}
ImGui::EndGroup();
}
inline void PeersScreen::renderPeersList() {
CardSpec cardSpec;
cardSpec.elevation = 1;
cardSpec.padding = 0;
if (BeginCard(cardSpec)) {
if (m_peers.empty()) {
ImGui::Dummy(ImVec2(0, spacing::dp(4)));
float availWidth = ImGui::GetContentRegionAvail().x;
const char* emptyText = "No peers connected";
ImVec2 textSize = ImGui::CalcTextSize(emptyText);
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (availWidth - textSize.x) * 0.5f);
Typography::instance().textColored(TypeStyle::Body1, OnSurfaceMedium(), emptyText);
ImGui::Dummy(ImVec2(0, spacing::dp(4)));
} else {
BeginList("peers_list", false);
for (const auto& peer : m_peers) {
ListItemSpec itemSpec;
// Direction icon
itemSpec.leadingIcon = peer.isInbound ? ICON_MD_ARROW_DOWNWARD : ICON_MD_ARROW_UPWARD;
// Address as primary
itemSpec.primaryText = peer.address.c_str();
// Version + latency as secondary
char secondaryStr[128];
snprintf(secondaryStr, sizeof(secondaryStr), "%s • %dms • Height: %d",
peer.subversion.c_str(), peer.latency, peer.currentHeight);
itemSpec.secondaryText = secondaryStr;
itemSpec.trailingIcon = ICON_MD_INFO;
itemSpec.dividerBelow = true;
char peerId[16];
snprintf(peerId, sizeof(peerId), "peer_%d", peer.id);
ImGui::PushID(peerId);
if (ListItem(itemSpec)) {
m_selectedPeerId = peer.id;
m_showDetails = true;
}
ImGui::PopID();
}
EndList();
}
EndCard();
}
}
inline void PeersScreen::renderPeerDetails() {
// Find selected peer
const PeerInfo* selected = nullptr;
for (const auto& peer : m_peers) {
if (peer.id == m_selectedPeerId) {
selected = &peer;
break;
}
}
if (!selected) {
m_showDetails = false;
return;
}
DialogSpec dialogSpec;
dialogSpec.title = "Peer Details";
dialogSpec.maxWidth = 450.0f;
DialogResult result = BeginDialog("peer_details", dialogSpec);
if (result.isOpen) {
const PeerInfo& peer = *selected;
// Direction badge
ChipSpec chipSpec;
chipSpec.variant = ChipVariant::Filled;
chipSpec.color = peer.isInbound ? colors::Blue500 : colors::Orange500;
Chip(peer.isInbound ? ICON_MD_ARROW_DOWNWARD " Inbound" : ICON_MD_ARROW_UPWARD " Outbound", chipSpec);
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
// Address
Typography::instance().textColored(TypeStyle::Overline, OnSurfaceMedium(), "ADDRESS");
Typography::instance().text(TypeStyle::Body1, peer.address.c_str());
ImGui::Dummy(ImVec2(0, spacing::dp(1)));
// Client
Typography::instance().textColored(TypeStyle::Overline, OnSurfaceMedium(), "CLIENT");
Typography::instance().text(TypeStyle::Body2, peer.subversion.c_str());
ImGui::Dummy(ImVec2(0, spacing::dp(1)));
// Height
char heightStr[64];
snprintf(heightStr, sizeof(heightStr), "%d (started at %d)",
peer.currentHeight, peer.startingHeight);
Typography::instance().textColored(TypeStyle::Overline, OnSurfaceMedium(), "BLOCK HEIGHT");
Typography::instance().text(TypeStyle::Body2, heightStr);
ImGui::Dummy(ImVec2(0, spacing::dp(1)));
// Latency
char latencyStr[32];
snprintf(latencyStr, sizeof(latencyStr), "%d ms", peer.latency);
Typography::instance().textColored(TypeStyle::Overline, OnSurfaceMedium(), "LATENCY");
Typography::instance().text(TypeStyle::Body2, latencyStr);
ImGui::Dummy(ImVec2(0, spacing::dp(1)));
// Connected time
Typography::instance().textColored(TypeStyle::Overline, OnSurfaceMedium(), "CONNECTED");
Typography::instance().text(TypeStyle::Body2, peer.connTime.c_str());
ImGui::Dummy(ImVec2(0, spacing::dp(1)));
// Data transferred
char dataStr[64];
snprintf(dataStr, sizeof(dataStr), ICON_MD_ARROW_UPWARD " %.2f MB " ICON_MD_ARROW_DOWNWARD " %.2f MB",
peer.bytesSent / (1024.0 * 1024.0),
peer.bytesReceived / (1024.0 * 1024.0));
Typography::instance().textColored(TypeStyle::Overline, OnSurfaceMedium(), "DATA TRANSFERRED");
Typography::instance().text(TypeStyle::Body2, dataStr);
ImGui::Dummy(ImVec2(0, spacing::dp(3)));
// Actions
float availWidth = ImGui::GetContentRegionAvail().x;
// Disconnect button (danger)
ImGui::PushStyleColor(ImGuiCol_Button, colors::withAlpha(colors::Red500, 0.1f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, colors::withAlpha(colors::Red500, 0.2f));
ImGui::PushStyleColor(ImGuiCol_Text, colors::Red500);
if (ImGui::Button("DISCONNECT", ImVec2(120, 36))) {
if (m_onDisconnectPeer) m_onDisconnectPeer(peer.id);
m_showDetails = false;
}
ImGui::PopStyleColor(3);
ImGui::SameLine(availWidth - 80);
if (ContainedButton("CLOSE")) {
m_showDetails = false;
}
EndDialog();
}
if (result.dismissed) {
m_showDetails = false;
}
}
inline void PeersScreen::renderAddNodeDialog() {
DialogSpec dialogSpec;
dialogSpec.title = "Add Node";
dialogSpec.maxWidth = 400.0f;
DialogResult result = BeginDialog("add_node", dialogSpec);
if (result.isOpen) {
Typography::instance().text(TypeStyle::Body1,
"Enter the IP address or hostname of a node to connect to:");
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
TextFieldSpec textSpec;
textSpec.label = "Node Address";
textSpec.placeholder = "192.168.1.1:18030 or node.example.com";
textSpec.variant = TextFieldVariant::Outlined;
textSpec.width = -1;
TextField("add_node_addr", m_addNodeBuffer, sizeof(m_addNodeBuffer), textSpec);
ImGui::Dummy(ImVec2(0, spacing::dp(3)));
float availWidth = ImGui::GetContentRegionAvail().x;
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + availWidth - 200);
if (TextButton("CANCEL")) {
m_showAddNode = false;
memset(m_addNodeBuffer, 0, sizeof(m_addNodeBuffer));
}
ImGui::SameLine(0, spacing::dp(2));
bool canAdd = strlen(m_addNodeBuffer) > 0;
ImGui::BeginDisabled(!canAdd);
if (ContainedButton("ADD")) {
if (m_onAddNode) m_onAddNode(m_addNodeBuffer);
m_showAddNode = false;
memset(m_addNodeBuffer, 0, sizeof(m_addNodeBuffer));
}
ImGui::EndDisabled();
EndDialog();
}
if (result.dismissed) {
m_showAddNode = false;
}
}
} // namespace screens
} // namespace ui
} // namespace dragonx

View File

@@ -1,318 +0,0 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include "../material/material.h"
#include "../../embedded/IconsMaterialDesign.h"
#include "imgui.h"
#include <string>
#include <vector>
#include <functional>
namespace dragonx {
namespace ui {
namespace screens {
using namespace material;
// ============================================================================
// Receive Screen
// ============================================================================
// Display receiving addresses with QR codes
/**
* @brief Wallet address for receiving
*/
struct WalletAddress {
std::string address;
std::string label;
std::string type; // "shielded" or "transparent"
double balance;
bool isDefault;
};
/**
* @brief Receive screen with address cards
*/
class ReceiveScreen {
public:
void setAddresses(const std::vector<WalletAddress>& addresses) {
m_addresses = addresses;
}
void setOnCopyAddress(std::function<void(const std::string&)> callback) {
m_onCopyAddress = callback;
}
void setOnNewAddress(std::function<void(bool shielded)> callback) {
m_onNewAddress = callback;
}
void setOnShowQR(std::function<void(const std::string&)> callback) {
m_onShowQR = callback;
}
void setOnEditLabel(std::function<void(const std::string&, const std::string&)> callback) {
m_onEditLabel = callback;
}
void render();
private:
void renderAddressCard(const WalletAddress& addr, int index);
void renderNewAddressButton();
void renderQRCodePopup();
std::vector<WalletAddress> m_addresses;
std::function<void(const std::string&)> m_onCopyAddress;
std::function<void(bool shielded)> m_onNewAddress;
std::function<void(const std::string&)> m_onShowQR;
std::function<void(const std::string&, const std::string&)> m_onEditLabel;
std::string m_selectedQRAddress;
bool m_showQRPopup = false;
int m_selectedTab = 0; // 0 = shielded, 1 = transparent
};
// ============================================================================
// Implementation
// ============================================================================
inline void ReceiveScreen::render() {
// Title
Typography::instance().text(TypeStyle::H5, "Receive DRGX");
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
// Tab selection
TabSpec tabSpec;
tabSpec.variant = TabVariant::Fixed;
if (BeginTabs("receive_tabs", tabSpec)) {
if (Tab(ICON_MD_SHIELD " Shielded", m_selectedTab == 0)) {
m_selectedTab = 0;
}
if (Tab(ICON_MD_DESCRIPTION " Transparent", m_selectedTab == 1)) {
m_selectedTab = 1;
}
EndTabs();
}
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
// Filter addresses by type
std::string filterType = m_selectedTab == 0 ? "shielded" : "transparent";
// Display filtered addresses
int visibleCount = 0;
for (size_t i = 0; i < m_addresses.size(); i++) {
if (m_addresses[i].type == filterType) {
renderAddressCard(m_addresses[i], static_cast<int>(i));
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
visibleCount++;
}
}
// Empty state
if (visibleCount == 0) {
CardSpec emptySpec;
emptySpec.elevation = 1;
emptySpec.padding = spacing::dp(4);
if (BeginCard(emptySpec)) {
float availWidth = ImGui::GetContentRegionAvail().x;
// Center text
const char* emptyText = m_selectedTab == 0 ?
"No shielded addresses yet" : "No transparent addresses yet";
ImVec2 textSize = ImGui::CalcTextSize(emptyText);
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (availWidth - textSize.x) * 0.5f);
Typography::instance().textColored(TypeStyle::Body1, OnSurfaceMedium(), emptyText);
EndCard();
}
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
}
// New address button
renderNewAddressButton();
// QR popup
if (m_showQRPopup) {
renderQRCodePopup();
}
}
inline void ReceiveScreen::renderAddressCard(const WalletAddress& addr, int index) {
CardSpec cardSpec;
cardSpec.elevation = addr.isDefault ? 3 : 1;
cardSpec.padding = spacing::dp(2);
char cardId[32];
snprintf(cardId, sizeof(cardId), "addr_card_%d", index);
ImGui::PushID(cardId);
if (BeginCard(cardSpec)) {
// Header row: Label + Default badge
ImGui::BeginGroup();
{
if (!addr.label.empty()) {
Typography::instance().text(TypeStyle::Subtitle1, addr.label.c_str());
} else {
Typography::instance().textColored(TypeStyle::Subtitle1, OnSurfaceMedium(), "Unnamed Address");
}
if (addr.isDefault) {
ImGui::SameLine();
ChipSpec chipSpec;
chipSpec.variant = ChipVariant::Filled;
chipSpec.color = Primary();
Chip("DEFAULT", chipSpec);
}
}
ImGui::EndGroup();
ImGui::Dummy(ImVec2(0, spacing::dp(1)));
// Address (truncated with copy)
ImGui::BeginGroup();
{
// Show truncated address
std::string displayAddr = addr.address;
if (displayAddr.length() > 40) {
displayAddr = displayAddr.substr(0, 20) + "..." +
displayAddr.substr(displayAddr.length() - 16);
}
Typography::instance().textColored(TypeStyle::Body2, OnSurfaceMedium(),
displayAddr.c_str());
}
ImGui::EndGroup();
ImGui::Dummy(ImVec2(0, spacing::dp(1)));
// Balance
if (addr.balance > 0) {
char balanceStr[64];
snprintf(balanceStr, sizeof(balanceStr), "Balance: %.8f DRGX", addr.balance);
Typography::instance().textColored(TypeStyle::Caption, OnSurfaceMedium(), balanceStr);
ImGui::Dummy(ImVec2(0, spacing::dp(1)));
}
// Action buttons
ImGui::BeginGroup();
{
if (OutlinedButton(ICON_MD_CONTENT_COPY " COPY")) {
if (m_onCopyAddress) m_onCopyAddress(addr.address);
}
ImGui::SameLine(0, spacing::dp(1));
if (OutlinedButton(ICON_MD_QR_CODE " QR")) {
m_selectedQRAddress = addr.address;
m_showQRPopup = true;
}
ImGui::SameLine(0, spacing::dp(1));
if (TextButton(ICON_MD_EDIT " EDIT")) {
// Would trigger edit label dialog
if (m_onEditLabel) m_onEditLabel(addr.address, addr.label);
}
}
ImGui::EndGroup();
EndCard();
}
ImGui::PopID();
}
inline void ReceiveScreen::renderNewAddressButton() {
// Floating action button style
float buttonWidth = 200.0f;
float availWidth = ImGui::GetContentRegionAvail().x;
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (availWidth - buttonWidth) * 0.5f);
if (ContainedButton("+ NEW ADDRESS")) {
if (m_onNewAddress) {
m_onNewAddress(m_selectedTab == 0); // true for shielded
}
}
}
inline void ReceiveScreen::renderQRCodePopup() {
DialogSpec dialogSpec;
dialogSpec.title = "QR Code";
dialogSpec.maxWidth = 400.0f;
DialogResult result = BeginDialog("qr_popup", dialogSpec);
if (result.isOpen) {
// QR code placeholder (actual QR generation would be separate)
float qrSize = 250.0f;
float availWidth = ImGui::GetContentRegionAvail().x;
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (availWidth - qrSize) * 0.5f);
// QR placeholder box
ImVec2 pos = ImGui::GetCursorScreenPos();
ImGui::GetWindowDrawList()->AddRectFilled(
pos,
ImVec2(pos.x + qrSize, pos.y + qrSize),
ImGui::GetColorU32(Surface())
);
ImGui::GetWindowDrawList()->AddRect(
pos,
ImVec2(pos.x + qrSize, pos.y + qrSize),
ImGui::GetColorU32(Divider()),
4.0f
);
// Center text in QR box
const char* qrText = "QR CODE";
ImVec2 textSize = ImGui::CalcTextSize(qrText);
ImGui::SetCursorScreenPos(ImVec2(
pos.x + (qrSize - textSize.x) * 0.5f,
pos.y + (qrSize - textSize.y) * 0.5f
));
Typography::instance().textColored(TypeStyle::Body1, OnSurfaceMedium(), qrText);
ImGui::Dummy(ImVec2(qrSize, qrSize + spacing::dp(2)));
// Address below QR
ImGui::TextWrapped("%s", m_selectedQRAddress.c_str());
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
// Actions
float btnWidth = 100.0f;
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (availWidth - btnWidth * 2 - spacing::dp(2)) * 0.5f);
if (OutlinedButton("COPY")) {
if (m_onCopyAddress) m_onCopyAddress(m_selectedQRAddress);
}
ImGui::SameLine(0, spacing::dp(2));
if (ContainedButton("CLOSE")) {
m_showQRPopup = false;
}
EndDialog();
}
if (result.dismissed) {
m_showQRPopup = false;
}
}
} // namespace screens
} // namespace ui
} // namespace dragonx

View File

@@ -1,91 +0,0 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
/**
* @file screens.h
* @brief Unified include for all wallet screens
*
* Phase 7: Component Redesign
*
* This header provides access to all Material Design wallet screens:
* - HomeScreen: Dashboard with balances and quick actions
* - SendScreen: Material form for sending DRGX
* - ReceiveScreen: Address cards with QR codes
* - TransactionsScreen: Transaction history list
* - MiningScreen: Mining controls and stats
* - PeersScreen: Network peer information
* - SettingsScreen: Application settings
* - MainLayout: Full application layout with navigation
* - ConfirmationDialog: Modal confirmations for critical actions
*
* Usage:
* @code
* #include "ui/screens/screens.h"
* using namespace dragonx::ui::screens;
*
* // Create main layout
* MainLayout layout;
*
* // Set up data
* BalanceInfo balance;
* balance.total = 12345.67;
* layout.homeScreen().setBalance(balance);
*
* // Render (in main loop)
* layout.render();
*
* // Handle confirmations
* ConfirmationDialog::instance().render();
* @endcode
*/
#pragma once
// Material Design system
#include "../material/material.h"
// Individual screens
#include "home_screen.h"
#include "send_screen.h"
#include "receive_screen.h"
#include "transactions_screen.h"
#include "mining_screen.h"
#include "peers_screen.h"
#include "settings_screen.h"
// Main layout and navigation
#include "main_layout.h"
// Dialogs
#include "confirmation_dialog.h"
namespace dragonx {
namespace ui {
namespace screens {
/**
* @brief Initialize all screen systems
*
* Call this once at application startup to initialize
* any global screen state.
*/
inline void InitializeScreens() {
// Currently no global initialization needed
// Reserved for future use
}
/**
* @brief Shutdown all screen systems
*
* Call this at application shutdown to clean up
* any global screen state.
*/
inline void ShutdownScreens() {
// Currently no global cleanup needed
// Reserved for future use
}
} // namespace screens
} // namespace ui
} // namespace dragonx

View File

@@ -1,430 +0,0 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include "../material/material.h"
#include "../../embedded/IconsMaterialDesign.h"
#include "../../config/version.h"
#include "imgui.h"
#include <string>
#include <vector>
#include <functional>
#include <cstring>
namespace dragonx {
namespace ui {
namespace screens {
using namespace material;
// ============================================================================
// Send Screen
// ============================================================================
// Material Design form for sending DRGX
/**
* @brief Address book entry
*/
struct AddressBookEntry {
std::string label;
std::string address;
};
/**
* @brief Send form data
*/
struct SendFormData {
char toAddress[512] = {0};
char amount[32] = {0};
char memo[512] = {0};
bool useShielded = true;
double fee = DRAGONX_DEFAULT_FEE;
// Validation state
bool addressValid = false;
bool amountValid = false;
std::string addressError;
std::string amountError;
};
/**
* @brief Send screen with Material form
*/
class SendScreen {
public:
SendScreen() = default;
void setAvailableBalance(double shielded, double transparent) {
m_shieldedBalance = shielded;
m_transparentBalance = transparent;
}
void setAddressBook(const std::vector<AddressBookEntry>& entries) {
m_addressBook = entries;
}
void setOnSend(std::function<void(const SendFormData&)> callback) {
m_onSend = callback;
}
void setOnCancel(std::function<void()> callback) {
m_onCancel = callback;
}
void setOnValidateAddress(std::function<bool(const std::string&)> callback) {
m_onValidateAddress = callback;
}
void clear() {
memset(m_formData.toAddress, 0, sizeof(m_formData.toAddress));
memset(m_formData.amount, 0, sizeof(m_formData.amount));
memset(m_formData.memo, 0, sizeof(m_formData.memo));
m_formData.useShielded = true;
m_formData.addressValid = false;
m_formData.amountValid = false;
m_formData.addressError.clear();
m_formData.amountError.clear();
}
void render();
private:
void renderFromSelector();
void renderAddressField();
void renderAmountField();
void renderMemoField();
void renderFeeDisplay();
void renderActions();
void validateForm();
SendFormData m_formData;
double m_shieldedBalance = 0.0;
double m_transparentBalance = 0.0;
std::vector<AddressBookEntry> m_addressBook;
std::function<void(const SendFormData&)> m_onSend;
std::function<void()> m_onCancel;
std::function<bool(const std::string&)> m_onValidateAddress;
bool m_showAddressBook = false;
};
// ============================================================================
// Implementation
// ============================================================================
inline void SendScreen::render() {
float availWidth = ImGui::GetContentRegionAvail().x;
float formWidth = std::min(availWidth, 500.0f);
float offsetX = (availWidth - formWidth) * 0.5f;
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + offsetX);
ImGui::BeginGroup();
{
// Title
Typography::instance().text(TypeStyle::H5, "Send DRGX");
ImGui::Dummy(ImVec2(0, spacing::dp(3)));
// Form card
CardSpec cardSpec;
cardSpec.elevation = 2;
cardSpec.padding = spacing::dp(3);
cardSpec.width = formWidth;
if (BeginCard(cardSpec)) {
// From selector (shielded vs transparent)
renderFromSelector();
ImGui::Dummy(ImVec2(0, spacing::dp(3)));
// To address field
renderAddressField();
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
// Amount field
renderAmountField();
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
// Memo field (only for shielded)
if (m_formData.useShielded) {
renderMemoField();
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
}
// Fee display
renderFeeDisplay();
ImGui::Dummy(ImVec2(0, spacing::dp(3)));
// Divider
ImGui::GetWindowDrawList()->AddLine(
ImVec2(ImGui::GetCursorScreenPos().x, ImGui::GetCursorScreenPos().y),
ImVec2(ImGui::GetCursorScreenPos().x + formWidth - spacing::dp(6), ImGui::GetCursorScreenPos().y),
ImGui::GetColorU32(Divider()),
1.0f
);
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
// Action buttons
renderActions();
EndCard();
}
}
ImGui::EndGroup();
}
inline void SendScreen::renderFromSelector() {
Typography::instance().textColored(TypeStyle::Overline, OnSurfaceMedium(), "SEND FROM");
ImGui::Dummy(ImVec2(0, spacing::dp(1)));
float buttonWidth = 200.0f;
// Shielded option
ImGui::PushID("from_shielded");
{
bool selected = m_formData.useShielded;
ImVec4 bgColor = selected ? colors::withAlpha(Primary(), 0.12f) : Surface();
ImVec4 borderColor = selected ? Primary() : Divider();
ImGui::PushStyleColor(ImGuiCol_Button, bgColor);
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, colors::withAlpha(Primary(), 0.16f));
ImGui::PushStyleColor(ImGuiCol_Border, borderColor);
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, selected ? 2.0f : 1.0f);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f);
char label[128];
snprintf(label, sizeof(label), ICON_MD_SHIELD " Shielded\n%.4f DRGX", m_shieldedBalance);
if (ImGui::Button(label, ImVec2(buttonWidth, 60))) {
m_formData.useShielded = true;
}
ImGui::PopStyleVar(2);
ImGui::PopStyleColor(3);
}
ImGui::PopID();
ImGui::SameLine(0, spacing::dp(2));
// Transparent option
ImGui::PushID("from_transparent");
{
bool selected = !m_formData.useShielded;
ImVec4 bgColor = selected ? colors::withAlpha(Secondary(), 0.12f) : Surface();
ImVec4 borderColor = selected ? Secondary() : Divider();
ImGui::PushStyleColor(ImGuiCol_Button, bgColor);
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, colors::withAlpha(Secondary(), 0.16f));
ImGui::PushStyleColor(ImGuiCol_Border, borderColor);
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, selected ? 2.0f : 1.0f);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f);
char label[128];
snprintf(label, sizeof(label), ICON_MD_DESCRIPTION " Transparent\n%.4f DRGX", m_transparentBalance);
if (ImGui::Button(label, ImVec2(buttonWidth, 60))) {
m_formData.useShielded = false;
}
ImGui::PopStyleVar(2);
ImGui::PopStyleColor(3);
}
ImGui::PopID();
}
inline void SendScreen::renderAddressField() {
TextFieldSpec spec;
spec.label = "Recipient Address";
spec.placeholder = "Enter DRGX address...";
spec.variant = TextFieldVariant::Outlined;
spec.width = -1; // Full width
spec.hasError = !m_formData.addressError.empty();
spec.helperText = m_formData.addressError.empty() ?
"z-address (shielded) or t-address (transparent)" :
m_formData.addressError.c_str();
spec.leadingIcon = ICON_MD_MARKUNREAD_MAILBOX;
spec.trailingIcon = ICON_MD_MENU_BOOK; // Address book
TextFieldResult result = TextField("send_address", m_formData.toAddress,
sizeof(m_formData.toAddress), spec);
if (result.trailingIconClicked) {
m_showAddressBook = !m_showAddressBook;
}
if (result.changed) {
// Validate address
if (m_onValidateAddress) {
m_formData.addressValid = m_onValidateAddress(m_formData.toAddress);
if (!m_formData.addressValid && strlen(m_formData.toAddress) > 10) {
m_formData.addressError = "Invalid address format";
} else {
m_formData.addressError.clear();
}
}
}
// Address book dropdown
if (m_showAddressBook && !m_addressBook.empty()) {
ImGui::Dummy(ImVec2(0, spacing::dp(1)));
CardSpec cardSpec;
cardSpec.elevation = 8;
cardSpec.padding = spacing::dp(1);
if (BeginCard(cardSpec)) {
BeginList("address_book_list", false);
for (const auto& entry : m_addressBook) {
ListItemSpec itemSpec;
itemSpec.leadingIcon = "👤";
itemSpec.primaryText = entry.label.c_str();
itemSpec.secondaryText = entry.address.substr(0, 20).c_str();
itemSpec.dividerBelow = true;
if (ListItem(itemSpec)) {
strncpy(m_formData.toAddress, entry.address.c_str(),
sizeof(m_formData.toAddress) - 1);
m_formData.addressValid = true;
m_formData.addressError.clear();
m_showAddressBook = false;
}
}
EndList();
EndCard();
}
}
}
inline void SendScreen::renderAmountField() {
TextFieldSpec spec;
spec.label = "Amount";
spec.placeholder = "0.00000000";
spec.variant = TextFieldVariant::Outlined;
spec.width = 200.0f;
spec.hasError = !m_formData.amountError.empty();
spec.helperText = m_formData.amountError.c_str();
spec.suffix = "DRGX";
TextFieldResult result = TextField("send_amount", m_formData.amount,
sizeof(m_formData.amount), spec);
if (result.changed) {
// Validate amount
double amt = atof(m_formData.amount);
double maxBalance = m_formData.useShielded ? m_shieldedBalance : m_transparentBalance;
if (amt <= 0) {
m_formData.amountValid = false;
if (strlen(m_formData.amount) > 0) {
m_formData.amountError = "Amount must be greater than 0";
}
} else if (amt > maxBalance - m_formData.fee) {
m_formData.amountValid = false;
m_formData.amountError = "Insufficient balance";
} else {
m_formData.amountValid = true;
m_formData.amountError.clear();
}
}
ImGui::SameLine();
// Max button
if (TextButton("MAX")) {
double maxBalance = m_formData.useShielded ? m_shieldedBalance : m_transparentBalance;
double maxAmount = maxBalance - m_formData.fee;
if (maxAmount > 0) {
snprintf(m_formData.amount, sizeof(m_formData.amount), "%.8f", maxAmount);
m_formData.amountValid = true;
m_formData.amountError.clear();
}
}
}
inline void SendScreen::renderMemoField() {
TextFieldSpec spec;
spec.label = "Memo (encrypted)";
spec.placeholder = "Optional private message...";
spec.variant = TextFieldVariant::Outlined;
spec.width = -1;
spec.multiline = true;
spec.helperText = "512 characters max. Only visible to recipient.";
spec.maxLength = 512;
TextField("send_memo", m_formData.memo, sizeof(m_formData.memo), spec);
}
inline void SendScreen::renderFeeDisplay() {
ImGui::BeginGroup();
{
Typography::instance().textColored(TypeStyle::Body2, OnSurfaceMedium(), "Network Fee:");
ImGui::SameLine();
char feeStr[32];
snprintf(feeStr, sizeof(feeStr), "%.8f DRGX", m_formData.fee);
Typography::instance().text(TypeStyle::Body2, feeStr);
}
ImGui::EndGroup();
// Total if amount is valid
if (m_formData.amountValid) {
double amt = atof(m_formData.amount);
double total = amt + m_formData.fee;
ImGui::BeginGroup();
{
Typography::instance().textColored(TypeStyle::Body1, OnSurfaceMedium(), "Total:");
ImGui::SameLine();
char totalStr[32];
snprintf(totalStr, sizeof(totalStr), "%.8f DRGX", total);
Typography::instance().textColored(TypeStyle::Body1, Primary(), totalStr);
}
ImGui::EndGroup();
}
}
inline void SendScreen::renderActions() {
float buttonWidth = 120.0f;
float availWidth = ImGui::GetContentRegionAvail().x;
// Right-align buttons
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + availWidth - buttonWidth * 2 - spacing::dp(2));
// Cancel button
if (TextButton("CANCEL")) {
if (m_onCancel) m_onCancel();
}
ImGui::SameLine(0, spacing::dp(2));
// Send button
bool canSend = m_formData.addressValid && m_formData.amountValid;
ImGui::BeginDisabled(!canSend);
if (ContainedButton("SEND")) {
if (m_onSend) m_onSend(m_formData);
}
ImGui::EndDisabled();
}
inline void SendScreen::validateForm() {
// Address validation
if (strlen(m_formData.toAddress) == 0) {
m_formData.addressValid = false;
m_formData.addressError = "Address is required";
}
// Amount validation
if (strlen(m_formData.amount) == 0) {
m_formData.amountValid = false;
m_formData.amountError = "Amount is required";
}
}
} // namespace screens
} // namespace ui
} // namespace dragonx

View File

@@ -1,561 +0,0 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include "../material/material.h"
#include "../../config/version.h"
#include "../../embedded/IconsMaterialDesign.h"
#include "imgui.h"
#include <string>
#include <vector>
#include <functional>
#include <cstring>
namespace dragonx {
namespace ui {
namespace screens {
using namespace material;
// ============================================================================
// Settings Screen
// ============================================================================
// Application settings with Material list items
/**
* @brief Setting category
*/
enum class SettingsCategory {
Wallet,
Display,
Network,
Privacy,
Advanced
};
/**
* @brief Settings data
*/
struct SettingsData {
// Wallet
bool autoShield = true;
double defaultFee = DRAGONX_DEFAULT_FEE;
// Display
std::string theme = "dark";
std::string language = "en";
std::string fiatCurrency = "USD";
bool showBalanceInFiat = true;
bool hideEmptyAddresses = true;
// Network
std::string proxyAddress;
int proxyPort = 9050;
bool useTor = false;
int maxConnections = 125;
// Privacy
bool rememberAddresses = true;
bool autoLock = true;
int autoLockTimeout = 5; // minutes
// Advanced
int confirmations = 10;
std::string dataDir;
bool debugMode = false;
};
/**
* @brief Settings screen with Material list
*/
class SettingsScreen {
public:
void setSettings(const SettingsData& settings) { m_settings = settings; }
const SettingsData& getSettings() const { return m_settings; }
void setOnSettingsChanged(std::function<void(const SettingsData&)> callback) {
m_onSettingsChanged = callback;
}
void setOnBackupWallet(std::function<void()> callback) {
m_onBackupWallet = callback;
}
void setOnExportKeys(std::function<void()> callback) {
m_onExportKeys = callback;
}
void setOnRescan(std::function<void()> callback) {
m_onRescan = callback;
}
void render();
private:
void renderWalletSettings();
void renderDisplaySettings();
void renderNetworkSettings();
void renderPrivacySettings();
void renderAdvancedSettings();
void renderAboutSection();
void notifySettingsChanged();
SettingsData m_settings;
std::function<void(const SettingsData&)> m_onSettingsChanged;
std::function<void()> m_onBackupWallet;
std::function<void()> m_onExportKeys;
std::function<void()> m_onRescan;
SettingsCategory m_selectedCategory = SettingsCategory::Wallet;
};
// ============================================================================
// Implementation
// ============================================================================
inline void SettingsScreen::render() {
// Title
Typography::instance().text(TypeStyle::H5, "Settings");
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
// Category tabs
TabSpec tabSpec;
tabSpec.variant = TabVariant::Scrollable;
if (BeginTabs("settings_tabs", tabSpec)) {
if (Tab(ICON_MD_ACCOUNT_BALANCE_WALLET " Wallet", m_selectedCategory == SettingsCategory::Wallet)) {
m_selectedCategory = SettingsCategory::Wallet;
}
if (Tab(ICON_MD_PALETTE " Display", m_selectedCategory == SettingsCategory::Display)) {
m_selectedCategory = SettingsCategory::Display;
}
if (Tab(ICON_MD_PUBLIC " Network", m_selectedCategory == SettingsCategory::Network)) {
m_selectedCategory = SettingsCategory::Network;
}
if (Tab(ICON_MD_LOCK " Privacy", m_selectedCategory == SettingsCategory::Privacy)) {
m_selectedCategory = SettingsCategory::Privacy;
}
if (Tab(ICON_MD_SETTINGS " Advanced", m_selectedCategory == SettingsCategory::Advanced)) {
m_selectedCategory = SettingsCategory::Advanced;
}
EndTabs();
}
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
// Settings content
switch (m_selectedCategory) {
case SettingsCategory::Wallet:
renderWalletSettings();
break;
case SettingsCategory::Display:
renderDisplaySettings();
break;
case SettingsCategory::Network:
renderNetworkSettings();
break;
case SettingsCategory::Privacy:
renderPrivacySettings();
break;
case SettingsCategory::Advanced:
renderAdvancedSettings();
break;
}
ImGui::Dummy(ImVec2(0, spacing::dp(3)));
// About section (always visible)
renderAboutSection();
}
inline void SettingsScreen::renderWalletSettings() {
CardSpec cardSpec;
cardSpec.elevation = 1;
cardSpec.padding = 0;
if (BeginCard(cardSpec)) {
BeginList("wallet_settings", false);
// Auto-shield toggle
{
ListItemSpec itemSpec;
itemSpec.primaryText = "Auto-Shield Transparent Funds";
itemSpec.secondaryText = "Automatically move transparent balance to shielded";
itemSpec.dividerBelow = true;
ImGui::PushID("auto_shield");
ListItem(itemSpec);
// Toggle positioned at end
ImGui::SameLine(ImGui::GetContentRegionAvail().x - 60);
if (Switch("##toggle", m_settings.autoShield)) {
notifySettingsChanged();
}
ImGui::PopID();
}
// Default fee
{
ListItemSpec itemSpec;
itemSpec.primaryText = "Default Transaction Fee";
char feeStr[32];
snprintf(feeStr, sizeof(feeStr), "%.8f DRGX", m_settings.defaultFee);
itemSpec.secondaryText = feeStr;
itemSpec.trailingIcon = ">";
itemSpec.dividerBelow = true;
ListItem(itemSpec);
}
// Backup wallet
{
ListItemSpec itemSpec;
itemSpec.leadingIcon = ICON_MD_SAVE;
itemSpec.primaryText = "Backup Wallet";
itemSpec.secondaryText = "Export wallet backup file";
itemSpec.trailingIcon = ">";
itemSpec.dividerBelow = true;
if (ListItem(itemSpec)) {
if (m_onBackupWallet) m_onBackupWallet();
}
}
// Export keys
{
ListItemSpec itemSpec;
itemSpec.leadingIcon = ICON_MD_KEY;
itemSpec.primaryText = "Export Private Keys";
itemSpec.secondaryText = "Export viewing and spending keys";
itemSpec.trailingIcon = ">";
if (ListItem(itemSpec)) {
if (m_onExportKeys) m_onExportKeys();
}
}
EndList();
EndCard();
}
}
inline void SettingsScreen::renderDisplaySettings() {
CardSpec cardSpec;
cardSpec.elevation = 1;
cardSpec.padding = 0;
if (BeginCard(cardSpec)) {
BeginList("display_settings", false);
// Theme selection
{
ListItemSpec itemSpec;
itemSpec.primaryText = "Theme";
itemSpec.secondaryText = m_settings.theme == "dark" ? "Dark" : "Light";
itemSpec.trailingIcon = ">";
itemSpec.dividerBelow = true;
ListItem(itemSpec);
}
// Language
{
ListItemSpec itemSpec;
itemSpec.primaryText = "Language";
itemSpec.secondaryText = "English"; // Would map from code
itemSpec.trailingIcon = ">";
itemSpec.dividerBelow = true;
ListItem(itemSpec);
}
// Fiat currency
{
ListItemSpec itemSpec;
itemSpec.primaryText = "Fiat Currency";
itemSpec.secondaryText = m_settings.fiatCurrency.c_str();
itemSpec.trailingIcon = ">";
itemSpec.dividerBelow = true;
ListItem(itemSpec);
}
// Show balance in fiat toggle
{
ListItemSpec itemSpec;
itemSpec.primaryText = "Show Balance in Fiat";
itemSpec.secondaryText = "Display fiat equivalent on home screen";
itemSpec.dividerBelow = true;
ImGui::PushID("show_fiat");
ListItem(itemSpec);
ImGui::SameLine(ImGui::GetContentRegionAvail().x - 60);
if (Switch("##toggle", m_settings.showBalanceInFiat)) {
notifySettingsChanged();
}
ImGui::PopID();
}
// Hide empty addresses
{
ListItemSpec itemSpec;
itemSpec.primaryText = "Hide Empty Addresses";
itemSpec.secondaryText = "Don't show addresses with zero balance";
ImGui::PushID("hide_empty");
ListItem(itemSpec);
ImGui::SameLine(ImGui::GetContentRegionAvail().x - 60);
if (Switch("##toggle", m_settings.hideEmptyAddresses)) {
notifySettingsChanged();
}
ImGui::PopID();
}
EndList();
EndCard();
}
}
inline void SettingsScreen::renderNetworkSettings() {
CardSpec cardSpec;
cardSpec.elevation = 1;
cardSpec.padding = 0;
if (BeginCard(cardSpec)) {
BeginList("network_settings", false);
// Use Tor
{
ListItemSpec itemSpec;
itemSpec.leadingIcon = "🧅";
itemSpec.primaryText = "Use Tor Network";
itemSpec.secondaryText = "Route connections through Tor for privacy";
itemSpec.dividerBelow = true;
ImGui::PushID("use_tor");
ListItem(itemSpec);
ImGui::SameLine(ImGui::GetContentRegionAvail().x - 60);
if (Switch("##toggle", m_settings.useTor)) {
notifySettingsChanged();
}
ImGui::PopID();
}
// Proxy settings (shown if Tor enabled)
if (m_settings.useTor) {
ListItemSpec itemSpec;
itemSpec.primaryText = "Proxy Address";
char proxyStr[128];
snprintf(proxyStr, sizeof(proxyStr), "%s:%d",
m_settings.proxyAddress.empty() ? "127.0.0.1" : m_settings.proxyAddress.c_str(),
m_settings.proxyPort);
itemSpec.secondaryText = proxyStr;
itemSpec.trailingIcon = ">";
itemSpec.dividerBelow = true;
ListItem(itemSpec);
}
// Max connections
{
ListItemSpec itemSpec;
itemSpec.primaryText = "Maximum Connections";
char connStr[32];
snprintf(connStr, sizeof(connStr), "%d peers", m_settings.maxConnections);
itemSpec.secondaryText = connStr;
itemSpec.trailingIcon = ">";
ListItem(itemSpec);
}
EndList();
EndCard();
}
}
inline void SettingsScreen::renderPrivacySettings() {
CardSpec cardSpec;
cardSpec.elevation = 1;
cardSpec.padding = 0;
if (BeginCard(cardSpec)) {
BeginList("privacy_settings", false);
// Remember addresses
{
ListItemSpec itemSpec;
itemSpec.primaryText = "Remember Addresses";
itemSpec.secondaryText = "Save recently used addresses";
itemSpec.dividerBelow = true;
ImGui::PushID("remember_addr");
ListItem(itemSpec);
ImGui::SameLine(ImGui::GetContentRegionAvail().x - 60);
if (Switch("##toggle", m_settings.rememberAddresses)) {
notifySettingsChanged();
}
ImGui::PopID();
}
// Auto-lock
{
ListItemSpec itemSpec;
itemSpec.primaryText = "Auto-Lock Wallet";
itemSpec.secondaryText = "Lock wallet after inactivity";
itemSpec.dividerBelow = true;
ImGui::PushID("auto_lock");
ListItem(itemSpec);
ImGui::SameLine(ImGui::GetContentRegionAvail().x - 60);
if (Switch("##toggle", m_settings.autoLock)) {
notifySettingsChanged();
}
ImGui::PopID();
}
// Lock timeout (if auto-lock enabled)
if (m_settings.autoLock) {
ListItemSpec itemSpec;
itemSpec.primaryText = "Lock Timeout";
char timeoutStr[32];
snprintf(timeoutStr, sizeof(timeoutStr), "%d minutes", m_settings.autoLockTimeout);
itemSpec.secondaryText = timeoutStr;
itemSpec.trailingIcon = ">";
ListItem(itemSpec);
}
EndList();
EndCard();
}
}
inline void SettingsScreen::renderAdvancedSettings() {
CardSpec cardSpec;
cardSpec.elevation = 1;
cardSpec.padding = 0;
if (BeginCard(cardSpec)) {
BeginList("advanced_settings", false);
// Required confirmations
{
ListItemSpec itemSpec;
itemSpec.primaryText = "Required Confirmations";
char confStr[32];
snprintf(confStr, sizeof(confStr), "%d blocks", m_settings.confirmations);
itemSpec.secondaryText = confStr;
itemSpec.trailingIcon = ">";
itemSpec.dividerBelow = true;
ListItem(itemSpec);
}
// Data directory
{
ListItemSpec itemSpec;
itemSpec.leadingIcon = "📁";
itemSpec.primaryText = "Data Directory";
itemSpec.secondaryText = m_settings.dataDir.empty() ?
"(default)" : m_settings.dataDir.c_str();
itemSpec.trailingIcon = ">";
itemSpec.dividerBelow = true;
ListItem(itemSpec);
}
// Rescan blockchain
{
ListItemSpec itemSpec;
itemSpec.leadingIcon = ICON_MD_SYNC;
itemSpec.primaryText = "Rescan Blockchain";
itemSpec.secondaryText = "Rebuild transaction history from blockchain";
itemSpec.trailingIcon = ">";
itemSpec.dividerBelow = true;
if (ListItem(itemSpec)) {
if (m_onRescan) m_onRescan();
}
}
// Debug mode
{
ListItemSpec itemSpec;
itemSpec.leadingIcon = "🐛";
itemSpec.primaryText = "Debug Mode";
itemSpec.secondaryText = "Enable verbose logging";
ImGui::PushID("debug_mode");
ListItem(itemSpec);
ImGui::SameLine(ImGui::GetContentRegionAvail().x - 60);
if (Switch("##toggle", m_settings.debugMode)) {
notifySettingsChanged();
}
ImGui::PopID();
}
EndList();
EndCard();
}
}
inline void SettingsScreen::renderAboutSection() {
CardSpec cardSpec;
cardSpec.elevation = 0;
cardSpec.padding = spacing::dp(3);
cardSpec.outlined = true;
if (BeginCard(cardSpec)) {
float availWidth = ImGui::GetContentRegionAvail().x;
// App name and version centered
const char* appName = "ObsidianDragon";
ImVec2 nameSize = ImGui::CalcTextSize(appName);
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (availWidth - nameSize.x) * 0.5f);
Typography::instance().text(TypeStyle::H6, appName);
char version[64];
snprintf(version, sizeof(version), "Version %s-imgui", DRAGONX_VERSION);
ImVec2 versionSize = ImGui::CalcTextSize(version);
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (availWidth - versionSize.x) * 0.5f);
Typography::instance().textColored(TypeStyle::Caption, OnSurfaceMedium(), version);
ImGui::Dummy(ImVec2(0, spacing::dp(1)));
const char* copyright = "© 2024-2026 The Hush Developers";
ImVec2 copySize = ImGui::CalcTextSize(copyright);
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (availWidth - copySize.x) * 0.5f);
Typography::instance().textColored(TypeStyle::Caption, OnSurfaceMedium(), copyright);
const char* license = "Released under GPLv3";
ImVec2 licenseSize = ImGui::CalcTextSize(license);
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (availWidth - licenseSize.x) * 0.5f);
Typography::instance().textColored(TypeStyle::Caption, OnSurfaceMedium(), license);
EndCard();
}
}
inline void SettingsScreen::notifySettingsChanged() {
if (m_onSettingsChanged) {
m_onSettingsChanged(m_settings);
}
}
} // namespace screens
} // namespace ui
} // namespace dragonx

View File

@@ -1,419 +0,0 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include "../material/material.h"
#include "../../embedded/IconsMaterialDesign.h"
#include "imgui.h"
#include <string>
#include <vector>
#include <functional>
namespace dragonx {
namespace ui {
namespace screens {
using namespace material;
// ============================================================================
// Transactions Screen
// ============================================================================
// Display transaction history with Material list
/**
* @brief Transaction details
*/
struct Transaction {
std::string txid;
std::string type; // "sent", "received", "mined", "self"
std::string fromAddress;
std::string toAddress;
double amount;
double fee;
std::string memo;
std::string datetime;
int confirmations;
int blockHeight;
bool isShielded;
};
/**
* @brief Transaction filter options
*/
enum class TxFilter {
All,
Sent,
Received,
Mined
};
/**
* @brief Transactions screen with Material list
*/
class TransactionsScreen {
public:
void setTransactions(const std::vector<Transaction>& txns) {
m_transactions = txns;
}
void setOnTransactionClick(std::function<void(const std::string&)> callback) {
m_onTxClick = callback;
}
void setOnCopyTxid(std::function<void(const std::string&)> callback) {
m_onCopyTxid = callback;
}
void setOnExport(std::function<void()> callback) {
m_onExport = callback;
}
void render();
private:
void renderFilters();
void renderTransactionList();
void renderTransactionItem(const Transaction& tx, int index);
void renderTransactionDetails();
void renderEmptyState();
std::vector<Transaction> m_transactions;
std::function<void(const std::string&)> m_onTxClick;
std::function<void(const std::string&)> m_onCopyTxid;
std::function<void()> m_onExport;
TxFilter m_filter = TxFilter::All;
std::string m_searchQuery;
char m_searchBuffer[256] = {0};
std::string m_selectedTxid;
bool m_showDetails = false;
};
// ============================================================================
// Implementation
// ============================================================================
inline void TransactionsScreen::render() {
// Title and export button
ImGui::BeginGroup();
{
Typography::instance().text(TypeStyle::H5, "Transactions");
ImGui::SameLine(ImGui::GetContentRegionAvail().x - 100);
if (OutlinedButton("📥 EXPORT")) {
if (m_onExport) m_onExport();
}
}
ImGui::EndGroup();
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
// Filters
renderFilters();
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
// Transaction list or empty state
std::vector<Transaction> filtered;
for (const auto& tx : m_transactions) {
// Apply type filter
if (m_filter != TxFilter::All) {
if (m_filter == TxFilter::Sent && tx.type != "sent") continue;
if (m_filter == TxFilter::Received && tx.type != "received") continue;
if (m_filter == TxFilter::Mined && tx.type != "mined") continue;
}
// Apply search filter
if (strlen(m_searchBuffer) > 0) {
std::string search = m_searchBuffer;
if (tx.txid.find(search) == std::string::npos &&
tx.toAddress.find(search) == std::string::npos &&
tx.fromAddress.find(search) == std::string::npos &&
tx.memo.find(search) == std::string::npos) {
continue;
}
}
filtered.push_back(tx);
}
if (filtered.empty()) {
renderEmptyState();
} else {
// Scrollable list
CardSpec cardSpec;
cardSpec.elevation = 1;
cardSpec.padding = 0;
if (BeginCard(cardSpec)) {
BeginList("tx_list", false);
for (size_t i = 0; i < filtered.size(); i++) {
renderTransactionItem(filtered[i], static_cast<int>(i));
}
EndList();
EndCard();
}
}
// Details popup
if (m_showDetails) {
renderTransactionDetails();
}
}
inline void TransactionsScreen::renderFilters() {
// Search field
TextFieldSpec searchSpec;
searchSpec.label = "Search";
searchSpec.placeholder = "Search by address, txid, memo...";
searchSpec.variant = TextFieldVariant::Outlined;
searchSpec.width = 300.0f;
searchSpec.leadingIcon = ICON_MD_SEARCH;
TextField("tx_search", m_searchBuffer, sizeof(m_searchBuffer), searchSpec);
ImGui::SameLine(0, spacing::dp(3));
// Filter chips
ImGui::BeginGroup();
{
ChipSpec chipSpec;
chipSpec.variant = ChipVariant::Filter;
chipSpec.selectable = true;
chipSpec.selected = (m_filter == TxFilter::All);
if (FilterChip("All", chipSpec)) {
m_filter = TxFilter::All;
}
ImGui::SameLine(0, spacing::dp(1));
chipSpec.selected = (m_filter == TxFilter::Sent);
if (FilterChip(ICON_MD_CALL_MADE " Sent", chipSpec)) {
m_filter = TxFilter::Sent;
}
ImGui::SameLine(0, spacing::dp(1));
chipSpec.selected = (m_filter == TxFilter::Received);
if (FilterChip(ICON_MD_CALL_RECEIVED " Received", chipSpec)) {
m_filter = TxFilter::Received;
}
ImGui::SameLine(0, spacing::dp(1));
chipSpec.selected = (m_filter == TxFilter::Mined);
if (FilterChip(ICON_MD_CONSTRUCTION " Mined", chipSpec)) {
m_filter = TxFilter::Mined;
}
}
ImGui::EndGroup();
}
inline void TransactionsScreen::renderTransactionItem(const Transaction& tx, int index) {
ListItemSpec itemSpec;
// Icon based on type
if (tx.type == "received") {
itemSpec.leadingIcon = ICON_MD_CALL_RECEIVED;
} else if (tx.type == "sent") {
itemSpec.leadingIcon = ICON_MD_CALL_MADE;
} else if (tx.type == "mined") {
itemSpec.leadingIcon = ICON_MD_CONSTRUCTION;
} else {
itemSpec.leadingIcon = ICON_MD_SWAP_HORIZ; // Self transfer
}
// Amount formatting
char amountStr[64];
if (tx.type == "sent") {
snprintf(amountStr, sizeof(amountStr), "-%.8f DRGX", tx.amount);
} else {
snprintf(amountStr, sizeof(amountStr), "+%.8f DRGX", tx.amount);
}
itemSpec.primaryText = amountStr;
// Secondary: date + confirmations
char secondaryStr[128];
if (tx.confirmations == 0) {
snprintf(secondaryStr, sizeof(secondaryStr), "%s • " ICON_MD_HOURGLASS_EMPTY " Pending", tx.datetime.c_str());
} else if (tx.confirmations < 10) {
snprintf(secondaryStr, sizeof(secondaryStr), "%s • %d confirmations",
tx.datetime.c_str(), tx.confirmations);
} else {
snprintf(secondaryStr, sizeof(secondaryStr), "%s • " ICON_MD_CHECK " Confirmed", tx.datetime.c_str());
}
itemSpec.secondaryText = secondaryStr;
// Privacy indicator
if (tx.isShielded) {
itemSpec.trailingIcon = ICON_MD_SHIELD;
}
itemSpec.dividerBelow = true;
char itemId[32];
snprintf(itemId, sizeof(itemId), "tx_%d", index);
ImGui::PushID(itemId);
if (ListItem(itemSpec)) {
m_selectedTxid = tx.txid;
m_showDetails = true;
}
ImGui::PopID();
}
inline void TransactionsScreen::renderTransactionDetails() {
// Find the selected transaction
const Transaction* selected = nullptr;
for (const auto& tx : m_transactions) {
if (tx.txid == m_selectedTxid) {
selected = &tx;
break;
}
}
if (!selected) {
m_showDetails = false;
return;
}
DialogSpec dialogSpec;
dialogSpec.title = "Transaction Details";
dialogSpec.maxWidth = 500.0f;
DialogResult result = BeginDialog("tx_details", dialogSpec);
if (result.isOpen) {
const Transaction& tx = *selected;
// Type badge
ChipSpec chipSpec;
chipSpec.variant = ChipVariant::Filled;
if (tx.type == "received") {
chipSpec.color = colors::Green500;
Chip(ICON_MD_CALL_RECEIVED " Received", chipSpec);
} else if (tx.type == "sent") {
chipSpec.color = colors::Red500;
Chip(ICON_MD_CALL_MADE " Sent", chipSpec);
} else if (tx.type == "mined") {
chipSpec.color = Secondary();
Chip(ICON_MD_CONSTRUCTION " Mined", chipSpec);
}
if (tx.isShielded) {
ImGui::SameLine();
chipSpec.color = Primary();
Chip(ICON_MD_SHIELD " Shielded", chipSpec);
}
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
// Amount
Typography::instance().textColored(TypeStyle::Overline, OnSurfaceMedium(), "AMOUNT");
char amountStr[64];
snprintf(amountStr, sizeof(amountStr), "%.8f DRGX", tx.amount);
Typography::instance().text(TypeStyle::H5, amountStr);
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
// Details grid
auto detailRow = [](const char* label, const char* value, bool canCopy = false) {
Typography::instance().textColored(TypeStyle::Caption, OnSurfaceMedium(), label);
Typography::instance().text(TypeStyle::Body2, value);
ImGui::Dummy(ImVec2(0, spacing::dp(1)));
};
detailRow("DATE", tx.datetime.c_str());
char confStr[32];
if (tx.confirmations == 0) {
snprintf(confStr, sizeof(confStr), "Pending");
} else {
snprintf(confStr, sizeof(confStr), "%d confirmations", tx.confirmations);
}
detailRow("STATUS", confStr);
if (tx.blockHeight > 0) {
char blockStr[32];
snprintf(blockStr, sizeof(blockStr), "%d", tx.blockHeight);
detailRow("BLOCK", blockStr);
}
char feeStr[32];
snprintf(feeStr, sizeof(feeStr), "%.8f DRGX", tx.fee);
detailRow("FEE", feeStr);
ImGui::Dummy(ImVec2(0, spacing::dp(1)));
// Transaction ID
Typography::instance().textColored(TypeStyle::Caption, OnSurfaceMedium(), "TRANSACTION ID");
ImGui::TextWrapped("%s", tx.txid.c_str());
if (TextButton(ICON_MD_CONTENT_COPY " COPY TXID")) {
if (m_onCopyTxid) m_onCopyTxid(tx.txid);
}
// Memo if present
if (!tx.memo.empty()) {
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
Typography::instance().textColored(TypeStyle::Caption, OnSurfaceMedium(), "MEMO");
ImGui::TextWrapped("%s", tx.memo.c_str());
}
ImGui::Dummy(ImVec2(0, spacing::dp(3)));
// Close button
float availWidth = ImGui::GetContentRegionAvail().x;
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + availWidth - 100);
if (ContainedButton("CLOSE")) {
m_showDetails = false;
}
EndDialog();
}
if (result.dismissed) {
m_showDetails = false;
}
}
inline void TransactionsScreen::renderEmptyState() {
CardSpec cardSpec;
cardSpec.elevation = 1;
cardSpec.padding = spacing::dp(6);
if (BeginCard(cardSpec)) {
float availWidth = ImGui::GetContentRegionAvail().x;
// Icon
const char* icon = ICON_MD_RECEIPT;
ImVec2 iconSize = ImGui::CalcTextSize(icon);
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (availWidth - iconSize.x) * 0.5f);
Typography::instance().text(TypeStyle::H3, icon);
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
// Message
const char* message = m_filter == TxFilter::All ?
"No transactions yet" : "No matching transactions";
ImVec2 textSize = ImGui::CalcTextSize(message);
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (availWidth - textSize.x) * 0.5f);
Typography::instance().textColored(TypeStyle::Body1, OnSurfaceMedium(), message);
EndCard();
}
}
} // namespace screens
} // namespace ui
} // namespace dragonx