Files
ObsidianDragon/src/ui/layout.h
dan_s 96c27bb949 feat: Full UI internationalization, pool hashrate stats, and layout caching
- Replace all hardcoded English strings with TR() translation keys across
  every tab, dialog, and component (~20 UI files)
- Expand all 8 language files (de, es, fr, ja, ko, pt, ru, zh) with
  complete translations (~37k lines added)
- Improve i18n loader with exe-relative path fallback and English base
  fallback for missing keys
- Add pool-side hashrate polling via pool stats API in xmrig_manager
- Introduce Layout::beginFrame() per-frame caching and refresh balance
  layout config only on schema generation change
- Offload daemon output parsing to worker thread
- Add CJK subset fallback font for Chinese/Japanese/Korean glyphs
2026-03-11 00:40:50 -05:00

706 lines
28 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.03.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.01.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