Settings tabs brought closer to the approved mockup: - ActionButton/renderCardButton retune (settings-scoped): 7px radius, 9px padX, Primary → accent-outline chip, Secondary/card buttons more defined. - Daemon-binary card: compact status right-aligned on the DAEMON BINARY heading (Up to date / Version differs / Not installed), filled/rounded status box, neutral danger divider (was alarming red), roomier spacing. - RPC Connection: two-row column-aligned layout (Host | Port, then Username | Password) so the password no longer clips off the card edge. - Chat settings tab: live conversation preview below the Appearance / Messaging cards; "Focus input on open" checkbox reflowed onto the console color-toggle row. - Debug Options: "Current theme only" toggle restricts either screenshot sweep to the active theme instead of cycling every skin. - Tabs fill the full content width (content-max-width cap disabled) and the sidebar nav panel centers within the true visible area. - i18n: new keys for the above (untranslated keys fall back to English). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
196 lines
8.9 KiB
C++
196 lines
8.9 KiB
C++
// 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 <algorithm>
|
|
|
|
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<float>(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 = 9.0f * dp, gap = 6.0f * dp; // mockup .btn padding: 9px horizontal
|
|
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 = 7.0f * dp; // mockup .btn radius: 7px (softer than the global 4px frame)
|
|
const float a = ImGui::GetStyle().Alpha; // BeginDisabled() lowers this
|
|
|
|
ImU32 bg = 0, border = 0, fg = OnSurface();
|
|
bool glass = false;
|
|
switch (tier) {
|
|
case ActionTier::Primary:
|
|
// Mockup .btn.acc: a dark accent-tinted chip with accent TEXT — not a bright filled button.
|
|
bg = WithAlpha(Primary(), hov ? 52 : 38);
|
|
border = WithAlpha(Primary(), hov ? 150 : 110);
|
|
fg = Primary();
|
|
break;
|
|
case ActionTier::Secondary:
|
|
bg = WithAlpha(OnSurface(), hov ? 30 : 20);
|
|
border = WithAlpha(OnSurface(), 48);
|
|
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
|