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