Qt: Add SVGWidget

pull/3745/head
Stenzek 4 months ago
parent f352619a93
commit 51457c22e3
No known key found for this signature in database

@ -166,6 +166,8 @@ set(SRCS
setupwizarddialog.cpp
setupwizarddialog.h
setupwizarddialog.ui
svgwidget.cpp
svgwidget.h
texturereplacementsettingsdialog.ui
themesvgiconengine.cpp
themesvgiconengine.h

@ -55,14 +55,17 @@
<ClCompile Include="selectdiscdialog.cpp" />
<ClCompile Include="settingswindow.cpp" />
<ClCompile Include="setupwizarddialog.cpp" />
<ClCompile Include="svgwidget.cpp" />
<ClCompile Include="themesvgiconengine.cpp">
<PrecompiledHeader>NotUsing</PrecompiledHeader>
<ForcedIncludeFiles></ForcedIncludeFiles>
<ForcedIncludeFiles>
</ForcedIncludeFiles>
</ClCompile>
<ClCompile Include="togglebutton.cpp" />
<ClCompile Include="vcruntimecheck.cpp" />
</ItemGroup>
<ItemGroup>
<QtMoc Include="svgwidget.h" />
<QtMoc Include="themesvgiconengine.h" />
<QtMoc Include="togglebutton.h" />
<QtMoc Include="setupwizarddialog.h" />

@ -55,6 +55,7 @@
<ClCompile Include="debuggingsettingswidget.cpp" />
<ClCompile Include="asynchttprequest.cpp" />
<ClCompile Include="themesvgiconengine.cpp" />
<ClCompile Include="svgwidget.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="qtutils.h" />
@ -122,6 +123,7 @@
<QtMoc Include="debuggingsettingswidget.h" />
<QtMoc Include="asynchttprequest.h" />
<QtMoc Include="themesvgiconengine.h" />
<QtMoc Include="svgwidget.h" />
</ItemGroup>
<ItemGroup>
<QtUi Include="consolesettingswidget.ui" />

@ -0,0 +1,182 @@
// SPDX-FileCopyrightText: 2019-2026 Connor McLaughlin <stenzek@gmail.com>
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
#include "svgwidget.h"
#include "qtutils.h"
#include <QtCore/QEvent>
#include <QtCore/QFile>
#include <QtGui/QPainter>
#include <QtGui/QPixmap>
#include <limits>
#include <plutosvg.h>
#include "moc_svgwidget.cpp"
static void CleanupSurface(void* surface)
{
plutovg_surface_destroy(static_cast<plutovg_surface_t*>(surface));
}
SVGWidget::SVGWidget(QWidget* parent) : QWidget(parent)
{
setAttribute(Qt::WA_OpaquePaintEvent, false);
}
SVGWidget::SVGWidget(const QString& resource_path, QWidget* parent /*= nullptr*/) : SVGWidget(parent)
{
setSource(resource_path);
}
SVGWidget::~SVGWidget()
{
destroyDocument();
}
void SVGWidget::setColor(const QColor& color)
{
m_pixmap = QPixmap();
rasterize();
update();
}
void SVGWidget::setSource(const QString& resource_path)
{
destroyDocument();
m_resource_path = resource_path;
m_pixmap = {};
m_last_raster_size = {};
if (resource_path.isEmpty())
{
update();
return;
}
QFile file(m_resource_path);
if (!file.open(QFile::ReadOnly) || !QtUtils::ReadFileToByteArray(&file, m_svg_data))
{
qCritical() << "Failed to open SVG file: " << m_resource_path;
update();
return;
}
// plutosvg borrows the raw pointer; m_svg_data must outlive m_document.
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() << "PlutoSVGWidget: failed to parse SVG" << resource_path;
m_svg_data.deallocate();
m_resource_path.clear();
update();
return;
}
rasterize();
update();
}
void SVGWidget::destroyDocument()
{
if (m_document)
{
plutosvg_document_destroy(m_document);
m_document = nullptr;
}
m_svg_data.deallocate();
}
void SVGWidget::rasterize()
{
m_pixmap = {};
if (!m_document || size().isEmpty())
return;
const qreal dpr = devicePixelRatioF();
// Physical pixel dimensions to render at.
const QSize physical = QtUtils::ApplyDevicePixelRatioToSize(size(), dpr);
// Avoid redundant re-renders when size hasn't actually changed.
if (physical == m_last_raster_size && !m_pixmap.isNull())
return;
m_last_raster_size = physical;
// Determine SVG intrinsic size and compute a uniform scale that fits inside physical,
// preserving aspect ratio.
const float svg_w = plutosvg_document_get_width(m_document);
const float svg_h = plutosvg_document_get_height(m_document);
int render_w = physical.width();
int render_h = physical.height();
if (svg_w > 0.0f && svg_h > 0.0f)
{
// Scale to fit, preserving aspect ratio (letterbox / pillarbox).
const float scale_x = static_cast<float>(physical.width()) / svg_w;
const float scale_y = static_cast<float>(physical.height()) / svg_h;
const float scale = std::min(scale_x, scale_y);
render_w = std::max(1, static_cast<int>(svg_w * scale));
render_h = std::max(1, static_cast<int>(svg_h * scale));
}
// Use white as currentColor so the SVG's own colours are preserved.
// Swap in a palette colour here if tinting is ever desired.
const plutovg_color_t current_color = {.r = 1.0f, .g = 1.0f, .b = 1.0f, .a = 1.0f};
plutovg_surface_t* const surface =
plutosvg_document_render_to_surface(m_document, nullptr, render_w, render_h, &current_color, nullptr, nullptr);
if (!surface)
{
qCritical() << "PlutoSVGWidget: render failed for" << m_resource_path;
return;
}
// plutovg surfaces are premultiplied ARGB (native-endian) == QImage::Format_ARGB32_Premultiplied.
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, CleanupSurface, surface);
m_pixmap = QPixmap::fromImage(img);
m_pixmap.setDevicePixelRatio(dpr);
}
void SVGWidget::paintEvent(QPaintEvent* event)
{
Q_UNUSED(event);
QPainter painter(this);
if (m_pixmap.isNull())
return;
// Center the (possibly smaller, aspect-ratio-corrected) pixmap inside the widget.
// m_pixmap.size() is in physical pixels; divide by DPR to get logical pixels.
const QSizeF logical_size = m_pixmap.size() / m_pixmap.devicePixelRatioF();
const QPointF top_left((width() - logical_size.width()) / 2.0, (height() - logical_size.height()) / 2.0);
painter.drawPixmap(QRectF(top_left, logical_size), m_pixmap, QRectF(QPointF(0, 0), m_pixmap.size()));
}
void SVGWidget::resizeEvent(QResizeEvent* event)
{
QWidget::resizeEvent(event);
rasterize();
}
void SVGWidget::changeEvent(QEvent* event)
{
QWidget::changeEvent(event);
// Re-rasterize when the screen changes (e.g. window moved to a display with a different DPR).
if (event->type() == QEvent::DevicePixelRatioChange || event->type() == QEvent::ScreenChangeInternal)
{
m_last_raster_size = {}; // force re-render
rasterize();
update();
}
}

