Files
ObsidianDragon/src/ui/material/components/tabs.h
DanS 3aee55b49c ObsidianDragon - DragonX ImGui Wallet
Full-node GUI wallet for DragonX cryptocurrency.
Built with Dear ImGui, SDL3, and OpenGL3/DX11.

Features:
- Send/receive shielded and transparent transactions
- Autoshield with merged transaction display
- Built-in CPU mining (xmrig)
- Peer management and network monitoring
- Wallet encryption with PIN lock
- QR code generation for receive addresses
- Transaction history with pagination
- Console for direct RPC commands
- Cross-platform (Linux, Windows)
2026-02-27 00:26:01 -06:00

320 lines
9.6 KiB
C++

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