Files
ObsidianDragon/src/ui/layout.h
T
2026-02-28 00:58:43 -06:00

621 lines
26 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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())
// ============================================================================
// ============================================================================
// User Font Scale (accessibility, 1.0–3.0, persisted in Settings)
// ============================================================================
namespace detail {
inline float& userFontScaleRef() { static float s = 1.0f; return s; }
inline bool& fontReloadNeededRef() { static bool s = false; return s; }
inline float& fontAtlasScaleRef() { static float s = 1.0f; return s; }
}
/**
* @brief Get the user's font scale preference (1.0–1.5).
* Multiplied into font loading so glyphs render at the chosen size.
*/
inline float userFontScale() { return detail::userFontScaleRef(); }
/**
* @brief Get the user font scale at which the font atlas was last built.
* Used to compute FontScaleMain compensation during smooth slider drag.
*/
inline float fontAtlasScale() { return detail::fontAtlasScaleRef(); }
/**
* @brief Record the font atlas scale after a rebuild.
* Called from Typography::load() after the atlas is built.
*/
inline void setFontAtlasScale(float v) { detail::fontAtlasScaleRef() = v; }
/**
* @brief Set the user's font scale and flag a font reload.
* Called on slider release for a crisp atlas rebuild.
*/
inline void setUserFontScale(float v) {
v = std::max(1.0f, std::min(1.5f, v));
if (v != detail::userFontScaleRef()) {
detail::userFontScaleRef() = v;
detail::fontReloadNeededRef() = true;
}
}
/**
* @brief Set the user's font scale WITHOUT flagging a font reload.
* Called every frame while the slider is being dragged for smooth
* visual scaling via FontScaleMain compensation.
*/
inline void setUserFontScaleVisual(float v) {
v = std::max(1.0f, std::min(1.5f, v));
detail::userFontScaleRef() = v;
}
/**
* @brief Consume the pending font-reload flag (returns true once).
*/
inline bool consumeUserFontReload() {
bool v = detail::fontReloadNeededRef();
detail::fontReloadNeededRef() = false;
return v;
}
// ============================================================================
// DPI Scaling (must be after userFontScale — dpiScale includes it)
// ============================================================================
/**
* @brief Get the raw hardware DPI scale factor (no user font scale).
*
* Returns the DPI scale set during typography initialization (e.g. 2.0 for
* 200 % Windows scaling). Use this when you need the pure hardware DPI
* without the user's accessibility font scale applied.
*/
inline float rawDpiScale() {
return dragonx::ui::material::Typography::instance().getDpiScale();
}
/**
* @brief Get the effective DPI scale factor including user font scale.
*
* Returns rawDpiScale() * userFontScale(). At userFontScale() == 1.0
* this is identical to the hardware DPI. All pixel constants from TOML
* are in *logical* pixels and should be multiplied by this factor so that
* containers grow proportionally when the user increases font scale.
*/
inline float dpiScale() {
return rawDpiScale() * userFontScale();
}
/**
* @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 rawDp = rawDpiScale(); // reference uses hardware DPI only
float dp = dpiScale(); // output includes user font scale
float rw = S.drawElement("responsive", "ref-width").sizeOr(1200.0f) * rawDp;
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 effective 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 rawDp = rawDpiScale(); // reference uses hardware DPI only
float dp = dpiScale(); // output includes user font scale
float rh = S.drawElement("responsive", "ref-height").sizeOr(700.0f) * rawDp;
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 rawDp = rawDpiScale(); // reference uses hardware DPI only
float dp = dpiScale(); // output includes user font scale
float rh = S.drawElement("responsive", "ref-height").sizeOr(700.0f) * rawDp;
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