feat(addresses): improve address labeling and view-only handling

- Add expanded address icon picker with search, bottom-aligned actions, and improved modal sizing
- Embed a pickaxe icon font subset and wire it into typography/address icon rendering
- Track view-only shielded addresses and prevent sends from non-spendable z-addresses
- Improve address transfer dialog sizing, max amount handling, and text clipping
- Tune main header layout values in ui.toml
- Update README, codebase overview, and third-party license documentation
This commit is contained in:
dan_s
2026-04-27 13:54:28 -05:00
parent 55a36e0d06
commit 9e1b1397ad
18 changed files with 567 additions and 90 deletions

View File

@@ -433,6 +433,7 @@ set_source_files_properties(
${CMAKE_SOURCE_DIR}/res/fonts/Ubuntu-Light.ttf;\
${CMAKE_SOURCE_DIR}/res/fonts/Ubuntu-Medium.ttf;\
${CMAKE_SOURCE_DIR}/res/fonts/MaterialIcons-Regular.ttf;\
${CMAKE_SOURCE_DIR}/res/fonts/MaterialDesignIcons-Pickaxe-Subset.ttf;\
${CMAKE_SOURCE_DIR}/res/fonts/NotoSansCJK-Subset.ttf"
)

View File

@@ -1,6 +1,8 @@
# DragonX Wallet - ImGui Edition
# ObsidianDragon - DragonX Wallet
A lightweight, portable cryptocurrency wallet for DragonX (DRGX), built with Dear ImGui.
A lightweight, portable full-node cryptocurrency wallet for DragonX (DRGX), built with Dear ImGui.
Current pre-release: **1.2.0-rc1**.
![License](https://img.shields.io/badge/License-GPLv3-blue.svg)
![Platform](https://img.shields.io/badge/Platform-Linux%20%7C%20Windows%20%7C%20macOS-green.svg)
@@ -9,10 +11,13 @@ A lightweight, portable cryptocurrency wallet for DragonX (DRGX), built with Dea
- **Full Node Support**: Connects to dragonxd for complete blockchain verification
- **Shielded Transactions**: Full z-address support with encrypted memos
- **Integrated Mining**: CPU mining controls with hashrate monitoring
- **Address Management**: Labels, icons, favorites, hidden addresses, and address-to-address transfers
- **Integrated Mining**: Solo CPU mining plus pool mining through xmrig, with idle-mining controls
- **Explorer Tools**: Block/transaction lookup and bootstrap snapshot download
- **Market Data**: Real-time price charts from CoinGecko
- **QR Codes**: Generate and display QR codes for receiving addresses
- **Multi-language**: i18n support (English, Spanish, more coming)
- **Multi-language**: i18n support for English, German, Spanish, French, Japanese, Korean, Portuguese, Russian, and Chinese
- **CJK Fonts**: Bundled CJK subset font for translated interfaces
- **Lightweight**: ~5-10MB binary vs ~50MB+ for Qt version
- **Fast Builds**: Compiles in seconds, not minutes
@@ -116,7 +121,8 @@ cd ObsidianDragon/
./ObsidianDragon
```
The wallet will automatically connect to the daemon using credentials from \`~/.hush/DRAGONX/DRAGONX.conf\`.
The wallet will automatically connect to the daemon using credentials from `~/.hush/DRAGONX/DRAGONX.conf`.
### Using Custom Node Binaries
The wallet checks its **own directory first** when looking for DragonX node binaries. This means you can test new or different branch builds of `hush-arrakis-chain`/`hushd` without waiting for a new wallet release:
@@ -131,9 +137,10 @@ The wallet checks its **own directory first** when looking for DragonX node bina
3. System-wide locations (`/usr/local/bin`, `~/dragonx/src`, etc.)
This is useful for testing new branches or hotfixes to the node software before they are bundled into a wallet release.
## Configuration
Configuration is stored in \`~/.hush/DRAGONX/DRAGONX.conf\`:
Configuration is stored in `~/.hush/DRAGONX/DRAGONX.conf`:
```
rpcuser=your_rpc_user
@@ -148,44 +155,46 @@ ObsidianDragon/
├── src/
│ ├── main.cpp # Entry point, SDL/ImGui setup
│ ├── app.cpp/h # Main application class
│ ├── wallet_state.h # Wallet data structures
│ ├── version.h # Version definitions
│ ├── data/ # WalletState, address book, exchange info
│ ├── config/ # Settings persistence and generated version.h
│ ├── ui/
│ │ ├── theme.cpp/h # DragonX theme
│ │ ── windows/ # UI tabs and dialogs
│ │ ├── schema/ # TOML UI schema and skin manager
│ │ ── material/ # Material components, typography, layout
│ │ ├── windows/ # Tabs and dialogs
│ │ └── pages/ # Multi-page screens such as Settings
│ ├── rpc/
│ │ ├── rpc_client.cpp # JSON-RPC client
│ │ └── connection.cpp # Daemon connection
│ ├── config/
│ └── settings.cpp # Settings persistence
│ ├── resources/ # Embedded resource extraction
├── platform/ # Windows DX11/backdrop helpers
│ ├── util/
│ │ ├── i18n.cpp # Internationalization
│ │ └── ...
│ └── daemon/
│ └── embedded_daemon.cpp
├── res/
│ ├── fonts/ # Ubuntu font
│ ├── fonts/ # Ubuntu, icon, and CJK fonts
│ └── lang/ # Translation files
├── libs/
│ └── qrcode/ # QR code generation
├── CMakeLists.txt
├── build-release.sh # Build script
└── create-appimage.sh # AppImage packaging
├── build.sh # Release/cross-platform build script
└── scripts/create-appimage.sh # AppImage packaging
```
## Dependencies
Fetched automatically by CMake (no manual install needed):
Fetched or discovered by CMake:
- **[SDL3](https://github.com/libsdl-org/SDL)** — Cross-platform windowing/input
- **[nlohmann/json](https://github.com/nlohmann/json)** — JSON parsing
- **[toml++](https://github.com/marzer/tomlplusplus)** — TOML parsing (UI schema/themes)
- **[libcurl](https://curl.se/libcurl/)** — HTTPS RPC transport (system on Linux, fetched on Windows)
- **[libcurl](https://curl.se/libcurl/)** — HTTP/HTTPS transport for daemon RPC and network calls (system on Linux/macOS, fetched on Windows)
Bundled in `libs/`:
- **[Dear ImGui](https://github.com/ocornut/imgui)** — Immediate mode GUI
- **[libsodium](https://libsodium.org)** — Cryptographic operations (fetched by `scripts/fetch-libsodium.sh`)
- **[libsodium](https://libsodium.org)** — Cryptographic operations (system on Linux or fetched by `scripts/fetch-libsodium.sh`)
- **[QR-Code-generator](https://github.com/nayuki/QR-Code-generator)** — QR code rendering
- **[miniz](https://github.com/richgel999/miniz)** — ZIP compression
- **[GLAD](https://glad.dav1d.de/)** — OpenGL loader (Linux/macOS)
@@ -202,9 +211,11 @@ Bundled in `libs/`:
## Translation
Current language files live in `res/lang/` as `de`, `es`, `fr`, `ja`, `ko`, `pt`, `ru`, and `zh` JSON files, with built-in English fallbacks.
To add a new language:
1. Copy \`res/lang/es.json\` to \`res/lang/<code>.json\`
1. Copy `res/lang/es.json` to `res/lang/<code>.json`
2. Translate all strings
3. The language will appear in Settings automatically

View File

@@ -361,7 +361,22 @@ https://www.apache.org/licenses/LICENSE-2.0
---
## 13. IconFontCppHeaders
## 13. Material Design Icons Pickaxe Subset Font
- **Location:** `res/fonts/MaterialDesignIcons-Pickaxe-Subset.ttf`
- **Source:** https://github.com/Templarian/MaterialDesign-Webfont
- **Derived from:** Pictogrammers Material Design Icons webfont (`materialdesignicons-webfont.ttf`)
- **Copyright:** Pictogrammers contributors
- **License:** Apache License 2.0
This bundled font is a local one-glyph subset containing only the MDI pickaxe
icon, remapped onto a BMP private-use codepoint for Dear ImGui compatibility.
The full text of the Apache License 2.0 is available at:
https://www.apache.org/licenses/LICENSE-2.0
---
## 14. IconFontCppHeaders
- **Location:** `src/embedded/IconsMaterialDesign.h`
- **Source:** https://github.com/juliettef/IconFontCppHeaders
@@ -390,7 +405,7 @@ freely, subject to the following restrictions:
---
## 14. Ubuntu Font Family
## 15. Ubuntu Font Family
- **Location:** `res/fonts/Ubuntu-Light.ttf`, `Ubuntu-Medium.ttf`, `Ubuntu-R.ttf`
- **Source:** https://design.ubuntu.com/font

70
docs/codebase-overview.md Normal file
View File

@@ -0,0 +1,70 @@
# Codebase Overview
Current as of 2026-04-27 for ObsidianDragon `1.2.0-rc1`.
## Purpose
ObsidianDragon is a Dear ImGui full-node wallet for DragonX (DRGX). It manages an embedded or external `dragonxd`, renders a schema-driven desktop UI, and provides shielded transactions, mining, market data, address management, explorer tools, and bootstrap download support.
## Runtime Architecture
- `src/main.cpp` initializes SDL3, graphics backends, ImGui, and the main loop.
- `src/app.cpp` owns application lifecycle, navigation, rendering, dialogs, and daemon/RPC startup.
- `src/app_network.cpp` contains refresh and transaction flows, including balance, address, transaction, mining, market, peer, and address metadata updates.
- `src/app_security.cpp` covers wallet encryption, PIN unlock, auto-lock, and secure vault integration.
- `src/app_wizard.cpp` handles first-run setup and bootstrap/encryption flow.
The UI thread renders ImGui and drains callback queues. RPC work runs through `RPCWorker`, which posts worker-thread RPC calls and returns UI-thread callbacks. Console commands can use a separate fast-lane RPC client/worker so they do not queue behind regular refresh batches.
## Source Map
| Path | Role |
|------|------|
| `src/config/` | Settings JSON persistence and generated `version.h` |
| `src/data/` | Wallet state, address book, exchange info |
| `src/rpc/` | libcurl JSON-RPC client, connection config, worker queue |
| `src/daemon/` | Embedded `dragonxd` manager and xmrig pool miner manager |
| `src/ui/windows/` | Main tabs and modal dialogs |
| `src/ui/pages/` | Page-style screens such as Settings |
| `src/ui/schema/` | TOML schema loader, skin manager, color resolver |
| `src/ui/material/` | Material-style typography, layout, drawing, components |
| `src/ui/effects/` | Acrylic, blur, noise, theme effects, low-spec fallback |
| `src/resources/` | Embedded resource extraction for params, daemon assets, themes, images |
| `src/platform/` | Windows DX11 and backdrop helpers |
| `src/util/` | i18n, logging, platform paths, bootstrap, vault, URI/base64/texture utilities |
## Build And Resources
- CMake uses C++17 and outputs `build/bin/ObsidianDragon`.
- Version comes from `project(... VERSION 1.2.0)` plus `DRAGONX_VERSION_SUFFIX=-rc1`.
- SDL3 is found from the system first, then fetched by CMake if unavailable.
- nlohmann/json and toml++ are fetched with CMake FetchContent.
- libcurl is system-provided on Linux/macOS and fetched statically for Windows.
- libsodium is system-provided on Linux or local under `libs/libsodium/`, `libs/libsodium-win/`, or `libs/libsodium-mac/`.
- Fonts are embedded with INCBIN: Ubuntu, Material Icons, a one-glyph MDI pickaxe subset, and Noto CJK subset.
- `res/themes/ui.toml` is embedded as a fallback and expanded into build themes with `scripts/expand_themes.py`.
- `res/default_banlist.txt` is embedded into `build/generated/default_banlist_embedded.h`.
## RPC And Daemon Notes
- Default RPC port is `21769`.
- `RPCClient` uses local HTTP with Basic auth. TLS is not assumed for localhost daemon RPC.
- DragonX daemon config paths:
- Linux: `~/.hush/DRAGONX/DRAGONX.conf`
- Windows: `%APPDATA%/Hush/DRAGONX/DRAGONX.conf`
- macOS: `~/Library/Application Support/Hush/DRAGONX/DRAGONX.conf`
- `Connection::autoDetectConfig()` creates missing config files, appends `exportdir`, `experimentalfeatures=1`, and `developerencryptwallet=1`, and falls back to `.cookie` auth if no `rpcpassword` is configured.
- The embedded daemon detects an external daemon on the RPC port and connects to it instead of taking ownership.
- Chain args include TLS-only mode, adaptive `-dbcache`, DragonX asset parameters, node seeds `node.dragonx.is` through `node4.dragonx.is`, and optional `-maxconnections=<n>` from Settings.
## UI And Data Notes
- Sidebar navigation is driven by `NavPage`: Overview, Send, Receive, History, Mining, Market, Console, Network, Explorer, Settings.
- Explorer lives in `src/ui/windows/explorer_tab.cpp`; Settings uses `src/ui/pages/settings_page.cpp`.
- Address labels, icons, favorites, hidden state, and manual ordering are persisted in Settings, especially `address_meta`.
- `AddressInfo::has_spending_key` tracks view-only shielded addresses; send flows filter or reject non-spendable z-addresses.
- The pickaxe icon is not a normal `ICON_MD_*` glyph. Use `AddressLabelDialog::drawIconByName()` or `Typography::pickaxeFontForSize()` for that special case.
## Remaining Work
- Investigate `todo.md`: determine whether DragonX/Komodo wallet storage supports a safe compaction or consolidation workflow for wallets with too many addresses.

Binary file not shown.

View File

@@ -1279,7 +1279,7 @@ page-fade-speed = { size = 8.0 }
collapse-hysteresis = { size = 60.0 }
header-icon = { icon-dark = "logos/logo_ObsidianDragon_dark.png", icon-light = "logos/logo_ObsidianDragon_light.png" }
coin-icon = { icon = "logos/logo_dragonx_128.png" }
header-title = { font = "subtitle1", size = 14.0, pad-x = 22.0, pad-y = 6.0, logo-gap = 4.0, opacity = 0.7, offset-y = 4.0 }
header-title = { font = "subtitle1", size = 12.0, pad-x = 8.0, pad-y = 10.0, logo-gap = 4.0, opacity = 0.7, offset-y = -2.0 }
[components.main-window.window]
padding = [12, 36]

View File

@@ -750,6 +750,19 @@ void App::refreshAddressData()
AddressInfo info;
info.address = addr.get<std::string>();
info.type = "shielded";
// Check if we have the spending key for this address
try {
json vResult = rpc_->call("z_validateaddress", json::array({info.address}));
if (vResult.is_object() && vResult.contains("ismine") && vResult["ismine"].get<bool>()) {
// "ismine" means we have the spending key
info.has_spending_key = true;
} else {
info.has_spending_key = false;
}
} catch (...) {
// If validation fails, assume spendable (safe default for older daemons)
info.has_spending_key = true;
}
zAddrs.push_back(info);
}
} catch (const std::exception& e) {
@@ -1850,6 +1863,21 @@ void App::sendTransaction(const std::string& from, const std::string& to,
return;
}
// Check that we have the spending key for the from address
if (!from.empty() && from[0] == 'z') {
bool spendable = false;
for (const auto& addr : state_.z_addresses) {
if (addr.address == from) {
spendable = addr.has_spending_key;
break;
}
}
if (!spendable) {
if (callback) callback(false, "This is a view-only address (no spending key). Import the spending key to send from this address.");
return;
}
}
// Build recipients array
nlohmann::json recipients = nlohmann::json::array();
nlohmann::json recipient;

View File

@@ -18,6 +18,7 @@ struct AddressInfo {
std::string address;
double balance = 0.0;
std::string type; // "shielded" or "transparent"
bool has_spending_key = true; // false for view-only (imported via z_importviewingkey)
// For display
std::string label;
@@ -25,6 +26,7 @@ struct AddressInfo {
// Derived
bool isZAddr() const { return !address.empty() && address[0] == 'z'; }
bool isShielded() const { return type == "shielded"; }
bool isSpendable() const { return has_spending_key; }
};
/**

View File

@@ -12,4 +12,5 @@ INCBIN(ubuntu_regular, "@CMAKE_SOURCE_DIR@/res/fonts/Ubuntu-R.ttf");
INCBIN(ubuntu_light, "@CMAKE_SOURCE_DIR@/res/fonts/Ubuntu-Light.ttf");
INCBIN(ubuntu_medium, "@CMAKE_SOURCE_DIR@/res/fonts/Ubuntu-Medium.ttf");
INCBIN(material_icons, "@CMAKE_SOURCE_DIR@/res/fonts/MaterialIcons-Regular.ttf");
INCBIN(mdi_pickaxe_subset, "@CMAKE_SOURCE_DIR@/res/fonts/MaterialDesignIcons-Pickaxe-Subset.ttf");
INCBIN(noto_cjk_subset, "@CMAKE_SOURCE_DIR@/res/fonts/NotoSansCJK-Subset.ttf");

View File

@@ -27,6 +27,9 @@ extern "C" {
extern const unsigned char g_material_icons_data[];
extern const unsigned int g_material_icons_size;
extern const unsigned char g_mdi_pickaxe_subset_data[];
extern const unsigned int g_mdi_pickaxe_subset_size;
extern const unsigned char g_noto_cjk_subset_data[];
extern const unsigned int g_noto_cjk_subset_size;
}

View File

@@ -90,10 +90,40 @@ inline void DrawTextShadow(ImDrawList* dl, const ImVec2& pos, ImU32 col,
// and will return true even when a modal popup covers the rect, which
// causes background elements to show hover highlights through dialogs.
inline int& OverlayDialogActiveFrame()
{
static int s_frame = -1;
return s_frame;
}
inline void MarkOverlayDialogActive()
{
OverlayDialogActiveFrame() = ImGui::GetFrameCount();
}
inline bool IsCurrentWindowOverlayDialog()
{
ImGuiWindow* window = ImGui::GetCurrentWindow();
for (ImGuiWindow* node = window; node; node = node->ParentWindow) {
if (node->Name && strcmp(node->Name, "##OverlayScrim") == 0)
return true;
}
return false;
}
inline bool IsOverlayDialogBlockingInput()
{
int activeFrame = OverlayDialogActiveFrame();
int currentFrame = ImGui::GetFrameCount();
return activeFrame == currentFrame || activeFrame == (currentFrame - 1);
}
inline bool IsRectHovered(const ImVec2& r_min, const ImVec2& r_max, bool clip = true)
{
if (!ImGui::IsMouseHoveringRect(r_min, r_max, clip))
return false;
if (IsOverlayDialogBlockingInput() && !IsCurrentWindowOverlayDialog())
return false;
// If a modal popup is open and it is not the current window, treat
// the content as non-hoverable (same logic ImGui uses internally
// inside IsWindowContentHoverable for modal blocking).
@@ -885,8 +915,11 @@ inline bool DrawDialogTitleBar(const char* title, bool* p_open, ImU32 accent_col
// Creates a fullscreen semi-transparent overlay with a centered card dialog.
// Similar to the shutdown screen pattern but for interactive dialogs.
inline bool BeginOverlayDialog(const char* title, bool* p_open, float cardWidth = 460.0f, float scrimOpacity = 0.92f)
inline bool BeginOverlayDialog(const char* title, bool* p_open, float cardWidth = 460.0f, float scrimOpacity = 0.92f,
float cardBottomViewportRatio = 0.85f)
{
MarkOverlayDialogActive();
ImGuiViewport* vp = ImGui::GetMainViewport();
ImVec2 vp_pos = vp->Pos;
ImVec2 vp_size = vp->Size;
@@ -914,6 +947,14 @@ inline bool BeginOverlayDialog(const char* title, bool* p_open, float cardWidth
}
ImDrawList* dl = ImGui::GetWindowDrawList();
// Consume pointer input on the scrim so the overlay owns clicks and wheel
// events even when the click lands outside the card content.
ImGui::SetCursorScreenPos(vp_pos);
ImGui::InvisibleButton("##OverlayInputBlocker", vp_size,
ImGuiButtonFlags_MouseButtonLeft |
ImGuiButtonFlags_MouseButtonRight |
ImGuiButtonFlags_MouseButtonMiddle);
// Calculate card position (centered)
float cardX = vp_pos.x + (vp_size.x - cardWidth) * 0.5f;
@@ -921,7 +962,7 @@ inline bool BeginOverlayDialog(const char* title, bool* p_open, float cardWidth
// Draw glass card background
ImVec2 cardMin(cardX, cardY);
ImVec2 cardMax(cardX + cardWidth, vp_pos.y + vp_size.y * 0.85f);
ImVec2 cardMax(cardX + cardWidth, vp_pos.y + vp_size.y * cardBottomViewportRatio);
// Card background with glass effect
GlassPanelSpec cardGlass;
@@ -930,6 +971,11 @@ inline bool BeginOverlayDialog(const char* title, bool* p_open, float cardWidth
cardGlass.borderAlpha = 50;
cardGlass.borderWidth = 1.0f;
DrawGlassPanel(dl, cardMin, cardMax, cardGlass);
if (p_open && ImGui::IsMouseClicked(ImGuiMouseButton_Left) &&
!ImGui::IsMouseHoveringRect(cardMin, cardMax, false)) {
*p_open = false;
}
// Set up child region for card content
ImGui::SetCursorScreenPos(ImVec2(cardX, cardY));

View File

@@ -101,6 +101,7 @@ bool Typography::reload(ImGuiIO& io, float dpiScale)
loaded_ = false;
for (int i = 0; i < kNumStyles; ++i) fonts_[i] = nullptr;
for (int i = 0; i < kNumIconSizes; ++i) iconFonts_[i] = nullptr;
for (int i = 0; i < kNumIconSizes; ++i) pickaxeFonts_[i] = nullptr;
return load(io, dpiScale);
}
@@ -190,6 +191,13 @@ bool Typography::load(ImGuiIO& io, float dpiScale)
iconFonts_[2] = loadIconFont(io, 24.0f * scale, "IconLarge");
iconFonts_[3] = loadIconFont(io, 40.0f * scale, "IconXL");
// Load a one-glyph MDI subset for pickaxe. The glyph is remapped onto a
// BMP private-use codepoint so it remains renderable with 16-bit ImWchar.
pickaxeFonts_[0] = loadPickaxeFont(io, 14.0f * scale, "PickaxeSmall");
pickaxeFonts_[1] = loadPickaxeFont(io, 18.0f * scale, "PickaxeMed");
pickaxeFonts_[2] = loadPickaxeFont(io, 24.0f * scale, "PickaxeLarge");
pickaxeFonts_[3] = loadPickaxeFont(io, 40.0f * scale, "PickaxeXL");
// Verify all fonts loaded
bool allLoaded = true;
for (int i = 0; i < kNumStyles; ++i) {
@@ -346,6 +354,51 @@ ImFont* Typography::loadIconFont(ImGuiIO& io, float size, const char* name)
return font;
}
ImFont* Typography::loadPickaxeFont(ImGuiIO& io, float size, const char* name)
{
if (g_mdi_pickaxe_subset_size == 0) {
DEBUG_LOGF("Typography: Pickaxe subset font is empty\n");
return nullptr;
}
void* fontDataCopy = IM_ALLOC(g_mdi_pickaxe_subset_size);
memcpy(fontDataCopy, g_mdi_pickaxe_subset_data, g_mdi_pickaxe_subset_size);
ImFontConfig cfg;
cfg.FontDataOwnedByAtlas = true;
cfg.OversampleH = 2;
cfg.OversampleV = 1;
cfg.PixelSnapH = true;
cfg.MergeMode = false;
cfg.GlyphMinAdvanceX = size;
static const ImWchar pickaxeRange[] = {
Typography::kPickaxeCodepoint,
Typography::kPickaxeCodepoint,
0,
};
cfg.GlyphRanges = pickaxeRange;
snprintf(cfg.Name, sizeof(cfg.Name), "MDIPickaxe %.0fpx", size);
ImFont* font = io.Fonts->AddFontFromMemoryTTF(fontDataCopy, g_mdi_pickaxe_subset_size, size, &cfg);
if (font) {
DEBUG_LOGF("Typography: Loaded pickaxe font %s (%.0fpx)\n", name, size);
} else {
DEBUG_LOGF("Typography: Failed to load pickaxe font %s\n", name);
IM_FREE(fontDataCopy);
}
return font;
}
ImFont* Typography::pickaxeFontForSize(float size) const
{
if (size <= 15.0f) return pickaxeSmall();
if (size <= 20.0f) return pickaxeMed();
if (size <= 32.0f) return pickaxeLarge();
return pickaxeXL();
}
ImFont* Typography::getFont(TypeStyle style) const
{
int index = static_cast<int>(style);

View File

@@ -175,6 +175,14 @@ public:
ImFont* iconMed() const { return iconFonts_[1] ? iconFonts_[1] : getFont(TypeStyle::Body1); }
ImFont* iconLarge() const { return iconFonts_[2] ? iconFonts_[2] : getFont(TypeStyle::H5); }
ImFont* iconXL() const { return iconFonts_[3] ? iconFonts_[3] : getFont(TypeStyle::H3); }
ImFont* pickaxeSmall() const { return pickaxeFonts_[0] ? pickaxeFonts_[0] : iconSmall(); }
ImFont* pickaxeMed() const { return pickaxeFonts_[1] ? pickaxeFonts_[1] : iconMed(); }
ImFont* pickaxeLarge() const { return pickaxeFonts_[2] ? pickaxeFonts_[2] : iconLarge(); }
ImFont* pickaxeXL() const { return pickaxeFonts_[3] ? pickaxeFonts_[3] : iconXL(); }
ImFont* pickaxeFontForSize(float size) const;
static constexpr ImWchar kPickaxeCodepoint = 0xE001;
/**
* @brief Resolve a font name string to ImFont*
@@ -254,10 +262,12 @@ private:
// Icon fonts at different sizes: [0]=small(14), [1]=med(18), [2]=large(24), [3]=xl(40)
ImFont* iconFonts_[4] = {};
ImFont* pickaxeFonts_[4] = {};
static constexpr int kNumIconSizes = 4;
// Load an icon-only font at a specific pixel size
ImFont* loadIconFont(ImGuiIO& io, float size, const char* name);
ImFont* loadPickaxeFont(ImGuiIO& io, float size, const char* name);
// Type specifications
static const TypeSpec* getTypeSpecs();

View File

@@ -5,10 +5,14 @@
#pragma once
#include <string>
#include <vector>
#include <cctype>
#include <cstring>
#include <cstddef>
#include "../../app.h"
#include "../../util/i18n.h"
#include "../material/draw_helpers.h"
#include "../material/typography.h"
#include "../theme.h"
#include "../../embedded/IconsMaterialDesign.h"
#include "imgui.h"
@@ -18,12 +22,41 @@ namespace ui {
class AddressLabelDialog {
public:
static constexpr const char* kPickaxeGlyph = "\xEE\x80\x81";
static bool drawIconByName(ImDrawList* dl,
const std::string& name,
ImVec2 center,
float /*emSize*/,
ImU32 color,
ImFont* iconFont,
float iconFontSize) {
if (name == "pickaxe") {
ImFont* pickaxeFont = material::Typography::instance().pickaxeFontForSize(iconFontSize);
if (!pickaxeFont) return false;
ImVec2 iSz = pickaxeFont->CalcTextSizeA(iconFontSize, 1000.0f, 0.0f, kPickaxeGlyph);
dl->AddText(pickaxeFont, iconFontSize,
ImVec2(center.x - iSz.x * 0.5f, center.y - iSz.y * 0.5f), color, kPickaxeGlyph);
return true;
}
const char* glyph = iconGlyphForName(name);
if (!glyph || !iconFont) return false;
ImVec2 iSz = iconFont->CalcTextSizeA(iconFontSize, 1000.0f, 0.0f, glyph);
dl->AddText(iconFont, iconFontSize,
ImVec2(center.x - iSz.x * 0.5f, center.y - iSz.y * 0.5f), color, glyph);
return true;
}
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);
@@ -44,7 +77,9 @@ public:
using namespace material;
if (BeginOverlayDialog(TR("set_label"), &s_open, 420.0f, 0.92f)) {
constexpr float cardTopViewportRatio = 0.15f;
constexpr float cardBottomViewportRatio = 0.80f;
if (BeginOverlayDialog(TR("set_label"), &s_open, 660.0f, 0.92f, cardBottomViewportRatio)) {
float dp = Layout::dpiScale();
// Address preview
@@ -70,16 +105,64 @@ public:
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 = 1.0f;
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(kIconCount);
{
// 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 < kIconCount; ++i) {
if (needle.empty() || std::strstr(kIconNames[i], 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();
for (int i = 0; i < kIconCount; ++i) {
if (i % cols != 0) ImGui::SameLine(0, 4.0f * dp);
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];
if (col != 0) ImGui::SameLine(0, 4.0f * dp);
ImVec2 pos = ImGui::GetCursorScreenPos();
ImVec2 mn = pos;
ImVec2 mx(pos.x + cellSz, pos.y + cellSz);
@@ -95,22 +178,33 @@ public:
}
// Icon centered in cell
ImVec2 iSz = iconFont->CalcTextSizeA(iconFsz, 1000.0f, 0.0f, kIconGlyphs[i]);
dl->AddText(iconFont, iconFsz,
ImVec2(mn.x + (cellSz - iSz.x) * 0.5f, mn.y + (cellSz - iSz.y) * 0.5f),
sel ? Primary() : (hov ? OnSurface() : OnSurfaceMedium()),
kIconGlyphs[i]);
drawIconByName(dl,
kIconNames[i],
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) ImGui::SetTooltip("%s", kIconNames[i]);
ImGui::PopID();
col = (col + 1) % cols;
}
ImGui::EndChild();
ImGui::PopStyleColor();
if (ImGui::GetCursorPosY() < controlsTopY) {
ImGui::SetCursorPosY(controlsTopY);
}
// "No icon" option
ImGui::Spacing();
if (s_selectedIcon >= 0) {
if (showClearIcon) {
ImGui::Spacing();
if (ImGui::SmallButton(TR("clear_icon"))) {
s_selectedIcon = -1;
}
@@ -121,16 +215,27 @@ public:
ImGui::Separator();
ImGui::Spacing();
// Buttons
float btnW = 120.0f;
float totalW = btnW * 2 + Layout::spacingMd();
ImGui::SetCursorPosX((ImGui::GetContentRegionAvail().x - totalW) * 0.5f);
if (ImGui::GetCursorPosY() < buttonY) {
ImGui::SetCursorPosY(buttonY);
}
if (TactileButton(TR("cancel"), ImVec2(btnW, 0))) {
// 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(TR("save"), ImVec2(btnW, 0))) {
if (TactileButton(saveLabel, ImVec2(saveW, 0), buttonFont)) {
// Apply changes
s_app->setAddressLabel(s_address, s_label);
if (s_selectedIcon >= 0)
@@ -153,23 +258,118 @@ private:
static inline bool s_isZ = false;
static inline char s_label[128] = {};
static inline int s_selectedIcon = -1;
static inline char s_iconSearch[64] = {};
// Icon palette — wallet-relevant Material Design icons
static constexpr int kIconCount = 20;
static inline const char* kIconNames[kIconCount] = {
"savings", "account_balance", "wallet", "payments",
"diamond", "shield", "lock", "swap_horiz",
"store", "home", "work", "rocket_launch",
"favorite", "bolt", "token", "category",
"label", "coffee", "volunteer", "star"
static inline const char* kIconNames[] = {
// Finance / Crypto
"savings", "account_balance", "account_balance_wallet", "wallet",
"payments", "credit_card", "local_atm", "diamond",
"attach_money", "currency_bitcoin", "currency_exchange", "balance",
"calculate", "trending_up", "euro", "leaderboard",
"paid", "sell", "receipt", "percent",
"price_change", "price_check", "toll", "money",
// Charts / Analytics
"show_chart", "candlestick_chart", "bar_chart", "pie_chart",
"area_chart", "stacked_bar_chart", "waterfall_chart", "scatter_plot",
"query_stats", "speed", "donut_large",
// Mining / Tools
"pickaxe",
"hardware", "construction", "handyman", "build",
"carpenter", "plumbing", "home_repair_service", "precision_manufacturing",
"factory", "warehouse", "inventory", "recycling",
"oil_barrel", "offline_bolt", "thunderstorm", "terminal",
"storage", "memory", "developer_board",
// Security / Auth
"shield", "security", "lock", "swap_horiz",
"verified", "verified_user", "key", "badge",
// Commerce / Business
"store", "storefront", "shopping_bag", "business",
"work", "real_estate_agent", "gavel", "local_shipping",
// Home / Property
"home", "apartment", "cottage", "landscape",
// People / Identity
"account_circle", "face", "manage_accounts", "groups", "mood",
// Travel / Transport
"rocket_launch", "flight", "directions_car", "travel_explore",
"explore", "location_on", "map", "luggage", "anchor",
// Nature / Outdoors
"public", "language", "forest", "park",
"water_drop", "beach_access", "energy_savings_leaf", "solar_power",
// Social / Lifestyle
"favorite", "star", "celebration", "casino",
"auto_awesome", "emoji_events", "military_tech", "flag",
// Tech / Science
"bolt", "tungsten", "lightbulb", "insights",
"hub", "token", "electric_bolt", "science", "biotech",
// Organisation
"category", "label", "school", "local_hospital", "local_florist",
// Food / Drink
"coffee", "restaurant", "wine_bar", "liquor",
"outdoor_grill", "nightlife", "sports_bar",
// Recreation / Health
"pets", "fitness_center", "spa", "self_improvement",
"psychology", "sports_soccer", "sports_esports",
"hiking", "palette", "museum", "church", "surfing",
// Community
"redeem", "handshake", "healing", "volunteer",
"stadium", "temple_buddhist", "theater_comedy", "watch",
};
static inline const char* kIconGlyphs[kIconCount] = {
ICON_MD_SAVINGS, ICON_MD_ACCOUNT_BALANCE, ICON_MD_WALLET, ICON_MD_PAYMENTS,
ICON_MD_DIAMOND, ICON_MD_SHIELD, ICON_MD_LOCK, ICON_MD_SWAP_HORIZ,
ICON_MD_STORE, ICON_MD_HOME, ICON_MD_WORK, ICON_MD_ROCKET_LAUNCH,
ICON_MD_FAVORITE, ICON_MD_BOLT, ICON_MD_TOKEN, ICON_MD_CATEGORY,
ICON_MD_LABEL, ICON_MD_LOCAL_CAFE, ICON_MD_VOLUNTEER_ACTIVISM, ICON_MD_STAR
static inline const char* kIconGlyphs[] = {
// Finance / Crypto
ICON_MD_SAVINGS, ICON_MD_ACCOUNT_BALANCE, ICON_MD_ACCOUNT_BALANCE_WALLET, ICON_MD_WALLET,
ICON_MD_PAYMENTS, ICON_MD_CREDIT_CARD, ICON_MD_LOCAL_ATM, ICON_MD_DIAMOND,
ICON_MD_ATTACH_MONEY, ICON_MD_CURRENCY_BITCOIN, ICON_MD_CURRENCY_EXCHANGE, ICON_MD_BALANCE,
ICON_MD_CALCULATE, ICON_MD_TRENDING_UP, ICON_MD_EURO, ICON_MD_LEADERBOARD,
ICON_MD_PAID, ICON_MD_SELL, ICON_MD_RECEIPT, ICON_MD_PERCENT,
ICON_MD_PRICE_CHANGE, ICON_MD_PRICE_CHECK, ICON_MD_TOLL, ICON_MD_MONEY,
// Charts / Analytics
ICON_MD_SHOW_CHART, ICON_MD_CANDLESTICK_CHART, ICON_MD_BAR_CHART, ICON_MD_PIE_CHART,
ICON_MD_AREA_CHART, ICON_MD_STACKED_BAR_CHART, ICON_MD_WATERFALL_CHART, ICON_MD_SCATTER_PLOT,
ICON_MD_QUERY_STATS, ICON_MD_SPEED, ICON_MD_DONUT_LARGE,
// Mining / Tools
nullptr,
ICON_MD_HARDWARE, ICON_MD_CONSTRUCTION, ICON_MD_HANDYMAN, ICON_MD_BUILD,
ICON_MD_CARPENTER, ICON_MD_PLUMBING, ICON_MD_HOME_REPAIR_SERVICE, ICON_MD_PRECISION_MANUFACTURING,
ICON_MD_FACTORY, ICON_MD_WAREHOUSE, ICON_MD_INVENTORY, ICON_MD_RECYCLING,
ICON_MD_OIL_BARREL, ICON_MD_OFFLINE_BOLT, ICON_MD_THUNDERSTORM, ICON_MD_TERMINAL,
ICON_MD_STORAGE, ICON_MD_MEMORY, ICON_MD_DEVELOPER_BOARD,
// Security / Auth
ICON_MD_SHIELD, ICON_MD_SECURITY, ICON_MD_LOCK, ICON_MD_SWAP_HORIZ,
ICON_MD_VERIFIED, ICON_MD_VERIFIED_USER, ICON_MD_KEY, ICON_MD_BADGE,
// Commerce / Business
ICON_MD_STORE, ICON_MD_STOREFRONT, ICON_MD_SHOPPING_BAG, ICON_MD_BUSINESS,
ICON_MD_WORK, ICON_MD_REAL_ESTATE_AGENT, ICON_MD_GAVEL, ICON_MD_LOCAL_SHIPPING,
// Home / Property
ICON_MD_HOME, ICON_MD_APARTMENT, ICON_MD_COTTAGE, ICON_MD_LANDSCAPE,
// People / Identity
ICON_MD_ACCOUNT_CIRCLE, ICON_MD_FACE, ICON_MD_MANAGE_ACCOUNTS, ICON_MD_GROUPS, ICON_MD_MOOD,
// Travel / Transport
ICON_MD_ROCKET_LAUNCH, ICON_MD_FLIGHT, ICON_MD_DIRECTIONS_CAR, ICON_MD_TRAVEL_EXPLORE,
ICON_MD_EXPLORE, ICON_MD_LOCATION_ON, ICON_MD_MAP, ICON_MD_LUGGAGE, ICON_MD_ANCHOR,
// Nature / Outdoors
ICON_MD_PUBLIC, ICON_MD_LANGUAGE, ICON_MD_FOREST, ICON_MD_PARK,
ICON_MD_WATER_DROP, ICON_MD_BEACH_ACCESS, ICON_MD_ENERGY_SAVINGS_LEAF, ICON_MD_SOLAR_POWER,
// Social / Lifestyle
ICON_MD_FAVORITE, ICON_MD_STAR, ICON_MD_CELEBRATION, ICON_MD_CASINO,
ICON_MD_AUTO_AWESOME, ICON_MD_EMOJI_EVENTS, ICON_MD_MILITARY_TECH, ICON_MD_FLAG,
// Tech / Science
ICON_MD_BOLT, ICON_MD_TUNGSTEN, ICON_MD_LIGHTBULB, ICON_MD_INSIGHTS,
ICON_MD_HUB, ICON_MD_TOKEN, ICON_MD_ELECTRIC_BOLT, ICON_MD_SCIENCE, ICON_MD_BIOTECH,
// Organisation
ICON_MD_CATEGORY, ICON_MD_LABEL, ICON_MD_SCHOOL, ICON_MD_LOCAL_HOSPITAL, ICON_MD_LOCAL_FLORIST,
// Food / Drink
ICON_MD_LOCAL_CAFE, ICON_MD_RESTAURANT, ICON_MD_WINE_BAR, ICON_MD_LIQUOR,
ICON_MD_OUTDOOR_GRILL, ICON_MD_NIGHTLIFE, ICON_MD_SPORTS_BAR,
// Recreation / Health
ICON_MD_PETS, ICON_MD_FITNESS_CENTER, ICON_MD_SPA, ICON_MD_SELF_IMPROVEMENT,
ICON_MD_PSYCHOLOGY, ICON_MD_SPORTS_SOCCER, ICON_MD_SPORTS_ESPORTS,
ICON_MD_HIKING, ICON_MD_PALETTE, ICON_MD_MUSEUM, ICON_MD_CHURCH, ICON_MD_SURFING,
// Community
ICON_MD_REDEEM, ICON_MD_HANDSHAKE, ICON_MD_HEALING, ICON_MD_VOLUNTEER_ACTIVISM,
ICON_MD_STADIUM, ICON_MD_TEMPLE_BUDDHIST, ICON_MD_THEATER_COMEDY, ICON_MD_WATCH,
};
static constexpr int kIconCount = static_cast<int>(std::size(kIconNames));
public:
// Expose for the address list to look up icon glyphs by name

View File

@@ -45,11 +45,12 @@ public:
s_resultMsg.clear();
s_success = false;
// Pre-fill amount with full source balance
snprintf(s_amount, sizeof(s_amount), "%.8f", info.fromBalance);
// Default fee
s_fee = 0.0001;
// Pre-fill with the maximum sendable amount so the dialog is valid
// immediately without requiring the user to press Max.
snprintf(s_amount, sizeof(s_amount), "%.8f", maxSendableAmount(info.fromBalance, s_fee));
}
static void render() {
@@ -57,7 +58,7 @@ public:
using namespace material;
if (BeginOverlayDialog(TR("transfer_funds"), &s_open, 480.0f, 0.94f)) {
if (BeginOverlayDialog(TR("transfer_funds"), &s_open, 620.0f, 0.94f)) {
float dp = Layout::dpiScale();
ImDrawList* dl = ImGui::GetWindowDrawList();
@@ -104,18 +105,22 @@ public:
ImGui::Spacing();
ImGui::Spacing();
// Amount input
// Amount input + Max button on same row without overflow
Type().text(TypeStyle::Subtitle2, TR("amount"));
ImGui::SetNextItemWidth(-1);
{
float spacing = ImGui::GetStyle().ItemSpacing.x;
float maxBtnW = ImGui::CalcTextSize(TR("max")).x
+ ImGui::GetStyle().FramePadding.x * 2.0f;
ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x - maxBtnW - spacing);
}
ImGui::InputText("##TransferAmt", s_amount, sizeof(s_amount),
ImGuiInputTextFlags_CharsDecimal);
// Max button
ImGui::SameLine();
if (ImGui::SmallButton(TR("max"))) {
double maxAmt = s_info.fromBalance - s_fee;
if (maxAmt < 0) maxAmt = 0;
snprintf(s_amount, sizeof(s_amount), "%.8f", maxAmt);
snprintf(s_amount, sizeof(s_amount), "%.8f",
maxSendableAmount(s_info.fromBalance, s_fee));
}
ImGui::Spacing();
@@ -140,15 +145,15 @@ public:
ImGui::Spacing();
{
char buf[128];
snprintf(buf, sizeof(buf), "%s: %.8f DRGX → %.8f DRGX",
TR("sender_balance"), s_info.fromBalance, amountValid ? newFromBal : s_info.fromBalance);
snprintf(buf, sizeof(buf), TR("sender_balance"),
s_info.fromBalance, amountValid ? newFromBal : s_info.fromBalance);
Type().textColored(TypeStyle::Caption,
(amountValid && newFromBal < 1e-9) ? Warning() : OnSurfaceMedium(), buf);
}
{
char buf[128];
snprintf(buf, sizeof(buf), "%s: %.8f DRGX → %.8f DRGX",
TR("recipient_balance"), s_info.toBalance, amountValid ? newToBal : s_info.toBalance);
snprintf(buf, sizeof(buf), TR("recipient_balance"),
s_info.toBalance, amountValid ? newToBal : s_info.toBalance);
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), buf);
}
@@ -172,17 +177,32 @@ public:
ImGui::Spacing();
// Buttons
float btnW = 140.0f;
float totalW = btnW * 2 + Layout::spacingMd();
ImGui::SetCursorPosX((ImGui::GetContentRegionAvail().x - totalW) * 0.5f);
const char* cancelLabel = TR("cancel");
const char* confirmLabel = TR("confirm_transfer");
const char* sendingLabel = TR("sending");
ImFont* buttonFont = Type().button();
float buttonFontSize = ScaledFontSize(buttonFont);
float minBtnW = 120.0f * dp;
float confirmMinW = 160.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 confirmTextW = std::max(
buttonFont->CalcTextSizeA(buttonFontSize, 1000.0f, 0.0f, confirmLabel).x,
buttonFont->CalcTextSizeA(buttonFontSize, 1000.0f, 0.0f, sendingLabel).x);
float confirmW = std::max(confirmMinW, confirmTextW + buttonPadW);
float totalW = cancelW + confirmW + Layout::spacingMd();
float rowStartX = ImGui::GetCursorPosX();
float contentW = ImGui::GetContentRegionAvail().x;
ImGui::SetCursorPosX(rowStartX + std::max(0.0f, (contentW - totalW) * 0.5f));
if (TactileButton(TR("cancel"), ImVec2(btnW, 0))) {
if (TactileButton(cancelLabel, ImVec2(cancelW, 0), buttonFont)) {
s_open = false;
}
ImGui::SameLine(0, Layout::spacingMd());
ImGui::BeginDisabled(!amountValid || s_sending);
if (TactileButton(s_sending ? TR("sending") : TR("confirm_transfer"), ImVec2(btnW, 0))) {
if (TactileButton(s_sending ? sendingLabel : confirmLabel, ImVec2(confirmW, 0), buttonFont)) {
s_sending = true;
s_app->sendTransaction(s_info.fromAddr, s_info.toAddr,
amount, s_fee, "",
@@ -205,6 +225,11 @@ public:
static void close() { s_open = false; }
private:
static double maxSendableAmount(double balance, double fee) {
double maxAmt = balance - fee;
return maxAmt > 0.0 ? maxAmt : 0.0;
}
static void renderAddressRow(const std::string& addr, double balance, bool isZ, float dp) {
using namespace material;
ImDrawList* dl = ImGui::GetWindowDrawList();
@@ -228,22 +253,25 @@ private:
dl->AddText(iconFont, iconFsz, ImVec2(mn.x + pad, mn.y + (h - iSz.y) * 0.5f), col, icon);
// Address (truncated)
float textX = mn.x + pad + iSz.x + 8.0f * dp;
std::string display = addr;
if (display.size() > 42) display = display.substr(0, 18) + "..." + display.substr(display.size() - 14);
ImFont* capFont = Type().caption();
float capFsz = ScaledFontSize(capFont);
dl->AddText(capFont, capFsz, ImVec2(textX, mn.y + (h * 0.5f - capFsz)), OnSurfaceMedium(), display.c_str());
// Balance (right-aligned)
// Balance (right-aligned) — computed first so we know how much space address gets
char balBuf[32];
snprintf(balBuf, sizeof(balBuf), "%.8f DRGX", balance);
ImFont* body = Type().body2();
float bodyFsz = ScaledFontSize(body);
ImVec2 balSz = body->CalcTextSizeA(bodyFsz, 1000.0f, 0.0f, balBuf);
dl->AddText(body, bodyFsz, ImVec2(mx.x - pad - balSz.x, mn.y + (h - balSz.y) * 0.5f),
float balX = mx.x - pad - balSz.x;
dl->AddText(body, bodyFsz, ImVec2(balX, mn.y + (h - balSz.y) * 0.5f),
balance > 0 ? OnSurface() : OnSurfaceDisabled(), balBuf);
float textX = mn.x + pad + iSz.x + 8.0f * dp;
ImFont* capFont = Type().caption();
float capFsz = ScaledFontSize(capFont);
// Clip address text so it never overlaps the balance
float addrMaxW = balX - textX - 8.0f * dp;
dl->PushClipRect(ImVec2(textX, mn.y), ImVec2(textX + addrMaxW, mn.y + h), true);
dl->AddText(capFont, capFsz, ImVec2(textX, mn.y + (h * 0.5f - capFsz)), OnSurfaceMedium(), addr.c_str());
dl->PopClipRect();
ImGui::Dummy(ImVec2(w, h));
}

View File

@@ -1027,8 +1027,9 @@ static void RenderBalanceClassic(App* app)
float labelX = cx + rowIconSz * 2.0f + Layout::spacingSm();
const char* typeLabel = row.isZ ? "Shielded" : "Transparent";
const char* hiddenTag = row.hidden ? " (hidden)" : "";
const char* viewOnlyTag = (!addr.has_spending_key) ? " (view-only)" : "";
char typeBuf[64];
snprintf(typeBuf, sizeof(typeBuf), "%s%s", typeLabel, hiddenTag);
snprintf(typeBuf, sizeof(typeBuf), "%s%s%s", typeLabel, hiddenTag, viewOnlyTag);
dl->AddText(capFont, capFont->LegacySize, ImVec2(labelX, cy), typeCol, typeBuf);
// Label (if present, next to type)
@@ -1739,13 +1740,19 @@ static void RenderSharedAddressList(App* app, float listH, float availW,
float iconCx = cx + rowIconSz;
float iconCy = cy + body2->LegacySize * 0.5f;
{
const char* customGlyph = row.icon.empty() ? nullptr : AddressLabelDialog::iconGlyphForName(row.icon);
ImFont* iconFont = Type().iconSmall();
const char* glyph = customGlyph ? customGlyph : (row.isZ ? ICON_MD_SHIELD : ICON_MD_CIRCLE);
ImU32 icCol = customGlyph ? OnSurfaceMedium() : typeCol;
ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, glyph);
dl->AddText(iconFont, iconFont->LegacySize,
ImVec2(iconCx - iSz.x * 0.5f, iconCy - iSz.y * 0.5f), icCol, glyph);
bool drewCustom = false;
if (!row.icon.empty()) {
drewCustom = AddressLabelDialog::drawIconByName(
dl, row.icon, ImVec2(iconCx, iconCy), iconFont->LegacySize,
OnSurfaceMedium(), iconFont, iconFont->LegacySize);
}
if (!drewCustom) {
const char* glyph = row.isZ ? ICON_MD_SHIELD : ICON_MD_CIRCLE;
ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, glyph);
dl->AddText(iconFont, iconFont->LegacySize,
ImVec2(iconCx - iSz.x * 0.5f, iconCy - iSz.y * 0.5f), typeCol, glyph);
}
}
// ---- Type label (first line) ----

View File

@@ -222,7 +222,7 @@ static void RenderSourceDropdown(App* app, float width) {
int bestIdx = -1;
double bestBal = 0.0;
for (size_t i = 0; i < state.addresses.size(); i++) {
if (state.addresses[i].balance > bestBal) {
if (state.addresses[i].balance > bestBal && state.addresses[i].isSpendable()) {
bestBal = state.addresses[i].balance;
bestIdx = static_cast<int>(i);
}
@@ -259,11 +259,11 @@ static void RenderSourceDropdown(App* app, float width) {
if (!app->isConnected() || state.addresses.empty()) {
ImGui::TextDisabled("%s", TR("no_addresses_available"));
} else {
// Sort by balance descending, only show addresses with balance
// Sort by balance descending, only show spendable addresses with balance
std::vector<size_t> sortedIdx;
sortedIdx.reserve(state.addresses.size());
for (size_t i = 0; i < state.addresses.size(); i++) {
if (state.addresses[i].balance > 0)
if (state.addresses[i].balance > 0 && state.addresses[i].isSpendable())
sortedIdx.push_back(i);
}
std::sort(sortedIdx.begin(), sortedIdx.end(),

View File

@@ -434,6 +434,8 @@ void I18n::loadBuiltinEnglish()
strings_["label_placeholder"] = "e.g. Savings, Mining...";
strings_["choose_icon"] = "Choose Icon";
strings_["clear_icon"] = "Clear Icon";
strings_["search_icons"] = "Search icons...";
strings_["no_icons_found"] = "No icons match your search.";
strings_["transfer_funds"] = "Transfer Funds";
strings_["transfer_to"] = "Transfer to:";
strings_["deshielding_warning"] = "Warning: This will de-shield funds from a private (Z) address to a transparent (T) address.";