// 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 List Component // ============================================================================ // Based on https://m2.material.io/components/lists // // Lists present content in a way that makes it easy to identify a specific // item in a collection and act on it. /** * @brief List item configuration */ struct ListItemSpec { const char* leadingIcon = nullptr; // Optional leading icon (text representation) const char* leadingAvatar = nullptr; // Optional avatar text (for initials) ImU32 leadingAvatarColor = 0; // Avatar background color (0 = primary) bool leadingCheckbox = false; // Show checkbox instead of icon bool checkboxChecked = false; // Checkbox state const char* primaryText = nullptr; // Main text (required) const char* secondaryText = nullptr; // Secondary text (optional) const char* trailingIcon = nullptr; // Optional trailing icon const char* trailingText = nullptr; // Optional trailing metadata text bool selected = false; // Selected state (highlight) bool disabled = false; // Disabled state bool dividerBelow = false; // Draw divider below item }; /** * @brief Begin a list container * * @param id Unique identifier * @param withPadding Add top/bottom padding */ void BeginList(const char* id, bool withPadding = true); /** * @brief End a list container */ void EndList(); /** * @brief Render a list item * * @param spec Item configuration * @return true if clicked */ bool ListItem(const ListItemSpec& spec); /** * @brief Simple single-line list item */ bool ListItem(const char* text); /** * @brief Two-line list item with primary and secondary text */ bool ListItem(const char* primary, const char* secondary); /** * @brief List divider (full width or inset) * * @param inset If true, indented to align with text */ void ListDivider(bool inset = false); /** * @brief List subheader text */ void ListSubheader(const char* text); // ============================================================================ // Implementation // ============================================================================ inline void BeginList(const char* id, bool withPadding) { ImGui::PushID(id); if (withPadding) { ImGui::Dummy(ImVec2(0, spacing::dp(1))); // 8dp top padding } } inline void EndList() { ImGui::PopID(); } inline bool ListItem(const ListItemSpec& spec) { ImGuiWindow* window = ImGui::GetCurrentWindow(); if (window->SkipItems) return false; // Calculate item height based on content bool hasSecondary = (spec.secondaryText != nullptr); bool hasLeadingElement = (spec.leadingIcon || spec.leadingAvatar || spec.leadingCheckbox); float itemHeight; if (hasSecondary) { itemHeight = size::ListItemTwoLineHeight; // 72dp for two-line } else if (hasLeadingElement) { itemHeight = size::ListItemHeight; // 56dp with leading element } else { itemHeight = 48.0f; // 48dp simple one-line } // Item dimensions ImVec2 pos = window->DC.CursorPos; float itemWidth = ImGui::GetContentRegionAvail().x; ImRect bb(pos, ImVec2(pos.x + itemWidth, pos.y + itemHeight)); // Interaction ImGuiID itemId = window->GetID(spec.primaryText); ImGui::ItemSize(bb); if (!ImGui::ItemAdd(bb, itemId)) return false; bool hovered, held; bool pressed = ImGui::ButtonBehavior(bb, itemId, &hovered, &held) && !spec.disabled; // Draw background ImDrawList* drawList = window->DrawList; ImU32 bgColor = 0; if (spec.selected) { bgColor = PrimaryContainer(); } else if (held && !spec.disabled) { bgColor = schema::UI().resolveColor("var(--hover-overlay)", IM_COL32(255, 255, 255, 25)); } else if (hovered && !spec.disabled) { bgColor = schema::UI().resolveColor("var(--active-overlay)", IM_COL32(255, 255, 255, 10)); } if (bgColor) { drawList->AddRectFilled(bb.Min, bb.Max, bgColor); } // Layout positions float leftPadding = spacing::dp(2); // 16dp left padding float currentX = bb.Min.x + leftPadding; float centerY = bb.Min.y + itemHeight * 0.5f; // Draw leading element if (spec.leadingAvatar) { // Avatar circle with text float avatarRadius = 20.0f; // 40dp diameter ImVec2 avatarCenter(currentX + avatarRadius, centerY); ImU32 avatarBg = spec.leadingAvatarColor ? spec.leadingAvatarColor : Primary(); drawList->AddCircleFilled(avatarCenter, avatarRadius, avatarBg); // Avatar text (centered) ImVec2 textSize = ImGui::CalcTextSize(spec.leadingAvatar); ImVec2 textPos(avatarCenter.x - textSize.x * 0.5f, avatarCenter.y - textSize.y * 0.5f); drawList->AddText(textPos, OnPrimary(), spec.leadingAvatar); currentX += 40.0f + spacing::dp(2); // 40dp avatar + 16dp gap } else if (spec.leadingIcon) { // Icon ImVec2 iconSize = ImGui::CalcTextSize(spec.leadingIcon); float iconY = centerY - iconSize.y * 0.5f; ImU32 iconColor = spec.disabled ? OnSurfaceDisabled() : OnSurfaceMedium(); drawList->AddText(ImVec2(currentX, iconY), iconColor, spec.leadingIcon); currentX += 24.0f + spacing::dp(2); // 24dp icon + 16dp gap } else if (spec.leadingCheckbox) { // Checkbox (simplified visual) float checkboxSize = 18.0f; ImVec2 checkMin(currentX, centerY - checkboxSize * 0.5f); ImVec2 checkMax(currentX + checkboxSize, centerY + checkboxSize * 0.5f); if (spec.checkboxChecked) { drawList->AddRectFilled(checkMin, checkMax, Primary(), 2.0f); // Checkmark ImFont* iconFont = Typography::instance().iconSmall(); ImVec2 chkSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, ICON_MD_CHECK); drawList->AddText(iconFont, iconFont->LegacySize, ImVec2(checkMin.x + (checkboxSize - chkSz.x) * 0.5f, checkMin.y + (checkboxSize - chkSz.y) * 0.5f), OnPrimary(), ICON_MD_CHECK); } else { drawList->AddRect(checkMin, checkMax, OnSurfaceMedium(), 2.0f, 0, 2.0f); } currentX += checkboxSize + spacing::dp(2); } // Calculate text area float rightPadding = spacing::dp(2); // 16dp right padding float trailingSpace = 0; if (spec.trailingIcon) trailingSpace += 24.0f + spacing::dp(1); if (spec.trailingText) trailingSpace += ImGui::CalcTextSize(spec.trailingText).x + spacing::dp(1); float textMaxX = bb.Max.x - rightPadding - trailingSpace; // Draw text ImU32 primaryColor = spec.disabled ? OnSurfaceDisabled() : OnSurface(); ImU32 secondaryColor = spec.disabled ? OnSurfaceDisabled() : OnSurfaceMedium(); if (hasSecondary) { // Two-line layout float primaryY = bb.Min.y + 16.0f; float secondaryY = primaryY + 20.0f; Typography::instance().pushFont(TypeStyle::Body1); drawList->AddText(ImVec2(currentX, primaryY), primaryColor, spec.primaryText); Typography::instance().popFont(); Typography::instance().pushFont(TypeStyle::Body2); drawList->AddText(ImVec2(currentX, secondaryY), secondaryColor, spec.secondaryText); Typography::instance().popFont(); } else { // Single-line layout Typography::instance().pushFont(TypeStyle::Body1); float textY = centerY - Typography::instance().getFont(TypeStyle::Body1)->FontSize * 0.5f; drawList->AddText(ImVec2(currentX, textY), primaryColor, spec.primaryText); Typography::instance().popFont(); } // Draw trailing elements float trailingX = bb.Max.x - rightPadding; if (spec.trailingText) { ImVec2 textSize = ImGui::CalcTextSize(spec.trailingText); trailingX -= textSize.x; float textY = centerY - textSize.y * 0.5f; drawList->AddText(ImVec2(trailingX, textY), secondaryColor, spec.trailingText); trailingX -= spacing::dp(1); } if (spec.trailingIcon) { ImVec2 iconSize = ImGui::CalcTextSize(spec.trailingIcon); trailingX -= 24.0f; float iconY = centerY - iconSize.y * 0.5f; drawList->AddText(ImVec2(trailingX, iconY), OnSurfaceMedium(), spec.trailingIcon); } // Draw divider if (spec.dividerBelow) { float dividerY = bb.Max.y - 0.5f; drawList->AddLine( ImVec2(bb.Min.x + leftPadding, dividerY), ImVec2(bb.Max.x, dividerY), OnSurfaceDisabled() ); } return pressed; } inline bool ListItem(const char* text) { ListItemSpec spec; spec.primaryText = text; return ListItem(spec); } inline bool ListItem(const char* primary, const char* secondary) { ListItemSpec spec; spec.primaryText = primary; spec.secondaryText = secondary; return ListItem(spec); } inline void ListDivider(bool inset) { ImGuiWindow* window = ImGui::GetCurrentWindow(); if (window->SkipItems) return; float width = ImGui::GetContentRegionAvail().x; float leftOffset = inset ? 72.0f : 0; // Align with text after avatar/icon ImVec2 pos = window->DC.CursorPos; ImDrawList* drawList = window->DrawList; drawList->AddLine( ImVec2(pos.x + leftOffset, pos.y), ImVec2(pos.x + width, pos.y), OnSurfaceDisabled() ); ImGui::Dummy(ImVec2(width, 1.0f)); } inline void ListSubheader(const char* text) { ImGui::Dummy(ImVec2(0, spacing::dp(1))); // 8dp top padding ImGui::SetCursorPosX(ImGui::GetCursorPosX() + spacing::dp(2)); // 16dp left padding Typography::instance().textColored(TypeStyle::Caption, Primary(), text); ImGui::Dummy(ImVec2(0, spacing::dp(1))); // 8dp bottom padding } } // namespace material } // namespace ui } // namespace dragonx