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)
403 lines
14 KiB
C++
403 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 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
|