// DragonX Wallet - ImGui Edition // Copyright 2024-2026 The Hush Developers // Released under the GPLv3 #pragma once #include "colors.h" #include "type.h" #include "tooltip_style.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 "../../embedded/IconsMaterialDesign.h" #include "imgui.h" #include "imgui_internal.h" #include #include #include #include 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(((col >> IM_COL32_A_SHIFT) & 0xFF) * scale); if (a > 255) a = 255; return (col & ~IM_COL32_A_MASK) | (static_cast(a) << IM_COL32_A_SHIFT); } // True when the active theme is light (app background luminance is high). inline bool IsLightTheme() { ImU32 bg = Background(); float r = ((bg >> IM_COL32_R_SHIFT) & 0xFF) / 255.0f; float g = ((bg >> IM_COL32_G_SHIFT) & 0xFF) / 255.0f; float b = ((bg >> IM_COL32_B_SHIFT) & 0xFF) / 255.0f; return (0.299f * r + 0.587f * g + 0.114f * b) > 0.5f; } // Animated "loading" ellipsis: "", ".", "..", "..." cycling on a ~3Hz phase. inline const char* LoadingDots() { int n = ((int)(ImGui::GetTime() * 3.0f)) % 4; static const char* kDots[4] = { "", ".", "..", "..." }; return kDots[n]; } // ============================================================================ // 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 int& OverlayDialogActiveFrame() { static int s_frame = -1; return s_frame; } inline void MarkOverlayDialogActive() { OverlayDialogActiveFrame() = ImGui::GetFrameCount(); } inline bool IsCurrentWindowOverlayDialog() { ImGuiWindow* window = ImGui::GetCurrentWindow(); for (ImGuiWindow* node = window; node; node = node->ParentWindow) { // Prefix match so both "##OverlayScrim" and suffixed "##OverlayScrim_" blur overlays // (used by nested dialogs and the unified blur-overlay framework) are recognized. if (node->Name && strncmp(node->Name, "##OverlayScrim", 14) == 0) return true; } return false; } inline bool IsOverlayDialogBlockingInput() { int activeFrame = OverlayDialogActiveFrame(); int currentFrame = ImGui::GetFrameCount(); return activeFrame == currentFrame || activeFrame == (currentFrame - 1); } 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 (IsOverlayDialogBlockingInput() && !IsCurrentWindowOverlayDialog()) 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(); } // Glass-card button surface: frosted-glass hover/idle fill (skipped while // pressed) + rim light + tactile depth overlay. Shared by the tactile // button wrappers. Call after computing the button rect / rounding / // active / hovered state. inline void DrawButtonGlassOverlay(ImDrawList* dl, const ImVec2& bMin, const ImVec2& bMax, float rounding, bool active, bool hovered) { // Light themes: flat buttons (like the Manage-portfolio modal's plain buttons). Skip the glass // sheen + tactile 3D — ImGui::Button already draws the fill/hover/active — and add just a subtle // defining edge, which reads cleaner on light surfaces than the raised glass look. if (IsLightTheme()) { ImU32 rim = schema::UI().resolveColor("var(--rim-light)", IM_COL32(0, 0, 0, 50)); dl->AddRect(bMin, bMax, active ? ScaleAlpha(rim, 0.6f) : rim, rounding, 0, 1.0f); return; } // 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); } // ── 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); } // ── Progress / fill bar ───────────────────────────────────────────────── // A rounded track with a fraction (0..1) fill, drawn at the current cursor and advancing the layout // cursor past it (matches the download-dialog usage). For a placement you control, use the pMin/pMax // overload. Defaults: theme Primary fill, a subtle white track, 4dp corners. inline void DrawProgressBar(ImDrawList* dl, const ImVec2& pMin, const ImVec2& pMax, float frac01, ImU32 fill = 0, ImU32 track = IM_COL32(255, 255, 255, 30), float rounding = -1.0f) { if (fill == 0) fill = Primary(); if (rounding < 0.0f) rounding = 4.0f * Layout::dpiScale(); frac01 = frac01 < 0.0f ? 0.0f : (frac01 > 1.0f ? 1.0f : frac01); dl->AddRectFilled(pMin, pMax, track, rounding); if (frac01 > 0.0f) dl->AddRectFilled(pMin, ImVec2(pMin.x + (pMax.x - pMin.x) * frac01, pMax.y), fill, rounding); } inline void ProgressBar(float width, float height, float frac01, ImU32 fill = 0, ImU32 track = IM_COL32(255, 255, 255, 30)) { ImVec2 pMin = ImGui::GetCursorScreenPos(); DrawProgressBar(ImGui::GetWindowDrawList(), pMin, ImVec2(pMin.x + width, pMin.y + height), frac01, fill, track); ImGui::Dummy(ImVec2(0, height)); } // ── Click-to-copy affordance ──────────────────────────────────────────── // Turns an already-drawn text run into a click-to-copy control. The caller draws the visible text // (font/color/position of its choosing); this places an invisible hit button (textSize + hitPad) at // `pos`, shows a hand cursor + tooltip + a hover underline, and copies `copyValue` on click. Returns // true the frame it was clicked (already on the clipboard) so the caller can show its own toast. // Caller manages any cursor save/restore around the call. inline bool CopyableTextOverlay(const char* id, const ImVec2& pos, const ImVec2& textSize, float fontSize, const char* copyValue, const char* tooltip, const ImVec2& hitPad) { const float dp = Layout::dpiScale(); ImGui::SetCursorScreenPos(pos); ImGui::InvisibleButton(id, ImVec2(textSize.x + hitPad.x, fontSize + hitPad.y)); if (ImGui::IsItemHovered()) { ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); if (tooltip && tooltip[0]) Tooltip("%s", tooltip); const float uy = pos.y + fontSize + 1.0f * dp; ImGui::GetWindowDrawList()->AddLine(ImVec2(pos.x, uy), ImVec2(pos.x + textSize.x, uy), WithAlpha(OnSurface(), 60), 1.0f * dp); } bool copied = false; if (ImGui::IsItemClicked()) { ImGui::SetClipboardText(copyValue); copied = true; } return copied; } // ── Pill / badge ───────────────────────────────────────────────────────── // A rounded chip: a filled background sized to `text` + `pad`, an optional 1px border, and the text // inset by `pad`. Use PillSize first when you need to right/center-anchor (compute the width, place // the pill, then draw). inline ImVec2 PillSize(const char* text, ImFont* font, const ImVec2& pad) { ImVec2 t = font->CalcTextSizeA(font->LegacySize, FLT_MAX, 0.0f, text); return ImVec2(t.x + pad.x * 2.0f, font->LegacySize + pad.y * 2.0f); } // Draw a pill/badge anchored at top-left `pos`. `border` == 0 draws no outline. rounding < 0 => fully // rounded (height/2). Returns the pill's total size. inline ImVec2 DrawPill(ImDrawList* dl, const ImVec2& pos, const char* text, ImFont* font, ImU32 fg, ImU32 bg, ImU32 border = 0, const ImVec2& pad = ImVec2(4.0f, 2.0f), float rounding = -1.0f) { ImVec2 sz = PillSize(text, font, pad); if (rounding < 0.0f) rounding = sz.y * 0.5f; ImVec2 pMax(pos.x + sz.x, pos.y + sz.y); dl->AddRectFilled(pos, pMax, bg, rounding); if (border != 0) dl->AddRect(pos, pMax, border, rounding, 0, 1.0f * Layout::dpiScale()); dl->AddText(font, font->LegacySize, ImVec2(pos.x + pad.x, pos.y + pad.y), fg, text); return sz; } // ── Labeled form input ─────────────────────────────────────────────────── // The app's common form-field idiom: a plain label above a fixed-width single-line input // (ImGui::Text label + SetNextItemWidth + InputText / InputTextWithHint). Mirrors the existing // pattern exactly (no visual change) so input styling can later evolve in ONE place. `width` < 0 // means full width (-1). Pass a non-null `hint` to use InputTextWithHint. Returns the input's // edited bool. The label is drawn via "%s" so a stray '%' in the text is safe. inline bool LabeledInput(const char* label, const char* id, char* buf, size_t bufSize, float width = -1.0f, const char* hint = nullptr, ImGuiInputTextFlags flags = 0) { ImGui::Text("%s", label); ImGui::SetNextItemWidth(width); if (hint && hint[0]) return ImGui::InputTextWithHint(id, hint, buf, bufSize, flags); return ImGui::InputText(id, buf, bufSize, flags); } // Horizontal variant: label on the left, input to its right on the same line (settings-style rows). // `labelPos` is the SameLine offset the input starts at (0 = default item spacing after the label). inline bool LabeledInputRow(const char* label, float labelPos, const char* id, char* buf, size_t bufSize, float width = -1.0f, const char* hint = nullptr, ImGuiInputTextFlags flags = 0) { ImGui::Text("%s", label); ImGui::SameLine(labelPos); ImGui::SetNextItemWidth(width); if (hint && hint[0]) return ImGui::InputTextWithHint(id, hint, buf, bufSize, flags); return ImGui::InputText(id, buf, bufSize, flags); } // Multiline variant: label above a multiline input of the given `size` (InputTextMultiline governs its // own width via size, so no SetNextItemWidth). Returns the input's edited bool. inline bool LabeledInputMultiline(const char* label, const char* id, char* buf, size_t bufSize, const ImVec2& size, ImGuiInputTextFlags flags = 0) { ImGui::Text("%s", label); return ImGui::InputTextMultiline(id, buf, bufSize, size, flags); } // ── Frameless icon button ──────────────────────────────────────────────── // Style for IconButton. All colors default to 0 (= unset): color falls back to OnSurfaceMedium(); // hoverColor==0 means "no hover recolor"; hoverBg/restBg==0 mean "no background". struct IconButtonStyle { ImU32 color = 0; // resting glyph color (0 => OnSurfaceMedium()) ImU32 hoverColor = 0; // glyph color while hovered (0 => same as color) ImU32 hoverBg = 0; // fill drawn behind the glyph on hover only (0 => none) ImU32 restBg = 0; // fill drawn every frame, e.g. an "active" pill (0 => none) float bgRounding = -1.0f; // <0 => size.y*0.5 (pill/circle for a square); else that radius bool handCursor = true; const char* tooltip = nullptr; }; // A frameless / pill icon button: an InvisibleButton(id, size) at the current cursor with a centered // `glyph` (in `font`), optional resting + hover backgrounds, hover recolor, hand cursor + tooltip. // Returns true the frame it is clicked. The caller resolves any state-dependent glyph/colors/tooltip // before the call and positions the cursor (SetCursorScreenPos / SameLine) as needed. inline bool IconButton(const char* id, const char* glyph, ImFont* font, const ImVec2& size, const IconButtonStyle& st) { const ImVec2 pos = ImGui::GetCursorScreenPos(); ImGui::InvisibleButton(id, size); const bool hov = ImGui::IsItemHovered(); const bool clk = ImGui::IsItemClicked(); ImDrawList* dl = ImGui::GetWindowDrawList(); const ImVec2 pMax(pos.x + size.x, pos.y + size.y); const float round = st.bgRounding < 0.0f ? size.y * 0.5f : st.bgRounding; if (st.restBg != 0) dl->AddRectFilled(pos, pMax, st.restBg, round); if (hov && st.hoverBg != 0) dl->AddRectFilled(pos, pMax, st.hoverBg, round); if (hov) { if (st.handCursor) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); if (st.tooltip && st.tooltip[0]) Tooltip("%s", st.tooltip); } const ImU32 gcol = (hov && st.hoverColor != 0) ? st.hoverColor : (st.color != 0 ? st.color : OnSurfaceMedium()); const ImVec2 gsz = font->CalcTextSizeA(font->LegacySize, FLT_MAX, 0, glyph); dl->AddText(font, font->LegacySize, ImVec2(pos.x + (size.x - gsz.x) * 0.5f, pos.y + (size.y - gsz.y) * 0.5f), gcol, glyph); return clk; } // ── 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(); DrawButtonGlassOverlay(dl, bMin, bMax, rounding, active, hovered); 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(); DrawButtonGlassOverlay(dl, bMin, bMax, rounding, active, hovered); return pressed; } // ============================================================================ // Collapsible section header // ============================================================================ // Renders a full-width transparent clickable header row: `label` drawn on the // left and an expand/collapse chevron on the right. Toggles `expanded` when // clicked and returns the (possibly toggled) state. The label font governs // the row height (frame height) and text metrics; the chevron uses the small // icon font. `arrowInset` shifts the chevron left of the right edge. inline bool CollapsibleHeader(ImDrawList* dl, const char* id, const char* label, bool& expanded, float width, ImFont* labelFont, ImU32 labelCol, float arrowInset = 0.0f) { const char* arrow = expanded ? ICON_MD_EXPAND_LESS : ICON_MD_EXPAND_MORE; ImGui::PushFont(labelFont); ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0,0,0,0)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(1,1,1,0.05f)); ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(1,1,1,0.08f)); { ImVec2 hdrPos = ImGui::GetCursorScreenPos(); if (ImGui::Button(id, ImVec2(width, ImGui::GetFrameHeight()))) { expanded = !expanded; } float textY = hdrPos.y + (ImGui::GetFrameHeight() - labelFont->LegacySize) * 0.5f; dl->AddText(labelFont, labelFont->LegacySize, ImVec2(hdrPos.x, textY), labelCol, label); ImFont* iconFont = Type().iconSmall(); if (!iconFont) iconFont = labelFont; float arrowW = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, arrow).x; dl->AddText(iconFont, iconFont->LegacySize, ImVec2(hdrPos.x + width - arrowW - arrowInset, textY), labelCol, arrow); } ImGui::PopStyleColor(3); ImGui::PopFont(); return expanded; } // ============================================================================ // 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; }; // When a full-window blur overlay (e.g. the portfolio modal) is active, glass panels use their // opaque fallback instead of the acrylic blur. They're covered by the overlay's backdrop anyway, and // this leaves the backdrop as the SOLE applyBlur caller — so it can use its own (stronger) blur // radius without thrashing the shared, radius-keyed blur cache. Toggled from App::render(). inline bool& FullWindowBlurOverlayActiveRef() { static bool v = false; return v; } inline void SetFullWindowBlurOverlayActive(bool v) { FullWindowBlurOverlayActiveRef() = v; } inline bool IsFullWindowBlurOverlayActive() { return FullWindowBlurOverlayActiveRef(); } // Light-surface themes (Light / Marble) want cards to lift off the pale background with a subtle // drop shadow. Cached per theme-generation so the string compare runs once per theme load, not per // panel. (Data-driven later if more themes want it.) inline bool CardDropShadowWanted() { static uint32_t s_gen = ~0u; static bool s_want = false; uint32_t g = schema::UI().generation(); if (g != s_gen) { s_gen = g; const std::string& tn = schema::UI().themeName(); s_want = (tn == "Light" || tn == "Marble"); } return s_want; } // Soft, UNIFORM shadow around a rounded rect — no directional offset, equal on all sides. Drawn as // fading rounded-rect STROKES (not fills), so it never paints across the card body — only the outer // blur remains once the card fill covers the interior. The "render then mask out the UI area" result // without an FBO. inline void DrawCardDropShadow(ImDrawList* dl, const ImVec2& pMin, const ImVec2& pMax, float rounding) { const float dp = Layout::dpiScale(); // Matches the design mockup's `box-shadow: 0 0 4px rgb(50 53 58 / 26%)`: a tight, darker, colored // uniform shadow. Sampled as a stack of rounded-rect strokes stepping OUTWARD from the card edge // with a Gaussian alpha falloff (darkest at the edge, ~0 by the tail). ~1px-spaced, near-1px-thick // rings keep overlap (and thus alpha accumulation) low, so the peak reads close to the target. const float blur = 4.0f * dp; // CSS-like blur radius const float reach = blur * 1.5f; // how far the shadow extends beyond the card edge (all sides) const float sigma = blur * 0.6f; const float peak = 34.0f; // edge alpha for rgb(50,53,58) (~13% after the card fill masks the inner half) const int steps = std::max(6, (int)(reach / dp + 0.5f)); // The shadow rides on the card's OWN draw list (so it keeps the right z-order in EVERY context — // main content, modals, foreground popups, notifications; a global background draw list would drop // a modal/popup card's shadow behind its scrim). But cards often live inside clipping child windows // whose clip rect chops the shadow wherever a card sits flush with the child's top/bottom/side edge. // Clip instead to the shadow's own bounding box (card ± reach), so all four sides show in full — // bounded on each side to at most `reach` beyond the child clip (and the viewport) so a card scrolled // partly out of a scroll area still can't smear its shadow more than the halo width into neighbours. ImGuiViewport* vp = ImGui::GetMainViewport(); const ImVec2 cur0 = dl->GetClipRectMin(); const ImVec2 cur1 = dl->GetClipRectMax(); const float x0 = std::max(vp->Pos.x, std::max(pMin.x - reach, cur0.x - reach)); const float y0 = std::max(vp->Pos.y, std::max(pMin.y - reach, cur0.y - reach)); const float x1 = std::min(vp->Pos.x + vp->Size.x, std::min(pMax.x + reach, cur1.x + reach)); const float y1 = std::min(vp->Pos.y + vp->Size.y, std::min(pMax.y + reach, cur1.y + reach)); dl->PushClipRect(ImVec2(x0, y0), ImVec2(x1, y1), false); for (int i = 0; i < steps; ++i) { float d = reach * (float)i / (float)(steps - 1); // 0 (card edge) .. reach (outer tail) float g = std::exp(-(d * d) / (2.0f * sigma * sigma)); int a = (int)(peak * g + 0.5f); if (a < 1) continue; ImVec2 smn(pMin.x - d, pMin.y - d); ImVec2 smx(pMax.x + d, pMax.y + d); dl->AddRect(smn, smx, IM_COL32(50, 53, 58, a), rounding + d, 0, 1.3f * dp); } dl->PopClipRect(); } inline void DrawGlassPanel(ImDrawList* dl, const ImVec2& pMin, const ImVec2& pMax, const GlassPanelSpec& spec = GlassPanelSpec()) { // Subtle drop shadow behind the card (light themes only). Drawn first so the card fill below // covers the interior, leaving only the outer soft blur — the card body is never contaminated. if (CardDropShadowWanted()) DrawCardDropShadow(dl, pMin, pMax, spec.rounding); if (IsBackdropActive() && !dragonx::ui::effects::isLowSpecMode() && !IsFullWindowBlurOverlayActive()) { // --- 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. // Noise tint is cached per-generation, opacity checked each panel. { float noiseMul = dragonx::ui::effects::ImGuiAcrylic::GetNoiseOpacity(); if (noiseMul > 0.0f) { // Tint base color changes only on theme reload; opacity slider may change per-frame static uint32_t s_noiseGen = 0; static uint8_t s_baseAlpha = 0; if (curGen != s_noiseGen) { s_noiseGen = curGen; s_baseAlpha = (s_glassNoiseTint >> IM_COL32_A_SHIFT) & 0xFF; } uint8_t scaledAlpha = static_cast(std::min(255.0f, s_baseAlpha * noiseMul)); ImU32 noiseTint = (s_glassNoiseTint & ~(0xFFu << IM_COL32_A_SHIFT)) | (scaledAlpha << IM_COL32_A_SHIFT); float inset = spec.rounding * 0.3f; ImVec2 noiseMin(pMin.x + inset, pMin.y + inset); ImVec2 noiseMax(pMax.x - inset, pMax.y - inset); // Image rect matches clip bounds exactly — no PushClipRect needed dragonx::util::DrawTiledNoiseRect(dl, noiseMin, noiseMax, noiseTint); } } // 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(); if (fx.hasAnyPanelEffect()) { 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); } } // Full-window backdrop for a modal overlay. Lays an OPAQUE base first (so the sheet reads as // opaque regardless of the UI-opacity slider — the acrylic path multiplies its alpha by // GetUIOpacity(), so it can't guarantee opacity on its own), then — only when the acrylic backdrop // is active and we are NOT in low-performance mode — draws a strong blur of the (static) window // backdrop on top plus a dim. In low-perf mode only the opaque base is drawn (no blur cost), // mirroring DrawGlassPanel's gating. // `allowBlur=false` draws only the opaque base (used on the overlay's first frame, before the live // content has been captured, to avoid a one-frame blur of stale/background pixels). inline void DrawFullWindowBlurBackdrop(ImDrawList* dl, const ImVec2& pMin, const ImVec2& pMax, bool allowBlur = true) { // Theme-aware tones: a light theme gets a light frosted backdrop, a dark theme a dark one — the // backdrop tints toward the app background rather than a hardcoded dark. dl->AddRectFilled(pMin, pMax, WithAlpha(Background(), 255)); // opaque base (fallback / faint-blur show-through) if (allowBlur && IsBackdropActive() && !dragonx::ui::effects::isLowSpecMode() && effects::ImGuiAcrylic::IsEnabled() && effects::ImGuiAcrylic::IsAvailable()) { // Safe to use a strong custom radius here: while a full-window overlay is active every glass // panel skips acrylic (IsFullWindowBlurOverlayActive), so the backdrop is the SOLE applyBlur // caller — no other radius contends for the shared, radius-keyed blur cache. auto params = GetCurrentAcrylicTheme().card; params.blurRadius = 64.0f; // strong full-window blur params.fallbackColor.w = 1.0f; // full-strength blur so the live content reads effects::ImGuiAcrylic::DrawAcrylicRect(dl, pMin, pMax, params, 0.0f); dl->AddRectFilled(pMin, pMax, WithAlpha(Background(), 70)); // subtle theme-tinted frost so the card reads as foreground } } // ============================================================================ // Glass Card Scope (RAII) — channel-split glass-card scaffold // ============================================================================ // Captures the current cursor as the card's top-left, splits the draw list // into two channels (content on 1, glass on 0), insets the content by `pad`, // and — on scope exit — appends the bottom pad, sizes the card to its content, // draws the glass panel behind it on channel 0, merges, and advances the // cursor past the card. `width` is the full card width, `bottomPad` the // trailing pad. Reproduces the hand-written prologue/epilogue exactly. // // { // material::GlassCardScope card(dl, availWidth, pad, bottomPad, glassSpec); // ... card content ... // } struct GlassCardScope { ImDrawList* dl; ImVec2 cardMin; float width; float pad; float bottomPad; GlassPanelSpec spec; GlassCardScope(ImDrawList* dl_, float width_, float pad_, float bottomPad_, const GlassPanelSpec& spec_) : dl(dl_), width(width_), pad(pad_), bottomPad(bottomPad_), spec(spec_) { cardMin = ImGui::GetCursorScreenPos(); dl->ChannelsSplit(2); dl->ChannelsSetCurrent(1); ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMin.y + pad)); ImGui::Indent(pad); } ~GlassCardScope() { ImGui::Dummy(ImVec2(0, bottomPad)); ImGui::Unindent(pad); ImVec2 cardMax(cardMin.x + width, ImGui::GetCursorScreenPos().y); dl->ChannelsSetCurrent(0); DrawGlassPanel(dl, cardMin, cardMax, spec); dl->ChannelsMerge(); ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y)); ImGui::Dummy(ImVec2(width, 0)); } GlassCardScope(const GlassCardScope&) = delete; GlassCardScope& operator=(const GlassCardScope&) = delete; }; // ============================================================================ // 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(); DrawButtonGlassOverlay(dl, bMin, bMax, rounding, active, hovered); 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(a * alpha); v.col = (v.col & ~IM_COL32_A_MASK) | (static_cast(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 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 the cursor is inside this child window. // Use a direct position check instead of IsWindowHovered() which can // return false when an ActiveId exists (focused input, held button) or // when tooltip/popup windows from row hovers interfere with hover // routing. Still skip when a real popup (combo dropdown, context menu) // is open so the popup handles its own scrolling. { bool popupOpen = ImGui::IsPopupOpen("", ImGuiPopupFlags_AnyPopupId | ImGuiPopupFlags_AnyPopupLevel); ImVec2 mpos = ImGui::GetIO().MousePos; ImVec2 wmin = win->Pos; ImVec2 wmax = ImVec2(wmin.x + win->Size.x, wmin.y + win->Size.y); bool inside = (mpos.x >= wmin.x && mpos.x < wmax.x && mpos.y >= wmin.y && mpos.y < wmax.y); if (!popupOpen && inside) { 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); } // ============================================================================ // Dialog Title Bar with Close Button // ============================================================================ // Draws a custom title bar for modal popups with a title and close button. // Returns true if the close button was clicked. inline bool DrawDialogTitleBar(const char* title, bool* p_open, ImU32 accent_col = 0) { bool closeClicked = false; ImDrawList* dl = ImGui::GetWindowDrawList(); ImVec2 winPos = ImGui::GetWindowPos(); float winWidth = ImGui::GetWindowWidth(); float barHeight = 36.0f; // Get accent color from theme if not provided if (!accent_col) { accent_col = schema::UI().resolveColor("var(--primary)", IM_COL32(76, 175, 80, 255)); } // Draw title bar background with subtle gradient ImVec2 barMin = winPos; ImVec2 barMax(winPos.x + winWidth, winPos.y + barHeight); // Slightly darker top edge ImU32 barTop = IM_COL32(0, 0, 0, 40); ImU32 barBot = IM_COL32(0, 0, 0, 20); dl->AddRectFilledMultiColor(barMin, barMax, barTop, barTop, barBot, barBot); // Accent line at bottom of title bar dl->AddLine(ImVec2(barMin.x, barMax.y), ImVec2(barMax.x, barMax.y), ScaleAlpha(accent_col, 0.6f), 1.0f); // Title text ImFont* titleFont = Type().subtitle1(); ImGui::PushFont(titleFont); ImVec2 titleSize = ImGui::CalcTextSize(title); float titleX = barMin.x + 16.0f; float titleY = barMin.y + (barHeight - titleSize.y) * 0.5f; DrawTextShadow(dl, ImVec2(titleX, titleY), OnSurface(), title); ImGui::PopFont(); // Close button (X) on right side if (p_open) { float btnSize = 24.0f; float btnX = barMax.x - btnSize - 12.0f; float btnY = barMin.y + (barHeight - btnSize) * 0.5f; ImVec2 btnMin(btnX, btnY); ImVec2 btnMax(btnX + btnSize, btnY + btnSize); bool hovered = ImGui::IsMouseHoveringRect(btnMin, btnMax); bool held = false; if (hovered && ImGui::IsMouseClicked(0)) { held = true; } // Button background on hover if (hovered) { dl->AddRectFilled(btnMin, btnMax, IM_COL32(255, 255, 255, held ? 40 : 25), 4.0f); } // Draw X icon ImGui::PushFont(Type().iconSmall()); ImVec2 iconSize = ImGui::CalcTextSize(ICON_MD_CLOSE); float iconX = btnX + (btnSize - iconSize.x) * 0.5f; float iconY = btnY + (btnSize - iconSize.y) * 0.5f; dl->AddText(ImVec2(iconX, iconY), hovered ? IM_COL32(255, 255, 255, 255) : OnSurfaceMedium(), ICON_MD_CLOSE); ImGui::PopFont(); // Handle click if (hovered && ImGui::IsMouseReleased(0)) { *p_open = false; closeClicked = true; } } // Reserve space for title bar so content starts below it ImGui::SetCursorPosY(ImGui::GetCursorPosY() + barHeight + 8.0f); return closeClicked; } // ============================================================================ // Blur-backdrop modal primitives // ============================================================================ // Shared building blocks for the "Manage Portfolio"-style overlay (live-blur backdrop, floating // content, segmented section nav, edge-fade scroll). Promoted verbatim from the Market tab so every // modal can adopt the same look. The overlay begin/end (below) drives these. // "A blur overlay was drawn this frame" flag + a 1-frame latch. App::render builds its // effect-suppression guard at the TOP of the frame, before dialogs dispatch, so it reads the // LAST-frame latch (a stable signal) rather than this frame's in-progress state. inline bool& BlurOverlayDrawnThisFrameRef() { static bool v = false; return v; } inline bool& BlurOverlayActiveLastFrameRef() { static bool v = false; return v; } inline bool*& BlurOverlayOpenFlagRef() { static bool* p = nullptr; return p; } // Record that a blur overlay drew this frame, along with its open flag (so the latch below can tell // "still open next frame" from "closed this frame" and match the modal's actual open state — no // extra frame of suppression on close). inline void MarkBlurOverlayDrawn(bool* openFlag) { BlurOverlayDrawnThisFrameRef() = true; BlurOverlayOpenFlagRef() = openFlag; } inline bool AnyBlurOverlayActiveLastFrame() { return BlurOverlayActiveLastFrameRef(); } // Latch whether an overlay will still be up next frame (drew this frame AND its open flag is still // set after this frame's Esc/Close/outside-click handling); on the open->closed transition, // invalidate the acrylic capture so the other glass panels re-capture the (overlay-free) // background. Call once at the end of App::render. inline void LatchBlurOverlayActive() { bool* pf = BlurOverlayOpenFlagRef(); bool stillOpen = BlurOverlayDrawnThisFrameRef() && (!pf || *pf); if (BlurOverlayActiveLastFrameRef() && !stillOpen) effects::ImGuiAcrylic::InvalidateCapture(); BlurOverlayActiveLastFrameRef() = stillOpen; BlurOverlayDrawnThisFrameRef() = false; BlurOverlayOpenFlagRef() = nullptr; } // True during the frames a blur overlay is (re)capturing the live backdrop — the scroll-fade shader // is suppressed then, since the acrylic capture rebinds render state on those frames. inline bool& CapturingBlurBackdropRef() { static bool v = false; return v; } inline bool IsCapturingBlurBackdrop() { return CapturingBlurBackdropRef(); } // Per-overlay capture-arming state (keyed by the scrim window id) so each blur overlay arms its // capture-once independently on open/resize, detected via frame-count gaps (no close sweep needed). struct BlurCaptureState { int lastSeenFrame = -2; int captureFrames = 0; float w = 0.0f, h = 0.0f; }; inline BlurCaptureState& BlurCaptureStateFor(const std::string& key) { static std::unordered_map m; return m[key]; } // Right-align the next item(s) of total width `w` within the remaining content region (cursor form). inline void RightAlignX(float w) { float avail = ImGui::GetContentRegionAvail().x; if (avail > w) ImGui::SetCursorPosX(ImGui::GetCursorPosX() + avail - w); } // Equal-width segmented control (rounded track + N cells, each a centered label + an active pill) at // screen-space `origin`, spanning `totalW` x `height`. Each cell is a hit-tested InvisibleButton // (Hand cursor on hover), disambiguated by `idBase`. Does NOT move the layout cursor. Returns the // clicked cell index, or -1. inline int SegmentedControl(ImDrawList* dl, ImVec2 origin, float totalW, float height, const char* const* labels, int count, int selected, ImFont* font, const char* idBase, float dp) { int clicked = -1; float cellW = totalW / (float)count; dl->AddRectFilled(origin, ImVec2(origin.x + totalW, origin.y + height), WithAlpha(OnSurface(), 20), height * 0.5f); for (int i = 0; i < count; i++) { ImVec2 cMin(origin.x + i * cellW, origin.y), cMax(cMin.x + cellW, origin.y + height); bool active = (selected == i); bool hov = ImGui::IsMouseHoveringRect(cMin, cMax); if (active) dl->AddRectFilled(ImVec2(cMin.x + 2.0f * dp, cMin.y + 2.0f * dp), ImVec2(cMax.x - 2.0f * dp, cMax.y - 2.0f * dp), WithAlpha(Primary(), 210), (height - 4.0f * dp) * 0.5f); ImVec2 ts = font->CalcTextSizeA(font->LegacySize, FLT_MAX, 0, labels[i]); dl->AddText(font, font->LegacySize, ImVec2(cMin.x + (cellW - ts.x) * 0.5f, cMin.y + (height - ts.y) * 0.5f), active ? IM_COL32(255, 255, 255, 255) : (hov ? OnSurface() : OnSurfaceMedium()), labels[i]); ImGui::PushID(i); ImGui::SetCursorScreenPos(cMin); if (ImGui::InvisibleButton(idBase, ImVec2(cellW, height))) clicked = i; if (ImGui::IsItemHovered()) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); ImGui::PopID(); } return clicked; } // The edge-fade scroll child (BeginFadeScrollChild/EndFadeScrollChild) depends on // effects::ScrollFadeShader, whose GL/DX headers must not be pulled into this widely-included base // header — it lives in material/overlay_scroll.h, included only by modal implementations. // ============================================================================ // Fullscreen Overlay Dialog // ============================================================================ // Creates a fullscreen semi-transparent overlay with a centered card dialog. // Similar to the shutdown screen pattern but for interactive dialogs. // Per-dialog content state (keyed by the child's id), measured at the end of each frame so the next // frame can position/size the (auto-height) card to its content. An auto-resize child reports a // too-small height on its first render and the true height a frame later, so we keep the card hidden // until the height holds steady (stableCount) before revealing it centered — otherwise it visibly // slides for a couple frames as the height converges. g_overlayCurrentKey carries the active dialog's // key from BeginOverlayDialog to EndOverlayDialog (overlays don't nest). struct OverlayCardState { float height = 0.0f; // last measured content-child height int stableCount = 0; // consecutive frames the height held steady (within 1px) int appearFrames = 0; // frames since (re)appearing while still hidden — a safety cap bool shown = false; // revealed (centered) at least once this open; don't re-hide after }; inline std::unordered_map g_overlayCardHeights; inline std::string g_overlayCurrentKey; // Set when BeginOverlayDialog clips the card on an auto-height dialog's measuring frame (so it never // flashes off-center); EndOverlayDialog pops the clip. Overlays don't nest, so a single flag is fine. inline bool g_overlayHideFrameClip = false; // The dark base color for any full-window overlay backdrop (dialog scrims + the shutdown screen), // so every overlay shares one backdrop tone. `opacity` is the alpha (dialogs vary it per call). inline ImVec4 OverlayScrimColor(float opacity) { return ImVec4(0.04f, 0.04f, 0.06f, opacity); } // Style of an overlay dialog. GlassCard = the classic scrim + glass card + boxed title bar. // BlurFloat = the "Manage Portfolio" look: live-blur backdrop + floating content + plain heading. enum class OverlayStyle { GlassCard, BlurFloat }; // Spec-driven overlay. The flags below are orthogonal modifiers on one scaffold; OverlayStyle is a // convenience preset (GlassCard = all off; BlurFloat = blurBackdrop+floatingContent+plainHeading). struct OverlayDialogSpec { const char* title = nullptr; bool* p_open = nullptr; OverlayStyle style = OverlayStyle::GlassCard; bool blurBackdrop = false; // live-blur backdrop instead of the opaque scrim bool floatingContent = false; // transparent card (no glass panel — content floats on the blur) bool plainHeading = false; // Type().h6() heading instead of the boxed DrawDialogTitleBar float cardWidth = 460.0f; // scaled by dpiScale; clamped to viewport float cardHeight = 0.0f; // 0 => auto-height (content-sized); else fixed height (large modals) float scrimOpacity = 0.92f; // GlassCard scrim only float cardBottomViewportRatio = 0.85f; // auto-height cap const char* idSuffix = nullptr; // distinct window/child ids for nesting + capture keying }; inline bool BeginOverlayDialog(const OverlayDialogSpec& spec) { const bool blur = spec.blurBackdrop || spec.style == OverlayStyle::BlurFloat; const bool floating = spec.floatingContent || spec.style == OverlayStyle::BlurFloat; const bool plainHd = spec.plainHeading || spec.style == OverlayStyle::BlurFloat; const float dp = Layout::dpiScale(); MarkOverlayDialogActive(); ImGuiViewport* vp = ImGui::GetMainViewport(); ImVec2 vp_pos = vp->Pos, vp_size = vp->Size; // Cards are authored in raw px but scale with dpiScale (font-size setting); clamp to viewport. float cardWidth = spec.cardWidth * dp; cardWidth = std::min(cardWidth, vp_size.x - 32.0f); ImGui::SetNextWindowPos(vp_pos); ImGui::SetNextWindowSize(vp_size); // Focus the scrim so the dialog owns input — but not while a popup (combo/color picker) opened // from the dialog is showing, or forcing focus every frame would immediately close that popup. const bool overlayPopupOpen = ImGui::IsPopupOpen("", ImGuiPopupFlags_AnyPopupId | ImGuiPopupFlags_AnyPopupLevel); if (!overlayPopupOpen) ImGui::SetNextWindowFocus(); // Blur overlays draw their own backdrop, so the window itself is transparent. ImGui::PushStyleColor(ImGuiCol_WindowBg, blur ? ImVec4(0, 0, 0, 0) : OverlayScrimColor(spec.scrimOpacity)); ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0)); std::string scrimId = "##OverlayScrim"; std::string childId = "##OverlayDialogContent"; if (spec.idSuffix && spec.idSuffix[0] != '\0') { scrimId += "_"; scrimId += spec.idSuffix; childId += "_"; childId += spec.idSuffix; } bool opened = ImGui::Begin(scrimId.c_str(), nullptr, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse | ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoSavedSettings); if (!opened) { ImGui::End(); ImGui::PopStyleVar(3); ImGui::PopStyleColor(); return false; } // True only on the frame the dialog (re)opens — used to re-arm the auto-height settle so each // fresh open reveals already-centered (rather than flashing last-open's stale position). const bool scrimAppearing = ImGui::IsWindowAppearing(); ImDrawList* dl = ImGui::GetWindowDrawList(); // Live-blur backdrop with capture-once: (re)capture the content drawn BELOW this overlay for a // few frames on open/resize, then reuse the frozen blurred snapshot. Low-spec / acrylic-off falls // back to an opaque base inside DrawFullWindowBlurBackdrop (no blur cost). if (blur) { MarkBlurOverlayDrawn(spec.p_open); BlurCaptureState& st = BlurCaptureStateFor(scrimId); int frame = ImGui::GetFrameCount(); bool justOpened = (st.lastSeenFrame < frame - 1); st.lastSeenFrame = frame; if (justOpened || st.w != vp_size.x || st.h != vp_size.y) st.captureFrames = 3; st.w = vp_size.x; st.h = vp_size.y; CapturingBlurBackdropRef() = (st.captureFrames > 0); if (st.captureFrames > 0) { if (ImDrawCallback liveCap = effects::ImGuiAcrylic::GetLiveCaptureCallback()) { dl->AddCallback(liveCap, nullptr); dl->AddCallback(ImDrawCallback_ResetRenderState, nullptr); } st.captureFrames--; } DrawFullWindowBlurBackdrop(dl, vp_pos, ImVec2(vp_pos.x + vp_size.x, vp_pos.y + vp_size.y), /*allowBlur=*/!justOpened); // opaque the first frame (nothing captured yet) } else { CapturingBlurBackdropRef() = false; } // Consume pointer input on the scrim so the overlay owns clicks/wheel even outside the card. ImGui::SetCursorScreenPos(vp_pos); ImGui::InvisibleButton("##OverlayInputBlocker", vp_size, ImGuiButtonFlags_MouseButtonLeft | ImGuiButtonFlags_MouseButtonRight | ImGuiButtonFlags_MouseButtonMiddle); // Card geometry: both fixed- and auto-height cards are VERTICALLY CENTERED in the window. // Fixed-height centers its known height; auto-height centers last frame's measured content // height (cached per dialog id, so a re-opened dialog is centered from frame 1 — only the very // first open of a given dialog this session briefly anchors near the top before it re-centers). float cardX = vp_pos.x + (vp_size.x - cardWidth) * 0.5f; float cardY, cardBottomY; bool hideForMeasure = false; // true on an auto-height dialog's first (unmeasured) frame const bool fixedHeight = (spec.cardHeight > 0.0f); if (fixedHeight) { float cardH = std::min(spec.cardHeight * dp, vp_size.y - 32.0f); cardY = vp_pos.y + (vp_size.y - cardH) * 0.5f; cardBottomY = cardY + cardH; g_overlayCurrentKey.clear(); } else { g_overlayCurrentKey = childId; OverlayCardState& cs = g_overlayCardHeights[childId]; if (scrimAppearing) { cs.shown = false; cs.stableCount = 0; cs.appearFrames = 0; } if (!cs.shown) cs.appearFrames++; const float measuredH = cs.height; // Reveal once the measured height has settled (auto-resize converges in ~2 frames) or it's // already been shown this open (don't re-hide on a mid-dialog content change); a frame cap // guarantees a pathological ever-changing height can't hide the dialog forever. const bool ready = measuredH > 0.0f && (cs.shown || cs.stableCount >= 1 || cs.appearFrames >= 8); if (ready) { cs.shown = true; // Center the measured content; if it's taller than the window, anchor at the top margin. cardY = (measuredH < vp_size.y - 32.0f) ? vp_pos.y + (vp_size.y - measuredH) * 0.5f : vp_pos.y + 16.0f; cardBottomY = cardY + measuredH; } else { // Still settling: lay the content out (so the auto-height child gets measured) but keep // the card hidden (hideForMeasure below) so it never flashes off-center — it appears, // already centered and stable, once the height stops changing. hideForMeasure = true; cardY = vp_pos.y + vp_size.y * 0.15f; cardBottomY = vp_pos.y + vp_size.y * spec.cardBottomViewportRatio; } } ImVec2 cardMin(cardX, cardY), cardMax(cardX + cardWidth, cardBottomY); // Card background — a glass panel unless the content floats directly on the backdrop. Skipped on // the measuring frame (its geometry is a placeholder; the whole card is hidden until centered). if (!floating && !hideForMeasure) { GlassPanelSpec cardGlass; cardGlass.rounding = 16.0f; cardGlass.fillAlpha = 35; cardGlass.borderAlpha = 50; cardGlass.borderWidth = 1.0f; DrawGlassPanel(dl, cardMin, cardMax, cardGlass); } // Click outside the card dismisses — but not on the measuring frame (placeholder geometry), the // appearing frame (the opening click), or while a popup opened from the dialog is showing. if (spec.p_open && !hideForMeasure && !overlayPopupOpen && !ImGui::IsWindowAppearing() && ImGui::IsMouseClicked(ImGuiMouseButton_Left) && !ImGui::IsMouseHoveringRect(cardMin, cardMax, false)) { *spec.p_open = false; } // Content child. ImGui::SetCursorScreenPos(ImVec2(cardX, cardY)); ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, floating ? 20.0f : 16.0f); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, floating ? ImVec2(28, 20) : ImVec2(28, 24)); ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0, 0, 0, 0)); // transparent (glass/blur behind) ImGuiChildFlags cflags = ImGuiChildFlags_AlwaysUseWindowPadding | (fixedHeight ? 0 : ImGuiChildFlags_AutoResizeY); // NoScrollWithMouse (not just NoScrollbar): a modal is a fixed frame — the wheel must never drift // the WHOLE card. If content marginally overflows a fixed card, the wheel would otherwise scroll // the entire dialog (title + footer and all). Inner scroll regions (lists, notes) still scroll on // their own; auto-height cards resize to content so they never overflow anyway. bool childVisible = ImGui::BeginChild(childId.c_str(), ImVec2(cardWidth, fixedHeight ? (cardBottomY - cardY) : 0.0f), cflags, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse); // Floating (portfolio-style) cards: the padding applies to this content child only, so pop it // now (nested children mustn't inherit it), and center button labels. Net style-var count stays // at 2 (ChildRounding + ButtonTextAlign) so EndOverlayDialog's PopStyleVar(2) is unchanged. if (floating) { ImGui::PopStyleVar(); // WindowPadding ImGui::PushStyleVar(ImGuiStyleVar_ButtonTextAlign, ImVec2(0.5f, 0.5f)); } // Measuring frame: clip the card to nothing. Content (title + the caller's body) still lays out, // so the child's auto-height is measured for next frame, but nothing is drawn — no off-center // flash. EndOverlayDialog pops this before EndChild. Only entered when childVisible (End runs). if (childVisible && hideForMeasure) { ImGui::PushClipRect(ImVec2(0, 0), ImVec2(0, 0), false); g_overlayHideFrameClip = true; } if (childVisible && spec.title) { if (plainHd) { ImGui::PushFont(Type().h6()); ImGui::TextUnformatted(spec.title); ImGui::PopFont(); ImGui::Dummy(ImVec2(0, Layout::spacingXs())); } else { DrawDialogTitleBar(spec.title, spec.p_open); } } return childVisible; } // Positional overload — the default look for the app's dialogs: content floats directly on the // live-blur backdrop (no glass card) under a plain h6 heading, matching the Manage-Portfolio // editor. Auto-height keeps short dialogs short. A dialog that needs the old contained card can // pass an explicit OverlayDialogSpec with style = OverlayStyle::GlassCard. inline bool BeginOverlayDialog(const char* title, bool* p_open, float cardWidth = 460.0f, float scrimOpacity = 0.92f, float cardBottomViewportRatio = 0.85f, const char* idSuffix = nullptr) { OverlayDialogSpec s; s.title = title; s.p_open = p_open; s.cardWidth = cardWidth; s.scrimOpacity = scrimOpacity; s.cardBottomViewportRatio = cardBottomViewportRatio; s.idSuffix = idSuffix; s.blurBackdrop = true; s.floatingContent = true; // no glass card — content floats on the blur s.plainHeading = true; // h6 heading instead of the boxed title bar (drops the ✕) return BeginOverlayDialog(s); } inline void EndOverlayDialog() { if (g_overlayHideFrameClip) { ImGui::PopClipRect(); g_overlayHideFrameClip = false; } ImGui::EndChild(); // Remember the rendered card height (the child is the last item) + track whether it has stopped // changing, so next frame's BeginOverlayDialog can reveal the card only once its height (and thus // its centered position) is stable. if (!g_overlayCurrentKey.empty()) { OverlayCardState& cs = g_overlayCardHeights[g_overlayCurrentKey]; const float h = ImGui::GetItemRectSize().y; const float d = (h >= cs.height) ? (h - cs.height) : (cs.height - h); if (h > 0.0f && d <= 1.0f) cs.stableCount++; else cs.stableCount = 0; cs.height = h; } ImGui::PopStyleColor(); // ChildBg ImGui::PopStyleVar(2); // ChildRounding, WindowPadding (for child) ImGui::End(); ImGui::PopStyleVar(3); // WindowRounding, WindowBorderSize, WindowPadding (for scrim) ImGui::PopStyleColor(); // WindowBg scrim } inline void PlaceOverlayDialogActions(float totalWidth) { float rowStartX = ImGui::GetCursorPosX(); float contentW = ImGui::GetContentRegionAvail().x; ImGui::SetCursorPosX(rowStartX + std::max(0.0f, (contentW - totalWidth) * 0.5f)); } inline void BeginOverlayDialogFooter(float totalActionWidth, bool drawSeparator = true) { ImGui::Spacing(); if (drawSeparator) { ImGui::Separator(); ImGui::Spacing(); } PlaceOverlayDialogActions(totalActionWidth); } // ── Confirmation dialog header / footer ───────────────────────────────── // Shared scaffolding for the "warning + Cancel/confirm" overlay dialogs. // Warning icon + "Warning" label row, drawn in `col` (the caller owns the // exact accent colour; new callers can omit it to use the theme warning). // Reproduces: PushFont(iconLarge) + ICON_MD_WARNING, PopFont, SameLine, // TextColored(label). The label text is passed in (already translated). inline void DialogWarningHeader(const char* warningLabel, const ImVec4& col = WarningVec4()) { ImGui::PushFont(Type().iconLarge()); ImGui::TextColored(col, ICON_MD_WARNING); ImGui::PopFont(); ImGui::SameLine(); ImGui::TextColored(col, "%s", warningLabel); } // 50/50 Cancel / confirm footer for overlay dialogs. Sets `outCancel` / // `outConfirm` when the respective button is pressed (the caller runs the // action bodies). When `danger`, the confirm button gets the red danger // style. Button height comes from ui.toml (falls back to 40). Reproduces // the hand-written footer exactly (btnW split + SameLine + optional danger // PushStyleColor/PopStyleColor(2)). inline void DialogConfirmFooter(const char* cancelId, const char* confirmLabel, bool danger, bool& outCancel, bool& outConfirm) { float btnH = schema::UI().drawElement("components.overlay-dialog", "confirm-btn-height").sizeOr(40.0f); float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f; if (ImGui::Button(cancelId, ImVec2(btnW, btnH))) { outCancel = true; } ImGui::SameLine(); if (danger) { ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.8f, 0.2f, 0.2f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.9f, 0.3f, 0.3f, 1.0f)); } if (ImGui::Button(confirmLabel, ImVec2(btnW, btnH))) { outConfirm = true; } if (danger) { ImGui::PopStyleColor(2); } } } // namespace material } // namespace ui } // namespace dragonx