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