// 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 Tabs Component // ============================================================================ // Based on https://m2.material.io/components/tabs // // Tabs organize content across different screens, data sets, and other // interactions. /** * @brief Tab bar configuration */ struct TabBarSpec { bool scrollable = false; // Enable horizontal scrolling bool fullWidth = true; // Tabs fill available width bool showIndicator = true; // Show selection indicator bool centered = false; // Center tabs (when not full width) }; /** * @brief Individual tab configuration */ struct TabSpec { const char* label = nullptr; const char* icon = nullptr; // Optional icon (text representation) bool disabled = false; int badgeCount = 0; // Badge count (0 = no badge) }; /** * @brief Begin a tab bar * * @param id Unique identifier * @param selectedIndex Pointer to selected tab index * @param spec Tab bar configuration * @return true if tab bar is visible */ bool BeginTabBar(const char* id, int* selectedIndex, const TabBarSpec& spec = TabBarSpec()); /** * @brief End a tab bar */ void EndTabBar(); /** * @brief Add a tab to current tab bar * * @param spec Tab configuration * @return true if this tab is selected */ bool Tab(const TabSpec& spec); /** * @brief Simple tab with just label */ bool Tab(const char* label); /** * @brief Simple tab bar - returns selected index * * @param id Unique identifier * @param labels Array of tab labels * @param count Number of tabs * @param selectedIndex Current selected index (will be updated) * @return true if selection changed */ bool TabBar(const char* id, const char** labels, int count, int* selectedIndex); // ============================================================================ // Implementation // ============================================================================ // Internal state for tab rendering struct TabBarState { int* selectedIndex; int currentTabIndex; TabBarSpec spec; float tabBarWidth; float tabWidth; float indicatorX; float indicatorWidth; ImVec2 barPos; }; static TabBarState g_tabBarState; inline bool BeginTabBar(const char* id, int* selectedIndex, const TabBarSpec& spec) { ImGuiWindow* window = ImGui::GetCurrentWindow(); if (window->SkipItems) return false; ImGui::PushID(id); g_tabBarState.selectedIndex = selectedIndex; g_tabBarState.currentTabIndex = 0; g_tabBarState.spec = spec; g_tabBarState.tabBarWidth = ImGui::GetContentRegionAvail().x; g_tabBarState.tabWidth = 0; // Will be calculated if fullWidth g_tabBarState.barPos = window->DC.CursorPos; g_tabBarState.indicatorX = 0; g_tabBarState.indicatorWidth = 0; // Reserve space for tab bar float barHeight = size::TabBarHeight; ImRect bb(g_tabBarState.barPos, ImVec2(g_tabBarState.barPos.x + g_tabBarState.tabBarWidth, g_tabBarState.barPos.y + barHeight)); ImGui::ItemSize(bb); // Draw tab bar background ImDrawList* drawList = window->DrawList; drawList->AddRectFilled(bb.Min, bb.Max, Surface(Elevation::Dp4)); // Begin horizontal layout for tabs ImGui::SetCursorScreenPos(g_tabBarState.barPos); ImGui::BeginGroup(); return true; } inline void EndTabBar() { ImGui::EndGroup(); // Draw indicator line if (g_tabBarState.spec.showIndicator && g_tabBarState.indicatorWidth > 0) { ImGuiWindow* window = ImGui::GetCurrentWindow(); ImDrawList* drawList = window->DrawList; float indicatorY = g_tabBarState.barPos.y + size::TabBarHeight - 2.0f; drawList->AddRectFilled( ImVec2(g_tabBarState.indicatorX, indicatorY), ImVec2(g_tabBarState.indicatorX + g_tabBarState.indicatorWidth, indicatorY + 2.0f), Primary() ); } // Add bottom divider ImGuiWindow* window = ImGui::GetCurrentWindow(); ImDrawList* drawList = window->DrawList; float dividerY = g_tabBarState.barPos.y + size::TabBarHeight; drawList->AddLine( ImVec2(g_tabBarState.barPos.x, dividerY), ImVec2(g_tabBarState.barPos.x + g_tabBarState.tabBarWidth, dividerY), OnSurfaceDisabled() ); ImGui::PopID(); } inline bool Tab(const TabSpec& spec) { ImGuiWindow* window = ImGui::GetCurrentWindow(); if (window->SkipItems) return false; int tabIndex = g_tabBarState.currentTabIndex++; bool isSelected = (*g_tabBarState.selectedIndex == tabIndex); // Calculate tab dimensions float minTabWidth = spec.icon ? 72.0f : 90.0f; // Material min widths float maxTabWidth = 360.0f; float labelWidth = ImGui::CalcTextSize(spec.label).x; float iconWidth = spec.icon ? 24.0f + spacing::dp(1) : 0; float contentWidth = labelWidth + iconWidth + spacing::dp(4); // 32dp padding float tabWidth; if (g_tabBarState.spec.fullWidth) { // Divide evenly (assuming we don't know total count here - simplified) tabWidth = ImMax(minTabWidth, contentWidth); } else { tabWidth = ImClamp(contentWidth, minTabWidth, maxTabWidth); } float tabHeight = size::TabBarHeight; ImVec2 tabPos = window->DC.CursorPos; ImRect tabBB(tabPos, ImVec2(tabPos.x + tabWidth, tabPos.y + tabHeight)); // Interaction ImGuiID id = window->GetID(spec.label); bool hovered, held; bool pressed = ImGui::ButtonBehavior(tabBB, id, &hovered, &held) && !spec.disabled; if (pressed && !isSelected) { *g_tabBarState.selectedIndex = tabIndex; } // Update indicator position for selected tab if (isSelected) { g_tabBarState.indicatorX = tabPos.x; g_tabBarState.indicatorWidth = tabWidth; } // Draw ImDrawList* drawList = window->DrawList; // Hover/press state overlay if (!spec.disabled) { if (held) { drawList->AddRectFilled(tabBB.Min, tabBB.Max, schema::UI().resolveColor("var(--hover-overlay)", IM_COL32(255, 255, 255, 25))); } else if (hovered) { drawList->AddRectFilled(tabBB.Min, tabBB.Max, schema::UI().resolveColor("var(--active-overlay)", IM_COL32(255, 255, 255, 10))); } } // Content color ImU32 contentColor; if (spec.disabled) { contentColor = OnSurfaceDisabled(); } else if (isSelected) { contentColor = Primary(); } else { contentColor = OnSurfaceMedium(); } // Draw content (icon and/or label) float contentX = tabPos.x + (tabWidth - labelWidth - iconWidth) * 0.5f; float centerY = tabPos.y + tabHeight * 0.5f; if (spec.icon) { ImFont* iconFont = Type().iconMed(); ImVec2 iconSize = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, spec.icon); ImVec2 iconPos(contentX, centerY - iconSize.y * 0.5f); drawList->AddText(iconFont, iconFont->LegacySize, iconPos, contentColor, spec.icon); contentX += iconSize.x + spacing::Xs; } // Label (uppercase) Typography::instance().pushFont(TypeStyle::Button); float labelY = centerY - ImGui::GetFontSize() * 0.5f; // Convert to uppercase char upperLabel[128]; size_t i = 0; for (const char* p = spec.label; *p && i < sizeof(upperLabel) - 1; p++, i++) { upperLabel[i] = (*p >= 'a' && *p <= 'z') ? (*p - 32) : *p; } upperLabel[i] = '\0'; drawList->AddText(ImVec2(contentX, labelY), contentColor, upperLabel); Typography::instance().popFont(); // Badge if (spec.badgeCount > 0) { float badgeX = tabPos.x + tabWidth - 16.0f; float badgeY = tabPos.y + 8.0f; float badgeRadius = 8.0f; drawList->AddCircleFilled(ImVec2(badgeX, badgeY), badgeRadius, Error()); char badgeText[8]; if (spec.badgeCount > 99) { snprintf(badgeText, sizeof(badgeText), "99+"); } else { snprintf(badgeText, sizeof(badgeText), "%d", spec.badgeCount); } ImVec2 badgeTextSize = ImGui::CalcTextSize(badgeText); ImVec2 badgeTextPos(badgeX - badgeTextSize.x * 0.5f, badgeY - badgeTextSize.y * 0.5f); Typography::instance().pushFont(TypeStyle::Caption); drawList->AddText(badgeTextPos, OnError(), badgeText); Typography::instance().popFont(); } // Advance cursor ImGui::SameLine(0, 0); ImGui::SetCursorScreenPos(ImVec2(tabPos.x + tabWidth, tabPos.y)); return isSelected; } inline bool Tab(const char* label) { TabSpec spec; spec.label = label; return Tab(spec); } inline bool TabBar(const char* id, const char** labels, int count, int* selectedIndex) { int oldIndex = *selectedIndex; TabBarSpec spec; spec.fullWidth = true; if (BeginTabBar(id, selectedIndex, spec)) { // Calculate tab width for full-width mode float tabWidth = ImGui::GetContentRegionAvail().x / count; for (int i = 0; i < count; i++) { TabSpec tabSpec; tabSpec.label = labels[i]; Tab(tabSpec); } EndTabBar(); } return (*selectedIndex != oldIndex); } } // namespace material } // namespace ui } // namespace dragonx