feat(ui): in-app FAQ, DPI-scaling audit fixes, mining/stratum polish + localize strings
Bundles the session's UI work (the touched files carry several of these changes together, so they are committed as one coherent UI batch): - FAQ: new RenderFaqDialog + data-driven faq_content, opened from a status-bar "?" (and the Windows title bar), styled like the Wallets modal with search, Wallet/Daemon tabs, and smooth scroll. - DPI/font-scale audit: multiply hand-drawn absolute geometry by Layout::dpiScale() across ~30 files so nothing renders native-size at HiDPI / font_scale 1.5 (verified with a full sweep at 1.5x). - Mining: chart now fills the horizontal space; thread stepper +/- buttons match the input-box height; move the stratum-host toggle into Node & Security (v1.3.0+). - Settings: fix the auto-shield status text overlapping the grid. - Sidebar: drop the peer-count badge on the Network button. - i18n: wrap 193 hardcoded literals with TR() (keys/translations added in the preceding i18n commit), so the security/PIN/lock flow, seed-backup wizard, and witness-rebuild dialog localize. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
// DragonX Wallet - ImGui Edition
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
|
||||
#include "faq_dialog.h"
|
||||
#include "faq_content.h"
|
||||
#include "../../app.h"
|
||||
#include "../../util/i18n.h"
|
||||
#include "../../util/text_format.h"
|
||||
#include "../../embedded/IconsMaterialDesign.h"
|
||||
#include "../schema/ui_schema.h"
|
||||
#include "../layout.h"
|
||||
#include "../material/type.h"
|
||||
#include "../material/colors.h"
|
||||
#include "../material/draw_helpers.h"
|
||||
#include "imgui.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace dragonx {
|
||||
namespace ui {
|
||||
|
||||
namespace {
|
||||
|
||||
// Persists across frames (the dialog is re-entered each frame while open). Group 0 = Wallet,
|
||||
// 1 = Daemon. `expanded` keys are FaqEntry::questionKey (stable string literals from faq_content).
|
||||
struct FaqDialogState {
|
||||
int group = 0;
|
||||
char search[128] = "";
|
||||
std::unordered_map<std::string, bool> expanded;
|
||||
};
|
||||
FaqDialogState s_faq;
|
||||
|
||||
bool entryMatches(const faq::FaqEntry& e, const char* query)
|
||||
{
|
||||
return util::containsIgnoreCase(TR(e.questionKey), query) ||
|
||||
util::containsIgnoreCase(TR(e.answerKey), query);
|
||||
}
|
||||
|
||||
// One answer body: wrapped, muted. Shared by the collapsible (non-search) and search paths.
|
||||
void renderAnswer(const char* answerKey)
|
||||
{
|
||||
ImGui::Indent(Layout::spacingMd());
|
||||
ImGui::PushFont(material::Type().body2());
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(material::OnSurfaceMedium()));
|
||||
ImGui::TextWrapped("%s", TR(answerKey));
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::PopFont();
|
||||
ImGui::Unindent(Layout::spacingMd());
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void RenderFaqDialog(App* app, bool* p_open)
|
||||
{
|
||||
auto& S = schema::UI();
|
||||
auto win = S.window("dialogs.faq");
|
||||
const float dp = Layout::dpiScale();
|
||||
|
||||
const bool daemonAvailable = app && app->supportsFullNodeLifecycleActions();
|
||||
if (!daemonAvailable) s_faq.group = 0; // no Daemon tab in lite builds
|
||||
|
||||
// Floating "BlurFloat" modal, matching the Wallets dialog: live-blur backdrop, no boxed card, a
|
||||
// plain heading (no ✕ — a Close button sits at the bottom). Roomy fixed width; height capped to the
|
||||
// viewport so the content flexes + scrolls on small / HiDPI screens.
|
||||
const float vpH = ImGui::GetMainViewport()->Size.y;
|
||||
const float wantH = (win.height > 0 ? win.height : 640.0f) * dp;
|
||||
material::OverlayDialogSpec spec;
|
||||
spec.title = TR("faq_title");
|
||||
spec.p_open = p_open;
|
||||
spec.style = material::OverlayStyle::BlurFloat;
|
||||
spec.cardWidth = (win.width > 0 ? win.width : 860.0f);
|
||||
spec.cardHeight = std::min(wantH, vpH * 0.86f) / dp;
|
||||
spec.idSuffix = "faq";
|
||||
if (!material::BeginOverlayDialog(spec)) {
|
||||
return;
|
||||
}
|
||||
// Esc closes (ImGui consumes Esc itself while the search box is being edited, so this only
|
||||
// fires when the field isn't capturing it).
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_Escape)) *p_open = false;
|
||||
|
||||
// Subtitle under the plain heading, matching the Wallets dialog's intro caption.
|
||||
material::Type().textColored(material::TypeStyle::Caption, material::OnSurfaceMedium(), TR("faq_intro"));
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
|
||||
const float contentW = ImGui::GetContentRegionAvail().x;
|
||||
|
||||
// ── Group tabs: Wallet | Daemon ──
|
||||
{
|
||||
const int nTabs = daemonAvailable ? 2 : 1;
|
||||
const float gap = ImGui::GetStyle().ItemSpacing.x;
|
||||
const float tabW = (contentW - gap * (nTabs - 1)) / static_cast<float>(nTabs);
|
||||
auto tab = [&](const char* label, int idx) {
|
||||
const bool active = (s_faq.group == idx);
|
||||
if (active) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(material::Primary()));
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(material::OnPrimary()));
|
||||
}
|
||||
if (material::TactileButton(label, ImVec2(tabW, 0))) s_faq.group = idx;
|
||||
if (active) ImGui::PopStyleColor(2);
|
||||
};
|
||||
tab(TR("faq_group_wallet"), 0);
|
||||
if (daemonAvailable) { ImGui::SameLine(); tab(TR("faq_group_daemon"), 1); }
|
||||
}
|
||||
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||
|
||||
// ── Search ──
|
||||
ImGui::SetNextItemWidth(contentW);
|
||||
ImGui::InputTextWithHint("##FaqSearch", TR("faq_search_hint"), s_faq.search, sizeof(s_faq.search));
|
||||
const bool searching = s_faq.search[0] != '\0';
|
||||
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
|
||||
// ── Scrollable Q&A body ── (reserve room for the Close button footer below)
|
||||
const float footerH = ImGui::GetFrameHeightWithSpacing() + Layout::spacingMd();
|
||||
float bodyH = ImGui::GetContentRegionAvail().y - footerH;
|
||||
if (bodyH < 80.0f * dp) bodyH = 80.0f * dp;
|
||||
// Inner padding gives the content breathing room and, on the right, a clear gap to the LEFT of the
|
||||
// scrollbar (WindowPadding.x is exactly that gap); the scrollbar itself is made chunkier than the
|
||||
// app default. NoScrollWithMouse + ApplySmoothScroll gives the wheel eased scrolling, matching the
|
||||
// Wallets dialog / Settings page (ApplySmoothScroll owns the wheel input and lerps ScrollY).
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(Layout::spacingMd(), Layout::spacingXs()));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarSize, 16.0f * dp);
|
||||
ImGui::BeginChild("##FaqScroll", ImVec2(0, bodyH), false,
|
||||
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollWithMouse);
|
||||
material::ApplySmoothScroll();
|
||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||
|
||||
const auto& groups = (s_faq.group == 1 && daemonAvailable) ? faq::daemonFaq() : faq::walletFaq();
|
||||
bool anyShown = false;
|
||||
|
||||
bool firstSection = true;
|
||||
for (const auto& subcat : groups) {
|
||||
// Collect the entries visible under the current search.
|
||||
std::vector<const faq::FaqEntry*> visible;
|
||||
for (const auto& e : subcat.entries) {
|
||||
if (!searching || entryMatches(e, s_faq.search)) visible.push_back(&e);
|
||||
}
|
||||
if (visible.empty()) continue;
|
||||
|
||||
// Section break: generous space above every section after the first, so topic groups read as
|
||||
// clearly separated bands rather than one uniform list.
|
||||
if (!firstSection) ImGui::Dummy(ImVec2(0, Layout::spacingLg()));
|
||||
firstSection = false;
|
||||
anyShown = true;
|
||||
|
||||
// Section header — accent overline + a thin full-width rule beneath it, anchoring the group
|
||||
// above its (brighter, normal-case) question rows.
|
||||
material::Type().textColored(material::TypeStyle::Overline,
|
||||
material::Primary(), TR(subcat.titleKey));
|
||||
{
|
||||
const ImVec2 rp = ImGui::GetCursorScreenPos();
|
||||
const float rw = ImGui::GetContentRegionAvail().x;
|
||||
dl->AddLine(ImVec2(rp.x, rp.y + dp), ImVec2(rp.x + rw, rp.y + dp),
|
||||
material::Divider(), 1.0f * dp);
|
||||
}
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
|
||||
for (const auto* e : visible) {
|
||||
const float rowW = ImGui::GetContentRegionAvail().x;
|
||||
if (searching) {
|
||||
// Search results: show question + answer directly (no collapsing).
|
||||
ImGui::PushFont(material::Type().subtitle2());
|
||||
ImGui::TextWrapped("%s", TR(e->questionKey));
|
||||
ImGui::PopFont();
|
||||
renderAnswer(e->answerKey);
|
||||
} else {
|
||||
bool& exp = s_faq.expanded[e->questionKey];
|
||||
std::string id = std::string("##faq_") + e->questionKey;
|
||||
material::CollapsibleHeader(dl, id.c_str(), TR(e->questionKey), exp, rowW,
|
||||
material::Type().subtitle2(), material::OnSurface());
|
||||
if (exp) renderAnswer(e->answerKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!anyShown) {
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingLg()));
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(material::OnSurfaceMedium()));
|
||||
ImGui::TextWrapped("%s", TR("faq_no_results"));
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
|
||||
ImGui::EndChild();
|
||||
ImGui::PopStyleVar(2); // ScrollbarSize, WindowPadding
|
||||
|
||||
// Close button footer (BlurFloat has no ✕ in the heading).
|
||||
const float closeW = 120.0f * dp;
|
||||
material::BeginOverlayDialogFooter(closeW, false);
|
||||
if (material::TactileButton(TR("close"), ImVec2(closeW, 0))) *p_open = false;
|
||||
|
||||
material::EndOverlayDialog();
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
} // namespace dragonx
|
||||
Reference in New Issue
Block a user