// 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