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)
This commit is contained in:
306
src/ui/material/components/lists.h
Normal file
306
src/ui/material/components/lists.h
Normal file
@@ -0,0 +1,306 @@
|
||||
// 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
|
||||
Reference in New Issue
Block a user