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:
132
src/app.cpp
132
src/app.cpp
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user