// 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 "../../embedded/IconsMaterialDesign.h" #include "imgui.h" #include "imgui_internal.h" namespace dragonx { namespace ui { namespace material { // ============================================================================ // Material Design Chips Component // ============================================================================ // Based on https://m2.material.io/components/chips // // Chips are compact elements that represent an input, attribute, or action. enum class ChipType { Input, // User input (deletable) Choice, // Single selection from set Filter, // Filter/checkbox style Action // Triggers action }; /** * @brief Chip configuration */ struct ChipSpec { ChipType type = ChipType::Action; const char* label = nullptr; const char* icon = nullptr; // Leading icon const char* avatar = nullptr; // Avatar text (overrides icon) ImU32 avatarColor = 0; // Avatar background color bool selected = false; // For choice/filter chips bool disabled = false; bool outlined = false; // Outlined vs filled style }; /** * @brief Render a chip * * @param spec Chip configuration * @return For filter/choice: true if clicked (toggle selection) * For input: true if delete clicked * For action: true if clicked */ bool Chip(const ChipSpec& spec); /** * @brief Simple action chip */ bool Chip(const char* label); /** * @brief Filter chip (toggleable) */ bool FilterChip(const char* label, bool* selected); /** * @brief Choice chip (radio-style) */ bool ChoiceChip(const char* label, bool selected); /** * @brief Input chip with delete */ bool InputChip(const char* label, const char* avatar = nullptr); /** * @brief Begin a chip group for layout */ void BeginChipGroup(); /** * @brief End a chip group */ void EndChipGroup(); // ============================================================================ // Implementation // ============================================================================ inline bool Chip(const ChipSpec& spec) { ImGuiWindow* window = ImGui::GetCurrentWindow(); if (window->SkipItems) return false; ImGui::PushID(spec.label); // Chip dimensions const float chipHeight = 32.0f; const float cornerRadius = chipHeight * 0.5f; const float horizontalPadding = 12.0f; const float iconSize = 18.0f; const float avatarSize = 24.0f; const float deleteIconSize = 18.0f; // Calculate content width float contentWidth = horizontalPadding * 2; bool hasLeading = spec.icon || spec.avatar; bool hasDelete = (spec.type == ChipType::Input); bool hasCheckmark = (spec.type == ChipType::Filter && spec.selected); if (spec.avatar) { contentWidth += avatarSize + 8.0f; } else if (spec.icon || hasCheckmark) { contentWidth += iconSize + 8.0f; } contentWidth += ImGui::CalcTextSize(spec.label).x; if (hasDelete) { contentWidth += deleteIconSize + 8.0f; } // Layout ImVec2 pos = window->DC.CursorPos; ImRect chipBB(pos, ImVec2(pos.x + contentWidth, pos.y + chipHeight)); // Interaction ImGuiID id = window->GetID("##chip"); ImGui::ItemSize(chipBB); if (!ImGui::ItemAdd(chipBB, id)) return false; bool hovered, held; bool clicked = ImGui::ButtonBehavior(chipBB, id, &hovered, &held) && !spec.disabled; // Delete button hit test (for input chips) bool deleteClicked = false; if (hasDelete) { float deleteX = chipBB.Max.x - horizontalPadding - deleteIconSize; ImRect deleteBB( ImVec2(deleteX, pos.y + (chipHeight - deleteIconSize) * 0.5f), ImVec2(deleteX + deleteIconSize, pos.y + (chipHeight + deleteIconSize) * 0.5f) ); ImGuiID deleteId = window->GetID("##delete"); bool deleteHovered, deleteHeld; deleteClicked = ImGui::ButtonBehavior(deleteBB, deleteId, &deleteHovered, &deleteHeld); } // Draw ImDrawList* drawList = window->DrawList; // Background ImU32 bgColor; ImU32 borderColor = 0; if (spec.disabled) { bgColor = schema::UI().resolveColor("var(--surface-hover)", IM_COL32(255, 255, 255, 30)); } else if (spec.selected) { bgColor = WithAlpha(Primary(), 51); // Primary at 20% } else if (spec.outlined) { bgColor = 0; // Transparent borderColor = OnSurfaceMedium(); } else { bgColor = schema::UI().resolveColor("var(--surface-hover)", IM_COL32(255, 255, 255, 30)); } // Hover/press overlay if (!spec.disabled) { if (held) { bgColor = IM_COL32_ADD(bgColor, schema::UI().resolveColor("var(--hover-overlay)", IM_COL32(255, 255, 255, 25))); } else if (hovered) { bgColor = IM_COL32_ADD(bgColor, schema::UI().resolveColor("var(--active-overlay)", IM_COL32(255, 255, 255, 10))); } } // Draw background if (bgColor) { drawList->AddRectFilled(chipBB.Min, chipBB.Max, bgColor, cornerRadius); } // Draw border for outlined if (borderColor) { drawList->AddRect(chipBB.Min, chipBB.Max, borderColor, cornerRadius, 0, 1.0f); } // Content float currentX = pos.x + horizontalPadding; float centerY = pos.y + chipHeight * 0.5f; ImU32 contentColor = spec.disabled ? OnSurfaceDisabled() : OnSurface(); ImU32 iconColor = spec.disabled ? OnSurfaceDisabled() : spec.selected ? Primary() : OnSurfaceMedium(); // Avatar or icon if (spec.avatar) { // Avatar circle ImVec2 avatarCenter(currentX + avatarSize * 0.5f - 4.0f, centerY); ImU32 avatarBg = spec.avatarColor ? spec.avatarColor : Primary(); drawList->AddCircleFilled(avatarCenter, avatarSize * 0.5f, avatarBg); // Avatar text ImVec2 textSize = ImGui::CalcTextSize(spec.avatar); ImVec2 textPos(avatarCenter.x - textSize.x * 0.5f, avatarCenter.y - textSize.y * 0.5f); drawList->AddText(textPos, OnPrimary(), spec.avatar); currentX += avatarSize + 4.0f; } else if (hasCheckmark) { // Checkmark for selected filter chips ImFont* iconFont = Typography::instance().iconSmall(); ImVec2 chkSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, ICON_MD_CHECK); drawList->AddText(iconFont, iconFont->LegacySize, ImVec2(currentX, centerY - chkSz.y * 0.5f), Primary(), ICON_MD_CHECK); currentX += iconSize + 4.0f; } else if (spec.icon) { drawList->AddText(ImVec2(currentX, centerY - iconSize * 0.5f), iconColor, spec.icon); currentX += iconSize + 4.0f; } // Label Typography::instance().pushFont(TypeStyle::Body2); float labelY = centerY - ImGui::GetFontSize() * 0.5f; drawList->AddText(ImVec2(currentX, labelY), contentColor, spec.label); Typography::instance().popFont(); // Delete icon (for input chips) if (hasDelete) { float deleteX = chipBB.Max.x - horizontalPadding - deleteIconSize; ImFont* iconFont = Typography::instance().iconSmall(); ImVec2 delSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, ICON_MD_CLOSE); drawList->AddText(iconFont, iconFont->LegacySize, ImVec2(deleteX, centerY - delSz.y * 0.5f), OnSurfaceMedium(), ICON_MD_CLOSE ); } ImGui::PopID(); // Return value depends on chip type if (spec.type == ChipType::Input) { return deleteClicked; } return clicked; } inline bool Chip(const char* label) { ChipSpec spec; spec.label = label; spec.type = ChipType::Action; return Chip(spec); } inline bool FilterChip(const char* label, bool* selected) { ChipSpec spec; spec.label = label; spec.type = ChipType::Filter; spec.selected = *selected; bool clicked = Chip(spec); if (clicked) { *selected = !*selected; } return clicked; } inline bool ChoiceChip(const char* label, bool selected) { ChipSpec spec; spec.label = label; spec.type = ChipType::Choice; spec.selected = selected; return Chip(spec); } inline bool InputChip(const char* label, const char* avatar) { ChipSpec spec; spec.label = label; spec.type = ChipType::Input; spec.avatar = avatar; return Chip(spec); } inline void BeginChipGroup() { ImGui::BeginGroup(); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(spacing::dp(1), spacing::dp(1))); // 8dp spacing } inline void EndChipGroup() { ImGui::PopStyleVar(); ImGui::EndGroup(); } } // namespace material } // namespace ui } // namespace dragonx