// 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 Navigation Drawer Component // ============================================================================ // Based on https://m2.material.io/components/navigation-drawer // // Navigation drawers provide access to destinations in your app. enum class NavDrawerType { Standard, // Permanent, always visible Modal, // Overlay with scrim, can be dismissed Dismissible // Can be shown/hidden, no scrim }; /** * @brief Navigation drawer configuration */ struct NavDrawerSpec { NavDrawerType type = NavDrawerType::Standard; float width = 256.0f; // 256dp standard width bool showDividerBottom = true; // Divider at bottom const char* headerTitle = nullptr; // Optional header title const char* headerSubtitle = nullptr; }; /** * @brief Navigation item configuration */ struct NavItemSpec { const char* icon = nullptr; // Leading icon const char* label = nullptr; // Item label (required) bool selected = false; // Selected state bool disabled = false; int badgeCount = 0; // Badge (0 = no badge) }; /** * @brief Begin a navigation drawer * * @param id Unique identifier * @param open Pointer to open state (for modal/dismissible) * @param spec Drawer configuration * @return true if drawer is visible */ bool BeginNavDrawer(const char* id, bool* open, const NavDrawerSpec& spec = NavDrawerSpec()); /** * @brief Begin standard (always visible) navigation drawer */ bool BeginNavDrawer(const char* id, const NavDrawerSpec& spec = NavDrawerSpec()); /** * @brief End navigation drawer */ void EndNavDrawer(); /** * @brief Render a navigation item * * @param spec Item configuration * @return true if clicked */ bool NavItem(const NavItemSpec& spec); /** * @brief Simple navigation item */ bool NavItem(const char* icon, const char* label, bool selected = false); /** * @brief Navigation divider */ void NavDivider(); /** * @brief Navigation subheader */ void NavSubheader(const char* text); // ============================================================================ // Implementation // ============================================================================ struct NavDrawerState { float width; ImVec2 contentMin; ImVec2 contentMax; bool isModal; }; static NavDrawerState g_navDrawerState; inline bool BeginNavDrawer(const char* id, bool* open, const NavDrawerSpec& spec) { // For modal drawers, check open state if (spec.type == NavDrawerType::Modal && !*open) { return false; } ImGuiWindow* window = ImGui::GetCurrentWindow(); if (window->SkipItems) return false; ImGui::PushID(id); g_navDrawerState.width = spec.width; g_navDrawerState.isModal = (spec.type == NavDrawerType::Modal); ImGuiIO& io = ImGui::GetIO(); ImDrawList* drawList = window->DrawList; // For modal, draw scrim and handle dismiss if (spec.type == NavDrawerType::Modal) { ImDrawList* bgDrawList = ImGui::GetBackgroundDrawList(); bgDrawList->AddRectFilled( ImVec2(0, 0), io.DisplaySize, schema::UI().resolveColor("var(--scrim)", IM_COL32(0, 0, 0, (int)(0.32f * 255))) ); // Click outside to dismiss if (ImGui::IsMouseClicked(0)) { ImVec2 mousePos = io.MousePos; if (mousePos.x > spec.width) { *open = false; } } } // Drawer position and size ImVec2 drawerPos(0, 0); ImVec2 drawerSize(spec.width, io.DisplaySize.y); // If not modal, account for app bar if (spec.type != NavDrawerType::Modal) { drawerPos.y = size::AppBarHeight; drawerSize.y = io.DisplaySize.y - size::AppBarHeight; } ImRect drawerBB(drawerPos, ImVec2(drawerPos.x + drawerSize.x, drawerPos.y + drawerSize.y)); // Draw drawer background ImU32 bgColor = Surface(Elevation::Dp16); drawList->AddRectFilled(drawerBB.Min, drawerBB.Max, bgColor); // Store content region g_navDrawerState.contentMin = ImVec2(drawerBB.Min.x, drawerBB.Min.y); g_navDrawerState.contentMax = drawerBB.Max; // Header float currentY = drawerBB.Min.y; if (spec.headerTitle || spec.headerSubtitle) { // Header area (optional) float headerHeight = 64.0f; ImVec2 headerMin(drawerBB.Min.x, currentY); ImVec2 headerMax(drawerBB.Max.x, currentY + headerHeight); // Header background (slightly elevated) drawList->AddRectFilled(headerMin, headerMax, Surface(Elevation::Dp16)); // Header title if (spec.headerTitle) { ImGui::SetCursorScreenPos(ImVec2(drawerBB.Min.x + spacing::dp(2), currentY + 20.0f)); Typography::instance().text(TypeStyle::H6, spec.headerTitle); } if (spec.headerSubtitle) { ImGui::SetCursorScreenPos(ImVec2(drawerBB.Min.x + spacing::dp(2), currentY + 42.0f)); Typography::instance().textColored(TypeStyle::Body2, OnSurfaceMedium(), spec.headerSubtitle); } currentY += headerHeight; // Divider under header drawList->AddLine( ImVec2(drawerBB.Min.x, currentY), ImVec2(drawerBB.Max.x, currentY), OnSurfaceDisabled() ); } // Set cursor for nav items ImGui::SetCursorScreenPos(ImVec2(drawerBB.Min.x, currentY + spacing::dp(1))); ImGui::BeginGroup(); return true; } inline bool BeginNavDrawer(const char* id, const NavDrawerSpec& spec) { static bool alwaysOpen = true; NavDrawerSpec standardSpec = spec; standardSpec.type = NavDrawerType::Standard; return BeginNavDrawer(id, &alwaysOpen, standardSpec); } inline void EndNavDrawer() { ImGui::EndGroup(); // Divider at bottom if configured ImGuiWindow* window = ImGui::GetCurrentWindow(); ImDrawList* drawList = window->DrawList; // Right edge divider drawList->AddLine( ImVec2(g_navDrawerState.contentMax.x - 1, g_navDrawerState.contentMin.y), ImVec2(g_navDrawerState.contentMax.x - 1, g_navDrawerState.contentMax.y), OnSurfaceDisabled() ); ImGui::PopID(); } inline bool NavItem(const NavItemSpec& spec) { ImGuiWindow* window = ImGui::GetCurrentWindow(); if (window->SkipItems) return false; ImGui::PushID(spec.label); // Item dimensions const float itemHeight = 48.0f; const float iconSize = 24.0f; const float horizontalPadding = spacing::dp(2); // 16dp const float iconLabelGap = spacing::dp(4); // 32dp from left edge to label float itemWidth = g_navDrawerState.width - spacing::dp(1); // 8dp margin right ImVec2 pos = window->DC.CursorPos; pos.x += spacing::dp(1); // 8dp margin left ImRect itemBB(pos, ImVec2(pos.x + itemWidth, pos.y + itemHeight)); // Interaction ImGuiID id = window->GetID("##navitem"); ImGui::ItemSize(ImRect(window->DC.CursorPos, ImVec2(window->DC.CursorPos.x + g_navDrawerState.width, window->DC.CursorPos.y + itemHeight))); if (!ImGui::ItemAdd(itemBB, id)) return false; bool hovered, held; bool pressed = ImGui::ButtonBehavior(itemBB, id, &hovered, &held) && !spec.disabled; // Draw background ImDrawList* drawList = window->DrawList; ImU32 bgColor = 0; if (spec.selected) { bgColor = WithAlpha(Primary(), 30); // Primary at ~12% } else if (held && !spec.disabled) { bgColor = schema::UI().resolveColor("var(--sidebar-hover)", 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(itemBB.Min, itemBB.Max, bgColor, size::ButtonCornerRadius); } // Selected indicator (left edge) if (spec.selected) { drawList->AddRectFilled( ImVec2(itemBB.Min.x, itemBB.Min.y + 8.0f), ImVec2(itemBB.Min.x + 4.0f, itemBB.Max.y - 8.0f), Primary(), 2.0f ); } // Content float contentX = pos.x + horizontalPadding; float centerY = pos.y + itemHeight * 0.5f; ImU32 iconColor = spec.disabled ? OnSurfaceDisabled() : spec.selected ? Primary() : OnSurfaceMedium(); ImU32 labelColor = spec.disabled ? OnSurfaceDisabled() : spec.selected ? Primary() : OnSurface(); // Icon if (spec.icon) { drawList->AddText( ImVec2(contentX, centerY - iconSize * 0.5f), iconColor, spec.icon ); contentX += iconSize + spacing::dp(2); // 16dp gap after icon } // Label Typography::instance().pushFont(TypeStyle::Body1); float labelY = centerY - ImGui::GetFontSize() * 0.5f; drawList->AddText(ImVec2(contentX, labelY), labelColor, spec.label); Typography::instance().popFont(); // Badge if (spec.badgeCount > 0) { char badgeText[8]; if (spec.badgeCount > 999) { snprintf(badgeText, sizeof(badgeText), "999+"); } else { snprintf(badgeText, sizeof(badgeText), "%d", spec.badgeCount); } ImVec2 badgeSize = ImGui::CalcTextSize(badgeText); float badgeWidth = ImMax(24.0f, badgeSize.x + 12.0f); float badgeHeight = 20.0f; float badgeX = itemBB.Max.x - horizontalPadding - badgeWidth; float badgeY = centerY - badgeHeight * 0.5f; drawList->AddRectFilled( ImVec2(badgeX, badgeY), ImVec2(badgeX + badgeWidth, badgeY + badgeHeight), Primary(), badgeHeight * 0.5f ); Typography::instance().pushFont(TypeStyle::Caption); ImVec2 textPos(badgeX + (badgeWidth - badgeSize.x) * 0.5f, badgeY + (badgeHeight - badgeSize.y) * 0.5f); drawList->AddText(textPos, OnPrimary(), badgeText); Typography::instance().popFont(); } ImGui::PopID(); return pressed; } inline bool NavItem(const char* icon, const char* label, bool selected) { NavItemSpec spec; spec.icon = icon; spec.label = label; spec.selected = selected; return NavItem(spec); } inline void NavDivider() { ImGuiWindow* window = ImGui::GetCurrentWindow(); if (window->SkipItems) return; ImVec2 pos = window->DC.CursorPos; ImDrawList* drawList = window->DrawList; ImGui::Dummy(ImVec2(0, spacing::dp(1))); // 8dp spacing above drawList->AddLine( ImVec2(pos.x + spacing::dp(2), pos.y + spacing::dp(1)), ImVec2(pos.x + g_navDrawerState.width - spacing::dp(2), pos.y + spacing::dp(1)), OnSurfaceDisabled() ); ImGui::Dummy(ImVec2(0, spacing::dp(1))); // 8dp spacing below } inline void NavSubheader(const char* text) { ImGui::Dummy(ImVec2(0, spacing::dp(1))); // 8dp above ImVec2 pos = ImGui::GetCursorScreenPos(); ImGui::SetCursorScreenPos(ImVec2(pos.x + spacing::dp(2), pos.y)); Typography::instance().textColored(TypeStyle::Caption, OnSurfaceMedium(), text); ImGui::Dummy(ImVec2(0, spacing::dp(1))); // 8dp below } } // namespace material } // namespace ui } // namespace dragonx