Files
ObsidianDragon/src/ui/material/draw_helpers.h
DanS 3aee55b49c 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)
2026-02-27 00:26:01 -06:00

780 lines
31 KiB
C++

// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include "colors.h"
#include "type.h"
#include "../layout.h"
#include "../schema/element_styles.h"
#include "../schema/color_var_resolver.h"
#include "../schema/ui_schema.h"
#include "../effects/theme_effects.h"
#include "../effects/low_spec.h"
#include "../effects/imgui_acrylic.h"
#include "../theme.h"
#include "../../util/noise_texture.h"
#include "imgui.h"
#include "imgui_internal.h"
#include <algorithm>
#include <unordered_map>
#include <cmath>
namespace dragonx {
namespace ui {
namespace material {
// Scale the alpha channel of an ImU32 color by a float factor.
inline ImU32 ScaleAlpha(ImU32 col, float scale) {
int a = static_cast<int>(((col >> IM_COL32_A_SHIFT) & 0xFF) * scale);
if (a > 255) a = 255;
return (col & ~IM_COL32_A_MASK) | (static_cast<ImU32>(a) << IM_COL32_A_SHIFT);
}
// ============================================================================
// Text Drop Shadow
// ============================================================================
// Draw text with a subtle dark shadow behind it for readability on
// translucent / glassy surfaces. The shadow is a 1px offset copy of
// the text in a near-black colour. When the OS backdrop (DWM Acrylic)
// is inactive, the shadow is skipped since opaque backgrounds already
// provide enough contrast.
inline void DrawTextShadow(ImDrawList* dl, ImFont* font, float fontSize,
const ImVec2& pos, ImU32 col, const char* text,
float offsetX = 1.0f, float offsetY = 1.0f,
ImU32 shadowCol = 0)
{
if (IsBackdropActive()) {
if (!shadowCol) {
static uint32_t s_gen = 0;
static ImU32 s_shadowCol = 0;
uint32_t g = schema::UI().generation();
if (g != s_gen) { s_gen = g; s_shadowCol = schema::UI().resolveColor("var(--text-shadow)", IM_COL32(0, 0, 0, 120)); }
shadowCol = s_shadowCol;
}
dl->AddText(font, fontSize,
ImVec2(pos.x + offsetX, pos.y + offsetY),
shadowCol, text);
}
dl->AddText(font, fontSize, pos, col, text);
}
// Convenience overload that uses the current default font.
inline void DrawTextShadow(ImDrawList* dl, const ImVec2& pos, ImU32 col,
const char* text,
float offsetX = 1.0f, float offsetY = 1.0f,
ImU32 shadowCol = 0)
{
if (IsBackdropActive()) {
if (!shadowCol) {
static uint32_t s_gen = 0;
static ImU32 s_shadowCol = 0;
uint32_t g = schema::UI().generation();
if (g != s_gen) { s_gen = g; s_shadowCol = schema::UI().resolveColor("var(--text-shadow)", IM_COL32(0, 0, 0, 120)); }
shadowCol = s_shadowCol;
}
dl->AddText(ImVec2(pos.x + offsetX, pos.y + offsetY),
shadowCol, text);
}
dl->AddText(pos, col, text);
}
// ============================================================================
// Modal-Aware Hover Check
// ============================================================================
// Drop-in replacement for ImGui::IsMouseHoveringRect that also respects
// modal popup blocking. The raw ImGui helper is a pure geometric test
// and will return true even when a modal popup covers the rect, which
// causes background elements to show hover highlights through dialogs.
inline bool IsRectHovered(const ImVec2& r_min, const ImVec2& r_max, bool clip = true)
{
if (!ImGui::IsMouseHoveringRect(r_min, r_max, clip))
return false;
// If a modal popup is open and it is not the current window, treat
// the content as non-hoverable (same logic ImGui uses internally
// inside IsWindowContentHoverable for modal blocking).
//
// We cannot rely solely on GetTopMostAndVisiblePopupModal() because
// it checks Active, which is only set when BeginPopupModal() is
// called in the current frame. Content tabs render BEFORE their
// associated dialogs, so the modal's Active flag is still false at
// this point. Checking WasActive (set from the previous frame)
// covers this render-order gap.
ImGuiContext& g = *ImGui::GetCurrentContext();
for (int n = g.OpenPopupStack.Size - 1; n >= 0; n--) {
ImGuiWindow* popup = g.OpenPopupStack.Data[n].Window;
if (popup && (popup->Flags & ImGuiWindowFlags_Modal)) {
if ((popup->Active || popup->WasActive) && !popup->Hidden) {
if (popup != ImGui::GetCurrentWindow())
return false;
}
}
}
return true;
}
// ============================================================================
// Tactile Button Overlay
// ============================================================================
// Adds a subtle top-edge highlight and bottom-edge shadow to a button rect,
// creating the illusion of a raised physical surface. On active (pressed),
// the highlight/shadow swap to create an inset "pushed" feel.
// Call this AFTER ImGui::Button() using GetItemRectMin()/GetItemRectMax().
inline void DrawTactileOverlay(ImDrawList* dl, const ImVec2& bMin,
const ImVec2& bMax, float rounding,
bool active = false)
{
float h = bMax.y - bMin.y;
float edgeH = std::min(h * 0.38f, 6.0f); // highlight/shadow strip height
// AddRectFilledMultiColor does not support rounding, so clip to the
// button's rounded rect to prevent sharp corners from poking out.
if (rounding > 0.0f) {
float inset = rounding * 0.29f; // enough to hide corners
dl->PushClipRect(
ImVec2(bMin.x + inset, bMin.y + inset),
ImVec2(bMax.x - inset, bMax.y - inset), true);
}
ImU32 tHi = schema::UI().resolveColor("var(--tactile-top)", IM_COL32(255, 255, 255, 18));
ImU32 tHiT = tHi & ~IM_COL32_A_MASK; // transparent version
if (!active) {
// Raised: bright top edge, dark bottom edge
// Top highlight
dl->AddRectFilledMultiColor(
bMin, ImVec2(bMax.x, bMin.y + edgeH),
tHi, tHi, tHiT, tHiT);
// Bottom shadow
dl->AddRectFilledMultiColor(
ImVec2(bMin.x, bMax.y - edgeH), bMax,
IM_COL32(0, 0, 0, 0), // top-left
IM_COL32(0, 0, 0, 0), // top-right
IM_COL32(0, 0, 0, 22), // bottom-right
IM_COL32(0, 0, 0, 22)); // bottom-left
} else {
// Pressed: dark top edge, bright bottom edge (inset)
// Top shadow
dl->AddRectFilledMultiColor(
bMin, ImVec2(bMax.x, bMin.y + edgeH),
IM_COL32(0, 0, 0, 20),
IM_COL32(0, 0, 0, 20),
IM_COL32(0, 0, 0, 0),
IM_COL32(0, 0, 0, 0));
// Bottom highlight (dimmer when pressed)
ImU32 pHi = ScaleAlpha(tHi, 0.55f);
ImU32 pHiT = pHi & ~IM_COL32_A_MASK;
dl->AddRectFilledMultiColor(
ImVec2(bMin.x, bMax.y - edgeH), bMax,
pHiT, pHiT, pHi, pHi);
}
if (rounding > 0.0f)
dl->PopClipRect();
}
// Convenience: call right after any ImGui::Button() / SmallButton() call
// to add tactile depth. Uses the last item rect automatically.
inline void ApplyTactile(ImDrawList* dl = nullptr)
{
if (!dl) dl = ImGui::GetWindowDrawList();
ImVec2 bMin = ImGui::GetItemRectMin();
ImVec2 bMax = ImGui::GetItemRectMax();
float rounding = ImGui::GetStyle().FrameRounding;
bool active = ImGui::IsItemActive();
DrawTactileOverlay(dl, bMin, bMax, rounding, active);
}
// ── Button font tier helper ─────────────────────────────────────────────
// Resolves an int tier (0=sm, 1=md/default, 2=lg) to the matching ImFont*.
// Passing -1 or any out-of-range value returns the default button font.
inline ImFont* resolveButtonFont(int tier)
{
switch (tier) {
case 0: return Type().buttonSm();
case 2: return Type().buttonLg();
default: return Type().button(); // 1 or any other value
}
}
// Resolve per-button font: if perButton >= 0 use it, else fall back to sectionDefault
inline ImFont* resolveButtonFont(int perButton, int sectionDefault)
{
return resolveButtonFont(perButton >= 0 ? perButton : sectionDefault);
}
// ── Tactile wrappers ────────────────────────────────────────────────────
// Drop-in replacements for ImGui::Button / SmallButton that automatically
// add the tactile highlight overlay after rendering.
inline bool TactileButton(const char* label, const ImVec2& size = ImVec2(0, 0), ImFont* font = nullptr)
{
// Draw button with glass-card styling: translucent fill + border + tactile overlay
ImDrawList* dl = ImGui::GetWindowDrawList();
ImFont* useFont = font ? font : Type().button();
// For icon fonts, use InvisibleButton + manual centered text rendering
// to ensure perfect centering (ImGui::Button alignment can be off for icons)
bool isIconFont = font && (font == Type().iconSmall() || font == Type().iconMed() ||
font == Type().iconLarge() || font == Type().iconXL());
bool pressed;
if (isIconFont && size.x > 0 && size.y > 0) {
pressed = ImGui::InvisibleButton(label, size);
} else {
ImGui::PushFont(useFont);
pressed = ImGui::Button(label, size);
ImGui::PopFont();
}
// Glass overlay on the button rect
ImVec2 bMin = ImGui::GetItemRectMin();
ImVec2 bMax = ImGui::GetItemRectMax();
// For icon fonts, manually draw centered icon after getting button rect
if (isIconFont && size.x > 0 && size.y > 0) {
ImVec2 textSz = useFont->CalcTextSizeA(useFont->LegacySize, FLT_MAX, 0, label);
ImVec2 textPos(bMin.x + (size.x - textSz.x) * 0.5f, bMin.y + (size.y - textSz.y) * 0.5f);
dl->AddText(useFont, useFont->LegacySize, textPos, ImGui::GetColorU32(ImGuiCol_Text), label);
}
float rounding = ImGui::GetStyle().FrameRounding;
bool active = ImGui::IsItemActive();
bool hovered = ImGui::IsItemHovered();
// Frosted glass highlight — subtle fill on top
if (!active) {
ImU32 col = hovered
? schema::UI().resolveColor("var(--hover-overlay)", IM_COL32(255, 255, 255, 12))
: schema::UI().resolveColor("var(--glass-fill)", IM_COL32(255, 255, 255, 6));
dl->AddRectFilled(bMin, bMax, col, rounding);
}
// Rim light
ImU32 rim = schema::UI().resolveColor("var(--rim-light)", IM_COL32(255, 255, 255, 25));
dl->AddRect(bMin, bMax,
active ? ScaleAlpha(rim, 0.6f) : rim,
rounding, 0, 1.0f);
// Tactile depth
DrawTactileOverlay(dl, bMin, bMax, rounding, active);
return pressed;
}
inline bool TactileSmallButton(const char* label, ImFont* font = nullptr)
{
ImDrawList* dl = ImGui::GetWindowDrawList();
ImGui::PushFont(font ? font : Type().button());
bool pressed = ImGui::SmallButton(label);
ImGui::PopFont();
ImVec2 bMin = ImGui::GetItemRectMin();
ImVec2 bMax = ImGui::GetItemRectMax();
float rounding = ImGui::GetStyle().FrameRounding;
bool active = ImGui::IsItemActive();
bool hovered = ImGui::IsItemHovered();
if (!active) {
ImU32 col = hovered
? schema::UI().resolveColor("var(--hover-overlay)", IM_COL32(255, 255, 255, 12))
: schema::UI().resolveColor("var(--glass-fill)", IM_COL32(255, 255, 255, 6));
dl->AddRectFilled(bMin, bMax, col, rounding);
}
ImU32 rim = schema::UI().resolveColor("var(--rim-light)", IM_COL32(255, 255, 255, 25));
dl->AddRect(bMin, bMax,
active ? ScaleAlpha(rim, 0.6f) : rim,
rounding, 0, 1.0f);
DrawTactileOverlay(dl, bMin, bMax, rounding, active);
return pressed;
}
// ============================================================================
// Glass Panel (glassmorphism card)
// ============================================================================
// Draws a frosted-glass style panel: a semi-transparent fill with a
// subtle light border. Used for card/panel containers that sit above
// the blurred background. When the backdrop is inactive the fill is
// simply the normal surface colour (fully opaque).
struct GlassPanelSpec {
float rounding = 6.0f;
int fillAlpha = 18; // fill brightness (white, 0-255)
int borderAlpha = 30; // border brightness (white, 0-255)
float borderWidth = 1.0f;
};
inline void DrawGlassPanel(ImDrawList* dl, const ImVec2& pMin,
const ImVec2& pMax,
const GlassPanelSpec& spec = GlassPanelSpec())
{
if (IsBackdropActive() && !dragonx::ui::effects::isLowSpecMode()) {
// --- Cached color lookups (invalidated on theme change) ---
// These 3 resolveColor() calls do string parsing + map lookup
// each time. Cache them per-frame using the schema generation
// counter so they resolve at most once per theme load.
static uint32_t s_gen = 0;
static ImU32 s_glassFill = 0;
static ImU32 s_glassNoiseTint = 0;
static ImU32 s_glassBorder = 0;
uint32_t curGen = schema::UI().generation();
if (curGen != s_gen) {
s_gen = curGen;
s_glassFill = schema::UI().resolveColor("var(--glass-fill)", IM_COL32(255, 255, 255, 18));
s_glassNoiseTint = schema::UI().resolveColor("var(--glass-noise-tint)", IM_COL32(255, 255, 255, 10));
s_glassBorder = schema::UI().resolveColor("var(--glass-border)", IM_COL32(255, 255, 255, 30));
}
float uiOp = effects::ImGuiAcrylic::GetUIOpacity();
bool useAcrylic = effects::ImGuiAcrylic::IsEnabled()
&& effects::ImGuiAcrylic::IsAvailable();
// Glass / acrylic layer — only rendered when not fully opaque
// (skip the blur pass at 100% for performance)
if (uiOp < 0.99f) {
if (useAcrylic) {
const auto& acrylicTheme = GetCurrentAcrylicTheme();
effects::ImGuiAcrylic::DrawAcrylicRect(dl, pMin, pMax,
acrylicTheme.card, spec.rounding);
} else {
// Lightweight fake-glass: translucent fill
ImU32 fill = (spec.fillAlpha == 18) ? s_glassFill : ScaleAlpha(s_glassFill, spec.fillAlpha / 18.0f);
dl->AddRectFilled(pMin, pMax, fill, spec.rounding);
}
}
// Surface overlay — provides smooth transition from glass to opaque.
// At uiOp=1.0 this fully covers the panel (opaque card).
// As uiOp decreases, glass/blur progressively shows through.
dl->AddRectFilled(pMin, pMax,
WithAlphaF(GetElevatedSurface(GetCurrentColorTheme(), 1), uiOp),
spec.rounding);
// Noise grain overlay — drawn OVER the surface overlay so card
// opacity doesn't hide it. Gives cards a tactile paper feel.
{
float noiseMul = dragonx::ui::effects::ImGuiAcrylic::GetNoiseOpacity();
if (noiseMul > 0.0f) {
uint8_t origAlpha = (s_glassNoiseTint >> IM_COL32_A_SHIFT) & 0xFF;
uint8_t scaledAlpha = static_cast<uint8_t>(std::min(255.0f, origAlpha * noiseMul));
ImU32 noiseTint = (s_glassNoiseTint & ~(0xFFu << IM_COL32_A_SHIFT)) | (scaledAlpha << IM_COL32_A_SHIFT);
float inset = spec.rounding * 0.3f;
ImVec2 clipMin(pMin.x + inset, pMin.y + inset);
ImVec2 clipMax(pMax.x - inset, pMax.y - inset);
dl->PushClipRect(clipMin, clipMax, true);
dragonx::util::DrawTiledNoiseRect(dl, clipMin, clipMax, noiseTint);
dl->PopClipRect();
}
}
// Border — fades with UI opacity
ImU32 border = (spec.borderAlpha == 30) ? s_glassBorder : ScaleAlpha(s_glassBorder, spec.borderAlpha / 30.0f);
if (uiOp < 0.99f) border = ScaleAlpha(border, uiOp);
dl->AddRect(pMin, pMax, border,
spec.rounding, 0, spec.borderWidth);
// Theme visual effects drawn on ForegroundDrawList so they
// render above card content (text, values, etc.), not below.
auto& fx = effects::ThemeEffects::instance();
ImDrawList* fxDl = ImGui::GetForegroundDrawList();
if (fx.hasRainbowBorder()) {
fx.drawRainbowBorder(fxDl, pMin, pMax, spec.rounding, spec.borderWidth);
}
if (fx.hasShimmer()) {
fx.drawShimmer(fxDl, pMin, pMax, spec.rounding);
}
if (fx.hasSpecularGlare()) {
fx.drawSpecularGlare(fxDl, pMin, pMax, spec.rounding);
}
// Per-panel theme effects: edge trace + ember rise
fx.drawPanelEffects(fxDl, pMin, pMax, spec.rounding);
} else {
// Low-spec opaque fallback
dl->AddRectFilled(pMin, pMax,
GetElevatedSurface(GetCurrentColorTheme(), 1), spec.rounding);
}
}
// ============================================================================
// Stat Card — reusable card with overline / value / subtitle + accent stripe
// ============================================================================
struct StatCardSpec {
const char* overline = nullptr; // small label at top (e.g. "LOCAL HASHRATE")
const char* value = nullptr; // main value text (e.g. "1.23 kH/s")
const char* subtitle = nullptr; // optional small line (e.g. "3 blocks")
ImU32 valueCol = 0; // value text colour (0 = OnSurface)
ImU32 accentCol = 0; // left-stripe colour (0 = no stripe)
bool centered = true; // centre text horizontally
bool hovered = false; // draw hover glow border
};
/// Compute a generous stat-card height with breathing room.
inline float StatCardHeight(float vs, float minH = 56.0f) {
float padV = Layout::spacingLg() * 2; // top + bottom
float content = Type().overline()->LegacySize
+ Layout::spacingMd()
+ Type().subtitle1()->LegacySize;
return std::max(minH, (content + padV) * std::max(vs, 0.85f));
}
inline void DrawStatCard(ImDrawList* dl,
const ImVec2& cMin, const ImVec2& cMax,
const StatCardSpec& card,
const GlassPanelSpec& glass = GlassPanelSpec())
{
float cardW = cMax.x - cMin.x;
float cardH = cMax.y - cMin.y;
float rnd = glass.rounding;
// 1. Glass background
DrawGlassPanel(dl, cMin, cMax, glass);
// 2. Accent stripe (left edge, clipped to card rounded corners).
// Draw a full-height rounded rect with card rounding (left corners)
// and clip to stripe width so the shape follows the corner radius.
if ((card.accentCol & IM_COL32_A_MASK) != 0) {
float stripeW = 4.0f;
dl->PushClipRect(cMin, ImVec2(cMin.x + stripeW, cMax.y), true);
dl->AddRectFilled(cMin, cMax, card.accentCol, rnd,
ImDrawFlags_RoundCornersLeft);
dl->PopClipRect();
}
// 3. Compute content block height
ImFont* ovFont = Type().overline();
ImFont* valFont = Type().subtitle1();
ImFont* subFont = Type().caption();
float blockH = 0;
if (card.overline) blockH += ovFont->LegacySize;
if (card.overline && card.value) blockH += Layout::spacingMd();
if (card.value) blockH += valFont->LegacySize;
if (card.subtitle) blockH += Layout::spacingSm() + subFont->LegacySize;
// 4. Vertically centre
float topY = cMin.y + (cardH - blockH) * 0.5f;
float padX = Layout::spacingLg();
float cy = topY;
// 5. Overline
if (card.overline) {
ImVec2 sz = ovFont->CalcTextSizeA(ovFont->LegacySize, 10000, 0, card.overline);
float tx = card.centered ? cMin.x + (cardW - sz.x) * 0.5f : cMin.x + padX;
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(tx, cy), OnSurfaceMedium(), card.overline);
cy += ovFont->LegacySize + Layout::spacingMd();
}
// 6. Value (with text shadow)
if (card.value) {
ImU32 col = card.valueCol ? card.valueCol : OnSurface();
ImVec2 sz = valFont->CalcTextSizeA(valFont->LegacySize, 10000, 0, card.value);
float tx = card.centered ? cMin.x + (cardW - sz.x) * 0.5f : cMin.x + padX;
DrawTextShadow(dl, valFont, valFont->LegacySize, ImVec2(tx, cy), col, card.value);
cy += valFont->LegacySize;
}
// 7. Subtitle
if (card.subtitle) {
cy += Layout::spacingSm();
ImVec2 sz = subFont->CalcTextSizeA(subFont->LegacySize, 10000, 0, card.subtitle);
float tx = card.centered ? cMin.x + (cardW - sz.x) * 0.5f : cMin.x + padX;
dl->AddText(subFont, subFont->LegacySize, ImVec2(tx, cy), OnSurfaceDisabled(), card.subtitle);
}
// 8. Hover glow
if (card.hovered) {
dl->AddRect(cMin, cMax,
schema::UI().resolveColor("var(--hover-overlay)", IM_COL32(255, 255, 255, 20)),
rnd, 0, 1.5f);
}
}
// ── Styled Button (font-only wrapper) ───────────────────────────────────
// Drop-in replacement for ImGui::Button that pushes the Button font style
// (from JSON config) without adding tactile visual effects.
inline bool StyledButton(const char* label, const ImVec2& size = ImVec2(0, 0), ImFont* font = nullptr)
{
ImGui::PushFont(font ? font : Type().button());
bool pressed = ImGui::Button(label, size);
ImGui::PopFont();
return pressed;
}
inline bool StyledSmallButton(const char* label, ImFont* font = nullptr)
{
ImGui::PushFont(font ? font : Type().button());
bool pressed = ImGui::SmallButton(label);
ImGui::PopFont();
return pressed;
}
// ============================================================================
// ButtonStyle-driven overloads (unified UI schema)
// ============================================================================
// These accept a ButtonStyle + ColorVarResolver to push font, colors,
// and size from the schema. Existing call sites are unaffected.
inline bool StyledButton(const char* label, const schema::ButtonStyle& style,
const schema::ColorVarResolver& colors)
{
// Resolve font
ImFont* font = nullptr;
if (!style.font.empty()) {
font = Type().resolveByName(style.font);
}
ImGui::PushFont(font ? font : Type().button());
// Push color overrides
int colorCount = 0;
if (!style.colors.background.empty()) {
ImGui::PushStyleColor(ImGuiCol_Button,
colors.resolve(style.colors.background));
colorCount++;
}
if (!style.colors.backgroundHover.empty()) {
ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
colors.resolve(style.colors.backgroundHover));
colorCount++;
}
if (!style.colors.backgroundActive.empty()) {
ImGui::PushStyleColor(ImGuiCol_ButtonActive,
colors.resolve(style.colors.backgroundActive));
colorCount++;
}
if (!style.colors.color.empty()) {
ImGui::PushStyleColor(ImGuiCol_Text,
colors.resolve(style.colors.color));
colorCount++;
}
// Push style overrides
int styleCount = 0;
if (style.borderRadius >= 0) {
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, style.borderRadius);
styleCount++;
}
ImVec2 size(style.width > 0 ? style.width : 0,
style.height > 0 ? style.height : 0);
bool pressed = ImGui::Button(label, size);
ImGui::PopStyleVar(styleCount);
ImGui::PopStyleColor(colorCount);
ImGui::PopFont();
return pressed;
}
inline bool TactileButton(const char* label, const schema::ButtonStyle& style,
const schema::ColorVarResolver& colors)
{
// Resolve font
ImFont* font = nullptr;
if (!style.font.empty()) {
font = Type().resolveByName(style.font);
}
ImGui::PushFont(font ? font : Type().button());
// Push color overrides
int colorCount = 0;
if (!style.colors.background.empty()) {
ImGui::PushStyleColor(ImGuiCol_Button,
colors.resolve(style.colors.background));
colorCount++;
}
if (!style.colors.backgroundHover.empty()) {
ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
colors.resolve(style.colors.backgroundHover));
colorCount++;
}
if (!style.colors.backgroundActive.empty()) {
ImGui::PushStyleColor(ImGuiCol_ButtonActive,
colors.resolve(style.colors.backgroundActive));
colorCount++;
}
if (!style.colors.color.empty()) {
ImGui::PushStyleColor(ImGuiCol_Text,
colors.resolve(style.colors.color));
colorCount++;
}
// Push style overrides
int styleCount = 0;
if (style.borderRadius >= 0) {
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, style.borderRadius);
styleCount++;
}
ImDrawList* dl = ImGui::GetWindowDrawList();
ImVec2 size(style.width > 0 ? style.width : 0,
style.height > 0 ? style.height : 0);
bool pressed = ImGui::Button(label, size);
// Glass overlay on the button rect
ImVec2 bMin = ImGui::GetItemRectMin();
ImVec2 bMax = ImGui::GetItemRectMax();
float rounding = ImGui::GetStyle().FrameRounding;
bool active = ImGui::IsItemActive();
bool hovered = ImGui::IsItemHovered();
if (!active) {
ImU32 col = hovered
? schema::UI().resolveColor("var(--hover-overlay)", IM_COL32(255, 255, 255, 12))
: schema::UI().resolveColor("var(--glass-fill)", IM_COL32(255, 255, 255, 6));
dl->AddRectFilled(bMin, bMax, col, rounding);
}
ImU32 rim = schema::UI().resolveColor("var(--rim-light)", IM_COL32(255, 255, 255, 25));
dl->AddRect(bMin, bMax,
active ? ScaleAlpha(rim, 0.6f) : rim,
rounding, 0, 1.0f);
DrawTactileOverlay(dl, bMin, bMax, rounding, active);
ImGui::PopStyleVar(styleCount);
ImGui::PopStyleColor(colorCount);
ImGui::PopFont();
return pressed;
}
} // namespace material
} // namespace ui
} // namespace dragonx
namespace dragonx {
namespace ui {
namespace material {
// ============================================================================
// Scroll-edge clipping mask — CSS mask-image style vertex alpha fade.
// Call ApplyScrollEdgeMask after EndChild() to fade content at the
// top/bottom edges of a scrollable panel. Works by walking vertices
// added during rendering and scaling their alpha based on distance
// to the panel edges. No opaque overlay rectangles — content becomes
// truly transparent, revealing whatever is behind the panel.
//
// Usage pattern:
// float scrollY = 0, scrollMaxY = 0;
// int parentVtx = parentDL->VtxBuffer.Size;
// ImGui::BeginChild(...);
// ImDrawList* childDL = ImGui::GetWindowDrawList();
// int childVtx = childDL->VtxBuffer.Size;
// { ... scrollY = GetScrollY(); scrollMaxY = GetScrollMaxY(); ... }
// ImGui::EndChild();
// ApplyScrollEdgeMask(parentDL, parentVtx, childDL, childVtx,
// panelMin.y, panelMax.y, fadeZone, scrollY, scrollMaxY);
// ============================================================================
inline void ApplyScrollEdgeMask(ImDrawList* parentDL, int parentVtxStart,
ImDrawList* childDL, int childVtxStart,
float topEdge, float bottomEdge,
float fadeZone,
float scrollY, float scrollMaxY)
{
auto mask = [&](ImDrawList* dl, int startIdx) {
for (int vi = startIdx; vi < dl->VtxBuffer.Size; vi++) {
ImDrawVert& v = dl->VtxBuffer[vi];
float alpha = 1.0f;
// Top fade — only when scrolled down
if (scrollY > 1.0f) {
float d = v.pos.y - topEdge;
if (d < fadeZone)
alpha = std::min(alpha, std::max(0.0f, d / fadeZone));
}
// Bottom fade — only when not at scroll bottom
if (scrollMaxY > 0 && scrollY < scrollMaxY - 1.0f) {
float d = bottomEdge - v.pos.y;
if (d < fadeZone)
alpha = std::min(alpha, std::max(0.0f, d / fadeZone));
}
if (alpha < 1.0f) {
int a = (v.col >> IM_COL32_A_SHIFT) & 0xFF;
a = static_cast<int>(a * alpha);
v.col = (v.col & ~IM_COL32_A_MASK) | (static_cast<ImU32>(a) << IM_COL32_A_SHIFT);
}
}
};
if (parentDL) mask(parentDL, parentVtxStart);
if (childDL) mask(childDL, childVtxStart);
}
// ============================================================================
// Smooth scrolling — exponential decay interpolation for mouse wheel.
// Call immediately after BeginChild() on a child that was created with
// ImGuiWindowFlags_NoScrollWithMouse. Captures wheel input and lerps
// scroll position each frame for a smooth feel.
//
// Usage:
// ImGui::BeginChild("##List", size, false,
// ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollWithMouse);
// ApplySmoothScroll();
// ... render content ...
// ImGui::EndChild();
// ============================================================================
/// Returns true when any smooth-scroll child is still interpolating toward
/// its target. Checked by the idle-rendering logic in main.cpp to keep
/// rendering frames until the animation settles.
inline bool& SmoothScrollAnimating() {
static bool sAnimating = false;
return sAnimating;
}
inline void ApplySmoothScroll(float speed = 12.0f)
{
struct ScrollState {
float target = 0.0f;
float current = 0.0f;
bool init = false;
};
static std::unordered_map<ImGuiID, ScrollState> sStates;
ImGuiWindow* win = ImGui::GetCurrentWindow();
if (!win) return;
ScrollState& s = sStates[win->ID];
float scrollMaxY = ImGui::GetScrollMaxY();
if (!s.init) {
s.target = ImGui::GetScrollY();
s.current = s.target;
s.init = true;
}
// If something external set scroll position (e.g. SetScrollHereY for
// auto-scroll), sync our state so the next wheel-up starts from the
// actual current position rather than an old remembered target.
float actualY = ImGui::GetScrollY();
if (std::abs(s.current - actualY) > 1.0f) {
s.target = actualY;
s.current = actualY;
}
// Capture mouse wheel when hovered
if (ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows)) {
float wheel = ImGui::GetIO().MouseWheel;
if (wheel != 0.0f) {
float step = ImGui::GetTextLineHeightWithSpacing() * 3.0f;
s.target -= wheel * step;
s.target = ImClamp(s.target, 0.0f, scrollMaxY);
}
}
// Clamp target if scrollMax changed (e.g., content resized)
if (s.target > scrollMaxY) s.target = scrollMaxY;
// Exponential decay lerp
float dt = ImGui::GetIO().DeltaTime;
s.current += (s.target - s.current) * (1.0f - expf(-speed * dt));
// Snap when close
if (std::abs(s.current - s.target) < 0.5f)
s.current = s.target;
else
SmoothScrollAnimating() = true; // still interpolating — keep rendering
ImGui::SetScrollY(s.current);
}
} // namespace material
} // namespace ui
} // namespace dragonx