Full-node GUI wallet for DragonX cryptocurrency. Built with Dear ImGui, SDL3, and OpenGL3/DX11. Features: - Send/receive shielded and transparent transactions - Autoshield with merged transaction display - Built-in CPU mining (xmrig) - Peer management and network monitoring - Wallet encryption with PIN lock - QR code generation for receive addresses - Transaction history with pagination - Console for direct RPC commands - Cross-platform (Linux, Windows)
415 lines
14 KiB
C++
415 lines
14 KiB
C++
// 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
|