mirror of
https://github.com/stenzek/duckstation.git
synced 2026-07-11 01:24:11 +02:00
Qt: Add ThemeSVGIconEngine to replace use of Qt SVG
Uses plutosvg to render icons with a different colour scheme dependent on the text colour. No more duplicating all the icons.
This commit is contained in:
@@ -119,10 +119,10 @@
|
||||
|
||||
<!--Copy the needed dlls-->
|
||||
<ItemGroup>
|
||||
<QtLibNames Include="Qt6Core$(QtLibSuffix);Qt6Gui$(QtLibSuffix);Qt6Widgets$(QtLibSuffix);Qt6Svg$(QtLibSuffix)" />
|
||||
<QtLibNames Include="Qt6Core$(QtLibSuffix);Qt6Gui$(QtLibSuffix);Qt6Widgets$(QtLibSuffix)" />
|
||||
<QtDlls Include="@(QtLibNames -> '$(QtBinDir)%(Identity).dll')" />
|
||||
<!--Filter plugins to copy based on the observation that all debug versions end in "d"-->
|
||||
<QtAllPlugins Include="$(QtPluginsDir)**\*$(QtLibSuffix).dll" />
|
||||
<QtAllPlugins Include="$(QtPluginsDir)**\*$(QtLibSuffix).dll" Exclude="$(QtPluginsDir)iconengines\qsvgicon$(QtLibSuffix).dll;$(QtPluginsDir)imageformats\qsvg$(QtLibSuffix).dll" />
|
||||
<QtPlugins Condition="$(Configuration.Contains(Debug))" Include="@(QtAllPlugins)" />
|
||||
<QtPlugins Condition="!$(Configuration.Contains(Debug))" Exclude="$(QtPluginsDir)**\*$(QtDebugSuffix).dll" Include="@(QtAllPlugins)" />
|
||||
<QtPluginsDest Include="@(QtPlugins -> '$(BinaryOutputDir)$(QtPluginFolder)\%(RecursiveDir)%(Filename)%(Extension)')" />
|
||||
|
||||
@@ -104,7 +104,7 @@ for i in $(find "$DEPSDIR" -iname '*.so'); do
|
||||
done
|
||||
|
||||
echo "Running linuxdeploy to create AppDir..."
|
||||
EXTRA_QT_MODULES="core;gui;svg;widgets;xcbqpa;waylandcompositor" \
|
||||
EXTRA_QT_MODULES="core;gui;widgets;xcbqpa;waylandcompositor" \
|
||||
EXTRA_PLATFORM_PLUGINS="libqwayland.so" \
|
||||
DEPLOY_PLATFORM_THEMES="1" \
|
||||
LINUXDEPLOY_EXCLUDED_LIBRARIES="libwayland-cursor*;libwayland-egl*" \
|
||||
|
||||
@@ -177,14 +177,12 @@ declare -a QTLIBS=(
|
||||
"libQt6DBus.so.6"
|
||||
"libQt6Gui.so.6"
|
||||
"libQt6OpenGL.so.6"
|
||||
"libQt6Svg.so.6"
|
||||
"libQt6WaylandClient.so.6"
|
||||
"libQt6Widgets.so.6"
|
||||
"libQt6XcbQpa.so.6"
|
||||
)
|
||||
|
||||
declare -a QTPLUGINS=(
|
||||
"plugins/iconengines"
|
||||
"plugins/imageformats"
|
||||
"plugins/platforminputcontexts"
|
||||
"plugins/platforms"
|
||||
|
||||
@@ -167,6 +167,8 @@ set(SRCS
|
||||
setupwizarddialog.h
|
||||
setupwizarddialog.ui
|
||||
texturereplacementsettingsdialog.ui
|
||||
themesvgiconengine.cpp
|
||||
themesvgiconengine.h
|
||||
togglebutton.cpp
|
||||
togglebutton.h
|
||||
)
|
||||
@@ -205,6 +207,10 @@ endif()
|
||||
# Our Qt builds may have exceptions on, so force them off.
|
||||
target_compile_definitions(duckstation-qt PRIVATE QT_NO_EXCEPTIONS QT_NO_SIGNALS_SLOTS_KEYWORDS)
|
||||
|
||||
# We need to override QT_STATICPLUGIN to force our SVG plugin to be statically linked to the exectuable.
|
||||
# Prevent the force include of the pch from interfering.
|
||||
set_source_files_properties("themesvgiconengine.cpp" PROPERTIES SKIP_PRECOMPILE_HEADERS TRUE)
|
||||
|
||||
add_core_resources(duckstation-qt)
|
||||
|
||||
# Automatically generate a list of .ui calls and call qt_wrap_ui() to generate targets.
|
||||
|
||||
@@ -55,10 +55,15 @@
|
||||
<ClCompile Include="selectdiscdialog.cpp" />
|
||||
<ClCompile Include="settingswindow.cpp" />
|
||||
<ClCompile Include="setupwizarddialog.cpp" />
|
||||
<ClCompile Include="themesvgiconengine.cpp">
|
||||
<PrecompiledHeader>NotUsing</PrecompiledHeader>
|
||||
<ForcedIncludeFiles></ForcedIncludeFiles>
|
||||
</ClCompile>
|
||||
<ClCompile Include="togglebutton.cpp" />
|
||||
<ClCompile Include="vcruntimecheck.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<QtMoc Include="themesvgiconengine.h" />
|
||||
<QtMoc Include="togglebutton.h" />
|
||||
<QtMoc Include="setupwizarddialog.h" />
|
||||
<QtMoc Include="aboutdialog.h" />
|
||||
@@ -331,6 +336,7 @@
|
||||
<QtUi Include="debuggingsettingswidget.ui">
|
||||
<FileType>Document</FileType>
|
||||
</QtUi>
|
||||
<None Include="themesvgiconengine.json" />
|
||||
<None Include="translations\duckstation-qt_az.ts" />
|
||||
<None Include="translations\duckstation-qt_es-es.ts" />
|
||||
<None Include="translations\duckstation-qt_sv.ts" />
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
<ClCompile Include="asyncpixmaploader.cpp" />
|
||||
<ClCompile Include="debuggingsettingswidget.cpp" />
|
||||
<ClCompile Include="asynchttprequest.cpp" />
|
||||
<ClCompile Include="themesvgiconengine.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="qtutils.h" />
|
||||
@@ -120,6 +121,7 @@
|
||||
<QtMoc Include="asyncpixmaploader.h" />
|
||||
<QtMoc Include="debuggingsettingswidget.h" />
|
||||
<QtMoc Include="asynchttprequest.h" />
|
||||
<QtMoc Include="themesvgiconengine.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<QtUi Include="consolesettingswidget.ui" />
|
||||
@@ -200,6 +202,7 @@
|
||||
<None Include="translations\duckstation-qt_az.ts">
|
||||
<Filter>translations</Filter>
|
||||
</None>
|
||||
<None Include="themesvgiconengine.json" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<QtTs Include="translations\duckstation-qt_de.ts">
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
#include <QtCore/QFile>
|
||||
#include <QtCore/QTimer>
|
||||
#include <QtCore/QTranslator>
|
||||
#include <QtCore/QtPlugin>
|
||||
#include <QtGui/QClipboard>
|
||||
#include <QtGui/QKeyEvent>
|
||||
#include <algorithm>
|
||||
@@ -95,6 +96,9 @@ QT_TRANSLATE_NOOP("MAC_APPLICATION_MENU", "Quit %1")
|
||||
QT_TRANSLATE_NOOP("MAC_APPLICATION_MENU", "About %1")
|
||||
#endif
|
||||
|
||||
Q_IMPORT_PLUGIN(ThemeSVGIconEnginePlugin);
|
||||
Q_IMPORT_PLUGIN(PlutoSVGImagePlugin);
|
||||
|
||||
static constexpr u32 SETTINGS_SAVE_DELAY = 1000;
|
||||
|
||||
/// Interval at which the controllers are polled when the system is not active.
|
||||
@@ -272,6 +276,7 @@ bool QtHost::EarlyProcessStartup()
|
||||
icon_theme_search_paths.emplace_back(":/icons"_L1);
|
||||
icon_theme_search_paths.emplace_back(":/standard-icons"_L1);
|
||||
QIcon::setThemeSearchPaths(icon_theme_search_paths);
|
||||
QIcon::setThemeName("monochrome");
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -929,6 +934,34 @@ void QtHost::ApplyMigrations()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
#ifdef _DEBUG
|
||||
#define SUFFIX "d"
|
||||
#else
|
||||
#define SUFFIX
|
||||
#endif
|
||||
// Remove Qt6Svg.dll and the plugins which use QtSvg, since the updater can't remove them itself...
|
||||
for (const char* path_to_remove : {
|
||||
"Qt6Svg" SUFFIX ".dll",
|
||||
"QtPlugins\\iconengines\\qsvgicon" SUFFIX ".dll",
|
||||
"QtPlugins\\imageformats\\qsvg" SUFFIX ".dll",
|
||||
})
|
||||
{
|
||||
const std::string full_path = Path::Combine(EmuFolders::AppRoot, path_to_remove);
|
||||
if (FileSystem::FileExists(full_path.c_str()))
|
||||
{
|
||||
Error error;
|
||||
if (!FileSystem::DeleteFile(full_path.c_str(), &error))
|
||||
{
|
||||
QMessageBox::critical(nullptr, "Error"_L1,
|
||||
QString::fromStdString(fmt::format("Failed to delete conflicting library {}: {}",
|
||||
path_to_remove, error.GetDescription())));
|
||||
}
|
||||
}
|
||||
}
|
||||
#undef SUFFIX
|
||||
#endif
|
||||
}
|
||||
|
||||
void CoreThread::applySettings(bool display_osd_messages /* = false */)
|
||||
|
||||
@@ -326,10 +326,6 @@ bool QtHost::HasGlobalStylesheet()
|
||||
|
||||
void QtHost::UpdateThemeOnStyleChange()
|
||||
{
|
||||
const QLatin1StringView new_theme_name = IsDarkApplicationTheme() ? "white"_L1 : "black"_L1;
|
||||
if (QIcon::themeName() != new_theme_name)
|
||||
QIcon::setThemeName(new_theme_name);
|
||||
|
||||
if (NativeThemeStylesheetNeedsUpdate())
|
||||
{
|
||||
const QString stylesheet = GetNativeThemeStylesheet();
|
||||
|
||||
@@ -0,0 +1,372 @@
|
||||
// SPDX-FileCopyrightText: 2019-2026 Connor McLaughlin <stenzek@gmail.com>
|
||||
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
|
||||
|
||||
// Needs to be defined before any includes.
|
||||
#define QT_STATICPLUGIN
|
||||
|
||||
#include "themesvgiconengine.h"
|
||||
#include "qtutils.h"
|
||||
|
||||
#include <QtCore/QFile>
|
||||
#include <QtCore/QtPlugin>
|
||||
#include <QtGui/QImage>
|
||||
#include <QtGui/QPainter>
|
||||
#include <QtGui/QPalette>
|
||||
#include <QtGui/QPixmapCache>
|
||||
#include <QtWidgets/QApplication>
|
||||
|
||||
#include <limits>
|
||||
#include <plutosvg.h>
|
||||
|
||||
#include "moc_themesvgiconengine.cpp"
|
||||
|
||||
/// Returns the icon color for the given mode based on the application palette.
|
||||
static QColor GetIconColorFromPalette(QIcon::Mode mode)
|
||||
{
|
||||
// Thank gosh these are copy-on-write...
|
||||
const QPalette palette = QApplication::palette();
|
||||
|
||||
// NOTE: Active and Normal are the same in QPalette.
|
||||
if (mode == QIcon::Disabled)
|
||||
return palette.color(QPalette::Disabled, QPalette::WindowText);
|
||||
else if (mode == QIcon::Selected)
|
||||
return palette.color(QPalette::Current, QPalette::HighlightedText);
|
||||
else
|
||||
return palette.color(QPalette::Normal, QPalette::WindowText);
|
||||
}
|
||||
|
||||
/// Constructs a cache key for the given resource path, pixel size, and RGBA color value.
|
||||
static QString BuildCacheKey(const QString& resource_path, const QSize& size, qreal dpr, const QColor& color)
|
||||
{
|
||||
// Cache key includes path, size, and colour so stale entries are not reused after a palette change.
|
||||
return QStringLiteral("%1_%2x%3@%4_%5")
|
||||
.arg(resource_path)
|
||||
.arg(size.width())
|
||||
.arg(size.height())
|
||||
.arg(dpr)
|
||||
.arg(static_cast<unsigned>(color.rgba()), 8, 16, QLatin1Char('0'));
|
||||
}
|
||||
|
||||
/// Callback used when freeing the QImage.
|
||||
static void CleanupPlutoSVGSurface(void* surface)
|
||||
{
|
||||
plutovg_surface_destroy(static_cast<plutovg_surface_t*>(surface));
|
||||
}
|
||||
|
||||
/// Renders the document at the given pixel dimensions with the given colour and inserts the
|
||||
/// result into QPixmapCache under cache_key. Returns a null QPixmap on failure.
|
||||
static bool RenderSVGToPixmap(QPixmap& pm, const plutosvg_document* doc, const QSize& size, const QColor& color)
|
||||
{
|
||||
// Pass the desired icon colour as CSS currentColor so plutosvg tints the monochrome SVG in a
|
||||
// single render pass, avoiding a separate SourceIn compositing step.
|
||||
const plutovg_color_t current_color = {
|
||||
.r = static_cast<float>(color.redF()),
|
||||
.g = static_cast<float>(color.greenF()),
|
||||
.b = static_cast<float>(color.blueF()),
|
||||
.a = static_cast<float>(color.alphaF()),
|
||||
};
|
||||
|
||||
plutovg_surface_t* surface =
|
||||
plutosvg_document_render_to_surface(doc, nullptr, size.width(), size.height(), ¤t_color, nullptr, nullptr);
|
||||
if (!surface)
|
||||
return false;
|
||||
|
||||
// plutovg surfaces are premultiplied ARGB (0xAARRGGBB, native-endian), which matches
|
||||
// QImage::Format_ARGB32_Premultiplied exactly, no pixel conversion required.
|
||||
// The cleanup function ensures the surface is destroyed when the QImage is done with the pixel data.
|
||||
const QImage img(plutovg_surface_get_data(surface), plutovg_surface_get_width(surface),
|
||||
plutovg_surface_get_height(surface), plutovg_surface_get_stride(surface),
|
||||
QImage::Format_ARGB32_Premultiplied, CleanupPlutoSVGSurface, surface);
|
||||
|
||||
pm = QPixmap::fromImage(img);
|
||||
return !pm.isNull();
|
||||
}
|
||||
|
||||
/// Helper function to read a QFile to a DynamicHeapArray<u8>.
|
||||
static bool ReadFileToByteArray(QIODevice* dev, DynamicHeapArray<u8>& out_data)
|
||||
{
|
||||
if (qint64 size; !dev->isSequential() && (size = dev->size()) > 0)
|
||||
{
|
||||
out_data.resize(static_cast<size_t>(
|
||||
(sizeof(size_t) == sizeof(qint64)) ? size : std::min<qint64>(size, std::numeric_limits<size_t>::max())));
|
||||
|
||||
if (dev->read(reinterpret_cast<char*>(out_data.data()), size) != size)
|
||||
{
|
||||
out_data.deallocate();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
constexpr size_t chunk_size = 1048576;
|
||||
size_t read_so_far = 0;
|
||||
for (;;)
|
||||
{
|
||||
const size_t prev_size = out_data.size();
|
||||
const size_t new_size =
|
||||
((read_so_far + chunk_size) < read_so_far) ? std::numeric_limits<size_t>::max() : (read_so_far + chunk_size);
|
||||
const size_t space = (new_size - prev_size);
|
||||
if (space > 0)
|
||||
out_data.resize(new_size);
|
||||
const qint64 bytes_read =
|
||||
(space > 0) ? dev->read(reinterpret_cast<char*>(out_data.data() + read_so_far), static_cast<qint64>(space)) : 0;
|
||||
if (bytes_read < 0)
|
||||
{
|
||||
out_data.deallocate();
|
||||
return false;
|
||||
}
|
||||
else if (bytes_read == 0)
|
||||
{
|
||||
out_data.resize(prev_size);
|
||||
break;
|
||||
}
|
||||
|
||||
read_so_far += static_cast<size_t>(bytes_read);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
ThemeSVGIconEngine::ThemeSVGIconEngine(const QString& resource_path) : m_resource_path(resource_path)
|
||||
{
|
||||
}
|
||||
|
||||
ThemeSVGIconEngine::~ThemeSVGIconEngine()
|
||||
{
|
||||
if (m_document)
|
||||
plutosvg_document_destroy(m_document);
|
||||
}
|
||||
|
||||
bool ThemeSVGIconEngine::ensureLoaded() const
|
||||
{
|
||||
// Previous load failed?
|
||||
if (m_resource_path.isEmpty())
|
||||
return false;
|
||||
|
||||
QFile file(m_resource_path);
|
||||
if (!file.open(QFile::ReadOnly) || !ReadFileToByteArray(&file, m_svg_data))
|
||||
{
|
||||
qCritical() << "Failed to open SVG file: " << m_resource_path;
|
||||
m_resource_path = {};
|
||||
return false;
|
||||
}
|
||||
|
||||
// The document borrows the data pointer; m_svg_data must remain valid for the document's lifetime.
|
||||
m_document = plutosvg_document_load_from_data(reinterpret_cast<const char*>(m_svg_data.data()),
|
||||
static_cast<int>(m_svg_data.size()), -1.0f, -1.0f, nullptr, nullptr);
|
||||
if (!m_document)
|
||||
{
|
||||
qCritical() << "Failed to parse SVG data: " << m_resource_path;
|
||||
m_resource_path = {};
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void ThemeSVGIconEngine::paint(QPainter* painter, const QRect& rect, QIcon::Mode mode, QIcon::State state)
|
||||
{
|
||||
Q_UNUSED(state);
|
||||
|
||||
if (rect.isEmpty())
|
||||
return;
|
||||
|
||||
// Apply device pixel ratio to requested size so we can cache pixmaps at the correct sizes for different DPRs.
|
||||
const QPaintDevice* const device = painter->device();
|
||||
const qreal dpr = device ? device->devicePixelRatio() : 1.0;
|
||||
const QSize size = QtUtils::ApplyDevicePixelRatioToSize(rect.size(), dpr);
|
||||
const QColor color = GetIconColorFromPalette(mode);
|
||||
const QString cache_key = BuildCacheKey(m_resource_path, size, dpr, color);
|
||||
|
||||
QPixmap pm;
|
||||
if (!QPixmapCache::find(cache_key, &pm))
|
||||
{
|
||||
// Don't reload multiple times if we hit the cache.
|
||||
if (ensureLoaded() && RenderSVGToPixmap(pm, m_document, size, color))
|
||||
{
|
||||
// DPR set must be before inserting into the cache, otherwise it copies.
|
||||
pm.setDevicePixelRatio(dpr);
|
||||
QPixmapCache::insert(cache_key, pm);
|
||||
}
|
||||
}
|
||||
|
||||
painter->drawPixmap(rect, pm);
|
||||
}
|
||||
|
||||
QPixmap ThemeSVGIconEngine::pixmap(const QSize& size, QIcon::Mode mode, QIcon::State state)
|
||||
{
|
||||
Q_UNUSED(state);
|
||||
|
||||
if (size.isEmpty())
|
||||
return {};
|
||||
|
||||
const QColor color = GetIconColorFromPalette(mode);
|
||||
const QString cache_key = BuildCacheKey(m_resource_path, size, 1.0, color);
|
||||
|
||||
QPixmap pm;
|
||||
if (!QPixmapCache::find(cache_key, &pm))
|
||||
{
|
||||
// Don't reload multiple times if we hit the cache.
|
||||
if (ensureLoaded() && RenderSVGToPixmap(pm, m_document, size, color))
|
||||
QPixmapCache::insert(cache_key, pm);
|
||||
}
|
||||
|
||||
return pm;
|
||||
}
|
||||
|
||||
QPixmap ThemeSVGIconEngine::scaledPixmap(const QSize& size, QIcon::Mode mode, QIcon::State state, qreal scale)
|
||||
{
|
||||
Q_UNUSED(state);
|
||||
|
||||
const QSize scaled_size = QtUtils::ApplyDevicePixelRatioToSize(size, scale);
|
||||
if (scaled_size.isEmpty())
|
||||
return {};
|
||||
|
||||
const QColor color = GetIconColorFromPalette(mode);
|
||||
const QString cache_key = BuildCacheKey(m_resource_path, scaled_size, scale, color);
|
||||
|
||||
QPixmap pm;
|
||||
if (!QPixmapCache::find(cache_key, &pm))
|
||||
{
|
||||
// Don't reload multiple times if we hit the cache.
|
||||
if (ensureLoaded() && RenderSVGToPixmap(pm, m_document, scaled_size, color))
|
||||
{
|
||||
pm.setDevicePixelRatio(scale);
|
||||
QPixmapCache::insert(cache_key, pm);
|
||||
}
|
||||
}
|
||||
|
||||
return pm;
|
||||
}
|
||||
|
||||
QIconEngine* ThemeSVGIconEngine::clone() const
|
||||
{
|
||||
return new ThemeSVGIconEngine(m_resource_path);
|
||||
}
|
||||
|
||||
QString ThemeSVGIconEngine::key() const
|
||||
{
|
||||
return QStringLiteral("svg");
|
||||
}
|
||||
|
||||
QString ThemeSVGIconEngine::iconName()
|
||||
{
|
||||
return m_resource_path;
|
||||
}
|
||||
|
||||
QIconEngine* ThemeSVGIconEnginePlugin::create(const QString& resource_path)
|
||||
{
|
||||
if (resource_path.endsWith(".svg", Qt::CaseInsensitive))
|
||||
return new ThemeSVGIconEngine(resource_path);
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool PlutoSVGImageHandler::canRead() const
|
||||
{
|
||||
if (m_document)
|
||||
return m_document;
|
||||
|
||||
// Read all data once and keep it alive; plutosvg_document borrows the raw pointer.
|
||||
QIODevice* const dev = device();
|
||||
if (!dev || !ReadFileToByteArray(dev, m_svg_data))
|
||||
{
|
||||
qCritical() << "Failed to read SVG data from device";
|
||||
return false;
|
||||
}
|
||||
|
||||
m_document = plutosvg_document_load_from_data(reinterpret_cast<const char*>(m_svg_data.data()),
|
||||
static_cast<int>(m_svg_data.size()), -1.0f, -1.0f, nullptr, nullptr);
|
||||
if (!m_document)
|
||||
{
|
||||
qCritical() << "Failed to parse SVG data";
|
||||
m_svg_data = {};
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PlutoSVGImageHandler::read(QImage* image)
|
||||
{
|
||||
if (!canRead())
|
||||
return false;
|
||||
|
||||
// Determine render size: honour caller's ScaledSize, fall back to intrinsic SVG dimensions.
|
||||
QSize render_size = m_scaled_size;
|
||||
if (!render_size.isValid())
|
||||
{
|
||||
render_size = QSize(qMax(1, qRound(plutosvg_document_get_width(m_document))),
|
||||
qMax(1, qRound(plutosvg_document_get_height(m_document))));
|
||||
}
|
||||
|
||||
// Render with a solid currentColor so the SVG's own fill/stroke colours are preserved.
|
||||
// Callers that want palette tinting should use ThemeSVGIconEngine instead.
|
||||
const plutovg_color_t current_color = {.r = 1.0f, .g = 1.0f, .b = 1.0f, .a = 1.0f};
|
||||
|
||||
plutovg_surface_t* surface = plutosvg_document_render_to_surface(
|
||||
m_document, nullptr, render_size.width(), render_size.height(), ¤t_color, nullptr, nullptr);
|
||||
if (!surface)
|
||||
return false;
|
||||
|
||||
// Wrap the surface pixels in a QImage; the cleanup function destroys the surface once Qt is done.
|
||||
// plutovg uses premultiplied ARGB (0xAARRGGBB, native-endian) == QImage::Format_ARGB32_Premultiplied.
|
||||
*image =
|
||||
QImage(plutovg_surface_get_data(surface), plutovg_surface_get_width(surface), plutovg_surface_get_height(surface),
|
||||
plutovg_surface_get_stride(surface), QImage::Format_ARGB32_Premultiplied, CleanupPlutoSVGSurface, surface);
|
||||
|
||||
return !image->isNull();
|
||||
}
|
||||
|
||||
bool PlutoSVGImageHandler::supportsOption(ImageOption option) const
|
||||
{
|
||||
return (option == Size || option == ScaledSize);
|
||||
}
|
||||
|
||||
QVariant PlutoSVGImageHandler::option(ImageOption option) const
|
||||
{
|
||||
if (option == ScaledSize)
|
||||
return m_scaled_size;
|
||||
|
||||
if (option == Size)
|
||||
{
|
||||
if (!canRead())
|
||||
return {};
|
||||
|
||||
return QSize(qMax(1, qRound(plutosvg_document_get_width(m_document))),
|
||||
qMax(1, qRound(plutosvg_document_get_height(m_document))));
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
void PlutoSVGImageHandler::setOption(ImageOption option, const QVariant& value)
|
||||
{
|
||||
if (option == ScaledSize)
|
||||
m_scaled_size = value.toSize();
|
||||
}
|
||||
|
||||
QImageIOPlugin::Capabilities PlutoSVGImagePlugin::capabilities(QIODevice* device, const QByteArray& format) const
|
||||
{
|
||||
if (format == "svg")
|
||||
return CanRead;
|
||||
|
||||
if (device)
|
||||
{
|
||||
// Require the byte sequence to contain an XML or SVG marker within a reasonable prefix.
|
||||
const QByteArray prefix = device->peek(256);
|
||||
if (prefix.contains("<svg") || prefix.contains("<?xml"))
|
||||
return CanRead;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
QImageIOHandler* PlutoSVGImagePlugin::create(QIODevice* device, const QByteArray& format) const
|
||||
{
|
||||
PlutoSVGImageHandler* const handler = new PlutoSVGImageHandler();
|
||||
handler->setDevice(device);
|
||||
handler->setFormat(format.isEmpty() ? QByteArray("svg") : format);
|
||||
return handler;
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
// SPDX-FileCopyrightText: 2019-2026 Connor McLaughlin <stenzek@gmail.com>
|
||||
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "common/heap_array.h"
|
||||
#include "common/types.h"
|
||||
|
||||
#include <QtCore/QByteArray>
|
||||
#include <QtCore/QString>
|
||||
#include <QtGui/QColor>
|
||||
#include <QtGui/QIcon>
|
||||
#include <QtGui/QIconEngine>
|
||||
#include <QtGui/QIconEnginePlugin>
|
||||
#include <QtGui/QImageIOHandler>
|
||||
#include <QtGui/QImageIOPlugin>
|
||||
|
||||
struct plutosvg_document;
|
||||
|
||||
/**
|
||||
* Custom icon engine that renders SVG icons tinted to the current palette's WindowText colour.
|
||||
* This allows a single set of monochrome SVG files to serve both light and dark themes without
|
||||
* duplication. The SVG is parsed once into a plutosvg_document and cached on the engine instance.
|
||||
* At paint/pixmap time plutosvg renders directly to a premultiplied-ARGB surface using the desired
|
||||
* palette colour as CSS currentColor, so no secondary SourceIn compositing pass is required.
|
||||
*
|
||||
* Pixmaps are cached in QPixmapCache keyed by resource path, size (in device pixels), and the RGBA
|
||||
* value of the resolved colour, so the cache is automatically invalidated when the palette changes.
|
||||
*/
|
||||
class ThemeSVGIconEngine final : public QIconEngine
|
||||
{
|
||||
public:
|
||||
explicit ThemeSVGIconEngine(const QString& resource_path);
|
||||
ThemeSVGIconEngine(const ThemeSVGIconEngine&) = delete;
|
||||
~ThemeSVGIconEngine() override;
|
||||
|
||||
void paint(QPainter* painter, const QRect& rect, QIcon::Mode mode, QIcon::State state) override;
|
||||
QPixmap pixmap(const QSize& size, QIcon::Mode mode, QIcon::State state) override;
|
||||
QPixmap scaledPixmap(const QSize& size, QIcon::Mode mode, QIcon::State state, qreal scale) override;
|
||||
QIconEngine* clone() const override;
|
||||
QString key() const override;
|
||||
QString iconName() override;
|
||||
|
||||
private:
|
||||
/// Ensures the SVG file has been loaded.
|
||||
bool ensureLoaded() const;
|
||||
|
||||
mutable QString m_resource_path;
|
||||
mutable DynamicHeapArray<u8> m_svg_data;
|
||||
mutable plutosvg_document* m_document = nullptr;
|
||||
};
|
||||
|
||||
class ThemeSVGIconEnginePlugin : public QIconEnginePlugin
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PLUGIN_METADATA(IID QIconEngineFactoryInterface_iid FILE "themesvgiconengine.json")
|
||||
|
||||
public:
|
||||
QIconEngine* create(const QString& resource_path) override;
|
||||
};
|
||||
|
||||
/**
|
||||
* Qt image I/O handler — replaces the QtSvg SVG image loader.
|
||||
*
|
||||
* Registered statically for "svg" so that any Qt code that loads SVG images
|
||||
* through QImageReader (e.g. QPixmap::load, QImage::load) will use plutosvg
|
||||
* instead of QtSvg's built-in rasteriser.
|
||||
*
|
||||
* Callers can influence the output size through the standard QImageIOHandler
|
||||
* ScaledSize option; if not set the SVG's intrinsic dimensions are used.
|
||||
*/
|
||||
class PlutoSVGImageHandler final : public QImageIOHandler
|
||||
{
|
||||
public:
|
||||
PlutoSVGImageHandler() = default;
|
||||
~PlutoSVGImageHandler() override = default;
|
||||
|
||||
/// Lazily load and parse the SVG data from device(). Returns false on failure.
|
||||
bool canRead() const override;
|
||||
bool read(QImage* image) override;
|
||||
|
||||
bool supportsOption(ImageOption option) const override;
|
||||
QVariant option(ImageOption option) const override;
|
||||
void setOption(ImageOption option, const QVariant& value) override;
|
||||
|
||||
static bool canRead(QIODevice* device);
|
||||
|
||||
private:
|
||||
mutable DynamicHeapArray<u8> m_svg_data;
|
||||
mutable plutosvg_document* m_document = nullptr;
|
||||
QSize m_scaled_size;
|
||||
};
|
||||
|
||||
/// QImageIOPlugin that installs PlutoSVGImageHandler for SVG files.
|
||||
class PlutoSVGImagePlugin final : public QImageIOPlugin
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PLUGIN_METADATA(IID QImageIOHandlerFactoryInterface_iid FILE "themesvgiconengine.json")
|
||||
|
||||
public:
|
||||
Capabilities capabilities(QIODevice* device, const QByteArray& format) const override;
|
||||
QImageIOHandler* create(QIODevice* device, const QByteArray& format = QByteArray()) const override;
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
{ "Keys": [ "svg" ] }
|
||||
Reference in New Issue
Block a user