// 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 #include #include 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)); detail::userFontScaleRef() = v; // Compare against the atlas scale, not the live ref — setUserFontScaleVisual // may have already updated the ref during slider drag. if (v != detail::fontAtlasScaleRef()) { 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(); } // ============================================================================ // Per-frame cache — populated once via beginFrame(), avoids repeated // schema hash-map lookups for the hottest accessors. // ============================================================================ namespace detail { struct FrameCache { uint32_t gen = 0; // schema generation when last populated float rawDp = 1.0f; // rawDpiScale() snapshot float dp = 1.0f; // dpiScale() snapshot // Spacing tokens (raw, unscaled) float spXs = 2.0f; float spSm = 4.0f; float spMd = 8.0f; float spLg = 12.0f; float spXl = 16.0f; float spXxl = 24.0f; // Responsive scale config float refW = 1200.0f; float refH = 700.0f; float minHS = 0.5f; float maxHS = 1.5f; float minVS = 0.5f; float maxVS = 1.4f; float minDen = 0.6f; float maxDen = 1.2f; // Breakpoint thresholds (raw, unscaled) float compactW = 500.0f; float compactH = 450.0f; float expandedW = 900.0f; float expandedH = 750.0f; // Glass / card helpers (raw, unscaled) float glassRnd = 8.0f; float cardPad = 12.0f; float cardGap = 8.0f; }; inline FrameCache& frameCache() { static FrameCache c; return c; } } // namespace detail /** * @brief Refresh the per-frame layout cache. * * Call once per frame (e.g., from App::preFrame) BEFORE any rendering. * Reads all high-frequency TOML values into fast statics. Invalidated * automatically when the schema generation changes (hot-reload). */ inline void beginFrame() { auto& c = detail::frameCache(); uint32_t g = schema::UI().generation(); // Also capture DPI each frame (can change on display-scale events) float curRawDp = rawDpiScale(); float curDp = dpiScale(); if (g == c.gen && curRawDp == c.rawDp && curDp == c.dp) return; c.gen = g; c.rawDp = curRawDp; c.dp = curDp; const auto& S = schema::UI(); // Spacing tokens c.spXs = S.drawElement("spacing-tokens", "xs").sizeOr(2.0f); c.spSm = S.drawElement("spacing-tokens", "sm").sizeOr(4.0f); c.spMd = S.drawElement("spacing-tokens", "md").sizeOr(8.0f); c.spLg = S.drawElement("spacing-tokens", "lg").sizeOr(12.0f); c.spXl = S.drawElement("spacing-tokens", "xl").sizeOr(16.0f); c.spXxl = S.drawElement("spacing-tokens", "xxl").sizeOr(24.0f); // Responsive config c.refW = S.drawElement("responsive", "ref-width").sizeOr(1200.0f); c.refH = S.drawElement("responsive", "ref-height").sizeOr(700.0f); c.minHS = S.drawElement("responsive", "min-h-scale").sizeOr(0.5f); c.maxHS = S.drawElement("responsive", "max-h-scale").sizeOr(1.5f); c.minVS = S.drawElement("responsive", "min-v-scale").sizeOr(0.5f); c.maxVS = S.drawElement("responsive", "max-v-scale").sizeOr(1.4f); c.minDen = S.drawElement("responsive", "min-density").sizeOr(0.6f); c.maxDen = S.drawElement("responsive", "max-density").sizeOr(1.2f); // Breakpoints c.compactW = S.drawElement("responsive", "compact-width").sizeOr(500.0f); c.compactH = S.drawElement("responsive", "compact-height").sizeOr(450.0f); c.expandedW = S.drawElement("responsive", "expanded-width").sizeOr(900.0f); c.expandedH = S.drawElement("responsive", "expanded-height").sizeOr(750.0f); // Glass / card c.glassRnd = S.drawElement("responsive", "glass-rounding").sizeOr(8.0f); c.cardPad = S.drawElement("responsive", "card-inner-padding").sizeOr(12.0f); c.cardGap = S.drawElement("responsive", "card-gap").sizeOr(8.0f); } // ============================================================================ // 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& c = detail::frameCache(); float dp = c.dp; float cw = c.compactW * dp; float ch = c.compactH * dp; float ew = c.expandedW * dp; float eh = c.expandedH * 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& c = detail::frameCache(); float dp = c.dp; float cw = c.compactW * dp; float ch = c.compactH * dp; float ew = c.expandedW * dp; float eh = c.expandedH * 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& c = detail::frameCache(); float rw = c.refW * c.rawDp; float logical = std::clamp(availWidth / rw, c.minHS, c.maxHS); return logical * c.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& c = detail::frameCache(); float rh = c.refH * c.rawDp; float logical = std::clamp(availHeight / rh, c.minVS, c.maxVS); return logical * c.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& c = detail::frameCache(); float rh = c.refH * c.rawDp; float logical = std::clamp(availHeight / rh, c.minDen, c.maxDen); return logical * c.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 detail::frameCache().spXs * densityScale(); } inline float spacingSm() { return detail::frameCache().spSm * densityScale(); } inline float spacingMd() { return detail::frameCache().spMd * densityScale(); } inline float spacingLg() { return detail::frameCache().spLg * densityScale(); } inline float spacingXl() { return detail::frameCache().spXl * densityScale(); } inline float spacingXxl() { return detail::frameCache().spXxl * densityScale(); } /** @brief Get raw (unscaled) spacing token. */ inline float spacingXsRaw() { return detail::frameCache().spXs; } inline float spacingSmRaw() { return detail::frameCache().spSm; } inline float spacingMdRaw() { return detail::frameCache().spMd; } inline float spacingLgRaw() { return detail::frameCache().spLg; } inline float spacingXlRaw() { return detail::frameCache().spXl; } inline float spacingXxlRaw() { return detail::frameCache().spXxl; } // ============================================================================ // Responsive Globals Helpers // ============================================================================ /** @brief Default glass panel rounding (8.0 default). */ inline float glassRounding() { return detail::frameCache().glassRnd * dpiScale(); } /** @brief Default card inner padding (12.0 default). */ inline float cardInnerPadding() { return detail::frameCache().cardPad * dpiScale(); } /** @brief Default card gap (8.0 default). */ inline float cardGap() { return detail::frameCache().cardGap * 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