// DragonX Wallet - ImGui Edition // Copyright 2024-2026 The Hush Developers // Released under the GPLv3 // // Settings design-system controls — the polished chat-settings look (accent subsection headers, // labeled rows with right-aligned controls, iOS-style segmented controls) promoted to reusable // components, plus a tiered ActionButton (Primary/Secondary/Tertiary/Destructive) with optional // leading Material icon and a ButtonFlow that wraps rows of buttons instead of shrinking them. #pragma once #include "draw_helpers.h" // colors, type, layout, icons, WithAlpha, ScaleAlpha, DrawButtonGlassOverlay #include "imgui.h" #include namespace dragonx { namespace ui { namespace material { // Accent small-caps subsection header ("KEYS & BACKUP", "APPEARANCE"…) — the chat-settings section() look. inline void SettingsSubheader(const char* text) { const float dp = Layout::dpiScale(); ImGui::Dummy(ImVec2(0.0f, 8.0f * dp)); ImGui::PushFont(Type().caption()); ImGui::PushStyleColor(ImGuiCol_Text, WithAlpha(Primary(), 235)); ImGui::TextUnformatted(text); ImGui::PopStyleColor(); ImGui::PopFont(); ImGui::Dummy(ImVec2(0.0f, 2.0f * dp)); } // A labeled settings row: label left, control right-aligned in a fixed column. Construct once per card // section with the content width (0 = auto), then call .label(text) before drawing each control (leaves // the cursor at the control origin and sets the next item width to the control column). struct SettingsRow { float leftX, rowW, ctrlW, rowGap; explicit SettingsRow(float contentWidth, float controlWidth = 250.0f) { const float dp = Layout::dpiScale(); leftX = ImGui::GetCursorPosX(); rowW = (contentWidth > 0.0f) ? contentWidth : ImGui::GetContentRegionAvail().x; ctrlW = controlWidth * dp; rowGap = 5.0f * dp; } void label(const char* text) { ImGui::Dummy(ImVec2(0.0f, rowGap)); ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted(text); ImGui::SameLine(); ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), leftX + std::max(0.0f, rowW - ctrlW))); ImGui::SetNextItemWidth(ctrlW); } }; // iOS-style segmented control (rounded track + inset pill on the selection). Draws its own labeled row // and returns the (possibly changed) index. inline int SegmentedControl(SettingsRow& row, const char* label, const char* const* items, int count, int value) { const float dp = Layout::dpiScale(); row.label(label); const ImVec2 origin = ImGui::GetCursorScreenPos(); const float h = ImGui::GetFrameHeight(); const float seg = row.ctrlW / static_cast(count); const float round = 7.0f * dp; ImDrawList* dl = ImGui::GetWindowDrawList(); dl->AddRectFilled(origin, ImVec2(origin.x + row.ctrlW, origin.y + h), WithAlpha(OnSurface(), 20), round); int result = value; ImGui::PushID(label); for (int i = 0; i < count; ++i) { ImGui::PushID(i); const ImVec2 mn(origin.x + i * seg, origin.y), mx(origin.x + (i + 1) * seg, origin.y + h); ImGui::SetCursorScreenPos(mn); if (ImGui::InvisibleButton("##s", ImVec2(seg, h))) result = i; const bool hov = ImGui::IsItemHovered(); if (hov) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); const bool sel = (value == i); if (sel) { const float in = 2.0f * dp; dl->AddRectFilled(ImVec2(mn.x + in, mn.y + in), ImVec2(mx.x - in, mx.y - in), WithAlpha(Primary(), 210), std::max(1.0f, round - in)); } else if (hov) { dl->AddRectFilled(mn, mx, WithAlpha(OnSurface(), 26), round); } const ImVec2 ts = ImGui::CalcTextSize(items[i]); const float lpad = 4.0f * dp; const float tx = (ts.x <= seg - 2.0f * lpad) ? (seg - ts.x) * 0.5f : lpad; ImGui::PushClipRect(mn, mx, true); dl->AddText(ImVec2(mn.x + tx, mn.y + (h - ts.y) * 0.5f), sel ? IM_COL32(255, 255, 255, 236) : OnSurfaceMedium(), items[i]); ImGui::PopClipRect(); ImGui::PopID(); } ImGui::PopID(); ImGui::SetCursorScreenPos(origin); ImGui::Dummy(ImVec2(row.ctrlW, h)); // reserve the control's rect for layout flow return result; } // ── Tiered action buttons ─────────────────────────────────────────────────── // Primary = filled accent (the one main action of a group) // Secondary = glass (default — common actions) // Tertiary = ghost / low-emphasis (rarely used) // Destructive = error-tinted outline (delete / reset) enum class ActionTier { Primary, Secondary, Tertiary, Destructive }; // The width an ActionButton will occupy (for ButtonFlow / manual layout). inline float ActionButtonWidth(const char* label, const char* icon, float minWidth = 0.0f) { ImFont* lf = Type().button(); ImFont* icf = Type().iconSmall(); const float dp = Layout::dpiScale(); const float padX = 12.0f * dp, gap = 6.0f * dp; const float labelW = lf->CalcTextSizeA(lf->LegacySize, FLT_MAX, 0, label).x; const float iconW = (icon && icon[0] && icf) ? icf->CalcTextSizeA(icf->LegacySize, FLT_MAX, 0, icon).x : 0.0f; const float w = padX * 2.0f + iconW + (iconW > 0.0f ? gap : 0.0f) + labelW; return std::max(w, minWidth); } // A tiered action button with an optional leading Material icon (ICON_MD_* or nullptr). Auto-sizes to // its content (>= minWidth). Respects BeginDisabled() (dims via the style alpha, not clickable). inline bool ActionButton(const char* id, const char* label, const char* icon, ActionTier tier, float minWidth = 0.0f) { ImFont* lf = Type().button(); ImFont* icf = Type().iconSmall(); const float dp = Layout::dpiScale(); const float gap = 6.0f * dp; const float h = ImGui::GetFrameHeight(); const bool hasIcon = icon && icon[0] && icf; const float labelW = lf->CalcTextSizeA(lf->LegacySize, FLT_MAX, 0, label).x; const float iconW = hasIcon ? icf->CalcTextSizeA(icf->LegacySize, FLT_MAX, 0, icon).x : 0.0f; const float w = ActionButtonWidth(label, icon, minWidth); const ImVec2 pos = ImGui::GetCursorScreenPos(); const bool pressed = ImGui::InvisibleButton(id, ImVec2(w, h)); const bool hov = ImGui::IsItemHovered(), act = ImGui::IsItemActive(); if (hov) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); ImDrawList* dl = ImGui::GetWindowDrawList(); const ImVec2 pMax(pos.x + w, pos.y + h); const float round = ImGui::GetStyle().FrameRounding; const float a = ImGui::GetStyle().Alpha; // BeginDisabled() lowers this ImU32 bg = 0, border = 0, fg = OnSurface(); bool glass = false; switch (tier) { case ActionTier::Primary: bg = WithAlpha(Primary(), act ? 255 : (hov ? 245 : 220)); fg = IM_COL32(255, 255, 255, 240); break; case ActionTier::Secondary: bg = WithAlpha(OnSurface(), hov ? 26 : 16); border = WithAlpha(OnSurface(), 40); glass = true; fg = OnSurface(); break; case ActionTier::Tertiary: bg = hov ? WithAlpha(OnSurface(), 18) : 0; fg = OnSurfaceMedium(); break; case ActionTier::Destructive: bg = hov ? WithAlpha(Error(), 32) : WithAlpha(Error(), 12); border = WithAlpha(Error(), 90); fg = Error(); break; } if (bg) dl->AddRectFilled(pos, pMax, ScaleAlpha(bg, a), round); if (border) dl->AddRect(pos, pMax, ScaleAlpha(border, a), round, 0, 1.0f); if (glass) DrawButtonGlassOverlay(dl, pos, pMax, round, act, hov); fg = ScaleAlpha(fg, a); const float contentW = iconW + (iconW > 0.0f ? gap : 0.0f) + labelW; float cx = pos.x + (w - contentW) * 0.5f; if (hasIcon) { dl->AddText(icf, icf->LegacySize, ImVec2(cx, pos.y + (h - icf->LegacySize) * 0.5f), fg, icon); cx += iconW + gap; } dl->AddText(lf, lf->LegacySize, ImVec2(cx, pos.y + (h - lf->LegacySize) * 0.5f), fg, label); return pressed; } // Places ActionButtons left→right, wrapping to a new row when the next one won't fit (instead of the // old font-scale-to-fit). Call next(width) before each ActionButton. struct ButtonFlow { float availW, gap; float x = 0.0f; bool firstOnRow = true; explicit ButtonFlow(float availWidth, float gapPx = 8.0f) : availW(availWidth) { gap = gapPx * Layout::dpiScale(); } void next(float w) { if (firstOnRow) { firstOnRow = false; x = w; return; } if (x + gap + w <= availW) { ImGui::SameLine(0, gap); x += gap + w; } else { x = w; } // natural newline wraps to the next row } }; } // namespace material } // namespace ui } // namespace dragonx