From ca14aaddc74e352e8f4fc0e455c796e45c34176f Mon Sep 17 00:00:00 2001 From: DanS Date: Sat, 6 Jun 2026 11:31:06 -0500 Subject: [PATCH] refactor(ui): remove abandoned Material-Design component library + screens layer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ~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 --- CLAUDE.md | 2 +- src/embedded/resources.h | 65 --- src/ui/material/app_layout.h | 501 -------------------- src/ui/material/components/app_bar.h | 330 ------------- src/ui/material/components/cards.h | 214 --------- src/ui/material/components/chips.h | 296 ------------ src/ui/material/components/components.h | 122 ----- src/ui/material/components/dialogs.h | 293 ------------ src/ui/material/components/inputs.h | 414 ----------------- src/ui/material/components/lists.h | 306 ------------- src/ui/material/components/nav_drawer.h | 379 --------------- src/ui/material/components/progress.h | 303 ------------ src/ui/material/components/slider.h | 402 ---------------- src/ui/material/components/snackbar.h | 242 ---------- src/ui/material/components/tabs.h | 319 ------------- src/ui/material/components/text_fields.h | 227 --------- src/ui/material/elevation.h | 345 -------------- src/ui/material/material.h | 160 ------- src/ui/material/motion.h | 452 ------------------ src/ui/material/ripple.h | 290 ------------ src/ui/material/transitions.h | 467 ------------------- src/ui/screens/confirmation_dialog.h | 352 -------------- src/ui/screens/home_screen.h | 304 ------------ src/ui/screens/main_layout.h | 553 ---------------------- src/ui/screens/mining_screen.h | 371 --------------- src/ui/screens/peers_screen.h | 462 ------------------- src/ui/screens/receive_screen.h | 318 ------------- src/ui/screens/screens.h | 91 ---- src/ui/screens/send_screen.h | 430 ----------------- src/ui/screens/settings_screen.h | 561 ----------------------- src/ui/screens/transactions_screen.h | 419 ----------------- 31 files changed, 1 insertion(+), 9989 deletions(-) delete mode 100644 src/embedded/resources.h delete mode 100644 src/ui/material/app_layout.h delete mode 100644 src/ui/material/components/app_bar.h delete mode 100644 src/ui/material/components/cards.h delete mode 100644 src/ui/material/components/chips.h delete mode 100644 src/ui/material/components/components.h delete mode 100644 src/ui/material/components/dialogs.h delete mode 100644 src/ui/material/components/inputs.h delete mode 100644 src/ui/material/components/lists.h delete mode 100644 src/ui/material/components/nav_drawer.h delete mode 100644 src/ui/material/components/progress.h delete mode 100644 src/ui/material/components/slider.h delete mode 100644 src/ui/material/components/snackbar.h delete mode 100644 src/ui/material/components/tabs.h delete mode 100644 src/ui/material/components/text_fields.h delete mode 100644 src/ui/material/elevation.h delete mode 100644 src/ui/material/material.h delete mode 100644 src/ui/material/motion.h delete mode 100644 src/ui/material/ripple.h delete mode 100644 src/ui/material/transitions.h delete mode 100644 src/ui/screens/confirmation_dialog.h delete mode 100644 src/ui/screens/home_screen.h delete mode 100644 src/ui/screens/main_layout.h delete mode 100644 src/ui/screens/mining_screen.h delete mode 100644 src/ui/screens/peers_screen.h delete mode 100644 src/ui/screens/receive_screen.h delete mode 100644 src/ui/screens/screens.h delete mode 100644 src/ui/screens/send_screen.h delete mode 100644 src/ui/screens/settings_screen.h delete mode 100644 src/ui/screens/transactions_screen.h diff --git a/CLAUDE.md b/CLAUDE.md index 339567f..58d73ca 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -49,7 +49,7 @@ There is no per-test filtering — it is one binary that runs every assertion. T **Data model** (`src/data/`): `WalletState`, `address_book`, `transaction_history_cache`, `exchange_info`. UI reads from these. -**UI** (`src/ui/`): `windows/` are the tabs and dialogs (one pair per screen, e.g. `send_tab`, `mining_tab`, `console_tab`), `pages/` are multi-section screens (Settings), `screens/` are layout headers, `material/` is the design-system layer, `schema/` loads the TOML UI schema/skins, `effects/` is GL post-processing (blur/acrylic). +**UI** (`src/ui/`): `windows/` are the tabs and dialogs (one pair per screen, e.g. `send_tab`, `mining_tab`, `console_tab`), `pages/` are multi-section screens (Settings), `material/` is the design-system layer (the live helpers `color_theme`, `colors`, `type`/`typography`, `draw_helpers`, `layout`, `project_icons`, `components/buttons`), `schema/` loads the TOML UI schema/skins, `effects/` is GL post-processing (blur/acrylic). **Lite wallet** (`src/wallet/`): the bridge to an external `litelib_*` C-ABI backend. `lite_client_bridge` loads the backend (via direct `litelib_*` externs in `linkedSdxl()`) and owns each Rust string through `lite_owned_string` (copy-before-free / free-once). On top sit `lite_connection_service`, `lite_sync_service`, `lite_result_parsers`, `lite_wallet_gateway`, `lite_wallet_state_mapper`, and `lite_wallet_lifecycle_service`, all driven by `lite_wallet_controller`. The real frontend entry points are `lite_wallet_lifecycle_ui_adapter` and `lite_wallet_server_selection_adapter` (used by `src/ui/pages/settings_page.cpp`); everything else is reachable through them. (The prebuilt-backend symbol check for `DRAGONX_ENABLE_LITE_BACKEND` is done in CMake against the symbols inventory — see below — not in C++.) diff --git a/src/embedded/resources.h b/src/embedded/resources.h deleted file mode 100644 index 1016680..0000000 --- a/src/embedded/resources.h +++ /dev/null @@ -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 -#include -#include -#include - -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 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 diff --git a/src/ui/material/app_layout.h b/src/ui/material/app_layout.h deleted file mode 100644 index 790ec64..0000000 --- a/src/ui/material/app_layout.h +++ /dev/null @@ -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 diff --git a/src/ui/material/components/app_bar.h b/src/ui/material/components/app_bar.h deleted file mode 100644 index b0042e8..0000000 --- a/src/ui/material/components/app_bar.h +++ /dev/null @@ -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 diff --git a/src/ui/material/components/cards.h b/src/ui/material/components/cards.h deleted file mode 100644 index dbd08f4..0000000 --- a/src/ui/material/components/cards.h +++ /dev/null @@ -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 diff --git a/src/ui/material/components/chips.h b/src/ui/material/components/chips.h deleted file mode 100644 index 075b58e..0000000 --- a/src/ui/material/components/chips.h +++ /dev/null @@ -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 diff --git a/src/ui/material/components/components.h b/src/ui/material/components/components.h deleted file mode 100644 index fed34bf..0000000 --- a/src/ui/material/components/components.h +++ /dev/null @@ -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 diff --git a/src/ui/material/components/dialogs.h b/src/ui/material/components/dialogs.h deleted file mode 100644 index 482b47a..0000000 --- a/src/ui/material/components/dialogs.h +++ /dev/null @@ -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 diff --git a/src/ui/material/components/inputs.h b/src/ui/material/components/inputs.h deleted file mode 100644 index c331f93..0000000 --- a/src/ui/material/components/inputs.h +++ /dev/null @@ -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 diff --git a/src/ui/material/components/lists.h b/src/ui/material/components/lists.h deleted file mode 100644 index bc578ae..0000000 --- a/src/ui/material/components/lists.h +++ /dev/null @@ -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 diff --git a/src/ui/material/components/nav_drawer.h b/src/ui/material/components/nav_drawer.h deleted file mode 100644 index 83f0c21..0000000 --- a/src/ui/material/components/nav_drawer.h +++ /dev/null @@ -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 diff --git a/src/ui/material/components/progress.h b/src/ui/material/components/progress.h deleted file mode 100644 index 6948632..0000000 --- a/src/ui/material/components/progress.h +++ /dev/null @@ -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 diff --git a/src/ui/material/components/slider.h b/src/ui/material/components/slider.h deleted file mode 100644 index 55a02de..0000000 --- a/src/ui/material/components/slider.h +++ /dev/null @@ -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 diff --git a/src/ui/material/components/snackbar.h b/src/ui/material/components/snackbar.h deleted file mode 100644 index b31b425..0000000 --- a/src/ui/material/components/snackbar.h +++ /dev/null @@ -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 diff --git a/src/ui/material/components/tabs.h b/src/ui/material/components/tabs.h deleted file mode 100644 index 794c742..0000000 --- a/src/ui/material/components/tabs.h +++ /dev/null @@ -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 diff --git a/src/ui/material/components/text_fields.h b/src/ui/material/components/text_fields.h deleted file mode 100644 index 42ef844..0000000 --- a/src/ui/material/components/text_fields.h +++ /dev/null @@ -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 diff --git a/src/ui/material/elevation.h b/src/ui/material/elevation.h deleted file mode 100644 index e883b4a..0000000 --- a/src/ui/material/elevation.h +++ /dev/null @@ -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 - -namespace dragonx { -namespace ui { -namespace material { - -// ============================================================================ -// Material Design Elevation and Shadow System -// ============================================================================ -// Based on https://m2.material.io/design/environment/elevation.html -// -// Material Design uses two light sources to create shadows: -// - Key light: Creates sharper, directional shadows -// - Ambient light: Creates softer, omnidirectional shadows -// -// In dark themes, elevation is primarily shown through surface color overlays -// rather than shadows. However, shadows can still enhance depth perception. - -// ============================================================================ -// Shadow Specifications -// ============================================================================ - -/** - * @brief Individual shadow layer specification - * - * Material shadows are composed of multiple layers with different - * blur radii and offsets to simulate real-world lighting. - */ -struct ShadowLayer { - float offsetX; // Horizontal offset (typically 0) - float offsetY; // Vertical offset (key light from above) - float blurRadius; // Blur spread - float spreadRadius; // Size adjustment - float opacity; // Alpha 0.0-1.0 -}; - -/** - * @brief Complete shadow specification for an elevation level - */ -struct ShadowSpec { - ShadowLayer umbra; // Darkest part, sharp edge - ShadowLayer penumbra; // Mid-tone, softer - ShadowLayer ambient; // Lightest, most diffuse -}; - -/** - * @brief Get shadow specification for elevation level - * - * @param elevationDp Elevation in dp (0, 1, 2, 3, 4, 6, 8, 12, 16, 24) - * @return ShadowSpec for the elevation - */ -ShadowSpec GetShadowSpec(int elevationDp); - -// ============================================================================ -// Shadow Rendering -// ============================================================================ - -/** - * @brief Draw Material Design shadow for a rectangle - * - * Uses multi-layer soft shadow rendering to approximate Material shadows. - * - * @param drawList ImGui draw list - * @param rect Rectangle bounds - * @param elevationDp Elevation in dp - * @param cornerRadius Corner radius for rounded rectangles - */ -void DrawShadow(ImDrawList* drawList, const ImRect& rect, int elevationDp, float cornerRadius = 0); - -/** - * @brief Draw shadow with position/size parameters - */ -void DrawShadow(ImDrawList* drawList, const ImVec2& pos, const ImVec2& size, - int elevationDp, float cornerRadius = 0); - -/** - * @brief Draw soft shadow (single layer, for custom effects) - * - * @param drawList ImGui draw list - * @param rect Rectangle bounds - * @param color Shadow color with alpha - * @param blurRadius Blur amount - * @param offset Shadow offset - * @param cornerRadius Corner radius - */ -void DrawSoftShadow(ImDrawList* drawList, const ImRect& rect, ImU32 color, - float blurRadius, const ImVec2& offset = ImVec2(0, 0), - float cornerRadius = 0); - -// ============================================================================ -// Elevation Transition Helper -// ============================================================================ - -/** - * @brief Animated elevation value - * - * Use this to smoothly transition between elevation levels (e.g., card hover) - */ -class ElevationAnimator { -public: - ElevationAnimator(int initialElevation = 0); - - /** - * @brief Set target elevation (will animate towards it) - */ - void setTarget(int targetElevation); - - /** - * @brief Update animation (call each frame) - * @param deltaTime Frame delta time - */ - void update(float deltaTime); - - /** - * @brief Get current animated elevation value - */ - float getCurrent() const { return m_current; } - - /** - * @brief Get current elevation as integer (for shadow lookup) - */ - int getCurrentInt() const { return static_cast(m_current + 0.5f); } - - /** - * @brief Check if currently animating - */ - bool isAnimating() const { return m_current != m_target; } - -private: - float m_current; - float m_target; - float m_animationSpeed = 16.0f; // dp per second -}; - -// ============================================================================ -// Implementation -// ============================================================================ - -inline ShadowSpec GetShadowSpec(int elevationDp) { - // Material Design shadow values adapted from the spec - // These approximate the CSS box-shadow values from material.io - - switch (elevationDp) { - case 0: - return { - {0, 0, 0, 0, 0}, // No shadow - {0, 0, 0, 0, 0}, - {0, 0, 0, 0, 0} - }; - case 1: - return { - {0, 2, 1, -1, 0.2f}, // Umbra - {0, 1, 1, 0, 0.14f}, // Penumbra - {0, 1, 3, 0, 0.12f} // Ambient - }; - case 2: - return { - {0, 3, 1, -2, 0.2f}, - {0, 2, 2, 0, 0.14f}, - {0, 1, 5, 0, 0.12f} - }; - case 3: - return { - {0, 3, 3, -2, 0.2f}, - {0, 3, 4, 0, 0.14f}, - {0, 1, 8, 0, 0.12f} - }; - case 4: - return { - {0, 2, 4, -1, 0.2f}, - {0, 4, 5, 0, 0.14f}, - {0, 1, 10, 0, 0.12f} - }; - case 6: - return { - {0, 3, 5, -1, 0.2f}, - {0, 6, 10, 0, 0.14f}, - {0, 1, 18, 0, 0.12f} - }; - case 8: - return { - {0, 5, 5, -3, 0.2f}, - {0, 8, 10, 1, 0.14f}, - {0, 3, 14, 2, 0.12f} - }; - case 12: - return { - {0, 7, 8, -4, 0.2f}, - {0, 12, 17, 2, 0.14f}, - {0, 5, 22, 4, 0.12f} - }; - case 16: - return { - {0, 8, 10, -5, 0.2f}, - {0, 16, 24, 2, 0.14f}, - {0, 6, 30, 5, 0.12f} - }; - case 24: - return { - {0, 11, 15, -7, 0.2f}, - {0, 24, 38, 3, 0.14f}, - {0, 9, 46, 8, 0.12f} - }; - default: - // Interpolate for non-standard elevations - if (elevationDp < 0) return GetShadowSpec(0); - if (elevationDp > 24) return GetShadowSpec(24); - - // Find nearest standard elevation - int lower = 0, upper = 1; - int standards[] = {0, 1, 2, 3, 4, 6, 8, 12, 16, 24}; - for (int i = 0; i < 9; i++) { - if (standards[i] <= elevationDp && standards[i + 1] >= elevationDp) { - lower = standards[i]; - upper = standards[i + 1]; - break; - } - } - - // Use nearest - return GetShadowSpec((elevationDp - lower < upper - elevationDp) ? lower : upper); - } -} - -inline void DrawSoftShadow(ImDrawList* drawList, const ImRect& rect, ImU32 color, - float blurRadius, const ImVec2& offset, float cornerRadius) { - if (blurRadius <= 0 || (color & IM_COL32_A_MASK) == 0) - return; - - // For ImGui, we'll simulate soft shadows using multiple semi-transparent layers - // This is a performance-friendly approximation - - // In low-spec mode use only 1 layer instead of up to 8 - const int numLayers = dragonx::ui::effects::isLowSpecMode() - ? 1 - : ImClamp((int)(blurRadius / 2), 2, 8); - const float layerStep = blurRadius / numLayers; - - // Extract base alpha - float baseAlpha = ((color >> IM_COL32_A_SHIFT) & 0xFF) / 255.0f; - ImU32 baseColor = color & ~IM_COL32_A_MASK; - - for (int i = numLayers - 1; i >= 0; i--) { - float expansion = layerStep * (i + 1); - float alpha = baseAlpha * (1.0f - (float)i / numLayers) / numLayers; - - ImU32 layerColor = baseColor | (((ImU32)(alpha * 255)) << IM_COL32_A_SHIFT); - - ImRect expandedRect( - rect.Min.x - expansion + offset.x, - rect.Min.y - expansion + offset.y, - rect.Max.x + expansion + offset.x, - rect.Max.y + expansion + offset.y - ); - - drawList->AddRectFilled(expandedRect.Min, expandedRect.Max, layerColor, - cornerRadius + expansion * 0.5f); - } -} - -inline void DrawShadow(ImDrawList* drawList, const ImRect& rect, int elevationDp, float cornerRadius) { - if (elevationDp <= 0) - return; - - ShadowSpec spec = GetShadowSpec(elevationDp); - - // Shadow multiplier: light themes need stronger shadows for card depth, - // dark themes rely more on surface color overlay for elevation. - // Configurable via ui.toml [style] shadow-multiplier / shadow-multiplier-light. - const float shadowMultiplier = schema::UI().isDarkTheme() - ? schema::UI().drawElement("style", "shadow-multiplier").sizeOr(0.6f) - : schema::UI().drawElement("style", "shadow-multiplier-light").sizeOr(1.0f); - - // Draw ambient shadow (largest, most diffuse) - if (spec.ambient.opacity > 0) { - ImU32 ambientColor = IM_COL32(0, 0, 0, (int)(spec.ambient.opacity * shadowMultiplier * 255)); - ImRect ambientRect = rect; - ambientRect.Expand(spec.ambient.spreadRadius); - DrawSoftShadow(drawList, ambientRect, ambientColor, spec.ambient.blurRadius, - ImVec2(spec.ambient.offsetX, spec.ambient.offsetY), cornerRadius); - } - - // Draw penumbra (medium) - if (spec.penumbra.opacity > 0) { - ImU32 penumbraColor = IM_COL32(0, 0, 0, (int)(spec.penumbra.opacity * shadowMultiplier * 255)); - ImRect penumbraRect = rect; - penumbraRect.Expand(spec.penumbra.spreadRadius); - DrawSoftShadow(drawList, penumbraRect, penumbraColor, spec.penumbra.blurRadius, - ImVec2(spec.penumbra.offsetX, spec.penumbra.offsetY), cornerRadius); - } - - // Draw umbra (sharpest, darkest) - if (spec.umbra.opacity > 0) { - ImU32 umbraColor = IM_COL32(0, 0, 0, (int)(spec.umbra.opacity * shadowMultiplier * 255)); - ImRect umbraRect = rect; - umbraRect.Expand(spec.umbra.spreadRadius); - DrawSoftShadow(drawList, umbraRect, umbraColor, spec.umbra.blurRadius, - ImVec2(spec.umbra.offsetX, spec.umbra.offsetY), cornerRadius); - } -} - -inline void DrawShadow(ImDrawList* drawList, const ImVec2& pos, const ImVec2& size, - int elevationDp, float cornerRadius) { - ImRect rect(pos, ImVec2(pos.x + size.x, pos.y + size.y)); - DrawShadow(drawList, rect, elevationDp, cornerRadius); -} - -inline ElevationAnimator::ElevationAnimator(int initialElevation) - : m_current(static_cast(initialElevation)) - , m_target(static_cast(initialElevation)) -{ -} - -inline void ElevationAnimator::setTarget(int targetElevation) { - m_target = static_cast(targetElevation); -} - -inline void ElevationAnimator::update(float deltaTime) { - if (m_current == m_target) - return; - - float diff = m_target - m_current; - float change = m_animationSpeed * deltaTime; - - if (std::abs(diff) <= change) { - m_current = m_target; - } else { - m_current += (diff > 0 ? 1 : -1) * change; - } -} - -} // namespace material -} // namespace ui -} // namespace dragonx diff --git a/src/ui/material/material.h b/src/ui/material/material.h deleted file mode 100644 index 32bc329..0000000 --- a/src/ui/material/material.h +++ /dev/null @@ -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 diff --git a/src/ui/material/motion.h b/src/ui/material/motion.h deleted file mode 100644 index ab35e09..0000000 --- a/src/ui/material/motion.h +++ /dev/null @@ -1,452 +0,0 @@ -// DragonX Wallet - ImGui Edition -// Copyright 2024-2026 The Hush Developers -// Released under the GPLv3 - -#pragma once - -#include "imgui.h" -#include -#include - -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 -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::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::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 diff --git a/src/ui/material/ripple.h b/src/ui/material/ripple.h deleted file mode 100644 index b6a2542..0000000 --- a/src/ui/material/ripple.h +++ /dev/null @@ -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 -#include - -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 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 diff --git a/src/ui/material/transitions.h b/src/ui/material/transitions.h deleted file mode 100644 index 10e4aca..0000000 --- a/src/ui/material/transitions.h +++ /dev/null @@ -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 -#include - -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 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 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 diff --git a/src/ui/screens/confirmation_dialog.h b/src/ui/screens/confirmation_dialog.h deleted file mode 100644 index a91f190..0000000 --- a/src/ui/screens/confirmation_dialog.h +++ /dev/null @@ -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 -#include - -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 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 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 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 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 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 callback) { - ConfirmationDialog::instance().show(title, message, ConfirmationType::Danger, callback); -} - -/** - * @brief Show a transaction confirmation dialog - */ -inline void ShowTransactionConfirmation(const TransactionConfirmation& tx, - std::function callback) { - ConfirmationDialog::instance().showTransaction(tx, callback); -} - -} // namespace screens -} // namespace ui -} // namespace dragonx diff --git a/src/ui/screens/home_screen.h b/src/ui/screens/home_screen.h deleted file mode 100644 index 2712152..0000000 --- a/src/ui/screens/home_screen.h +++ /dev/null @@ -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 -#include -#include - -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& txns) { m_recentTxns = txns; } - void setOnSendClick(std::function callback) { m_onSendClick = callback; } - void setOnReceiveClick(std::function callback) { m_onReceiveClick = callback; } - void setOnTransactionClick(std::function callback) { m_onTxClick = callback; } - - void render(); - -private: - void renderBalanceCard(); - void renderQuickActions(); - void renderShieldedCard(); - void renderTransparentCard(); - void renderRecentTransactions(); - - BalanceInfo m_balance; - std::vector m_recentTxns; - std::function m_onSendClick; - std::function m_onReceiveClick; - std::function 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 diff --git a/src/ui/screens/main_layout.h b/src/ui/screens/main_layout.h deleted file mode 100644 index c8045b1..0000000 --- a/src/ui/screens/main_layout.h +++ /dev/null @@ -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 -#include - -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 callback) { - m_onNavigation = callback; - } - - void setOnSearch(std::function callback) { - m_onSearch = callback; - } - - void setOnSettingsClick(std::function 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 m_onNavigation; - std::function m_onSearch; - std::function m_onSettingsClick; - - // Navigation drawer state - bool m_drawerOpen = false; - AnimatedValue 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 diff --git a/src/ui/screens/mining_screen.h b/src/ui/screens/mining_screen.h deleted file mode 100644 index a9b8b65..0000000 --- a/src/ui/screens/mining_screen.h +++ /dev/null @@ -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 -#include -#include - -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& blocks) { m_minedBlocks = blocks; } - - void setOnStartMining(std::function callback) { - m_onStartMining = callback; - } - void setOnStopMining(std::function callback) { - m_onStopMining = callback; - } - void setOnSetAddress(std::function callback) { - m_onSetAddress = callback; - } - - void render(); - -private: - void renderMiningControls(); - void renderStatsCards(); - void renderMinedBlocksList(); - - MiningStats m_stats; - std::vector m_minedBlocks; - - std::function m_onStartMining; - std::function m_onStopMining; - std::function 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 diff --git a/src/ui/screens/peers_screen.h b/src/ui/screens/peers_screen.h deleted file mode 100644 index 706e7aa..0000000 --- a/src/ui/screens/peers_screen.h +++ /dev/null @@ -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 -#include -#include - -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& peers) { m_peers = peers; } - void setNetworkStats(const NetworkStats& stats) { m_networkStats = stats; } - - void setOnDisconnectPeer(std::function callback) { - m_onDisconnectPeer = callback; - } - void setOnAddNode(std::function callback) { - m_onAddNode = callback; - } - - void render(); - -private: - void renderNetworkStats(); - void renderPeersList(); - void renderPeerDetails(); - void renderAddNodeDialog(); - - std::vector m_peers; - NetworkStats m_networkStats; - - std::function m_onDisconnectPeer; - std::function 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 diff --git a/src/ui/screens/receive_screen.h b/src/ui/screens/receive_screen.h deleted file mode 100644 index 004bd42..0000000 --- a/src/ui/screens/receive_screen.h +++ /dev/null @@ -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 -#include -#include - -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& addresses) { - m_addresses = addresses; - } - - void setOnCopyAddress(std::function callback) { - m_onCopyAddress = callback; - } - - void setOnNewAddress(std::function callback) { - m_onNewAddress = callback; - } - - void setOnShowQR(std::function callback) { - m_onShowQR = callback; - } - - void setOnEditLabel(std::function callback) { - m_onEditLabel = callback; - } - - void render(); - -private: - void renderAddressCard(const WalletAddress& addr, int index); - void renderNewAddressButton(); - void renderQRCodePopup(); - - std::vector m_addresses; - std::function m_onCopyAddress; - std::function m_onNewAddress; - std::function m_onShowQR; - std::function 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(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 diff --git a/src/ui/screens/screens.h b/src/ui/screens/screens.h deleted file mode 100644 index 5c42cc2..0000000 --- a/src/ui/screens/screens.h +++ /dev/null @@ -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 diff --git a/src/ui/screens/send_screen.h b/src/ui/screens/send_screen.h deleted file mode 100644 index 4dd1c03..0000000 --- a/src/ui/screens/send_screen.h +++ /dev/null @@ -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 -#include -#include -#include - -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& entries) { - m_addressBook = entries; - } - - void setOnSend(std::function callback) { - m_onSend = callback; - } - - void setOnCancel(std::function callback) { - m_onCancel = callback; - } - - void setOnValidateAddress(std::function 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 m_addressBook; - - std::function m_onSend; - std::function m_onCancel; - std::function 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 diff --git a/src/ui/screens/settings_screen.h b/src/ui/screens/settings_screen.h deleted file mode 100644 index 97448a7..0000000 --- a/src/ui/screens/settings_screen.h +++ /dev/null @@ -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 -#include -#include -#include - -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 callback) { - m_onSettingsChanged = callback; - } - - void setOnBackupWallet(std::function callback) { - m_onBackupWallet = callback; - } - - void setOnExportKeys(std::function callback) { - m_onExportKeys = callback; - } - - void setOnRescan(std::function 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 m_onSettingsChanged; - std::function m_onBackupWallet; - std::function m_onExportKeys; - std::function 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 diff --git a/src/ui/screens/transactions_screen.h b/src/ui/screens/transactions_screen.h deleted file mode 100644 index 97ea435..0000000 --- a/src/ui/screens/transactions_screen.h +++ /dev/null @@ -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 -#include -#include - -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& txns) { - m_transactions = txns; - } - - void setOnTransactionClick(std::function callback) { - m_onTxClick = callback; - } - - void setOnCopyTxid(std::function callback) { - m_onCopyTxid = callback; - } - - void setOnExport(std::function callback) { - m_onExport = callback; - } - - void render(); - -private: - void renderFilters(); - void renderTransactionList(); - void renderTransactionItem(const Transaction& tx, int index); - void renderTransactionDetails(); - void renderEmptyState(); - - std::vector m_transactions; - std::function m_onTxClick; - std::function m_onCopyTxid; - std::function 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 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(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