Files
ObsidianDragon/src/ui/material/components/slider.h
DanS 3aee55b49c ObsidianDragon - DragonX ImGui Wallet
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)
2026-02-27 00:26:01 -06:00

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