Replace fragile Dummy()-based cursor flow with a deterministic two-pass layout system: - Pass 1: compute exact Y positions for all elements (pure math) - Pass 2: render at computed positions using SetCursorScreenPos + draw list Eliminates the dual-coordinate mismatch that caused persistent centering and overflow bugs. Height is computed once, not estimated then measured. Also tune sidebar spacing via ui.toml: - button-spacing: 4 → 6 - section-gap: 4 → 8 - Add section-label-pad-bottom (4px) below category labels - bottom-padding: 0 → 4
795 lines
34 KiB
C++
795 lines
34 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 "material/colors.h"
|
|
#include "material/draw_helpers.h"
|
|
#include "layout.h"
|
|
#include "schema/ui_schema.h"
|
|
#include "../embedded/IconsMaterialDesign.h"
|
|
#include "../util/i18n.h"
|
|
#include <cstdio>
|
|
#include <cmath>
|
|
|
|
namespace dragonx {
|
|
namespace ui {
|
|
|
|
// Navigation pages — order matches sidebar display order
|
|
enum class NavPage {
|
|
Overview = 0,
|
|
Send,
|
|
Receive,
|
|
History,
|
|
// --- separator ---
|
|
Mining,
|
|
Market,
|
|
// --- separator ---
|
|
Console,
|
|
Peers,
|
|
Explorer,
|
|
Settings,
|
|
Count_
|
|
};
|
|
|
|
struct NavItem {
|
|
const char* label; // fallback label (English)
|
|
NavPage page;
|
|
const char* section_label; // if non-null, render section label above this item
|
|
const char* tr_key; // i18n key for label
|
|
const char* section_tr_key; // i18n key for section_label
|
|
};
|
|
|
|
inline const NavItem kNavItems[] = {
|
|
{ "Overview", NavPage::Overview, nullptr, "overview", nullptr },
|
|
{ "Send", NavPage::Send, nullptr, "send", nullptr },
|
|
{ "Receive", NavPage::Receive, nullptr, "receive", nullptr },
|
|
{ "History", NavPage::History, nullptr, "history", nullptr },
|
|
{ "Mining", NavPage::Mining, "TOOLS", "mining", "tools" },
|
|
{ "Market", NavPage::Market, nullptr, "market", nullptr },
|
|
{ "Console", NavPage::Console, "ADVANCED","console", "advanced" },
|
|
{ "Network", NavPage::Peers, nullptr, "network", nullptr },
|
|
{ "Explorer", NavPage::Explorer, nullptr, "explorer", nullptr },
|
|
{ "Settings", NavPage::Settings, nullptr, "settings", nullptr },
|
|
};
|
|
static_assert(sizeof(kNavItems) / sizeof(kNavItems[0]) == (int)NavPage::Count_,
|
|
"kNavItems must match NavPage::Count_");
|
|
|
|
// Get translated nav label at runtime
|
|
inline const char* NavLabel(const NavItem& item) {
|
|
return item.tr_key ? TR(item.tr_key) : item.label;
|
|
}
|
|
inline const char* NavSectionLabel(const NavItem& item) {
|
|
return item.section_tr_key ? TR(item.section_tr_key) : item.section_label;
|
|
}
|
|
|
|
// Get the Material Design icon string for a navigation page.
|
|
inline const char* GetNavIconMD(NavPage page)
|
|
{
|
|
switch (page) {
|
|
case NavPage::Overview: return ICON_MD_HOME;
|
|
case NavPage::Send: return ICON_MD_CALL_MADE;
|
|
case NavPage::Receive: return ICON_MD_CALL_RECEIVED;
|
|
case NavPage::History: return ICON_MD_HISTORY;
|
|
case NavPage::Mining: return ICON_MD_CONSTRUCTION;
|
|
case NavPage::Market: return ICON_MD_TRENDING_UP;
|
|
case NavPage::Console: return ICON_MD_TERMINAL;
|
|
case NavPage::Peers: return ICON_MD_HUB;
|
|
case NavPage::Explorer: return ICON_MD_EXPLORE;
|
|
case NavPage::Settings: return ICON_MD_SETTINGS;
|
|
default: return ICON_MD_HOME;
|
|
}
|
|
}
|
|
|
|
// Compute the effective draw-list font size for a given font.
|
|
// During smooth font-scale drag, FontScaleMain compensates for the atlas
|
|
// not yet being rebuilt. drawList->AddText bypasses that, so we apply
|
|
// the factor manually to keep sidebar text in sync with the rest of the UI.
|
|
inline float ScaledFontSize(ImFont* f) {
|
|
return f->LegacySize * ImGui::GetStyle().FontScaleMain;
|
|
}
|
|
|
|
// Draw a Material Design icon centered at (cx, cy) with the given color.
|
|
// Uses the medium (18px) icon font from Typography.
|
|
inline void DrawNavIcon(ImDrawList* dl, NavPage page, float cx, float cy, float /*s*/, ImU32 col)
|
|
{
|
|
ImFont* iconFont = material::Type().iconMed();
|
|
const char* icon = GetNavIconMD(page);
|
|
float fsz = ScaledFontSize(iconFont);
|
|
ImVec2 sz = iconFont->CalcTextSizeA(fsz, 1000.0f, 0.0f, icon);
|
|
dl->AddText(iconFont, fsz,
|
|
ImVec2(cx - sz.x * 0.5f, cy - sz.y * 0.5f), col, icon);
|
|
}
|
|
|
|
// Lightweight badge / status data the caller provides each frame.
|
|
// Counts <= 0 mean "no badge". -1 means "show dot only" (no number).
|
|
struct SidebarStatus {
|
|
int unconfirmedTxCount = 0; // badge on History
|
|
bool miningActive = false; // green dot on Mining
|
|
int peerCount = 0; // badge on Peers
|
|
// Exit
|
|
bool exitClicked = false;
|
|
// Branding logo (optional — loaded at startup)
|
|
ImTextureID logoTexID = 0;
|
|
int logoW = 0;
|
|
int logoH = 0;
|
|
// Gradient overlay texture (optional)
|
|
ImTextureID gradientTexID = 0;
|
|
};
|
|
|
|
// Draw an inset cutout bevel around a button rect — the "channel" carved
|
|
// into the sidebar surface that the raised button sits inside.
|
|
// Highlights and shadows are flipped relative to the button bevel so the
|
|
// cutout looks like it is recessed into the material. Light source: top-left.
|
|
inline void DrawGlassCutout(ImDrawList* dl, ImVec2 mn, ImVec2 mx,
|
|
float rnd, float gap = 2.0f)
|
|
{
|
|
float w = mx.x - mn.x + gap * 2.0f;
|
|
float h = mx.y - mn.y + gap * 2.0f;
|
|
if (w < 1.0f || h < 1.0f) return;
|
|
|
|
// Cached cutout style — refreshed once per theme reload
|
|
struct CutoutStyleCache {
|
|
uint32_t gen = 0;
|
|
float shadowAlpha, highlightAlpha, lineW;
|
|
float glowExpand, glowAlpha, glowLineW;
|
|
};
|
|
static CutoutStyleCache s_cc;
|
|
{
|
|
uint32_t g = schema::UI().generation();
|
|
if (g != s_cc.gen) {
|
|
s_cc.gen = g;
|
|
auto rd = [](const char* key, float fb) {
|
|
auto e = schema::UI().drawElement("components.sidebar", key);
|
|
return e.size >= 0 ? e.size : fb;
|
|
};
|
|
s_cc.shadowAlpha = rd("cutout-shadow-alpha", 55.0f);
|
|
s_cc.highlightAlpha = rd("cutout-highlight-alpha", 16.0f);
|
|
s_cc.lineW = rd("cutout-line-width", 0.75f);
|
|
s_cc.glowExpand = rd("cutout-glow-expand", 1.5f);
|
|
s_cc.glowAlpha = rd("cutout-glow-alpha", 35.0f);
|
|
s_cc.glowLineW = rd("cutout-glow-line-width", 1.5f);
|
|
}
|
|
}
|
|
|
|
ImVec2 cMn(mn.x - gap, mn.y - gap);
|
|
ImVec2 cMx(mx.x + gap, mx.y + gap);
|
|
float cRnd = rnd + gap;
|
|
|
|
float cx = (cMn.x + cMx.x) * 0.5f;
|
|
float cy = (cMn.y + cMx.y) * 0.5f;
|
|
|
|
// Fast directional light factor — algebraic approximation of angular
|
|
// proximity to fixed light at -135°. Replaces atan2f per vertex with
|
|
// a dot-product + clamp (no trig, no sqrt).
|
|
float invHalfW = 2.0f / w;
|
|
float invHalfH = 2.0f / h;
|
|
auto lightFactor = [cx, cy, invHalfW, invHalfH](float px, float py) -> float {
|
|
float nx = (px - cx) * invHalfW;
|
|
float ny = (py - cy) * invHalfH;
|
|
return ImClamp(0.5f + 0.3536f * (-nx - ny), 0.0f, 1.0f);
|
|
};
|
|
|
|
float fadeStart = 0.30f, fadeEnd = 0.55f;
|
|
int shA = (int)s_cc.shadowAlpha;
|
|
int hiA = (int)s_cc.highlightAlpha;
|
|
float lineW = s_cc.lineW;
|
|
|
|
// --- Outer glow pass: wider, softer dark edge on top-left ---
|
|
float glowExpand = s_cc.glowExpand;
|
|
int glowA = (int)s_cc.glowAlpha;
|
|
float glowLineW = s_cc.glowLineW;
|
|
{
|
|
ImVec2 gMn(cMn.x - glowExpand, cMn.y - glowExpand);
|
|
ImVec2 gMx(cMx.x + glowExpand, cMx.y + glowExpand);
|
|
float gRnd = cRnd + glowExpand;
|
|
int v0 = dl->VtxBuffer.Size;
|
|
dl->AddRect(gMn, gMx, IM_COL32(0, 0, 0, 1), gRnd, 0, glowLineW);
|
|
int v1 = dl->VtxBuffer.Size;
|
|
for (int i = v0; i < v1; i++) {
|
|
ImDrawVert& v = dl->VtxBuffer[i];
|
|
float lf = lightFactor(v.pos.x, v.pos.y);
|
|
float fade = ImClamp((lf - 0.25f) / 0.30f, 0.0f, 1.0f);
|
|
int a = (int)(glowA * fade);
|
|
v.col = IM_COL32(0, 0, 0, (unsigned char)ImClamp(a, 0, 255));
|
|
}
|
|
}
|
|
|
|
// --- Crisp shadow pass (on light-facing edges — top-left dark edge) ---
|
|
{
|
|
int v0 = dl->VtxBuffer.Size;
|
|
dl->AddRect(cMn, cMx, IM_COL32(0, 0, 0, 1), cRnd, 0, lineW);
|
|
int v1 = dl->VtxBuffer.Size;
|
|
for (int i = v0; i < v1; i++) {
|
|
ImDrawVert& v = dl->VtxBuffer[i];
|
|
float lf = lightFactor(v.pos.x, v.pos.y);
|
|
float fade = ImClamp((lf - fadeStart) / (fadeEnd - fadeStart), 0.0f, 1.0f);
|
|
int a = (int)(shA * fade);
|
|
v.col = IM_COL32(0, 0, 0, (unsigned char)ImClamp(a, 0, 255));
|
|
}
|
|
}
|
|
|
|
// --- Highlight pass (on shadow-facing edges — bottom-right highlight) ---
|
|
{
|
|
int v0 = dl->VtxBuffer.Size;
|
|
dl->AddRect(cMn, cMx, IM_COL32(255, 255, 255, 1), cRnd, 0, lineW);
|
|
int v1 = dl->VtxBuffer.Size;
|
|
for (int i = v0; i < v1; i++) {
|
|
ImDrawVert& v = dl->VtxBuffer[i];
|
|
float lf = lightFactor(v.pos.x, v.pos.y);
|
|
float sf = 1.0f - lf;
|
|
float fade = ImClamp((sf - fadeStart) / (fadeEnd - fadeStart), 0.0f, 1.0f);
|
|
int a = (int)(hiA * fade);
|
|
v.col = IM_COL32(255, 255, 255, (unsigned char)ImClamp(a, 0, 255));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Draw a neumorphic button molded from the sidebar surface itself.
|
|
// No contrasting fill — uses only shadows and highlights so the button
|
|
// appears to be the exact same material as the sidebar, just shaped.
|
|
// Light source: top-left. depth controls the press amount:
|
|
// 0.0 = fully raised / convex (normal idle state)
|
|
// 0.5 = half-pressed (hover state)
|
|
// 1.0 = fully pressed in (selected / clicked)
|
|
inline void DrawGlassBevelButton(ImDrawList* dl, ImVec2 mn, ImVec2 mx,
|
|
float rnd, float depth = 0.0f, int /*fillAlpha*/ = 18)
|
|
{
|
|
float w = mx.x - mn.x;
|
|
float h = mx.y - mn.y;
|
|
if (w < 1.0f || h < 1.0f) return;
|
|
float inv = 1.0f - depth; // 1 raised, 0 pressed
|
|
|
|
// ---- Directional bevel + neumorphic glow ----
|
|
// Multiple concentric AddRect outline passes with directional vertex
|
|
// coloring. Wider outer passes (expanded rect) create soft shadow glow;
|
|
// inner pass gives crisp bevel edge. All use AddRect with rounding so
|
|
// every layer follows the rounded corners perfectly — no clip rects needed.
|
|
{
|
|
float cx = (mn.x + mx.x) * 0.5f;
|
|
float cy = (mn.y + mx.y) * 0.5f;
|
|
|
|
// Fast directional light factor — algebraic dot-product approximation
|
|
// of angular proximity to fixed light at -135°. No trig, no sqrt.
|
|
float invHalfW = 2.0f / w;
|
|
float invHalfH = 2.0f / h;
|
|
auto lightFactor = [cx, cy, invHalfW, invHalfH](float px, float py) -> float {
|
|
float nx = (px - cx) * invHalfW;
|
|
float ny = (py - cy) * invHalfH;
|
|
return ImClamp(0.5f + 0.3536f * (-nx - ny), 0.0f, 1.0f);
|
|
};
|
|
|
|
struct BevelPass { float expand; float lineW; float fadeStart; float fadeEnd; };
|
|
BevelPass passes[] = {
|
|
{ 0.5f, 0.75f, 0.30f, 0.55f }, // Outer glow (thin)
|
|
{ 0.0f, 0.75f, 0.38f, 0.58f }, // Inner crisp bevel
|
|
};
|
|
|
|
for (auto& bp : passes) {
|
|
// Compute directional alpha.
|
|
// Outer passes fade out when pressed; inner bevel swaps direction.
|
|
int hiA, shA;
|
|
if (bp.expand > 0.0f) {
|
|
float baseHi = (bp.expand > 1.0f) ? 12.0f : 18.0f;
|
|
float baseSh = (bp.expand > 1.0f) ? 20.0f : 30.0f;
|
|
hiA = (int)(baseHi * inv);
|
|
shA = (int)(baseSh * inv);
|
|
} else {
|
|
hiA = (int)(40.0f * inv + 15.0f * depth);
|
|
shA = (int)(45.0f * inv + 60.0f * depth);
|
|
}
|
|
|
|
ImVec2 pMn(mn.x - bp.expand, mn.y - bp.expand);
|
|
ImVec2 pMx(mx.x + bp.expand, mx.y + bp.expand);
|
|
float pRnd = rnd + bp.expand;
|
|
|
|
// Highlight pass (light-facing edges)
|
|
{
|
|
int v0 = dl->VtxBuffer.Size;
|
|
dl->AddRect(pMn, pMx, IM_COL32(255, 255, 255, 1), pRnd, 0, bp.lineW);
|
|
int v1 = dl->VtxBuffer.Size;
|
|
for (int i = v0; i < v1; i++) {
|
|
ImDrawVert& v = dl->VtxBuffer[i];
|
|
float lf = lightFactor(v.pos.x, v.pos.y);
|
|
float fade = ImClamp((lf - bp.fadeStart) / (bp.fadeEnd - bp.fadeStart), 0.0f, 1.0f);
|
|
int targetA = (depth < 0.5f) ? hiA : shA;
|
|
int a = (int)(targetA * fade);
|
|
v.col = (depth < 0.5f)
|
|
? IM_COL32(255, 255, 255, (unsigned char)ImClamp(a, 0, 255))
|
|
: IM_COL32(0, 0, 0, (unsigned char)ImClamp(a, 0, 255));
|
|
}
|
|
}
|
|
|
|
// Shadow pass (shadow-facing edges)
|
|
{
|
|
int v0 = dl->VtxBuffer.Size;
|
|
dl->AddRect(pMn, pMx, IM_COL32(0, 0, 0, 1), pRnd, 0, bp.lineW);
|
|
int v1 = dl->VtxBuffer.Size;
|
|
for (int i = v0; i < v1; i++) {
|
|
ImDrawVert& v = dl->VtxBuffer[i];
|
|
float lf = lightFactor(v.pos.x, v.pos.y);
|
|
float sf = 1.0f - lf;
|
|
float fade = ImClamp((sf - bp.fadeStart) / (bp.fadeEnd - bp.fadeStart), 0.0f, 1.0f);
|
|
int targetA = (depth < 0.5f) ? shA : hiA;
|
|
int a = (int)(targetA * fade);
|
|
v.col = (depth < 0.5f)
|
|
? IM_COL32(0, 0, 0, (unsigned char)ImClamp(a, 0, 255))
|
|
: IM_COL32(255, 255, 255, (unsigned char)ImClamp(a, 0, 255));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---- 3. Inset shadow (scales with depth, emanates from all edges inward) ----
|
|
// Uses concentric rounded rects with decreasing alpha to avoid triangle
|
|
// interpolation artifacts that occur with single-rect vertex color hacking.
|
|
// Cached inset shadow style — refreshed once per theme reload
|
|
struct InsetCache {
|
|
uint32_t gen = 0;
|
|
float threshold, inset, maxAlpha, fadeRatio;
|
|
};
|
|
static InsetCache s_ic;
|
|
{
|
|
uint32_t g = schema::UI().generation();
|
|
if (g != s_ic.gen) {
|
|
s_ic.gen = g;
|
|
auto rd = [](const char* key, float fb) {
|
|
auto e = schema::UI().drawElement("components.sidebar", key);
|
|
return e.size >= 0 ? e.size : fb;
|
|
};
|
|
s_ic.threshold = rd("inset-shadow-threshold", 0.1f);
|
|
s_ic.inset = rd("inset-shadow-inset", 1.0f);
|
|
s_ic.maxAlpha = rd("inset-shadow-max-alpha", 140.0f);
|
|
s_ic.fadeRatio = rd("inset-shadow-fade-ratio", 0.35f);
|
|
}
|
|
}
|
|
if (depth > s_ic.threshold) {
|
|
float baseInset = s_ic.inset;
|
|
int shadowMax = (int)(s_ic.maxAlpha * depth);
|
|
float fadeRatio = s_ic.fadeRatio;
|
|
float bW = mx.x - mn.x - baseInset * 2.0f;
|
|
float bH = mx.y - mn.y - baseInset * 2.0f;
|
|
if (bW > 0.0f && bH > 0.0f) {
|
|
float fadeDepth = ImMin(bW, bH) * fadeRatio;
|
|
const int steps = 8;
|
|
// Draw inside-out: innermost (lightest) first, then progressively
|
|
// larger/darker rects on top. Each outer rect extends further toward
|
|
// the edges, adding darkness only at the perimeter.
|
|
for (int s = steps - 1; s >= 0; s--) {
|
|
float t = (float)s / (float)(steps - 1); // 0 = edge, 1 = deepest
|
|
float shrink = baseInset + t * fadeDepth;
|
|
ImVec2 sMn(mn.x + shrink, mn.y + shrink);
|
|
ImVec2 sMx(mx.x - shrink, mx.y - shrink);
|
|
if (sMx.x <= sMn.x || sMx.y <= sMn.y) continue;
|
|
float sRnd = ImMax(rnd - shrink, 0.0f);
|
|
// Quadratic falloff: darkest at edge (t=0), zero at center (t=1)
|
|
float alpha01 = (1.0f - t) * (1.0f - t);
|
|
int a = (int)((float)shadowMax * alpha01);
|
|
if (a < 1) continue;
|
|
dl->AddRectFilled(sMn, sMx, IM_COL32(0, 0, 0, (unsigned char)ImClamp(a, 0, 255)), sRnd);
|
|
}
|
|
}
|
|
}
|
|
// (No specular highlight — same matte material as sidebar)
|
|
}
|
|
|
|
// Render the sidebar navigation. Returns true if the page changed.
|
|
// collapsed: when true, sidebar is in icon-only mode (narrow width).
|
|
// The caller can toggle collapsed via a reference if a toggle button is desired.
|
|
inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHeight,
|
|
SidebarStatus& status, bool& collapsed, bool locked = false)
|
|
{
|
|
using namespace material;
|
|
bool changed = false;
|
|
|
|
// Collect button rects so we can punch holes in the glass panel fill
|
|
// to avoid opacity stacking (buttons should be same opacity as sidebar).
|
|
struct Rect { ImVec2 mn, mx; float rnd; };
|
|
ImVector<Rect> buttonRects;
|
|
|
|
const auto& S = schema::UISchema::instance();
|
|
|
|
// Read sidebar layout from schema (with fallbacks)
|
|
// All TOML values are in logical pixels; multiply by DPI scale
|
|
// for correct physical pixel sizing on high-DPI displays.
|
|
const float dp = Layout::dpiScale();
|
|
auto sde = [&](const char* key, float fallback) {
|
|
auto e = S.drawElement("components.sidebar", key);
|
|
return (e.size >= 0 ? e.size : fallback) * dp;
|
|
};
|
|
const float sbWidth = sde("width", 160.0f);
|
|
const float sbCollapsedWidth = sde("collapsed-width", 64.0f);
|
|
const float glassMarginY = sde("glass-margin-y", 6.0f);
|
|
const float glassMarginL = sde("glass-margin-left", 6.0f);
|
|
const float glassMarginR = sde("glass-margin-right", 2.0f);
|
|
const float stripH = sde("strip-height", 20.0f);
|
|
const float btnPadCollapsed = sde("button-pad-collapsed", 8.0f);
|
|
const float btnPadExpanded = sde("button-pad-expanded", 14.0f);
|
|
const float iconHalfSize = sde("icon-half-size", 7.0f);
|
|
const float iconLabelGap = sde("icon-label-gap", 8.0f);
|
|
const float badgeRadiusDot = sde("badge-radius-dot", 4.0f);
|
|
const float badgeRadiusNumber = sde("badge-radius-number", 8.0f);
|
|
const float bottomPadding = sde("bottom-padding", 6.0f);
|
|
const float exitIconGap = sde("exit-icon-gap", 4.0f);
|
|
const float sbSectionLabelPadLeft = sde("section-label-pad-left", 16.0f);
|
|
|
|
// Base values for responsive scaling
|
|
const float baseItemHeight = sde("item-height", 46.0f);
|
|
const float baseNavGap = sde("nav-gap", 20.0f);
|
|
const float baseSectionGap = sde("section-gap", 8.0f);
|
|
const float baseButtonSpacing = sde("button-spacing", 6.0f);
|
|
const float sectionLabelPadBot = sde("section-label-pad-bottom", 4.0f);
|
|
|
|
// How "expanded" are we? 0.0 = fully collapsed, 1.0 = fully expanded
|
|
float expandFrac = (sbWidth > sbCollapsedWidth)
|
|
? (sidebarWidth - sbCollapsedWidth) / (sbWidth - sbCollapsedWidth)
|
|
: 1.0f;
|
|
if (expandFrac < 0.0f) expandFrac = 0.0f;
|
|
if (expandFrac > 1.0f) expandFrac = 1.0f;
|
|
bool showLabels = expandFrac > 0.3f;
|
|
|
|
// Font size for section labels (fixed, doesn't scale)
|
|
float olFsz = ScaledFontSize(Type().overline());
|
|
|
|
// Glass panel rounding from responsive schema
|
|
float glassRounding = [&]() {
|
|
float v = S.drawElement("responsive", "glass-rounding").size;
|
|
return (v >= 0 ? v : 8.0f) * dp;
|
|
}();
|
|
|
|
// ===================================================================
|
|
// PASS 1: Compute layout — all Y positions relative to panel top
|
|
// ===================================================================
|
|
// Separate fixed parts (don't scale) from flex parts (scale when tight).
|
|
float fixedH = stripH; // collapse strip
|
|
for (int i = 0; i < (int)NavPage::Count_; ++i)
|
|
if (kNavItems[i].section_label && showLabels)
|
|
fixedH += olFsz + 2.0f + sectionLabelPadBot; // section label + pad below
|
|
fixedH += bottomPadding + stripH; // exit area
|
|
|
|
float baseFlexH = baseNavGap;
|
|
for (int i = 0; i < (int)NavPage::Count_; ++i) {
|
|
if (kNavItems[i].section_label) {
|
|
if (showLabels) baseFlexH += baseSectionGap;
|
|
else baseFlexH += baseSectionGap * 0.8f;
|
|
}
|
|
baseFlexH += baseItemHeight + baseButtonSpacing;
|
|
}
|
|
|
|
// Responsive shrink (only scales flex parts)
|
|
float sidebarMinHeight = sde("min-height", 360.0f);
|
|
float scaleFloor = (sidebarMinHeight > fixedH && baseFlexH > 0.0f)
|
|
? std::max(0.55f, (sidebarMinHeight - fixedH) / baseFlexH) : 0.55f;
|
|
float sidebarScale = 1.0f;
|
|
if (fixedH + baseFlexH > contentHeight && baseFlexH > 0.0f)
|
|
sidebarScale = std::max(scaleFloor, (contentHeight - fixedH) / baseFlexH);
|
|
|
|
const float itemH = baseItemHeight * sidebarScale;
|
|
const float navGap = baseNavGap * sidebarScale;
|
|
const float sectionGap = baseSectionGap * sidebarScale;
|
|
const float btnSpacing = baseButtonSpacing * sidebarScale;
|
|
|
|
// Compute Y position for every element (relative to panel top)
|
|
float itemY[(int)NavPage::Count_];
|
|
float sectionLabelY[4] = {}; int nSectionLabels = 0;
|
|
float separatorY[4] = {}; int nSeparators = 0;
|
|
|
|
float curY = stripH + navGap; // after collapse strip + gap
|
|
for (int i = 0; i < (int)NavPage::Count_; ++i) {
|
|
if (kNavItems[i].section_label) {
|
|
if (showLabels) {
|
|
curY += sectionGap;
|
|
if (nSectionLabels < 4) sectionLabelY[nSectionLabels++] = curY;
|
|
curY += olFsz + 2.0f + sectionLabelPadBot;
|
|
} else {
|
|
curY += sectionGap * 0.4f;
|
|
if (nSeparators < 4) separatorY[nSeparators++] = curY;
|
|
curY += sectionGap * 0.4f;
|
|
}
|
|
}
|
|
itemY[i] = curY;
|
|
curY += itemH + btnSpacing;
|
|
}
|
|
float exitRelY = curY + bottomPadding;
|
|
float panelH = exitRelY + stripH;
|
|
|
|
// Vertical centering — offset so panel is centered in the child window
|
|
float centerOffset = std::max(glassMarginY, (contentHeight - panelH) * 0.5f);
|
|
if (centerOffset + panelH > contentHeight)
|
|
centerOffset = std::max(0.0f, contentHeight - panelH);
|
|
|
|
// ===================================================================
|
|
// PASS 2: Render using computed positions
|
|
// ===================================================================
|
|
ImGui::BeginChild("##Sidebar", ImVec2(sidebarWidth, contentHeight), false,
|
|
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse | ImGuiWindowFlags_NoBackground);
|
|
|
|
ImDrawList* dl = ImGui::GetWindowDrawList();
|
|
ImVec2 wp = ImGui::GetWindowPos();
|
|
|
|
float panelLeft = wp.x + glassMarginL;
|
|
float panelRight = wp.x + sidebarWidth - glassMarginR;
|
|
float panelTopY = wp.y + centerOffset;
|
|
float panelBotY = std::min(panelTopY + panelH, wp.y + contentHeight - glassMarginY);
|
|
|
|
// Draw list splitter: channel 0 = glass background, channel 1 = content
|
|
ImDrawListSplitter splitter;
|
|
splitter.Split(dl, 2);
|
|
splitter.SetCurrentChannel(dl, 1);
|
|
|
|
// ---- Collapse toggle strip (panel top) ----
|
|
{
|
|
float stripX = panelLeft;
|
|
float stripW = panelRight - panelLeft;
|
|
ImVec2 stripMin(stripX, panelTopY);
|
|
ImVec2 stripMax(stripX + stripW, panelTopY + stripH);
|
|
|
|
ImGui::SetCursorScreenPos(stripMin);
|
|
if (ImGui::InvisibleButton("##SidebarCollapse", ImVec2(stripW, stripH)))
|
|
collapsed = !collapsed;
|
|
bool btnHover = ImGui::IsItemHovered();
|
|
|
|
if (btnHover) {
|
|
dl->AddRectFilled(stripMin, stripMax,
|
|
schema::UI().resolveColor("var(--sidebar-hover)", IM_COL32(255, 255, 255, 25)),
|
|
glassRounding, ImDrawFlags_RoundCornersTop);
|
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
|
}
|
|
dl->AddLine(ImVec2(stripMin.x + glassRounding * 0.5f, stripMax.y),
|
|
ImVec2(stripMax.x - glassRounding * 0.5f, stripMax.y),
|
|
schema::UI().resolveColor("var(--sidebar-divider)", IM_COL32(255, 255, 255, 15)), 1.0f);
|
|
|
|
ImU32 iconCol = btnHover ? OnSurface() : schema::UI().resolveColor("var(--sidebar-icon)", IM_COL32(255, 255, 255, 60));
|
|
float cx = stripX + stripW * 0.5f;
|
|
float cy = panelTopY + stripH * 0.5f;
|
|
ImFont* iconFont = Type().iconSmall();
|
|
const char* chevIcon = collapsed ? ICON_MD_CHEVRON_RIGHT : ICON_MD_CHEVRON_LEFT;
|
|
float chevFsz = ScaledFontSize(iconFont);
|
|
ImVec2 chevSz = iconFont->CalcTextSizeA(chevFsz, 1000.0f, 0.0f, chevIcon);
|
|
dl->AddText(iconFont, chevFsz,
|
|
ImVec2(cx - chevSz.x * 0.5f, cy - chevSz.y * 0.5f), iconCol, chevIcon);
|
|
}
|
|
|
|
// ---- Section labels / separators ----
|
|
if (showLabels) {
|
|
ImFont* olFont = Type().overline();
|
|
float olFontSz = ScaledFontSize(olFont);
|
|
int si = 0;
|
|
for (int i = 0; i < (int)NavPage::Count_; ++i) {
|
|
if (kNavItems[i].section_label && si < nSectionLabels) {
|
|
float ly = panelTopY + sectionLabelY[si++];
|
|
ImVec4 olCol = ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium());
|
|
olCol.w *= expandFrac;
|
|
dl->AddText(olFont, olFontSz,
|
|
ImVec2(wp.x + sbSectionLabelPadLeft, ly),
|
|
ImGui::ColorConvertFloat4ToU32(olCol), NavSectionLabel(kNavItems[i]));
|
|
}
|
|
}
|
|
} else {
|
|
for (int si = 0; si < nSeparators; ++si) {
|
|
float sy = panelTopY + separatorY[si];
|
|
dl->AddLine(ImVec2(wp.x + btnPadCollapsed, sy),
|
|
ImVec2(wp.x + sidebarWidth - btnPadCollapsed, sy),
|
|
Divider(), 1.0f);
|
|
}
|
|
}
|
|
|
|
// ---- Navigation items ----
|
|
float btnPadX = collapsed ? btnPadCollapsed : btnPadExpanded;
|
|
for (int i = 0; i < (int)NavPage::Count_; ++i) {
|
|
const NavItem& item = kNavItems[i];
|
|
bool selected = (current == item.page);
|
|
float btnY = panelTopY + itemY[i];
|
|
float btnRnd = itemH * 0.22f;
|
|
|
|
ImVec2 indMin(panelLeft + btnPadX, btnY);
|
|
ImVec2 indMax(panelRight - btnPadX, btnY + itemH);
|
|
|
|
bool hovered = material::IsRectHovered(indMin, indMax);
|
|
|
|
// Glass bevel + theme effects
|
|
{
|
|
float btnDepth = selected ? 1.0f : (hovered ? 0.5f : 0.0f);
|
|
if (selected) {
|
|
auto& fx = effects::ThemeEffects::instance();
|
|
fx.drawGlowPulse(dl, indMin, indMax, btnRnd);
|
|
fx.drawEdgeTrace(dl, indMin, indMax, btnRnd);
|
|
fx.drawEmberRise(dl, indMin, indMax);
|
|
fx.drawShimmer(dl, indMin, indMax, btnRnd);
|
|
fx.drawGradientBorderShift(dl, indMin, indMax, btnRnd);
|
|
}
|
|
DrawGlassCutout(dl, indMin, indMax, btnRnd, 1.5f);
|
|
DrawGlassBevelButton(dl, indMin, indMax, btnRnd, btnDepth, 18);
|
|
buttonRects.push_back({indMin, indMax, btnRnd});
|
|
}
|
|
|
|
if (hovered) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
|
|
|
// Click detection
|
|
bool pageNeedsUnlock = locked &&
|
|
item.page != NavPage::Console &&
|
|
item.page != NavPage::Peers &&
|
|
item.page != NavPage::Settings;
|
|
if (hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Left) && !pageNeedsUnlock) {
|
|
current = item.page;
|
|
changed = true;
|
|
}
|
|
|
|
// Icon + label
|
|
float iconS = iconHalfSize;
|
|
float iconCY = btnY + itemH * 0.5f;
|
|
float textY = btnY + (itemH - ImGui::GetTextLineHeight()) * 0.5f;
|
|
ImU32 textCol = selected ? Primary() : (pageNeedsUnlock ? OnSurfaceDisabled() : OnSurfaceMedium());
|
|
|
|
if (showLabels) {
|
|
ImFont* font = selected ? Type().subtitle2() : Type().body2();
|
|
float lblFsz = ScaledFontSize(font);
|
|
float btnW = indMax.x - indMin.x;
|
|
float maxLabelW = btnW - iconS * 2.0f - iconLabelGap - Layout::spacingXs() * 2;
|
|
ImVec2 labelSz = font->CalcTextSizeA(lblFsz, 1000.0f, 0.0f, NavLabel(item));
|
|
if (labelSz.x > maxLabelW && maxLabelW > 0) {
|
|
lblFsz *= maxLabelW / labelSz.x;
|
|
labelSz = font->CalcTextSizeA(lblFsz, 1000.0f, 0.0f, NavLabel(item));
|
|
}
|
|
float totalW = iconS * 2.0f + iconLabelGap + labelSz.x;
|
|
float btnCX = (indMin.x + indMax.x) * 0.5f;
|
|
float startX = btnCX - totalW * 0.5f;
|
|
|
|
DrawNavIcon(dl, item.page, startX + iconS, iconCY, iconS, textCol);
|
|
|
|
ImVec4 lc = ImGui::ColorConvertU32ToFloat4(textCol);
|
|
lc.w *= expandFrac;
|
|
dl->AddText(font, lblFsz,
|
|
ImVec2(startX + iconS * 2.0f + iconLabelGap, textY),
|
|
ImGui::ColorConvertFloat4ToU32(lc), NavLabel(item));
|
|
} else {
|
|
float iconCX = (indMin.x + indMax.x) * 0.5f;
|
|
DrawNavIcon(dl, item.page, iconCX, iconCY, iconS, textCol);
|
|
if (hovered) ImGui::SetTooltip("%s", NavLabel(item));
|
|
}
|
|
|
|
// Badge indicator
|
|
{
|
|
int badgeCount = 0;
|
|
bool dotOnly = false;
|
|
ImU32 badgeCol = Primary();
|
|
ImU32 badgeTextCol = OnPrimary();
|
|
|
|
if (item.page == NavPage::History && status.unconfirmedTxCount > 0) {
|
|
badgeCount = status.unconfirmedTxCount;
|
|
badgeCol = Warning(); badgeTextCol = OnWarning();
|
|
} else if (item.page == NavPage::Mining && status.miningActive) {
|
|
dotOnly = true; badgeCol = Success();
|
|
} else if (item.page == NavPage::Peers && status.peerCount > 0) {
|
|
badgeCount = status.peerCount;
|
|
}
|
|
|
|
if (badgeCount > 0 || dotOnly) {
|
|
float badgeR = dotOnly ? badgeRadiusDot : badgeRadiusNumber;
|
|
float bx = indMax.x - badgeR - 6.0f;
|
|
float by = indMin.y + badgeR + 5.0f;
|
|
dl->AddCircleFilled(ImVec2(bx, by), badgeR, badgeCol);
|
|
if (!dotOnly && showLabels) {
|
|
char buf[16];
|
|
snprintf(buf, sizeof(buf), "%d", badgeCount > 99 ? 99 : badgeCount);
|
|
ImFont* capFont = Type().caption();
|
|
float capFsz = ScaledFontSize(capFont);
|
|
ImVec2 ts = capFont->CalcTextSizeA(capFsz, 1000.0f, 0.0f, buf);
|
|
dl->AddText(capFont, capFsz,
|
|
ImVec2(bx - ts.x * 0.5f, by - ts.y * 0.5f), badgeTextCol, buf);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---- Keyboard navigation ----
|
|
if (!ImGui::GetIO().WantTextInput && !ImGui::GetIO().KeyCtrl) {
|
|
int idx = static_cast<int>(current);
|
|
bool nav = false;
|
|
if (ImGui::IsKeyPressed(ImGuiKey_UpArrow) || ImGui::IsKeyPressed(ImGuiKey_K))
|
|
if (idx > 0) { idx--; nav = true; }
|
|
if (ImGui::IsKeyPressed(ImGuiKey_DownArrow) || ImGui::IsKeyPressed(ImGuiKey_J))
|
|
if (idx < (int)NavPage::Count_ - 1) { idx++; nav = true; }
|
|
if (ImGui::IsKeyPressed(ImGuiKey_LeftBracket))
|
|
collapsed = !collapsed;
|
|
if (nav) { current = static_cast<NavPage>(idx); changed = true; }
|
|
}
|
|
|
|
// ---- Glass panel background (channel 0) ----
|
|
splitter.SetCurrentChannel(dl, 0);
|
|
{
|
|
ImVec2 panelMin(panelLeft, panelTopY);
|
|
ImVec2 panelMax(panelRight, panelBotY);
|
|
GlassPanelSpec glass;
|
|
glass.rounding = glassRounding;
|
|
glass.fillAlpha = 14;
|
|
glass.borderAlpha = 25;
|
|
glass.borderWidth = 1.0f;
|
|
int vtx0 = dl->VtxBuffer.Size;
|
|
DrawGlassPanel(dl, panelMin, panelMax, glass);
|
|
int vtx1 = dl->VtxBuffer.Size;
|
|
|
|
// Punch holes in glass where buttons are to avoid double opacity
|
|
for (int vi = vtx0; vi < vtx1; vi++) {
|
|
ImDrawVert& v = dl->VtxBuffer[vi];
|
|
for (int bi = 0; bi < buttonRects.Size; bi++) {
|
|
const Rect& r = buttonRects[bi];
|
|
if (v.pos.x > r.mn.x + 0.5f && v.pos.x < r.mx.x - 0.5f &&
|
|
v.pos.y > r.mn.y + 0.5f && v.pos.y < r.mx.y - 0.5f) {
|
|
v.col = (v.col & 0x00FFFFFFu);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---- Exit button strip (panel bottom) ----
|
|
splitter.SetCurrentChannel(dl, 1);
|
|
{
|
|
float exitY = panelTopY + exitRelY;
|
|
if (exitY + stripH > panelBotY) exitY = panelBotY - stripH;
|
|
float exitX = panelLeft;
|
|
float exitW = panelRight - panelLeft;
|
|
ImVec2 exitMin(exitX, exitY);
|
|
ImVec2 exitMax(exitX + exitW, exitY + stripH);
|
|
|
|
ImGui::SetCursorScreenPos(exitMin);
|
|
if (ImGui::InvisibleButton("##ExitBtn", ImVec2(exitW, stripH)))
|
|
status.exitClicked = true;
|
|
bool exitHover = ImGui::IsItemHovered();
|
|
|
|
if (exitHover) {
|
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
|
dl->AddRectFilled(exitMin, exitMax,
|
|
schema::UI().resolveColor("var(--sidebar-hover)", IM_COL32(255, 255, 255, 25)),
|
|
glassRounding, ImDrawFlags_RoundCornersBottom);
|
|
}
|
|
dl->AddLine(ImVec2(exitMin.x + glassRounding * 0.5f, exitMin.y),
|
|
ImVec2(exitMax.x - glassRounding * 0.5f, exitMin.y),
|
|
schema::UI().resolveColor("var(--sidebar-divider)", IM_COL32(255, 255, 255, 15)), 1.0f);
|
|
|
|
ImU32 exitCol = exitHover ? Error() : schema::UI().resolveColor("var(--sidebar-icon)", IM_COL32(255, 255, 255, 60));
|
|
float cx = exitX + exitW * 0.5f;
|
|
float cy = exitY + stripH * 0.5f;
|
|
|
|
if (showLabels) {
|
|
ImFont* iconFont = Type().iconSmall();
|
|
ImFont* font = Type().caption();
|
|
const char* exitIcon = ICON_MD_EXIT_TO_APP;
|
|
float eIconFsz = ScaledFontSize(iconFont);
|
|
float eLblFsz = ScaledFontSize(font);
|
|
ImVec2 iconSz = iconFont->CalcTextSizeA(eIconFsz, 1000.0f, 0.0f, exitIcon);
|
|
const char* exitLabel = TR("exit");
|
|
ImVec2 labelSz = font->CalcTextSizeA(eLblFsz, 1000.0f, 0.0f, exitLabel);
|
|
float totalW = iconSz.x + exitIconGap + labelSz.x;
|
|
float startX = cx - totalW * 0.5f;
|
|
dl->AddText(iconFont, eIconFsz,
|
|
ImVec2(startX, cy - iconSz.y * 0.5f), exitCol, exitIcon);
|
|
ImVec4 lc = ImGui::ColorConvertU32ToFloat4(exitCol);
|
|
lc.w *= expandFrac;
|
|
dl->AddText(font, eLblFsz,
|
|
ImVec2(startX + iconSz.x + exitIconGap, cy - labelSz.y * 0.5f),
|
|
ImGui::ColorConvertFloat4ToU32(lc), exitLabel);
|
|
} else {
|
|
ImFont* iconFont = Type().iconSmall();
|
|
const char* exitIcon = ICON_MD_EXIT_TO_APP;
|
|
float eIconFsz = ScaledFontSize(iconFont);
|
|
ImVec2 iconSz = iconFont->CalcTextSizeA(eIconFsz, 1000.0f, 0.0f, exitIcon);
|
|
dl->AddText(iconFont, eIconFsz,
|
|
ImVec2(cx - iconSz.x * 0.5f, cy - iconSz.y * 0.5f), exitCol, exitIcon);
|
|
if (exitHover) ImGui::SetTooltip("%s", TR("exit"));
|
|
}
|
|
}
|
|
|
|
splitter.Merge(dl);
|
|
|
|
ImGui::EndChild();
|
|
return changed;
|
|
}
|
|
|
|
} // namespace ui
|
|
} // namespace dragonx
|