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:
@@ -433,6 +433,7 @@ set_source_files_properties(
|
|||||||
${CMAKE_SOURCE_DIR}/res/fonts/Ubuntu-Light.ttf;\
|
${CMAKE_SOURCE_DIR}/res/fonts/Ubuntu-Light.ttf;\
|
||||||
${CMAKE_SOURCE_DIR}/res/fonts/Ubuntu-Medium.ttf;\
|
${CMAKE_SOURCE_DIR}/res/fonts/Ubuntu-Medium.ttf;\
|
||||||
${CMAKE_SOURCE_DIR}/res/fonts/MaterialIcons-Regular.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"
|
${CMAKE_SOURCE_DIR}/res/fonts/NotoSansCJK-Subset.ttf"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
49
README.md
49
README.md
@@ -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**.
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||
@@ -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
|
- **Full Node Support**: Connects to dragonxd for complete blockchain verification
|
||||||
- **Shielded Transactions**: Full z-address support with encrypted memos
|
- **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
|
- **Market Data**: Real-time price charts from CoinGecko
|
||||||
- **QR Codes**: Generate and display QR codes for receiving addresses
|
- **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
|
- **Lightweight**: ~5-10MB binary vs ~50MB+ for Qt version
|
||||||
- **Fast Builds**: Compiles in seconds, not minutes
|
- **Fast Builds**: Compiles in seconds, not minutes
|
||||||
|
|
||||||
@@ -116,7 +121,8 @@ cd ObsidianDragon/
|
|||||||
./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
|
### 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:
|
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.)
|
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.
|
This is useful for testing new branches or hotfixes to the node software before they are bundled into a wallet release.
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
Configuration is stored in \`~/.hush/DRAGONX/DRAGONX.conf\`:
|
Configuration is stored in `~/.hush/DRAGONX/DRAGONX.conf`:
|
||||||
|
|
||||||
```
|
```
|
||||||
rpcuser=your_rpc_user
|
rpcuser=your_rpc_user
|
||||||
@@ -148,44 +155,46 @@ ObsidianDragon/
|
|||||||
├── src/
|
├── src/
|
||||||
│ ├── main.cpp # Entry point, SDL/ImGui setup
|
│ ├── main.cpp # Entry point, SDL/ImGui setup
|
||||||
│ ├── app.cpp/h # Main application class
|
│ ├── app.cpp/h # Main application class
|
||||||
│ ├── wallet_state.h # Wallet data structures
|
│ ├── data/ # WalletState, address book, exchange info
|
||||||
│ ├── version.h # Version definitions
|
│ ├── config/ # Settings persistence and generated version.h
|
||||||
│ ├── ui/
|
│ ├── ui/
|
||||||
│ │ ├── theme.cpp/h # DragonX theme
|
│ │ ├── schema/ # TOML UI schema and skin manager
|
||||||
│ │ └── windows/ # UI tabs and dialogs
|
│ │ ├── material/ # Material components, typography, layout
|
||||||
|
│ │ ├── windows/ # Tabs and dialogs
|
||||||
|
│ │ └── pages/ # Multi-page screens such as Settings
|
||||||
│ ├── rpc/
|
│ ├── rpc/
|
||||||
│ │ ├── rpc_client.cpp # JSON-RPC client
|
│ │ ├── rpc_client.cpp # JSON-RPC client
|
||||||
│ │ └── connection.cpp # Daemon connection
|
│ │ └── connection.cpp # Daemon connection
|
||||||
│ ├── config/
|
│ ├── resources/ # Embedded resource extraction
|
||||||
│ │ └── settings.cpp # Settings persistence
|
│ ├── platform/ # Windows DX11/backdrop helpers
|
||||||
│ ├── util/
|
│ ├── util/
|
||||||
│ │ ├── i18n.cpp # Internationalization
|
│ │ ├── i18n.cpp # Internationalization
|
||||||
│ │ └── ...
|
│ │ └── ...
|
||||||
│ └── daemon/
|
│ └── daemon/
|
||||||
│ └── embedded_daemon.cpp
|
│ └── embedded_daemon.cpp
|
||||||
├── res/
|
├── res/
|
||||||
│ ├── fonts/ # Ubuntu font
|
│ ├── fonts/ # Ubuntu, icon, and CJK fonts
|
||||||
│ └── lang/ # Translation files
|
│ └── lang/ # Translation files
|
||||||
├── libs/
|
├── libs/
|
||||||
│ └── qrcode/ # QR code generation
|
│ └── qrcode/ # QR code generation
|
||||||
├── CMakeLists.txt
|
├── CMakeLists.txt
|
||||||
├── build-release.sh # Build script
|
├── build.sh # Release/cross-platform build script
|
||||||
└── create-appimage.sh # AppImage packaging
|
└── scripts/create-appimage.sh # AppImage packaging
|
||||||
```
|
```
|
||||||
|
|
||||||
## Dependencies
|
## 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
|
- **[SDL3](https://github.com/libsdl-org/SDL)** — Cross-platform windowing/input
|
||||||
- **[nlohmann/json](https://github.com/nlohmann/json)** — JSON parsing
|
- **[nlohmann/json](https://github.com/nlohmann/json)** — JSON parsing
|
||||||
- **[toml++](https://github.com/marzer/tomlplusplus)** — TOML parsing (UI schema/themes)
|
- **[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/`:
|
Bundled in `libs/`:
|
||||||
|
|
||||||
- **[Dear ImGui](https://github.com/ocornut/imgui)** — Immediate mode GUI
|
- **[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
|
- **[QR-Code-generator](https://github.com/nayuki/QR-Code-generator)** — QR code rendering
|
||||||
- **[miniz](https://github.com/richgel999/miniz)** — ZIP compression
|
- **[miniz](https://github.com/richgel999/miniz)** — ZIP compression
|
||||||
- **[GLAD](https://glad.dav1d.de/)** — OpenGL loader (Linux/macOS)
|
- **[GLAD](https://glad.dav1d.de/)** — OpenGL loader (Linux/macOS)
|
||||||
@@ -202,9 +211,11 @@ Bundled in `libs/`:
|
|||||||
|
|
||||||
## Translation
|
## 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:
|
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
|
2. Translate all strings
|
||||||
3. The language will appear in Settings automatically
|
3. The language will appear in Settings automatically
|
||||||
|
|
||||||
|
|||||||
@@ -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`
|
- **Location:** `src/embedded/IconsMaterialDesign.h`
|
||||||
- **Source:** https://github.com/juliettef/IconFontCppHeaders
|
- **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`
|
- **Location:** `res/fonts/Ubuntu-Light.ttf`, `Ubuntu-Medium.ttf`, `Ubuntu-R.ttf`
|
||||||
- **Source:** https://design.ubuntu.com/font
|
- **Source:** https://design.ubuntu.com/font
|
||||||
|
|||||||
70
docs/codebase-overview.md
Normal file
70
docs/codebase-overview.md
Normal 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.
|
||||||
BIN
res/fonts/MaterialDesignIcons-Pickaxe-Subset.ttf
Normal file
BIN
res/fonts/MaterialDesignIcons-Pickaxe-Subset.ttf
Normal file
Binary file not shown.
@@ -1279,7 +1279,7 @@ page-fade-speed = { size = 8.0 }
|
|||||||
collapse-hysteresis = { size = 60.0 }
|
collapse-hysteresis = { size = 60.0 }
|
||||||
header-icon = { icon-dark = "logos/logo_ObsidianDragon_dark.png", icon-light = "logos/logo_ObsidianDragon_light.png" }
|
header-icon = { icon-dark = "logos/logo_ObsidianDragon_dark.png", icon-light = "logos/logo_ObsidianDragon_light.png" }
|
||||||
coin-icon = { icon = "logos/logo_dragonx_128.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]
|
[components.main-window.window]
|
||||||
padding = [12, 36]
|
padding = [12, 36]
|
||||||
|
|||||||
@@ -750,6 +750,19 @@ void App::refreshAddressData()
|
|||||||
AddressInfo info;
|
AddressInfo info;
|
||||||
info.address = addr.get<std::string>();
|
info.address = addr.get<std::string>();
|
||||||
info.type = "shielded";
|
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);
|
zAddrs.push_back(info);
|
||||||
}
|
}
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
@@ -1850,6 +1863,21 @@ void App::sendTransaction(const std::string& from, const std::string& to,
|
|||||||
return;
|
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
|
// Build recipients array
|
||||||
nlohmann::json recipients = nlohmann::json::array();
|
nlohmann::json recipients = nlohmann::json::array();
|
||||||
nlohmann::json recipient;
|
nlohmann::json recipient;
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ struct AddressInfo {
|
|||||||
std::string address;
|
std::string address;
|
||||||
double balance = 0.0;
|
double balance = 0.0;
|
||||||
std::string type; // "shielded" or "transparent"
|
std::string type; // "shielded" or "transparent"
|
||||||
|
bool has_spending_key = true; // false for view-only (imported via z_importviewingkey)
|
||||||
|
|
||||||
// For display
|
// For display
|
||||||
std::string label;
|
std::string label;
|
||||||
@@ -25,6 +26,7 @@ struct AddressInfo {
|
|||||||
// Derived
|
// Derived
|
||||||
bool isZAddr() const { return !address.empty() && address[0] == 'z'; }
|
bool isZAddr() const { return !address.empty() && address[0] == 'z'; }
|
||||||
bool isShielded() const { return type == "shielded"; }
|
bool isShielded() const { return type == "shielded"; }
|
||||||
|
bool isSpendable() const { return has_spending_key; }
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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_light, "@CMAKE_SOURCE_DIR@/res/fonts/Ubuntu-Light.ttf");
|
||||||
INCBIN(ubuntu_medium, "@CMAKE_SOURCE_DIR@/res/fonts/Ubuntu-Medium.ttf");
|
INCBIN(ubuntu_medium, "@CMAKE_SOURCE_DIR@/res/fonts/Ubuntu-Medium.ttf");
|
||||||
INCBIN(material_icons, "@CMAKE_SOURCE_DIR@/res/fonts/MaterialIcons-Regular.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");
|
INCBIN(noto_cjk_subset, "@CMAKE_SOURCE_DIR@/res/fonts/NotoSansCJK-Subset.ttf");
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ extern "C" {
|
|||||||
extern const unsigned char g_material_icons_data[];
|
extern const unsigned char g_material_icons_data[];
|
||||||
extern const unsigned int g_material_icons_size;
|
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 char g_noto_cjk_subset_data[];
|
||||||
extern const unsigned int g_noto_cjk_subset_size;
|
extern const unsigned int g_noto_cjk_subset_size;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
// and will return true even when a modal popup covers the rect, which
|
||||||
// causes background elements to show hover highlights through dialogs.
|
// 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)
|
inline bool IsRectHovered(const ImVec2& r_min, const ImVec2& r_max, bool clip = true)
|
||||||
{
|
{
|
||||||
if (!ImGui::IsMouseHoveringRect(r_min, r_max, clip))
|
if (!ImGui::IsMouseHoveringRect(r_min, r_max, clip))
|
||||||
return false;
|
return false;
|
||||||
|
if (IsOverlayDialogBlockingInput() && !IsCurrentWindowOverlayDialog())
|
||||||
|
return false;
|
||||||
// If a modal popup is open and it is not the current window, treat
|
// If a modal popup is open and it is not the current window, treat
|
||||||
// the content as non-hoverable (same logic ImGui uses internally
|
// the content as non-hoverable (same logic ImGui uses internally
|
||||||
// inside IsWindowContentHoverable for modal blocking).
|
// 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.
|
// Creates a fullscreen semi-transparent overlay with a centered card dialog.
|
||||||
// Similar to the shutdown screen pattern but for interactive dialogs.
|
// 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();
|
ImGuiViewport* vp = ImGui::GetMainViewport();
|
||||||
ImVec2 vp_pos = vp->Pos;
|
ImVec2 vp_pos = vp->Pos;
|
||||||
ImVec2 vp_size = vp->Size;
|
ImVec2 vp_size = vp->Size;
|
||||||
@@ -914,6 +947,14 @@ inline bool BeginOverlayDialog(const char* title, bool* p_open, float cardWidth
|
|||||||
}
|
}
|
||||||
|
|
||||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
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)
|
// Calculate card position (centered)
|
||||||
float cardX = vp_pos.x + (vp_size.x - cardWidth) * 0.5f;
|
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
|
// Draw glass card background
|
||||||
ImVec2 cardMin(cardX, cardY);
|
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
|
// Card background with glass effect
|
||||||
GlassPanelSpec cardGlass;
|
GlassPanelSpec cardGlass;
|
||||||
@@ -930,6 +971,11 @@ inline bool BeginOverlayDialog(const char* title, bool* p_open, float cardWidth
|
|||||||
cardGlass.borderAlpha = 50;
|
cardGlass.borderAlpha = 50;
|
||||||
cardGlass.borderWidth = 1.0f;
|
cardGlass.borderWidth = 1.0f;
|
||||||
DrawGlassPanel(dl, cardMin, cardMax, cardGlass);
|
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
|
// Set up child region for card content
|
||||||
ImGui::SetCursorScreenPos(ImVec2(cardX, cardY));
|
ImGui::SetCursorScreenPos(ImVec2(cardX, cardY));
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ bool Typography::reload(ImGuiIO& io, float dpiScale)
|
|||||||
loaded_ = false;
|
loaded_ = false;
|
||||||
for (int i = 0; i < kNumStyles; ++i) fonts_[i] = nullptr;
|
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) iconFonts_[i] = nullptr;
|
||||||
|
for (int i = 0; i < kNumIconSizes; ++i) pickaxeFonts_[i] = nullptr;
|
||||||
return load(io, dpiScale);
|
return load(io, dpiScale);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,6 +191,13 @@ bool Typography::load(ImGuiIO& io, float dpiScale)
|
|||||||
iconFonts_[2] = loadIconFont(io, 24.0f * scale, "IconLarge");
|
iconFonts_[2] = loadIconFont(io, 24.0f * scale, "IconLarge");
|
||||||
iconFonts_[3] = loadIconFont(io, 40.0f * scale, "IconXL");
|
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
|
// Verify all fonts loaded
|
||||||
bool allLoaded = true;
|
bool allLoaded = true;
|
||||||
for (int i = 0; i < kNumStyles; ++i) {
|
for (int i = 0; i < kNumStyles; ++i) {
|
||||||
@@ -346,6 +354,51 @@ ImFont* Typography::loadIconFont(ImGuiIO& io, float size, const char* name)
|
|||||||
return font;
|
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
|
ImFont* Typography::getFont(TypeStyle style) const
|
||||||
{
|
{
|
||||||
int index = static_cast<int>(style);
|
int index = static_cast<int>(style);
|
||||||
|
|||||||
@@ -175,6 +175,14 @@ public:
|
|||||||
ImFont* iconMed() const { return iconFonts_[1] ? iconFonts_[1] : getFont(TypeStyle::Body1); }
|
ImFont* iconMed() const { return iconFonts_[1] ? iconFonts_[1] : getFont(TypeStyle::Body1); }
|
||||||
ImFont* iconLarge() const { return iconFonts_[2] ? iconFonts_[2] : getFont(TypeStyle::H5); }
|
ImFont* iconLarge() const { return iconFonts_[2] ? iconFonts_[2] : getFont(TypeStyle::H5); }
|
||||||
ImFont* iconXL() const { return iconFonts_[3] ? iconFonts_[3] : getFont(TypeStyle::H3); }
|
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*
|
* @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)
|
// Icon fonts at different sizes: [0]=small(14), [1]=med(18), [2]=large(24), [3]=xl(40)
|
||||||
ImFont* iconFonts_[4] = {};
|
ImFont* iconFonts_[4] = {};
|
||||||
|
ImFont* pickaxeFonts_[4] = {};
|
||||||
static constexpr int kNumIconSizes = 4;
|
static constexpr int kNumIconSizes = 4;
|
||||||
|
|
||||||
// Load an icon-only font at a specific pixel size
|
// Load an icon-only font at a specific pixel size
|
||||||
ImFont* loadIconFont(ImGuiIO& io, float size, const char* name);
|
ImFont* loadIconFont(ImGuiIO& io, float size, const char* name);
|
||||||
|
ImFont* loadPickaxeFont(ImGuiIO& io, float size, const char* name);
|
||||||
|
|
||||||
// Type specifications
|
// Type specifications
|
||||||
static const TypeSpec* getTypeSpecs();
|
static const TypeSpec* getTypeSpecs();
|
||||||
|
|||||||
@@ -5,10 +5,14 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <cctype>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
#include <cstddef>
|
||||||
#include "../../app.h"
|
#include "../../app.h"
|
||||||
#include "../../util/i18n.h"
|
#include "../../util/i18n.h"
|
||||||
#include "../material/draw_helpers.h"
|
#include "../material/draw_helpers.h"
|
||||||
|
#include "../material/typography.h"
|
||||||
#include "../theme.h"
|
#include "../theme.h"
|
||||||
#include "../../embedded/IconsMaterialDesign.h"
|
#include "../../embedded/IconsMaterialDesign.h"
|
||||||
#include "imgui.h"
|
#include "imgui.h"
|
||||||
@@ -18,12 +22,41 @@ namespace ui {
|
|||||||
|
|
||||||
class AddressLabelDialog {
|
class AddressLabelDialog {
|
||||||
public:
|
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) {
|
static void show(App* app, const std::string& address, bool isZ) {
|
||||||
s_open = true;
|
s_open = true;
|
||||||
s_app = app;
|
s_app = app;
|
||||||
s_address = address;
|
s_address = address;
|
||||||
s_isZ = isZ;
|
s_isZ = isZ;
|
||||||
s_selectedIcon = -1;
|
s_selectedIcon = -1;
|
||||||
|
s_iconSearch[0] = '\0';
|
||||||
|
|
||||||
// Pre-fill from existing metadata
|
// Pre-fill from existing metadata
|
||||||
std::string existing = app->getAddressLabel(address);
|
std::string existing = app->getAddressLabel(address);
|
||||||
@@ -44,7 +77,9 @@ public:
|
|||||||
|
|
||||||
using namespace material;
|
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();
|
float dp = Layout::dpiScale();
|
||||||
|
|
||||||
// Address preview
|
// Address preview
|
||||||
@@ -70,16 +105,64 @@ public:
|
|||||||
Type().text(TypeStyle::Subtitle2, TR("choose_icon"));
|
Type().text(TypeStyle::Subtitle2, TR("choose_icon"));
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
|
|
||||||
|
// Search bar
|
||||||
|
ImGui::SetNextItemWidth(-1);
|
||||||
|
ImGui::InputTextWithHint("##IconSearch", TR("search_icons"), s_iconSearch, sizeof(s_iconSearch));
|
||||||
|
ImGui::Spacing();
|
||||||
|
|
||||||
ImFont* iconFont = Type().iconMed();
|
ImFont* iconFont = Type().iconMed();
|
||||||
float iconFsz = ScaledFontSize(iconFont);
|
float iconFsz = ScaledFontSize(iconFont);
|
||||||
float cellSz = iconFsz + 16.0f * dp;
|
float cellSz = iconFsz + 16.0f * dp;
|
||||||
float avail = ImGui::GetContentRegionAvail().x;
|
float avail = ImGui::GetContentRegionAvail().x;
|
||||||
int cols = std::max(1, (int)(avail / (cellSz + 4.0f * dp)));
|
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();
|
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||||
|
|
||||||
for (int i = 0; i < kIconCount; ++i) {
|
if (visible.empty()) {
|
||||||
if (i % cols != 0) ImGui::SameLine(0, 4.0f * dp);
|
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 pos = ImGui::GetCursorScreenPos();
|
||||||
ImVec2 mn = pos;
|
ImVec2 mn = pos;
|
||||||
ImVec2 mx(pos.x + cellSz, pos.y + cellSz);
|
ImVec2 mx(pos.x + cellSz, pos.y + cellSz);
|
||||||
@@ -95,22 +178,33 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Icon centered in cell
|
// Icon centered in cell
|
||||||
ImVec2 iSz = iconFont->CalcTextSizeA(iconFsz, 1000.0f, 0.0f, kIconGlyphs[i]);
|
drawIconByName(dl,
|
||||||
dl->AddText(iconFont, iconFsz,
|
kIconNames[i],
|
||||||
ImVec2(mn.x + (cellSz - iSz.x) * 0.5f, mn.y + (cellSz - iSz.y) * 0.5f),
|
ImVec2(mn.x + cellSz * 0.5f, mn.y + cellSz * 0.5f),
|
||||||
sel ? Primary() : (hov ? OnSurface() : OnSurfaceMedium()),
|
iconFsz,
|
||||||
kIconGlyphs[i]);
|
sel ? Primary() : (hov ? OnSurface() : OnSurfaceMedium()),
|
||||||
|
iconFont,
|
||||||
|
iconFsz);
|
||||||
|
|
||||||
ImGui::PushID(i);
|
ImGui::PushID(i);
|
||||||
ImGui::InvisibleButton("##icon", ImVec2(cellSz, cellSz));
|
ImGui::InvisibleButton("##icon", ImVec2(cellSz, cellSz));
|
||||||
if (ImGui::IsItemClicked()) s_selectedIcon = i;
|
if (ImGui::IsItemClicked()) s_selectedIcon = i;
|
||||||
if (hov) ImGui::SetTooltip("%s", kIconNames[i]);
|
if (hov) ImGui::SetTooltip("%s", kIconNames[i]);
|
||||||
ImGui::PopID();
|
ImGui::PopID();
|
||||||
|
|
||||||
|
col = (col + 1) % cols;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::EndChild();
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
|
||||||
|
if (ImGui::GetCursorPosY() < controlsTopY) {
|
||||||
|
ImGui::SetCursorPosY(controlsTopY);
|
||||||
}
|
}
|
||||||
|
|
||||||
// "No icon" option
|
// "No icon" option
|
||||||
ImGui::Spacing();
|
if (showClearIcon) {
|
||||||
if (s_selectedIcon >= 0) {
|
ImGui::Spacing();
|
||||||
if (ImGui::SmallButton(TR("clear_icon"))) {
|
if (ImGui::SmallButton(TR("clear_icon"))) {
|
||||||
s_selectedIcon = -1;
|
s_selectedIcon = -1;
|
||||||
}
|
}
|
||||||
@@ -121,16 +215,27 @@ public:
|
|||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
|
|
||||||
// Buttons
|
if (ImGui::GetCursorPosY() < buttonY) {
|
||||||
float btnW = 120.0f;
|
ImGui::SetCursorPosY(buttonY);
|
||||||
float totalW = btnW * 2 + Layout::spacingMd();
|
}
|
||||||
ImGui::SetCursorPosX((ImGui::GetContentRegionAvail().x - totalW) * 0.5f);
|
|
||||||
|
|
||||||
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;
|
s_open = false;
|
||||||
}
|
}
|
||||||
ImGui::SameLine(0, Layout::spacingMd());
|
ImGui::SameLine(0, Layout::spacingMd());
|
||||||
if (TactileButton(TR("save"), ImVec2(btnW, 0))) {
|
if (TactileButton(saveLabel, ImVec2(saveW, 0), buttonFont)) {
|
||||||
// Apply changes
|
// Apply changes
|
||||||
s_app->setAddressLabel(s_address, s_label);
|
s_app->setAddressLabel(s_address, s_label);
|
||||||
if (s_selectedIcon >= 0)
|
if (s_selectedIcon >= 0)
|
||||||
@@ -153,23 +258,118 @@ private:
|
|||||||
static inline bool s_isZ = false;
|
static inline bool s_isZ = false;
|
||||||
static inline char s_label[128] = {};
|
static inline char s_label[128] = {};
|
||||||
static inline int s_selectedIcon = -1;
|
static inline int s_selectedIcon = -1;
|
||||||
|
static inline char s_iconSearch[64] = {};
|
||||||
|
|
||||||
// Icon palette — wallet-relevant Material Design icons
|
// Icon palette — wallet-relevant Material Design icons
|
||||||
static constexpr int kIconCount = 20;
|
static inline const char* kIconNames[] = {
|
||||||
static inline const char* kIconNames[kIconCount] = {
|
// Finance / Crypto
|
||||||
"savings", "account_balance", "wallet", "payments",
|
"savings", "account_balance", "account_balance_wallet", "wallet",
|
||||||
"diamond", "shield", "lock", "swap_horiz",
|
"payments", "credit_card", "local_atm", "diamond",
|
||||||
"store", "home", "work", "rocket_launch",
|
"attach_money", "currency_bitcoin", "currency_exchange", "balance",
|
||||||
"favorite", "bolt", "token", "category",
|
"calculate", "trending_up", "euro", "leaderboard",
|
||||||
"label", "coffee", "volunteer", "star"
|
"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] = {
|
static inline const char* kIconGlyphs[] = {
|
||||||
ICON_MD_SAVINGS, ICON_MD_ACCOUNT_BALANCE, ICON_MD_WALLET, ICON_MD_PAYMENTS,
|
// Finance / Crypto
|
||||||
ICON_MD_DIAMOND, ICON_MD_SHIELD, ICON_MD_LOCK, ICON_MD_SWAP_HORIZ,
|
ICON_MD_SAVINGS, ICON_MD_ACCOUNT_BALANCE, ICON_MD_ACCOUNT_BALANCE_WALLET, ICON_MD_WALLET,
|
||||||
ICON_MD_STORE, ICON_MD_HOME, ICON_MD_WORK, ICON_MD_ROCKET_LAUNCH,
|
ICON_MD_PAYMENTS, ICON_MD_CREDIT_CARD, ICON_MD_LOCAL_ATM, ICON_MD_DIAMOND,
|
||||||
ICON_MD_FAVORITE, ICON_MD_BOLT, ICON_MD_TOKEN, ICON_MD_CATEGORY,
|
ICON_MD_ATTACH_MONEY, ICON_MD_CURRENCY_BITCOIN, ICON_MD_CURRENCY_EXCHANGE, ICON_MD_BALANCE,
|
||||||
ICON_MD_LABEL, ICON_MD_LOCAL_CAFE, ICON_MD_VOLUNTEER_ACTIVISM, ICON_MD_STAR
|
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:
|
public:
|
||||||
// Expose for the address list to look up icon glyphs by name
|
// Expose for the address list to look up icon glyphs by name
|
||||||
|
|||||||
@@ -45,11 +45,12 @@ public:
|
|||||||
s_resultMsg.clear();
|
s_resultMsg.clear();
|
||||||
s_success = false;
|
s_success = false;
|
||||||
|
|
||||||
// Pre-fill amount with full source balance
|
|
||||||
snprintf(s_amount, sizeof(s_amount), "%.8f", info.fromBalance);
|
|
||||||
|
|
||||||
// Default fee
|
// Default fee
|
||||||
s_fee = 0.0001;
|
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() {
|
static void render() {
|
||||||
@@ -57,7 +58,7 @@ public:
|
|||||||
|
|
||||||
using namespace material;
|
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();
|
float dp = Layout::dpiScale();
|
||||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||||
|
|
||||||
@@ -104,18 +105,22 @@ public:
|
|||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
|
|
||||||
// Amount input
|
// Amount input + Max button on same row without overflow
|
||||||
Type().text(TypeStyle::Subtitle2, TR("amount"));
|
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),
|
ImGui::InputText("##TransferAmt", s_amount, sizeof(s_amount),
|
||||||
ImGuiInputTextFlags_CharsDecimal);
|
ImGuiInputTextFlags_CharsDecimal);
|
||||||
|
|
||||||
// Max button
|
// Max button
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
if (ImGui::SmallButton(TR("max"))) {
|
if (ImGui::SmallButton(TR("max"))) {
|
||||||
double maxAmt = s_info.fromBalance - s_fee;
|
snprintf(s_amount, sizeof(s_amount), "%.8f",
|
||||||
if (maxAmt < 0) maxAmt = 0;
|
maxSendableAmount(s_info.fromBalance, s_fee));
|
||||||
snprintf(s_amount, sizeof(s_amount), "%.8f", maxAmt);
|
|
||||||
}
|
}
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
|
|
||||||
@@ -140,15 +145,15 @@ public:
|
|||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
{
|
{
|
||||||
char buf[128];
|
char buf[128];
|
||||||
snprintf(buf, sizeof(buf), "%s: %.8f DRGX → %.8f DRGX",
|
snprintf(buf, sizeof(buf), TR("sender_balance"),
|
||||||
TR("sender_balance"), s_info.fromBalance, amountValid ? newFromBal : s_info.fromBalance);
|
s_info.fromBalance, amountValid ? newFromBal : s_info.fromBalance);
|
||||||
Type().textColored(TypeStyle::Caption,
|
Type().textColored(TypeStyle::Caption,
|
||||||
(amountValid && newFromBal < 1e-9) ? Warning() : OnSurfaceMedium(), buf);
|
(amountValid && newFromBal < 1e-9) ? Warning() : OnSurfaceMedium(), buf);
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
char buf[128];
|
char buf[128];
|
||||||
snprintf(buf, sizeof(buf), "%s: %.8f DRGX → %.8f DRGX",
|
snprintf(buf, sizeof(buf), TR("recipient_balance"),
|
||||||
TR("recipient_balance"), s_info.toBalance, amountValid ? newToBal : s_info.toBalance);
|
s_info.toBalance, amountValid ? newToBal : s_info.toBalance);
|
||||||
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), buf);
|
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,17 +177,32 @@ public:
|
|||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
|
|
||||||
// Buttons
|
// Buttons
|
||||||
float btnW = 140.0f;
|
const char* cancelLabel = TR("cancel");
|
||||||
float totalW = btnW * 2 + Layout::spacingMd();
|
const char* confirmLabel = TR("confirm_transfer");
|
||||||
ImGui::SetCursorPosX((ImGui::GetContentRegionAvail().x - totalW) * 0.5f);
|
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;
|
s_open = false;
|
||||||
}
|
}
|
||||||
ImGui::SameLine(0, Layout::spacingMd());
|
ImGui::SameLine(0, Layout::spacingMd());
|
||||||
|
|
||||||
ImGui::BeginDisabled(!amountValid || s_sending);
|
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_sending = true;
|
||||||
s_app->sendTransaction(s_info.fromAddr, s_info.toAddr,
|
s_app->sendTransaction(s_info.fromAddr, s_info.toAddr,
|
||||||
amount, s_fee, "",
|
amount, s_fee, "",
|
||||||
@@ -205,6 +225,11 @@ public:
|
|||||||
static void close() { s_open = false; }
|
static void close() { s_open = false; }
|
||||||
|
|
||||||
private:
|
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) {
|
static void renderAddressRow(const std::string& addr, double balance, bool isZ, float dp) {
|
||||||
using namespace material;
|
using namespace material;
|
||||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
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);
|
dl->AddText(iconFont, iconFsz, ImVec2(mn.x + pad, mn.y + (h - iSz.y) * 0.5f), col, icon);
|
||||||
|
|
||||||
// Address (truncated)
|
// Address (truncated)
|
||||||
float textX = mn.x + pad + iSz.x + 8.0f * dp;
|
// Balance (right-aligned) — computed first so we know how much space address gets
|
||||||
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)
|
|
||||||
char balBuf[32];
|
char balBuf[32];
|
||||||
snprintf(balBuf, sizeof(balBuf), "%.8f DRGX", balance);
|
snprintf(balBuf, sizeof(balBuf), "%.8f DRGX", balance);
|
||||||
ImFont* body = Type().body2();
|
ImFont* body = Type().body2();
|
||||||
float bodyFsz = ScaledFontSize(body);
|
float bodyFsz = ScaledFontSize(body);
|
||||||
ImVec2 balSz = body->CalcTextSizeA(bodyFsz, 1000.0f, 0.0f, balBuf);
|
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);
|
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));
|
ImGui::Dummy(ImVec2(w, h));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1027,8 +1027,9 @@ static void RenderBalanceClassic(App* app)
|
|||||||
float labelX = cx + rowIconSz * 2.0f + Layout::spacingSm();
|
float labelX = cx + rowIconSz * 2.0f + Layout::spacingSm();
|
||||||
const char* typeLabel = row.isZ ? "Shielded" : "Transparent";
|
const char* typeLabel = row.isZ ? "Shielded" : "Transparent";
|
||||||
const char* hiddenTag = row.hidden ? " (hidden)" : "";
|
const char* hiddenTag = row.hidden ? " (hidden)" : "";
|
||||||
|
const char* viewOnlyTag = (!addr.has_spending_key) ? " (view-only)" : "";
|
||||||
char typeBuf[64];
|
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);
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(labelX, cy), typeCol, typeBuf);
|
||||||
|
|
||||||
// Label (if present, next to type)
|
// Label (if present, next to type)
|
||||||
@@ -1739,13 +1740,19 @@ static void RenderSharedAddressList(App* app, float listH, float availW,
|
|||||||
float iconCx = cx + rowIconSz;
|
float iconCx = cx + rowIconSz;
|
||||||
float iconCy = cy + body2->LegacySize * 0.5f;
|
float iconCy = cy + body2->LegacySize * 0.5f;
|
||||||
{
|
{
|
||||||
const char* customGlyph = row.icon.empty() ? nullptr : AddressLabelDialog::iconGlyphForName(row.icon);
|
|
||||||
ImFont* iconFont = Type().iconSmall();
|
ImFont* iconFont = Type().iconSmall();
|
||||||
const char* glyph = customGlyph ? customGlyph : (row.isZ ? ICON_MD_SHIELD : ICON_MD_CIRCLE);
|
bool drewCustom = false;
|
||||||
ImU32 icCol = customGlyph ? OnSurfaceMedium() : typeCol;
|
if (!row.icon.empty()) {
|
||||||
ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, glyph);
|
drewCustom = AddressLabelDialog::drawIconByName(
|
||||||
dl->AddText(iconFont, iconFont->LegacySize,
|
dl, row.icon, ImVec2(iconCx, iconCy), iconFont->LegacySize,
|
||||||
ImVec2(iconCx - iSz.x * 0.5f, iconCy - iSz.y * 0.5f), icCol, glyph);
|
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) ----
|
// ---- Type label (first line) ----
|
||||||
|
|||||||
@@ -222,7 +222,7 @@ static void RenderSourceDropdown(App* app, float width) {
|
|||||||
int bestIdx = -1;
|
int bestIdx = -1;
|
||||||
double bestBal = 0.0;
|
double bestBal = 0.0;
|
||||||
for (size_t i = 0; i < state.addresses.size(); i++) {
|
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;
|
bestBal = state.addresses[i].balance;
|
||||||
bestIdx = static_cast<int>(i);
|
bestIdx = static_cast<int>(i);
|
||||||
}
|
}
|
||||||
@@ -259,11 +259,11 @@ static void RenderSourceDropdown(App* app, float width) {
|
|||||||
if (!app->isConnected() || state.addresses.empty()) {
|
if (!app->isConnected() || state.addresses.empty()) {
|
||||||
ImGui::TextDisabled("%s", TR("no_addresses_available"));
|
ImGui::TextDisabled("%s", TR("no_addresses_available"));
|
||||||
} else {
|
} 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;
|
std::vector<size_t> sortedIdx;
|
||||||
sortedIdx.reserve(state.addresses.size());
|
sortedIdx.reserve(state.addresses.size());
|
||||||
for (size_t i = 0; i < state.addresses.size(); i++) {
|
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);
|
sortedIdx.push_back(i);
|
||||||
}
|
}
|
||||||
std::sort(sortedIdx.begin(), sortedIdx.end(),
|
std::sort(sortedIdx.begin(), sortedIdx.end(),
|
||||||
|
|||||||
@@ -434,6 +434,8 @@ void I18n::loadBuiltinEnglish()
|
|||||||
strings_["label_placeholder"] = "e.g. Savings, Mining...";
|
strings_["label_placeholder"] = "e.g. Savings, Mining...";
|
||||||
strings_["choose_icon"] = "Choose Icon";
|
strings_["choose_icon"] = "Choose Icon";
|
||||||
strings_["clear_icon"] = "Clear 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_funds"] = "Transfer Funds";
|
||||||
strings_["transfer_to"] = "Transfer to:";
|
strings_["transfer_to"] = "Transfer to:";
|
||||||
strings_["deshielding_warning"] = "Warning: This will de-shield funds from a private (Z) address to a transparent (T) address.";
|
strings_["deshielding_warning"] = "Warning: This will de-shield funds from a private (Z) address to a transparent (T) address.";
|
||||||
|
|||||||
Reference in New Issue
Block a user