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:
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user