- 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
706 lines
28 KiB
C++
706 lines
28 KiB
C++
// 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));
|
||
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
|