Files
ObsidianDragon/src/ui/windows/address_label_dialog.h
DanS 21be39c725 test(sweep): add capture surfaces for the Wave-1 standalone modals
Register sweep surfaces so the redesigned Wave-1 modals get captured for visual
review: modal-qr-popup, modal-request-payment, modal-validate-address,
modal-address-label, modal-daemon-prompt, and modal-antivirus (the last renders
only on Windows, where its #ifdef body compiles). Each is driven through the
dialog's public show()/flag; teardown closes it. Adds a static hide() to
RequestPaymentDialog / ValidateAddressDialog / AddressLabelDialog for clean sweep
teardown (they had no public close). The contact add/edit and explorer block-detail
modals are tab-embedded and not yet surfaced.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 17:15:24 -05:00

275 lines
11 KiB
C++

// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include <string>
#include <vector>
#include <cctype>
#include <cstring>
#include <cstddef>
#include "../../app.h"
#include "../../util/i18n.h"
#include "../../util/text_format.h"
#include "../material/draw_helpers.h"
#include "../material/project_icons.h"
#include "../theme.h"
#include "imgui.h"
namespace dragonx {
namespace ui {
class AddressLabelDialog {
public:
static bool drawIconByName(ImDrawList* dl,
const std::string& name,
ImVec2 center,
float /*emSize*/,
ImU32 color,
ImFont* iconFont,
float iconFontSize) {
return material::project_icons::drawByName(dl, name, center, color, iconFont, iconFontSize);
}
static void show(App* app, const std::string& address, bool isZ) {
s_open = true;
s_app = app;
s_address = address;
s_isZ = isZ;
s_selectedIcon = -1;
s_iconSearch[0] = '\0';
// Pre-fill from existing metadata
std::string existing = app->getAddressLabel(address);
std::strncpy(s_label, existing.c_str(), sizeof(s_label) - 1);
s_label[sizeof(s_label) - 1] = '\0';
std::string existingIcon = app->getAddressIcon(address);
for (int i = 0; i < material::project_icons::walletIconCount(); ++i) {
if (material::project_icons::walletIconName(i) == existingIcon) {
s_selectedIcon = i;
break;
}
}
}
static void render() {
if (!s_open || !s_app) return;
using namespace material;
constexpr float cardTopViewportRatio = 0.15f;
constexpr float cardBottomViewportRatio = 0.80f;
OverlayDialogSpec ov;
ov.title = TR("set_label"); ov.p_open = &s_open;
ov.style = OverlayStyle::BlurFloat;
ov.cardWidth = 660.0f; ov.idSuffix = "addrlabel";
ov.cardBottomViewportRatio = cardBottomViewportRatio;
if (BeginOverlayDialog(ov)) {
float dp = Layout::dpiScale();
// Address preview
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(),
s_isZ ? TR("shielded_address") : TR("transparent_address"));
ImGui::Spacing();
{
std::string display = s_address;
if (display.size() > 42) display = util::truncateMiddle(display, 20, 16);
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), display.c_str());
}
ImGui::Spacing();
ImGui::Spacing();
// Label input
Type().text(TypeStyle::Subtitle2, TR("label"));
ImGui::SetNextItemWidth(-1);
ImGui::InputTextWithHint("##AddrLabel", TR("label_placeholder"), s_label, sizeof(s_label));
ImGui::Spacing();
ImGui::Spacing();
// Icon picker grid
Type().text(TypeStyle::Subtitle2, TR("choose_icon"));
ImGui::Spacing();
// Search bar
ImGui::SetNextItemWidth(-1);
ImGui::InputTextWithHint("##IconSearch", TR("search_icons"), s_iconSearch, sizeof(s_iconSearch));
ImGui::Spacing();
ImFont* iconFont = Type().iconMed();
float iconFsz = ScaledFontSize(iconFont);
float cellSz = iconFsz + 16.0f * dp;
float avail = ImGui::GetContentRegionAvail().x;
int cols = std::max(1, (int)(avail / (cellSz + 4.0f * dp)));
const char* cancelLabel = TR("cancel");
const char* saveLabel = TR("save");
ImFont* buttonFont = Type().button();
float buttonFontSize = ScaledFontSize(buttonFont);
const ImGuiStyle& style = ImGui::GetStyle();
const float cardHeight = ImGui::GetMainViewport()->Size.y * (cardBottomViewportRatio - cardTopViewportRatio);
const float bottomPadding = style.WindowPadding.y;
const bool showClearIcon = (s_selectedIcon >= 0);
const float clearRowH = showClearIcon ? ImGui::GetTextLineHeight() : 0.0f;
const float buttonRowH = ImGui::GetFrameHeight();
const float separatorH = 0.0f; // footer divider removed (reference design)
const float preButtonReserve =
(showClearIcon ? (style.ItemSpacing.y + clearRowH) : 0.0f) +
style.ItemSpacing.y * 3.0f + separatorH;
const float buttonY = cardHeight - bottomPadding - buttonRowH;
// Build filtered index list
std::vector<int> visible;
visible.reserve(material::project_icons::walletIconCount());
{
// Simple case-insensitive substring match on icon name
std::string needle(s_iconSearch);
for (char& c : needle) c = (char)std::tolower((unsigned char)c);
for (int i = 0; i < material::project_icons::walletIconCount(); ++i) {
const char* iconName = material::project_icons::walletIconName(i);
if (needle.empty() || std::strstr(iconName, needle.c_str()) != nullptr)
visible.push_back(i);
}
}
// The grid owns all vertical space above the bottom action band.
const float gridStartY = ImGui::GetCursorPosY();
const float controlsTopY = std::max(gridStartY + cellSz * 2.0f, buttonY - preButtonReserve);
const float gridMaxH = std::max(cellSz * 2.0f, controlsTopY - gridStartY);
ImGui::PushStyleColor(ImGuiCol_ChildBg, IM_COL32(0, 0, 0, 0));
ImGui::BeginChild("##IconGrid", ImVec2(avail, gridMaxH), ImGuiChildFlags_None,
ImGuiWindowFlags_NoScrollbar);
ImDrawList* dl = ImGui::GetWindowDrawList();
if (visible.empty()) {
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("no_icons_found"));
}
int col = 0;
for (int vi = 0; vi < (int)visible.size(); ++vi) {
int i = visible[vi];
const char* iconName = material::project_icons::walletIconName(i);
if (col != 0) ImGui::SameLine(0, 4.0f * dp);
ImVec2 pos = ImGui::GetCursorScreenPos();
ImVec2 mn = pos;
ImVec2 mx(pos.x + cellSz, pos.y + cellSz);
bool hov = ImGui::IsMouseHoveringRect(mn, mx);
bool sel = (s_selectedIcon == i);
// Background
if (sel) {
dl->AddRectFilled(mn, mx, WithAlpha(Primary(), 40), 6.0f * dp);
dl->AddRect(mn, mx, WithAlpha(Primary(), 120), 6.0f * dp, 0, 1.5f * dp);
} else if (hov) {
dl->AddRectFilled(mn, mx, IM_COL32(255, 255, 255, 20), 6.0f * dp);
}
// Icon centered in cell
drawIconByName(dl,
iconName,
ImVec2(mn.x + cellSz * 0.5f, mn.y + cellSz * 0.5f),
iconFsz,
sel ? Primary() : (hov ? OnSurface() : OnSurfaceMedium()),
iconFont,
iconFsz);
ImGui::PushID(i);
ImGui::InvisibleButton("##icon", ImVec2(cellSz, cellSz));
if (ImGui::IsItemClicked()) s_selectedIcon = i;
if (hov) material::Tooltip("%s", iconName);
ImGui::PopID();
col = (col + 1) % cols;
}
ImGui::EndChild();
ImGui::PopStyleColor();
if (ImGui::GetCursorPosY() < controlsTopY) {
ImGui::SetCursorPosY(controlsTopY);
}
// "No icon" option
if (showClearIcon) {
ImGui::Spacing();
if (material::TactileSmallButton(TR("clear_icon"))) {
s_selectedIcon = -1;
}
}
ImGui::Spacing();
ImGui::Spacing();
ImGui::Spacing();
if (ImGui::GetCursorPosY() < buttonY) {
ImGui::SetCursorPosY(buttonY);
}
// Buttons
float minBtnW = 120.0f * dp;
float buttonPadW = ImGui::GetStyle().FramePadding.x * 2.0f + 24.0f * dp;
float cancelW = std::max(minBtnW,
buttonFont->CalcTextSizeA(buttonFontSize, 1000.0f, 0.0f, cancelLabel).x + buttonPadW);
float saveW = std::max(minBtnW,
buttonFont->CalcTextSizeA(buttonFontSize, 1000.0f, 0.0f, saveLabel).x + buttonPadW);
float totalW = cancelW + saveW + Layout::spacingMd();
float rowStartX = ImGui::GetCursorPosX();
float contentW = ImGui::GetContentRegionAvail().x;
ImGui::SetCursorPosX(rowStartX + std::max(0.0f, (contentW - totalW) * 0.5f));
if (TactileButton(cancelLabel, ImVec2(cancelW, 0), buttonFont)) {
s_open = false;
}
ImGui::SameLine(0, Layout::spacingMd());
if (TactileButton(saveLabel, ImVec2(saveW, 0), buttonFont)) {
// Apply changes
// Trim surrounding whitespace; a whitespace-only label clears it
// (mirroring how an empty icon selection clears the icon).
std::string trimmedLabel(s_label);
{
std::size_t b = trimmedLabel.find_first_not_of(" \t\r\n");
if (b == std::string::npos) {
trimmedLabel.clear();
} else {
std::size_t e = trimmedLabel.find_last_not_of(" \t\r\n");
trimmedLabel = trimmedLabel.substr(b, e - b + 1);
}
}
s_app->setAddressLabel(s_address, trimmedLabel);
if (s_selectedIcon >= 0)
s_app->setAddressIcon(s_address, material::project_icons::walletIconName(s_selectedIcon));
else
s_app->setAddressIcon(s_address, "");
s_open = false;
}
EndOverlayDialog();
}
}
static bool isOpen() { return s_open; }
// Close the dialog (used by the UI sweep teardown).
static void hide() { s_open = false; }
private:
static inline bool s_open = false;
static inline App* s_app = nullptr;
static inline std::string s_address;
static inline bool s_isZ = false;
static inline char s_label[128] = {};
static inline int s_selectedIcon = -1;
static inline char s_iconSearch[64] = {};
public:
// Expose for the address list to look up icon glyphs by name
static const char* iconGlyphForName(const std::string& name) {
return material::project_icons::glyphForName(name);
}
};
} // namespace ui
} // namespace dragonx