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:
2026-07-21 13:28:51 -05:00
parent 267d839d5b
commit 17525003c5
9 changed files with 4991 additions and 58 deletions

View File

@@ -73,6 +73,9 @@
#include "util/text_format.h"
#include "util/payment_uri.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/secure_vault.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()
{
// 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.)
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
if (wizard_phase_ != WizardPhase::None && wizard_phase_ != WizardPhase::Done) {
renderFirstRunWizard();
@@ -1560,63 +1630,7 @@ void App::render()
sbStatus.miningActive = state_.mining.generate || state_.pool_mining.xmrig_running;
sbStatus.chatUnreadCount = chatUnreadCount(); // unread badge on the Chat nav item (Q1)
// Load logo texture lazily on first frame (or after theme change)
// 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());
}
}
}
}
// (DragonX logo is loaded at the top of render() via ensureLogoTexture().)
// Load coin logo texture lazily (DragonX currency icon for balance tab)
if (!coin_logo_loaded_) {
@@ -2429,7 +2443,9 @@ void App::reloadThemeImages(const std::string& bgPath, const std::string& logoPa
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_tex_ = 0;
logo_w_ = 0;