Files
ObsidianDragon/src/ui/widgets/copy_field.h
DanS b20e7efb16 feat(lite): network-tab polish, reworked key-export/QR dialogs
Network tab: glow only the active node, drop the left accent bar. Key-export
dialog: fix the lite-wallet "Not connected" failure by exporting the key
locally via the SDXL backend when there's no daemon; rework the layout to
wrapping click-to-copy fields with a side QR (empty placeholder when hidden),
85% modal width, HRP-preserving key chunking, and a centered, emphasized
warning. QR popup matched to the same sizing and click-to-copy address. Shared
field rendering extracted to widgets/copy_field.h so both dialogs stay in sync.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 21:27:25 -05:00

114 lines
4.8 KiB
C++

// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
//
// Shared "click-to-copy field" widgets used by the key-export and QR-popup dialogs, so an address
// renders identically in both. A field is a bordered box of wrapping text whose whole area copies
// to the clipboard on click; long secrets/addresses are grouped into space-separated blocks.
#pragma once
#include <string>
#include <cstring>
#include "imgui.h"
#include "../material/colors.h"
#include "../material/tooltip_style.h"
#include "../../util/i18n.h"
namespace dragonx {
namespace ui {
namespace widgets {
// Draw `text` wrapped within wrapW with every visual line horizontally centered (returns the height
// used). ImGui has no center+wrap, so we walk the wrap points the way it would.
inline float DrawCenteredWrapped(ImDrawList* dl, ImFont* font, float fs, ImVec2 origin,
float wrapW, ImU32 col, const char* text)
{
const float scale = fs / font->LegacySize;
const float lineH = ImGui::GetTextLineHeight();
const char* s = text;
const char* e = text + std::strlen(text);
float y = origin.y;
while (s < e) {
const char* wrap = font->CalcWordWrapPositionA(scale, s, e, wrapW);
if (wrap <= s) wrap = s + 1;
const ImVec2 lw = font->CalcTextSizeA(fs, 1.0e30f, 0.0f, s, wrap);
float lx = origin.x + (wrapW - lw.x) * 0.5f;
if (lx < origin.x) lx = origin.x;
dl->AddText(font, fs, ImVec2(lx, y), col, s, wrap);
y += lineH;
s = wrap;
while (s < e && (*s == ' ' || *s == '\n')) s++; // ImGui consumes the break char
}
return y - origin.y;
}
// Group a long string into space-separated blocks so it reads in scannable chunks and wraps on
// block boundaries. Callers copy the ORIGINAL (un-grouped) string.
inline std::string ChunkString(const std::string& s, size_t group)
{
if (group == 0 || s.size() <= group) return s;
std::string out;
out.reserve(s.size() + s.size() / group + 1);
for (size_t i = 0; i < s.size(); ++i) {
if (i && (i % group) == 0) out.push_back(' ');
out.push_back(s[i]);
}
return out;
}
// A wrapping, click-to-copy "field": draws `display` in a bordered box (the whole box is the copy
// affordance) and returns true when clicked. `center` centers each wrapped line. The caller is
// responsible for the actual clipboard write (so secrets can use an auto-clearing clipboard).
inline bool CopyField(const char* id, const std::string& display, float width, float minH, bool center)
{
namespace m = material;
ImDrawList* dl = ImGui::GetWindowDrawList();
ImGuiStyle& st = ImGui::GetStyle();
ImFont* font = ImGui::GetFont();
const float fs = ImGui::GetFontSize();
const float padX = st.FramePadding.x + 4.0f;
const float padY = st.FramePadding.y + 4.0f;
const float wrapW = (width - padX * 2.0f) > 1.0f ? (width - padX * 2.0f) : 1.0f;
const ImVec2 ts = ImGui::CalcTextSize(display.c_str(), nullptr, false, wrapW);
float h = ts.y + padY * 2.0f;
if (h < minH) h = minH;
const ImVec2 p0 = ImGui::GetCursorScreenPos();
const ImVec2 p1 = ImVec2(p0.x + width, p0.y + h);
ImGui::InvisibleButton(id, ImVec2(width, h));
const bool hovered = ImGui::IsItemHovered();
const bool clicked = ImGui::IsItemClicked();
dl->AddRectFilled(p0, p1, m::WithAlpha(m::OnSurface(), hovered ? 18 : 8), 6.0f);
dl->AddRect(p0, p1, m::WithAlpha(m::OnSurface(), hovered ? 70 : 30), 6.0f, 0, 1.0f);
const float textY = p0.y + (h - ts.y) * 0.5f; // vertical center
if (center)
DrawCenteredWrapped(dl, font, fs, ImVec2(p0.x + padX, textY), wrapW, m::OnSurface(), display.c_str());
else
dl->AddText(font, fs, ImVec2(p0.x + padX, textY), m::OnSurface(), display.c_str(), nullptr, wrapW);
if (hovered) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
material::Tooltip("%s", TR("copy"));
}
return clicked;
}
// An address field: chunked by 4 for readability, width clamped to its text, box centered in the
// available region. Copies the raw address to the clipboard on click; returns true when clicked.
inline bool AddressCopyField(const char* id, const std::string& address)
{
const std::string at = ChunkString(address, 4);
const float avail = ImGui::GetContentRegionAvail().x;
const float padX = ImGui::GetStyle().FramePadding.x + 4.0f;
float boxW = ImGui::CalcTextSize(at.c_str()).x + padX * 2.0f + 2.0f; // clamp to text width
if (boxW > avail) boxW = avail;
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (avail > boxW ? (avail - boxW) * 0.5f : 0.0f));
const bool clicked = CopyField(id, at, boxW, 0.0f, /*center=*/true);
if (clicked) ImGui::SetClipboardText(address.c_str());
return clicked;
}
} // namespace widgets
} // namespace ui
} // namespace dragonx