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:
2026-02-26 02:31:52 -06:00
commit 3aee55b49c
306 changed files with 177789 additions and 0 deletions

543
src/ui/layout.h Normal file
View File

@@ -0,0 +1,543 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include "imgui.h"
#include "material/type.h"
#include "schema/ui_schema.h"
#include <algorithm>
#include <cmath>
#include <cstdio>
namespace dragonx {
namespace ui {
// Import material helpers into ui namespace for convenience
using material::Type;
using material::OverlineLabel;
using material::OnSurface;
using material::OnSurfaceMedium;
using material::OnSurfaceDisabled;
using material::Primary;
using material::Secondary;
using material::Error;
/**
* @brief Centralized layout configuration for consistent UI across all tabs
*
* Values are now driven by UISchema (JSON-configurable with hot-reload).
* The k* names are preserved as inline accessors for backward compatibility.
*/
namespace Layout {
// ============================================================================
// DPI Scaling (must be first — other accessors multiply by dpiScale())
// ============================================================================
/**
* @brief Get the current display DPI scale factor.
*
* Returns the DPI scale set during typography initialization (e.g. 2.0 for
* 200 % Windows scaling). All pixel constants from TOML are in *logical*
* pixels and must be multiplied by this factor before being used as ImGui
* coordinates (which are physical pixels on Windows Per-Monitor DPI v2).
*/
inline float dpiScale() {
return dragonx::ui::material::Typography::instance().getDpiScale();
}
/**
* @brief Scale a logical pixel value by the current DPI factor.
*
* Convenience wrapper: Scale(16.0f) returns 16 * dpiScale.
*/
inline float Scale(float px) {
return px * dpiScale();
}
// ============================================================================
// Font Sizes (in pixels, before DPI scaling)
// ============================================================================
// These read from UISchema which loads layout from ui.toml.
// Editing res/themes/ui.toml will hot-reload these at runtime.
inline float kFontH1() { return schema::UI().drawElement("fonts", "h1").sizeOr(24.0f); }
inline float kFontH2() { return schema::UI().drawElement("fonts", "h2").sizeOr(20.0f); }
inline float kFontH3() { return schema::UI().drawElement("fonts", "h3").sizeOr(18.0f); }
inline float kFontH4() { return schema::UI().drawElement("fonts", "h4").sizeOr(16.0f); }
inline float kFontH5() { return schema::UI().drawElement("fonts", "h5").sizeOr(14.0f); }
inline float kFontH6() { return schema::UI().drawElement("fonts", "h6").sizeOr(14.0f); }
inline float kFontSubtitle1() { return schema::UI().drawElement("fonts", "subtitle1").sizeOr(16.0f); }
inline float kFontSubtitle2() { return schema::UI().drawElement("fonts", "subtitle2").sizeOr(14.0f); }
inline float kFontBody1() { return schema::UI().drawElement("fonts", "body1").sizeOr(14.0f); }
inline float kFontBody2() { return schema::UI().drawElement("fonts", "body2").sizeOr(12.0f); }
inline float kFontButton() { return schema::UI().drawElement("fonts", "button").sizeOr(13.0f); }
inline float kFontButtonSm() { return schema::UI().drawElement("fonts", "button-sm").sizeOr(10.0f); }
inline float kFontButtonLg() { return schema::UI().drawElement("fonts", "button-lg").sizeOr(14.0f); }
inline float kFontCaption() { return schema::UI().drawElement("fonts", "caption").sizeOr(11.0f); }
inline float kFontOverline() { return schema::UI().drawElement("fonts", "overline").sizeOr(11.0f); }
// Global font scale
inline float kFontScale() { return schema::UI().drawElement("fonts", "scale").sizeOr(1.0f); }
// ============================================================================
// Panel Sizing (responsive)
// ============================================================================
inline float kSummaryPanelMinWidth() { return schema::UI().drawElement("panels", "summary").getFloat("min-width", 280.0f) * dpiScale(); }
inline float kSummaryPanelMaxWidth() { return schema::UI().drawElement("panels", "summary").getFloat("max-width", 400.0f) * dpiScale(); }
inline float kSummaryPanelWidthRatio() { return schema::UI().drawElement("panels", "summary").getFloat("width-ratio", 0.32f); }
inline float kSummaryPanelMinHeight() { return schema::UI().drawElement("panels", "summary").getFloat("min-height", 200.0f) * dpiScale(); }
inline float kSummaryPanelMaxHeight() { return schema::UI().drawElement("panels", "summary").getFloat("max-height", 350.0f) * dpiScale(); }
inline float kSummaryPanelHeightRatio() { return schema::UI().drawElement("panels", "summary").getFloat("height-ratio", 0.8f); }
inline float kSidePanelMinWidth() { return schema::UI().drawElement("panels", "side-panel").getFloat("min-width", 280.0f) * dpiScale(); }
inline float kSidePanelMaxWidth() { return schema::UI().drawElement("panels", "side-panel").getFloat("max-width", 450.0f) * dpiScale(); }
inline float kSidePanelWidthRatio() { return schema::UI().drawElement("panels", "side-panel").getFloat("width-ratio", 0.4f); }
inline float kTableMinHeight() { return schema::UI().drawElement("panels", "table").getFloat("min-height", 150.0f) * dpiScale(); }
inline float kTableHeightRatio() { return schema::UI().drawElement("panels", "table").getFloat("height-ratio", 0.45f); }
// ============================================================================
// Spacing
// ============================================================================
inline float kSectionSpacing() { return schema::UI().drawElement("spacing", "section").sizeOr(16.0f) * dpiScale(); }
inline float kItemSpacing() { return schema::UI().drawElement("spacing", "item").sizeOr(8.0f) * dpiScale(); }
inline float kLabelValueGap() { return schema::UI().drawElement("spacing", "label-value").sizeOr(4.0f) * dpiScale(); }
inline float kSeparatorGap() { return schema::UI().drawElement("spacing", "separator").sizeOr(20.0f) * dpiScale(); }
// ============================================================================
// Layout Tier (responsive breakpoints)
// ============================================================================
/**
* @brief Three-tier layout system based on content area dimensions.
*
* Compact — narrow/short window: single-column, collapsed elements
* Normal — default layout
* Expanded — large window: extra spacing, optional extra columns
*/
enum class LayoutTier { Compact, Normal, Expanded };
/**
* @brief Determine the current layout tier from content area dimensions.
* Call after ImGui::BeginChild for the content area, or pass explicit avail.
*/
inline LayoutTier currentTier() {
const auto& S = schema::UI();
float dp = dpiScale();
float cw = S.drawElement("responsive", "compact-width").sizeOr(500.0f) * dp;
float ch = S.drawElement("responsive", "compact-height").sizeOr(450.0f) * dp;
float ew = S.drawElement("responsive", "expanded-width").sizeOr(900.0f) * dp;
float eh = S.drawElement("responsive", "expanded-height").sizeOr(750.0f) * dp;
ImVec2 avail = ImGui::GetContentRegionAvail();
if (avail.x < cw || avail.y < ch) return LayoutTier::Compact;
if (avail.x > ew && avail.y > eh) return LayoutTier::Expanded;
return LayoutTier::Normal;
}
inline LayoutTier currentTier(float availW, float availH) {
const auto& S = schema::UI();
float dp = dpiScale();
float cw = S.drawElement("responsive", "compact-width").sizeOr(500.0f) * dp;
float ch = S.drawElement("responsive", "compact-height").sizeOr(450.0f) * dp;
float ew = S.drawElement("responsive", "expanded-width").sizeOr(900.0f) * dp;
float eh = S.drawElement("responsive", "expanded-height").sizeOr(750.0f) * dp;
if (availW < cw || availH < ch) return LayoutTier::Compact;
if (availW > ew && availH > eh) return LayoutTier::Expanded;
return LayoutTier::Normal;
}
// ============================================================================
// Responsive Scale Factors
// ============================================================================
/**
* @brief Horizontal scale factor relative to reference width (default 1200px).
*
* The scale is decomposed into a *logical* portion (responsive to window
* size, clamped to the configured range) multiplied by the display DPI
* factor. This ensures a DPI transition produces the same logical scale
* while emitting physical-pixel results.
*/
inline float hScale(float availWidth) {
const auto& S = schema::UI();
float dp = dpiScale();
float rw = S.drawElement("responsive", "ref-width").sizeOr(1200.0f) * dp;
float minH = S.drawElement("responsive", "min-h-scale").sizeOr(0.5f);
float maxH = S.drawElement("responsive", "max-h-scale").sizeOr(1.5f);
// Clamp the logical (DPI-neutral) portion, then apply DPI.
float logical = std::clamp(availWidth / rw, minH, maxH);
return logical * dp;
}
inline float hScale() {
return hScale(ImGui::GetContentRegionAvail().x);
}
/**
* @brief Vertical scale factor relative to reference height (default 700px).
*
* Same decomposition as hScale — logical clamp × DPI.
*/
inline float vScale(float availHeight) {
const auto& S = schema::UI();
float dp = dpiScale();
float rh = S.drawElement("responsive", "ref-height").sizeOr(700.0f) * dp;
float minV = S.drawElement("responsive", "min-v-scale").sizeOr(0.5f);
float maxV = S.drawElement("responsive", "max-v-scale").sizeOr(1.4f);
float logical = std::clamp(availHeight / rh, minV, maxV);
return logical * dp;
}
inline float vScale() {
return vScale(ImGui::GetContentRegionAvail().y);
}
/**
* @brief Density scale factor for spacing tokens.
*
* Logical portion is clamped, then multiplied by DPI so pixel spacing
* values scale proportionally with fonts and style.
*/
inline float densityScale(float availHeight) {
const auto& S = schema::UI();
float dp = dpiScale();
float rh = S.drawElement("responsive", "ref-height").sizeOr(700.0f) * dp;
float minDen = S.drawElement("responsive", "min-density").sizeOr(0.6f);
float maxDen = S.drawElement("responsive", "max-density").sizeOr(1.2f);
float logical = std::clamp(availHeight / rh, minDen, maxDen);
return logical * dp;
}
inline float densityScale() {
return densityScale(ImGui::GetContentRegionAvail().y);
}
// ============================================================================
// Spacing Tokens (density-scaled)
// ============================================================================
/** @brief Get spacing token scaled by current density. */
inline float spacingXs() { return schema::UI().drawElement("spacing-tokens", "xs").sizeOr(2.0f) * densityScale(); }
inline float spacingSm() { return schema::UI().drawElement("spacing-tokens", "sm").sizeOr(4.0f) * densityScale(); }
inline float spacingMd() { return schema::UI().drawElement("spacing-tokens", "md").sizeOr(8.0f) * densityScale(); }
inline float spacingLg() { return schema::UI().drawElement("spacing-tokens", "lg").sizeOr(12.0f) * densityScale(); }
inline float spacingXl() { return schema::UI().drawElement("spacing-tokens", "xl").sizeOr(16.0f) * densityScale(); }
inline float spacingXxl() { return schema::UI().drawElement("spacing-tokens", "xxl").sizeOr(24.0f) * densityScale(); }
/** @brief Get raw (unscaled) spacing token. */
inline float spacingXsRaw() { return schema::UI().drawElement("spacing-tokens", "xs").sizeOr(2.0f); }
inline float spacingSmRaw() { return schema::UI().drawElement("spacing-tokens", "sm").sizeOr(4.0f); }
inline float spacingMdRaw() { return schema::UI().drawElement("spacing-tokens", "md").sizeOr(8.0f); }
inline float spacingLgRaw() { return schema::UI().drawElement("spacing-tokens", "lg").sizeOr(12.0f); }
inline float spacingXlRaw() { return schema::UI().drawElement("spacing-tokens", "xl").sizeOr(16.0f); }
inline float spacingXxlRaw() { return schema::UI().drawElement("spacing-tokens", "xxl").sizeOr(24.0f); }
// ============================================================================
// Responsive Globals Helpers
// ============================================================================
/** @brief Default glass panel rounding (8.0 default). */
inline float glassRounding() { return schema::UI().drawElement("responsive", "glass-rounding").sizeOr(8.0f) * dpiScale(); }
/** @brief Default card inner padding (12.0 default). */
inline float cardInnerPadding() { return schema::UI().drawElement("responsive", "card-inner-padding").sizeOr(12.0f) * dpiScale(); }
/** @brief Default card gap (8.0 default). */
inline float cardGap() { return schema::UI().drawElement("responsive", "card-gap").sizeOr(8.0f) * dpiScale(); }
/**
* @brief Compute a responsive card height from a base value.
* @param base Design-time card height (e.g. 110.0f) in logical pixels
* @param vs Vertical scale factor (from vScale(), already DPI-scaled)
* @return Scaled height with a DPI-aware floor of base * 0.4 * dpiScale
*/
inline float cardHeight(float base, float vs) {
return std::max(base * 0.4f * dpiScale(), base * vs);
}
/**
* @brief Compute a column offset as a ratio of available width.
* Replaces hardcoded "cx + 100" patterns.
* @param ratio Fraction of available width (e.g. 0.12)
* @param availW Available width
*/
inline float columnOffset(float ratio, float availW) {
return availW * ratio;
}
// ============================================================================
// Buttons
// ============================================================================
inline float kButtonMinWidth() { return schema::UI().drawElement("button", "min-width").sizeOr(180.0f) * dpiScale(); }
inline float kButtonStandardWidth() { return schema::UI().drawElement("button", "width").sizeOr(140.0f) * dpiScale(); }
inline float kButtonLargeWidth() { return schema::UI().drawElement("button", "width-lg").sizeOr(160.0f) * dpiScale(); }
inline float kButtonHeight() { float h = schema::UI().drawElement("button", "height").sizeOr(0.0f); return h > 0.0f ? h * dpiScale() : 0.0f; }
// ============================================================================
// Input Fields
// ============================================================================
inline float kInputMinWidth() { return schema::UI().drawElement("input", "min-width").sizeOr(150.0f) * dpiScale(); }
inline float kInputMediumWidth() { return schema::UI().drawElement("input", "width-md").sizeOr(200.0f) * dpiScale(); }
inline float kInputLargeWidth() { return schema::UI().drawElement("input", "width-lg").sizeOr(300.0f) * dpiScale(); }
inline float kSearchFieldWidthRatio() { return schema::UI().drawElement("input", "search-width-ratio").sizeOr(0.30f); }
// ============================================================================
// Status Bar
// ============================================================================
inline float kStatusBarHeight() { float dp = dpiScale(); auto h = schema::UI().window("components.status-bar", "window").height; return (h > 0 ? h : 60.0f) * dp; }
inline float kStatusBarPadding() { float dp = dpiScale(); auto w = schema::UI().window("components.status-bar", "window"); return (w.padding[0] > 0 ? w.padding[0] : 8.0f) * dp; }
// ============================================================================
// Helper Functions
// ============================================================================
/**
* @brief Calculate responsive width with min/max bounds
* @param availWidth Available width from GetContentRegionAvail().x
* @param ratio Ratio of available width (0.0-1.0)
* @param minWidth Minimum width in pixels
* @param maxWidth Maximum width in pixels
*/
inline float responsiveWidth(float availWidth, float ratio, float minWidth, float maxWidth) {
return std::max(minWidth, std::min(maxWidth, availWidth * ratio));
}
/**
* @brief Calculate responsive height with min/max bounds
*/
inline float responsiveHeight(float availHeight, float ratio, float minHeight, float maxHeight) {
return std::max(minHeight, std::min(maxHeight, availHeight * ratio));
}
/**
* @brief Get summary panel dimensions
*/
inline ImVec2 getSummaryPanelSize() {
ImVec2 avail = ImGui::GetContentRegionAvail();
return ImVec2(
responsiveWidth(avail.x, kSummaryPanelWidthRatio(), kSummaryPanelMinWidth(), kSummaryPanelMaxWidth()),
responsiveHeight(avail.y, kSummaryPanelHeightRatio(), kSummaryPanelMinHeight(), kSummaryPanelMaxHeight())
);
}
/**
* @brief Get side panel width (height fills available)
*/
inline float getSidePanelWidth() {
float avail = ImGui::GetContentRegionAvail().x;
return responsiveWidth(avail, kSidePanelWidthRatio(), kSidePanelMinWidth(), kSidePanelMaxWidth());
}
/**
* @brief Get table height for split view (two tables)
*/
inline float getTableHeight() {
float avail = ImGui::GetContentRegionAvail().y;
return responsiveHeight(avail, kTableHeightRatio(), kTableMinHeight(), avail * 0.6f);
}
/**
* @brief Get remaining height (for second table/section)
*/
inline float getRemainingHeight(float reserveSpace) {
float avail = ImGui::GetContentRegionAvail().y;
return std::max(schema::UI().drawElement("panels", "table").getFloat("min-remaining", 100.0f) * dpiScale(), avail - reserveSpace);
}
inline float getRemainingHeight() {
return getRemainingHeight(schema::UI().drawElement("panels", "table").getFloat("default-reserve", 30.0f) * dpiScale());
}
/**
* @brief Get search/filter input width
*/
inline float getSearchWidth() {
float avail = ImGui::GetContentRegionAvail().x;
return std::min(kInputLargeWidth(), avail * kSearchFieldWidthRatio());
}
/**
* @brief Calculate position for right-aligned buttons
* @param buttonCount Number of buttons
* @param buttonWidth Width of each button
* @param spacing Spacing between buttons
*/
inline float getRightButtonsPos(int buttonCount, float buttonWidth, float spacing) {
float dp = dpiScale();
float margin = schema::UI().drawElement("button", "right-align-margin").sizeOr(16.0f) * dp;
float minPos = schema::UI().drawElement("button", "right-align-min-pos").sizeOr(200.0f) * dp;
float totalWidth = buttonCount * buttonWidth + (buttonCount - 1) * spacing + margin;
return std::max(minPos, ImGui::GetWindowWidth() - totalWidth);
}
inline float getRightButtonsPos(int buttonCount, float buttonWidth) {
return getRightButtonsPos(buttonCount, buttonWidth, schema::UI().drawElement("button", "right-align-gap").sizeOr(8.0f) * dpiScale());
}
// ============================================================================
// Shared Card Height (send/receive tab parity)
// ============================================================================
/**
* @brief Compute the target glass card height for send/receive tabs.
*
* Both tabs call this with the same formW + vs so their cards match.
* The height models the receive tab's QR-code-driven layout:
* topPad + totalQrSize + innerGap + actionBtnH + bottomPad
*
* @param formW Total card / form width in pixels
* @param vs Vertical scale factor (from vScale())
*/
inline float mainCardTargetH(float formW, float vs) {
float dp = dpiScale();
float pad = spacingLg();
float innerW = formW - pad * 2;
float qrColW = innerW * 0.35f;
float qrPad = spacingMd();
float maxQrSz = std::min(qrColW - qrPad * 2, 280.0f * dp);
float qrSize = std::max(100.0f * dp, maxQrSz);
float totalQr = qrSize + qrPad * 2;
float innerGap = spacingLg();
float btnH = std::max(26.0f * dp, 30.0f * vs);
return pad + totalQr + innerGap + btnH + pad;
}
// ============================================================================
// Section Budget Allocator
// ============================================================================
/**
* @brief Proportional height-budget allocator for tab content.
*
* Each tab creates a SectionBudget from the available content height and
* allocates fractions to its sections. Sections receive a proportional
* share of the total height clamped to [minPx, maxPx], guaranteeing
* that all content fits without scrolling at any window size.
*
* Usage:
* SectionBudget b(ImGui::GetContentRegionAvail().y);
* float heroH = b.allocate(0.13f, 55.0f);
* float listH = b.rest(60.0f); // whatever is left
*/
struct SectionBudget {
float total; ///< Total available height passed at construction
float remaining; ///< Decrements as sections are allocated
explicit SectionBudget(float avail)
: total(avail), remaining(avail) {}
/**
* @brief Allocate a fraction of the total budget.
* @param fraction Fraction of *total* (e.g. 0.12 = 12%)
* @param minPx Minimum pixel height in logical pixels (auto DPI-scaled)
* @param maxPx Maximum pixel height in logical pixels (auto DPI-scaled, default unlimited)
* @return The allocated height in physical pixels.
*/
float allocate(float fraction, float minPx, float maxPx = FLT_MAX) {
float dp = dpiScale();
float scaledMin = minPx * dp;
float scaledMax = (maxPx >= FLT_MAX * 0.5f) ? FLT_MAX : maxPx * dp;
float h = std::clamp(total * fraction, scaledMin, scaledMax);
remaining -= h;
if (remaining < 0.0f) remaining = 0.0f;
return h;
}
/**
* @brief Allocate whatever height remains (for the final section).
* @param minPx Minimum logical pixels guaranteed even if budget is exhausted.
* @return Remaining height (at least minPx * dpiScale).
*/
float rest(float minPx = 0.0f) {
return std::max(minPx * dpiScale(), remaining);
}
};
} // namespace Layout
// ============================================================================
// Convenience Macros/Functions for Common Patterns
// ============================================================================
/**
* @brief Begin a summary panel with responsive sizing
*/
inline bool BeginSummaryPanel(const char* id) {
ImVec2 size = Layout::getSummaryPanelSize();
return ImGui::BeginChild(id, size, true);
}
/**
* @brief Begin a side panel with responsive width
*/
inline bool BeginSidePanel(const char* id) {
float width = Layout::getSidePanelWidth();
return ImGui::BeginChild(id, ImVec2(width, 0), true);
}
/**
* @brief Begin a content panel that fills remaining space
*/
inline bool BeginContentPanel(const char* id) {
return ImGui::BeginChild(id, ImVec2(0, 0), true);
}
/**
* @brief Add standard section header with separator
*/
inline void SectionHeader(const char* label) {
OverlineLabel(label);
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
}
/**
* @brief Add spacing between sections
*/
inline void SectionSpacing() {
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
}
/**
* @brief Render a label-value pair
*/
inline void LabelValue(const char* label, const char* value) {
Type().textColored(material::TypeStyle::Caption, OnSurfaceMedium(), label);
ImGui::Text("%s", value);
}
/**
* @brief Render a label-value pair with colored value
*/
inline void LabelValueColored(const char* label, const char* value, ImU32 color) {
Type().textColored(material::TypeStyle::Caption, OnSurfaceMedium(), label);
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(color), "%s", value);
}
/**
* @brief Render a balance display (amount + ticker)
*/
inline void BalanceDisplay(double amount, const char* ticker, ImU32 color = 0) {
char buf[64];
snprintf(buf, sizeof(buf), "%.8f", amount);
if (color != 0) {
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(color), "%s", buf);
} else {
ImGui::Text("%s", buf);
}
ImGui::SameLine();
Type().textColored(material::TypeStyle::Body2, OnSurfaceMedium(), ticker);
}
} // namespace ui
} // namespace dragonx