feat(ui): themed DragonX logo — runtime-rasterized SVG recolored per skin
Replace the shared app logo (Overview header, first-run wizard, lock screen) with the DragonX mark rendered from res/img/logos/logo_dragonx.svg, recolored to each theme: the dragon body takes the theme accent (Primary()), the inner detail stays light. Vendor nanosvg (memononen, zlib/public-domain) in libs/nanosvg/ and add util::LoadTextureFromSvg (parse a copy, recolor shapes by luminance, rasterize to RGBA, upload via the existing CreateRawTexture — GL + DX11). The SVG is embedded as a string (src/embedded/logo_dragonx_svg.h) so it's available in every build. The load moves into App::ensureLogoTexture(), called at the top of render() BEFORE the wizard/lock early-returns (so those screens get the logo too, which they never did before), and re-rasterizes when the accent or the dark/light variant changes. The per-skin PNG remains a fallback if rasterization ever fails; logo-texture lifetime is now freed on replace + on theme switch (was leaked). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -555,6 +555,7 @@ set(APP_SOURCES
|
|||||||
src/util/platform.cpp
|
src/util/platform.cpp
|
||||||
src/util/payment_uri.cpp
|
src/util/payment_uri.cpp
|
||||||
src/util/texture_loader.cpp
|
src/util/texture_loader.cpp
|
||||||
|
src/util/svg_texture.cpp
|
||||||
src/util/noise_texture.cpp
|
src/util/noise_texture.cpp
|
||||||
src/daemon/embedded_daemon.cpp
|
src/daemon/embedded_daemon.cpp
|
||||||
src/daemon/seed_wallet_creator.cpp
|
src/daemon/seed_wallet_creator.cpp
|
||||||
|
|||||||
3278
libs/nanosvg/nanosvg.h
Normal file
3278
libs/nanosvg/nanosvg.h
Normal file
File diff suppressed because it is too large
Load Diff
1472
libs/nanosvg/nanosvgrast.h
Normal file
1472
libs/nanosvg/nanosvgrast.h
Normal file
File diff suppressed because it is too large
Load Diff
23
res/img/logos/logo_dragonx.svg
Normal file
23
res/img/logos/logo_dragonx.svg
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
|
||||||
|
<defs>
|
||||||
|
<style>
|
||||||
|
.cls-1 {
|
||||||
|
fill: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-2 {
|
||||||
|
fill: #d82652;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<path class="cls-2" d="M103.98,128s-6.29-24.7-18.73-34.43c-8.53-8.03-15.63-16.49-21.25-24.16-5.62,7.68-12.72,16.17-21.25,24.16-12.4,9.74-18.73,34.43-18.73,34.43-2.38-24.34,7.85-35.82,12.87-41.29,7.82-8.5,15.31-16.56,21.75-25.02-7.64-11.44-11.41-19.72-11.41-19.72-.89-3.27-3.84-6.64-3.84-6.64,6.08-8.1-1.6-16.98-1.6-16.98,5.79-5.62,6.71-10.02,8.32-18.34-1.96,22.35,4.02,39.09,13.93,54.12,9.84-15.03,15.81-31.77,13.86-54.12,1.6,8.35,2.52,12.72,8.32,18.34,0,0-7.68,8.88-1.6,16.98,0,0-2.95,3.38-3.84,6.64,0,0-3.77,8.28-11.37,19.72,6.43,8.45,13.97,16.56,21.75,25.02,4.97,5.47,15.21,16.95,12.83,41.29h0Z"/>
|
||||||
|
<g>
|
||||||
|
<path class="cls-1" d="M55.33,61.62c-3.55,4.55-7.39,8.99-11.44,13.47-5.29-4.48-11.23-5.33-11.23-5.33,22.92-7.82,2.81-15.17.28-16.31C9.28,42.78,9.1,14.78,9.1,14.78c11.51,34.15,36.42,32.3,36.42,32.3.35-.21.67-.46.92-.71,1.64,3.27,4.58,8.67,8.88,15.24h0Z"/>
|
||||||
|
<g>
|
||||||
|
<path class="cls-1" d="M68.62,40.41c-1.35,2.98-2.91,5.83-4.62,8.63-1.71-2.81-3.23-5.69-4.62-8.63,1.74-3.45,4.62-20.58,4.62-20.58,0,0,2.88,17.13,4.62,20.58Z"/>
|
||||||
|
<path class="cls-1" d="M76.01,97.93l-3.48,2.34s-.1-4.44-3.52-1.84c-.42.32-2.38,2.21-.03,4.27,0,0-4.05,4.08-4.97,8.21-.92-4.12-4.97-8.21-4.97-8.21,2.34-2.06.39-3.95-.03-4.27-3.41-2.59-3.52,1.84-3.52,1.84l-3.48-2.34c.28-3.55.1-6.68-.46-9.42,4.69-4.94,8.85-9.88,12.47-14.61,3.66,4.72,7.78,9.67,12.47,14.61-.57,2.74-.75,5.86-.46,9.42Z"/>
|
||||||
|
<path class="cls-1" d="M95.34,69.76s-5.94.85-11.23,5.33c-4.02-4.48-7.89-8.92-11.44-13.47,4.3-6.57,7.25-11.98,8.88-15.24.25.25.57.5.92.71,0,0,24.91,1.84,36.42-32.3,0,0-.18,28-23.84,38.66-2.52,1.14-22.64,8.5.28,16.31h0Z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.9 KiB |
132
src/app.cpp
132
src/app.cpp
@@ -73,6 +73,9 @@
|
|||||||
#include "util/text_format.h"
|
#include "util/text_format.h"
|
||||||
#include "util/payment_uri.h"
|
#include "util/payment_uri.h"
|
||||||
#include "util/texture_loader.h"
|
#include "util/texture_loader.h"
|
||||||
|
#include "util/svg_texture.h"
|
||||||
|
#include "ui/material/colors.h"
|
||||||
|
#include "logo_dragonx_svg.h"
|
||||||
#include "util/bootstrap.h"
|
#include "util/bootstrap.h"
|
||||||
#include "util/secure_vault.h"
|
#include "util/secure_vault.h"
|
||||||
#include "resources/embedded_resources.h"
|
#include "resources/embedded_resources.h"
|
||||||
@@ -1353,6 +1356,70 @@ void App::handleGlobalShortcuts()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load (and recolor per theme) the DragonX header logo texture. Called at the top of render() — BEFORE
|
||||||
|
// the first-run-wizard / lock-screen paths — so every screen shows the mark. The SVG is recolored to the
|
||||||
|
// theme accent, so it re-rasterizes whenever the accent or the dark↔light variant changes.
|
||||||
|
void App::ensureLogoTexture()
|
||||||
|
{
|
||||||
|
const bool wantDark = ui::material::IsDarkTheme();
|
||||||
|
const ImU32 logoAccent = ui::material::Primary();
|
||||||
|
if (logo_loaded_ && wantDark == logo_is_dark_variant_ && logoAccent == logo_accent_)
|
||||||
|
return; // already current
|
||||||
|
logo_loaded_ = true;
|
||||||
|
logo_is_dark_variant_ = wantDark;
|
||||||
|
logo_accent_ = logoAccent;
|
||||||
|
if (logo_tex_) util::DestroyTexture(logo_tex_); // free the previous texture before replacing
|
||||||
|
logo_tex_ = 0; logo_w_ = 0; logo_h_ = 0;
|
||||||
|
|
||||||
|
// 0) DragonX mark — rasterize the embedded SVG recolored to the theme (body = accent, detail = white)
|
||||||
|
// at ~2x the 128px viewBox for crisp downscaling. This is the branding on every skin; the per-skin
|
||||||
|
// PNG path below is only a fallback if rasterization ever fails.
|
||||||
|
if (util::LoadTextureFromSvg(embedded::kLogoDragonXSvg, 256, logoAccent,
|
||||||
|
IM_COL32(255, 255, 255, 255), &logo_tex_, &logo_w_, &logo_h_)) {
|
||||||
|
DEBUG_LOGF("Rendered DragonX SVG logo (%dx%d, accent %08X)\n", logo_w_, logo_h_, logoAccent);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1) Fallback — theme-override logo from the active skin
|
||||||
|
const auto* activeSkin = ui::schema::SkinManager::instance().findById(
|
||||||
|
ui::schema::SkinManager::instance().activeSkinId());
|
||||||
|
std::string logoPath;
|
||||||
|
if (activeSkin && !activeSkin->logoPath.empty()) {
|
||||||
|
logoPath = activeSkin->logoPath;
|
||||||
|
} else {
|
||||||
|
// 2) Read icon filename from ui.toml (dark/light variant)
|
||||||
|
auto iconElem = ui::schema::UI().drawElement("components.main-window", "header-icon");
|
||||||
|
const char* iconKey = wantDark ? "icon-dark" : "icon-light";
|
||||||
|
auto it = iconElem.extraColors.find(iconKey);
|
||||||
|
std::string iconFile;
|
||||||
|
if (it != iconElem.extraColors.end() && !it->second.empty())
|
||||||
|
iconFile = it->second;
|
||||||
|
else
|
||||||
|
iconFile = wantDark ? "logos/logo_ObsidianDragon_dark.png" : "logos/logo_ObsidianDragon_light.png";
|
||||||
|
logoPath = util::getExecutableDirectory() + "/res/img/" + iconFile;
|
||||||
|
}
|
||||||
|
// Only attempt the disk read when the file is actually present (dev build / theme drop-in). The
|
||||||
|
// portable single-file build has no res/img/ beside it, so skip straight to the embedded copy.
|
||||||
|
std::error_code logoEc;
|
||||||
|
if (std::filesystem::exists(logoPath, logoEc) &&
|
||||||
|
util::LoadTextureFromFile(logoPath.c_str(), &logo_tex_, &logo_w_, &logo_h_)) {
|
||||||
|
DEBUG_LOGF("Loaded header logo from %s (%dx%d)\n", logoPath.c_str(), logo_w_, logo_h_);
|
||||||
|
} else {
|
||||||
|
std::string embeddedName = std::filesystem::path(logoPath).filename().string();
|
||||||
|
const auto* logoRes = resources::getEmbeddedResource(embeddedName);
|
||||||
|
if (!logoRes || !logoRes->data || logoRes->size == 0)
|
||||||
|
logoRes = resources::getEmbeddedResource(resources::RESOURCE_LOGO);
|
||||||
|
if (logoRes && logoRes->data && logoRes->size > 0) {
|
||||||
|
if (util::LoadTextureFromMemory(logoRes->data, logoRes->size, &logo_tex_, &logo_w_, &logo_h_))
|
||||||
|
DEBUG_LOGF("Loaded header logo from embedded: %s (%dx%d)\n", embeddedName.c_str(), logo_w_, logo_h_);
|
||||||
|
else
|
||||||
|
DEBUG_LOGF("Note: Failed to decode embedded logo (text-only header)\n");
|
||||||
|
} else {
|
||||||
|
DEBUG_LOGF("Note: Header logo not found at %s (text-only header)\n", logoPath.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void App::render()
|
void App::render()
|
||||||
{
|
{
|
||||||
// Advance the screenshot sweep FIRST — before the first-run-wizard early-return below — so the
|
// Advance the screenshot sweep FIRST — before the first-run-wizard early-return below — so the
|
||||||
@@ -1360,6 +1427,9 @@ void App::render()
|
|||||||
// (Pins current_page_ too, before the sidebar reads it further down.)
|
// (Pins current_page_ too, before the sidebar reads it further down.)
|
||||||
updateScreenshotSweep();
|
updateScreenshotSweep();
|
||||||
|
|
||||||
|
// DragonX logo — load/recolor before the wizard/lock early-returns so every screen shows it.
|
||||||
|
ensureLogoTexture();
|
||||||
|
|
||||||
// First-run wizard gate — blocks all normal UI
|
// First-run wizard gate — blocks all normal UI
|
||||||
if (wizard_phase_ != WizardPhase::None && wizard_phase_ != WizardPhase::Done) {
|
if (wizard_phase_ != WizardPhase::None && wizard_phase_ != WizardPhase::Done) {
|
||||||
renderFirstRunWizard();
|
renderFirstRunWizard();
|
||||||
@@ -1560,63 +1630,7 @@ void App::render()
|
|||||||
sbStatus.miningActive = state_.mining.generate || state_.pool_mining.xmrig_running;
|
sbStatus.miningActive = state_.mining.generate || state_.pool_mining.xmrig_running;
|
||||||
sbStatus.chatUnreadCount = chatUnreadCount(); // unread badge on the Chat nav item (Q1)
|
sbStatus.chatUnreadCount = chatUnreadCount(); // unread badge on the Chat nav item (Q1)
|
||||||
|
|
||||||
// Load logo texture lazily on first frame (or after theme change)
|
// (DragonX logo is loaded at the top of render() via ensureLogoTexture().)
|
||||||
// Also reload when dark↔light mode changes so the correct variant shows
|
|
||||||
{
|
|
||||||
bool wantDark = ui::material::IsDarkTheme();
|
|
||||||
if (!logo_loaded_ || (wantDark != logo_is_dark_variant_)) {
|
|
||||||
logo_loaded_ = true;
|
|
||||||
logo_is_dark_variant_ = wantDark;
|
|
||||||
logo_tex_ = 0; logo_w_ = 0; logo_h_ = 0;
|
|
||||||
|
|
||||||
// 1) Check for theme-override logo from active skin
|
|
||||||
const auto* activeSkin = ui::schema::SkinManager::instance().findById(
|
|
||||||
ui::schema::SkinManager::instance().activeSkinId());
|
|
||||||
std::string logoPath;
|
|
||||||
if (activeSkin && !activeSkin->logoPath.empty()) {
|
|
||||||
logoPath = activeSkin->logoPath;
|
|
||||||
} else {
|
|
||||||
// 2) Read icon filename from ui.toml (dark/light variant)
|
|
||||||
auto iconElem = ui::schema::UI().drawElement("components.main-window", "header-icon");
|
|
||||||
const char* iconKey = wantDark ? "icon-dark" : "icon-light";
|
|
||||||
auto it = iconElem.extraColors.find(iconKey);
|
|
||||||
std::string iconFile;
|
|
||||||
if (it != iconElem.extraColors.end() && !it->second.empty()) {
|
|
||||||
iconFile = it->second;
|
|
||||||
} else {
|
|
||||||
// Fallback filenames
|
|
||||||
iconFile = wantDark ? "logos/logo_ObsidianDragon_dark.png" : "logos/logo_ObsidianDragon_light.png";
|
|
||||||
}
|
|
||||||
logoPath = util::getExecutableDirectory() + "/res/img/" + iconFile;
|
|
||||||
}
|
|
||||||
// Only attempt the disk read when the file is actually present (dev build / theme drop-in).
|
|
||||||
// The portable single-file build has no res/img/ beside it, so skip straight to the
|
|
||||||
// embedded copy instead of logging a spurious "failed to read".
|
|
||||||
std::error_code logoEc;
|
|
||||||
if (std::filesystem::exists(logoPath, logoEc) &&
|
|
||||||
util::LoadTextureFromFile(logoPath.c_str(), &logo_tex_, &logo_w_, &logo_h_)) {
|
|
||||||
DEBUG_LOGF("Loaded header logo from %s (%dx%d)\n", logoPath.c_str(), logo_w_, logo_h_);
|
|
||||||
} else {
|
|
||||||
// Try embedded data fallback — use actual filename from path
|
|
||||||
// so light/dark variants resolve correctly on Windows single-file
|
|
||||||
std::string embeddedName = std::filesystem::path(logoPath).filename().string();
|
|
||||||
const auto* logoRes = resources::getEmbeddedResource(embeddedName);
|
|
||||||
if (!logoRes || !logoRes->data || logoRes->size == 0) {
|
|
||||||
// Final fallback: try the default dark logo constant
|
|
||||||
logoRes = resources::getEmbeddedResource(resources::RESOURCE_LOGO);
|
|
||||||
}
|
|
||||||
if (logoRes && logoRes->data && logoRes->size > 0) {
|
|
||||||
if (util::LoadTextureFromMemory(logoRes->data, logoRes->size, &logo_tex_, &logo_w_, &logo_h_)) {
|
|
||||||
DEBUG_LOGF("Loaded header logo from embedded: %s (%dx%d)\n", embeddedName.c_str(), logo_w_, logo_h_);
|
|
||||||
} else {
|
|
||||||
DEBUG_LOGF("Note: Failed to decode embedded logo (text-only header)\n");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
DEBUG_LOGF("Note: Header logo not found at %s (text-only header)\n", logoPath.c_str());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load coin logo texture lazily (DragonX currency icon for balance tab)
|
// Load coin logo texture lazily (DragonX currency icon for balance tab)
|
||||||
if (!coin_logo_loaded_) {
|
if (!coin_logo_loaded_) {
|
||||||
@@ -2429,7 +2443,9 @@ void App::reloadThemeImages(const std::string& bgPath, const std::string& logoPa
|
|||||||
gradient_tex_ = 0;
|
gradient_tex_ = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset logo loaded flags — will reload on next render frame
|
// Reset logo loaded flags — will reload on next render frame (ensureLogoTexture re-rasterizes the
|
||||||
|
// SVG for the new theme). Free the current texture first so a theme switch doesn't leak it.
|
||||||
|
if (logo_tex_) util::DestroyTexture(logo_tex_);
|
||||||
logo_loaded_ = false;
|
logo_loaded_ = false;
|
||||||
logo_tex_ = 0;
|
logo_tex_ = 0;
|
||||||
logo_w_ = 0;
|
logo_w_ = 0;
|
||||||
|
|||||||
@@ -486,6 +486,10 @@ public:
|
|||||||
*/
|
*/
|
||||||
void reloadThemeImages(const std::string& bgPath, const std::string& logoPath);
|
void reloadThemeImages(const std::string& bgPath, const std::string& logoPath);
|
||||||
|
|
||||||
|
// Load / recolor-per-theme the DragonX header logo (SVG rasterized to the theme accent). Called at
|
||||||
|
// the top of render() so the wizard, lock screen, and main header all show it.
|
||||||
|
void ensureLogoTexture();
|
||||||
|
|
||||||
// Wizard / first-run
|
// Wizard / first-run
|
||||||
WizardPhase getWizardPhase() const { return wizard_phase_; }
|
WizardPhase getWizardPhase() const { return wizard_phase_; }
|
||||||
bool isFirstRun() const;
|
bool isFirstRun() const;
|
||||||
@@ -1080,6 +1084,7 @@ private:
|
|||||||
int logo_h_ = 0;
|
int logo_h_ = 0;
|
||||||
bool logo_loaded_ = false;
|
bool logo_loaded_ = false;
|
||||||
bool logo_is_dark_variant_ = true; // tracks which variant is currently loaded
|
bool logo_is_dark_variant_ = true; // tracks which variant is currently loaded
|
||||||
|
ImU32 logo_accent_ = 0; // theme accent the SVG logo was last rasterized with (re-render on change)
|
||||||
|
|
||||||
// Coin logo texture (DragonX currency icon, separate from wallet branding)
|
// Coin logo texture (DragonX currency icon, separate from wallet branding)
|
||||||
ImTextureID coin_logo_tex_ = 0;
|
ImTextureID coin_logo_tex_ = 0;
|
||||||
|
|||||||
39
src/embedded/logo_dragonx_svg.h
Normal file
39
src/embedded/logo_dragonx_svg.h
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
// DragonX Wallet - ImGui Edition
|
||||||
|
// Copyright 2024-2026 The Hush Developers
|
||||||
|
// Released under the GPLv3
|
||||||
|
//
|
||||||
|
// Embedded source of the DragonX mark (res/img/logos/logo_dragonx.svg). Kept as a string so the logo
|
||||||
|
// is available in every build (dev + portable single-file) with no resource-pipeline or file dependency.
|
||||||
|
// It is rasterized + recolored per theme at runtime (see util/svg_texture.*). Two fills: the crimson
|
||||||
|
// body (.cls-2 #d82652) becomes the theme accent; the white detail (.cls-1 #fff) becomes the light tone.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
// Global `embedded` namespace to match the generated embedded resources (embedded::ui_toml_data, etc.).
|
||||||
|
namespace embedded {
|
||||||
|
|
||||||
|
inline constexpr const char* kLogoDragonXSvg = R"SVG(<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
|
||||||
|
<defs>
|
||||||
|
<style>
|
||||||
|
.cls-1 {
|
||||||
|
fill: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-2 {
|
||||||
|
fill: #d82652;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<path class="cls-2" d="M103.98,128s-6.29-24.7-18.73-34.43c-8.53-8.03-15.63-16.49-21.25-24.16-5.62,7.68-12.72,16.17-21.25,24.16-12.4,9.74-18.73,34.43-18.73,34.43-2.38-24.34,7.85-35.82,12.87-41.29,7.82-8.5,15.31-16.56,21.75-25.02-7.64-11.44-11.41-19.72-11.41-19.72-.89-3.27-3.84-6.64-3.84-6.64,6.08-8.1-1.6-16.98-1.6-16.98,5.79-5.62,6.71-10.02,8.32-18.34-1.96,22.35,4.02,39.09,13.93,54.12,9.84-15.03,15.81-31.77,13.86-54.12,1.6,8.35,2.52,12.72,8.32,18.34,0,0-7.68,8.88-1.6,16.98,0,0-2.95,3.38-3.84,6.64,0,0-3.77,8.28-11.37,19.72,6.43,8.45,13.97,16.56,21.75,25.02,4.97,5.47,15.21,16.95,12.83,41.29h0Z"/>
|
||||||
|
<g>
|
||||||
|
<path class="cls-1" d="M55.33,61.62c-3.55,4.55-7.39,8.99-11.44,13.47-5.29-4.48-11.23-5.33-11.23-5.33,22.92-7.82,2.81-15.17.28-16.31C9.28,42.78,9.1,14.78,9.1,14.78c11.51,34.15,36.42,32.3,36.42,32.3.35-.21.67-.46.92-.71,1.64,3.27,4.58,8.67,8.88,15.24h0Z"/>
|
||||||
|
<g>
|
||||||
|
<path class="cls-1" d="M68.62,40.41c-1.35,2.98-2.91,5.83-4.62,8.63-1.71-2.81-3.23-5.69-4.62-8.63,1.74-3.45,4.62-20.58,4.62-20.58,0,0,2.88,17.13,4.62,20.58Z"/>
|
||||||
|
<path class="cls-1" d="M76.01,97.93l-3.48,2.34s-.1-4.44-3.52-1.84c-.42.32-2.38,2.21-.03,4.27,0,0-4.05,4.08-4.97,8.21-.92-4.12-4.97-8.21-4.97-8.21,2.34-2.06.39-3.95-.03-4.27-3.41-2.59-3.52,1.84-3.52,1.84l-3.48-2.34c.28-3.55.1-6.68-.46-9.42,4.69-4.94,8.85-9.88,12.47-14.61,3.66,4.72,7.78,9.67,12.47,14.61-.57,2.74-.75,5.86-.46,9.42Z"/>
|
||||||
|
<path class="cls-1" d="M95.34,69.76s-5.94.85-11.23,5.33c-4.02-4.48-7.89-8.92-11.44-13.47,4.3-6.57,7.25-11.98,8.88-15.24.25.25.57.5.92.71,0,0,24.91,1.84,36.42-32.3,0,0-.18,28-23.84,38.66-2.52,1.14-22.64,8.5.28,16.31h0Z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>)SVG";
|
||||||
|
|
||||||
|
} // namespace embedded
|
||||||
75
src/util/svg_texture.cpp
Normal file
75
src/util/svg_texture.cpp
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
// DragonX Wallet - ImGui Edition
|
||||||
|
// Copyright 2024-2026 The Hush Developers
|
||||||
|
// Released under the GPLv3
|
||||||
|
|
||||||
|
#include "svg_texture.h"
|
||||||
|
#include "texture_loader.h" // CreateRawTexture
|
||||||
|
#include "logger.h"
|
||||||
|
|
||||||
|
// nanosvg (memononen, zlib/public-domain, vendored in libs/nanosvg). Implementations compiled ONLY here.
|
||||||
|
#define NANOSVG_IMPLEMENTATION
|
||||||
|
#include "nanosvg/nanosvg.h"
|
||||||
|
#define NANOSVGRAST_IMPLEMENTATION
|
||||||
|
#include "nanosvg/nanosvgrast.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace dragonx {
|
||||||
|
namespace util {
|
||||||
|
|
||||||
|
// Perceptual brightness of a nanosvg color (byte order R,G,B,A — matches ImU32/IM_COL32).
|
||||||
|
static inline float svgLuma(unsigned int c) {
|
||||||
|
const float r = (c & 0xFFu) / 255.0f;
|
||||||
|
const float g = ((c >> 8) & 0xFFu) / 255.0f;
|
||||||
|
const float b = ((c >> 16) & 0xFFu) / 255.0f;
|
||||||
|
return 0.299f * r + 0.587f * g + 0.114f * b;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool LoadTextureFromSvg(const char* svgText, int pxSize, ImU32 bodyColor, ImU32 detailColor,
|
||||||
|
ImTextureID* outTex, int* outW, int* outH) {
|
||||||
|
if (!svgText || pxSize <= 0 || !outTex) return false;
|
||||||
|
|
||||||
|
// nsvgParse mutates its input buffer — parse a copy.
|
||||||
|
std::string buf(svgText);
|
||||||
|
NSVGimage* img = nsvgParse(&buf[0], "px", 96.0f);
|
||||||
|
if (!img || img->width <= 0.0f || img->height <= 0.0f) {
|
||||||
|
if (img) nsvgDelete(img);
|
||||||
|
DEBUG_LOGF("LoadTextureFromSvg: parse failed / empty image\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recolor by brightness: light fills -> detail, darker fills -> body. Force opaque so a theme color
|
||||||
|
// with partial alpha can't make the mark translucent. ImU32 and nanosvg color share byte order.
|
||||||
|
const unsigned int body = (static_cast<unsigned int>(bodyColor) & 0x00FFFFFFu) | 0xFF000000u;
|
||||||
|
const unsigned int detail = (static_cast<unsigned int>(detailColor) & 0x00FFFFFFu) | 0xFF000000u;
|
||||||
|
for (NSVGshape* s = img->shapes; s; s = s->next) {
|
||||||
|
if (s->fill.type == NSVG_PAINT_COLOR)
|
||||||
|
s->fill.color = (svgLuma(s->fill.color) > 0.6f) ? detail : body;
|
||||||
|
if (s->stroke.type == NSVG_PAINT_COLOR)
|
||||||
|
s->stroke.color = (svgLuma(s->stroke.color) > 0.6f) ? detail : body;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float scale = static_cast<float>(pxSize) / img->width;
|
||||||
|
const int w = pxSize;
|
||||||
|
const int h = std::max(1, static_cast<int>(img->height * scale + 0.5f));
|
||||||
|
|
||||||
|
std::vector<unsigned char> rgba(static_cast<size_t>(w) * h * 4, 0);
|
||||||
|
NSVGrasterizer* rast = nsvgCreateRasterizer();
|
||||||
|
if (!rast) { nsvgDelete(img); return false; }
|
||||||
|
nsvgRasterize(rast, img, 0.0f, 0.0f, scale, rgba.data(), w, h, w * 4);
|
||||||
|
nsvgDeleteRasterizer(rast);
|
||||||
|
nsvgDelete(img);
|
||||||
|
|
||||||
|
if (!CreateRawTexture(rgba.data(), w, h, /*repeat=*/false, outTex)) {
|
||||||
|
DEBUG_LOGF("LoadTextureFromSvg: CreateRawTexture failed (%dx%d)\n", w, h);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (outW) *outW = w;
|
||||||
|
if (outH) *outH = h;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace util
|
||||||
|
} // namespace dragonx
|
||||||
24
src/util/svg_texture.h
Normal file
24
src/util/svg_texture.h
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
// DragonX Wallet - ImGui Edition
|
||||||
|
// Copyright 2024-2026 The Hush Developers
|
||||||
|
// Released under the GPLv3
|
||||||
|
//
|
||||||
|
// Rasterize a (two-tone) SVG to a themed RGBA GPU texture via nanosvg. Used for the DragonX logo /
|
||||||
|
// custom emoji: the darker fill becomes `bodyColor` (the theme accent), the light fill `detailColor`.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "imgui.h"
|
||||||
|
|
||||||
|
namespace dragonx {
|
||||||
|
namespace util {
|
||||||
|
|
||||||
|
// Rasterize `svgText` (nul-terminated SVG markup) to a texture of width `pxSize` (height follows the
|
||||||
|
// SVG aspect). Fills are recolored by brightness — light fills -> detailColor, darker fills -> bodyColor
|
||||||
|
// (both forced opaque). Returns false on parse/rasterize/upload failure; on success *outTex is a texture
|
||||||
|
// the caller owns (release via util::DestroyTexture). Safe to call off the initial frame (needs a live
|
||||||
|
// GL/DX device, like the other texture loaders).
|
||||||
|
bool LoadTextureFromSvg(const char* svgText, int pxSize, ImU32 bodyColor, ImU32 detailColor,
|
||||||
|
ImTextureID* outTex, int* outW, int* outH);
|
||||||
|
|
||||||
|
} // namespace util
|
||||||
|
} // namespace dragonx
|
||||||
Reference in New Issue
Block a user