@ -0,0 +1,61 @@
// 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/QSize>
#include <QtCore/QString>
#include <QtGui/QColor>
#include <QtGui/QPixmap>
#include <QtWidgets/QWidget>
struct plutosvg_document;
/**
* A widget that loads a monochrome SVG file via plutosvg and rasterizes it at the correct device
* pixel ratio. The SVG is re-rasterized whenever the widget is resized so the image stays crisp
* at any size. The rendered image is centered inside the widget; if the SVG aspect ratio differs
* from the widget's aspect ratio the image is letterboxed / pillarboxed with a transparent
* background.
*/
class SVGWidget final : public QWidget
{
Q_OBJECT
public:
explicit SVGWidget(QWidget* parent = nullptr);
SVGWidget(const QString& resource_path, QWidget* parent = nullptr);
~SVGWidget() override;
const QColor& color() const { return m_color; }
void setColor(const QColor& color);
/// Load (or reload) an SVG from the given Qt resource / file path.
/// Clears any previously loaded document. Triggers a repaint.
void setSource(const QString& resource_path);
/// Returns the path that was passed to setSource().
const QString& source() const { return m_resource_path; }
protected:
void paintEvent(QPaintEvent* event) override;
void resizeEvent(QResizeEvent* event) override;
void changeEvent(QEvent* event) override;
private:
/// (Re)render m_document at the current widget size + DPR and store the result in m_pixmap.
void rasterize();
/// Free the parsed document and clear associated data.
void destroyDocument();
QString m_resource_path;
DynamicHeapArray<u8> m_svg_data; ///< Raw bytes; plutosvg borrows this pointer.
plutosvg_document* m_document = nullptr;
QPixmap m_pixmap; ///< Last rasterized pixmap (physical pixels, DPR set).
QSize m_last_raster_size; ///< Physical size used for the last rasterize() call.
QColor m_color; ///< Color to use when rasterizing the SVG (overrides currentColor in the SVG).
};
Loading…
Cancel
Save