utils: Add support for svg cursors

With this change, KXcursorTheme will be able to load svg cursors provided
by breeze.

If a cursor theme provides both xcursor cursors and svg cursors, the svg
cursors will be preferred.

At the moment, KXcursorTheme doesn't cache svg render results but it could
do that if it becomes a noticeable issue.
This commit is contained in:
Vlad Zahorodnii 2024-07-31 12:55:55 +03:00
parent e7c1144e2c
commit 266c6ee855
7 changed files with 316 additions and 60 deletions

View file

@ -63,6 +63,7 @@ find_package(Qt6 ${QT_MIN_VERSION} CONFIG REQUIRED COMPONENTS
WaylandClient
Widgets
Sensors
Svg
)
find_package(Qt6Test ${QT_MIN_VERSION} CONFIG QUIET)

View file

@ -206,7 +206,9 @@ target_sources(kwin PRIVATE
tiles/tilemanager.cpp
touch_input.cpp
useractions.cpp
utils/svgcursorreader.cpp
utils/version.cpp
utils/xcursorreader.cpp
virtualdesktops.cpp
virtualdesktopsdbustypes.cpp
virtualkeyboard_dbus.cpp
@ -234,8 +236,9 @@ target_link_libraries(kwin
PRIVATE
Qt::Concurrent
Qt::Sensors
Qt::GuiPrivate
Qt::Sensors
Qt::Svg
KF6::ColorScheme
KF6::ConfigGui

View file

@ -0,0 +1,143 @@
/*
SPDX-FileCopyrightText: 2024 Vlad Zahorodnii <vlad.zahorodnii@kde.org>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "utils/svgcursorreader.h"
#include "utils/common.h"
#include <QDir>
#include <QFile>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QPainter>
#include <QSvgRenderer>
namespace KWin
{
struct SvgCursorMetaDataEntry
{
static std::optional<SvgCursorMetaDataEntry> parse(const QJsonObject &object);
QString fileName;
QPointF hotspot;
std::chrono::milliseconds delay;
};
std::optional<SvgCursorMetaDataEntry> SvgCursorMetaDataEntry::parse(const QJsonObject &object)
{
const QJsonValue fileName = object.value(QLatin1String("filename"));
if (!fileName.isString()) {
return std::nullopt;
}
const QJsonValue hotspotX = object.value(QLatin1String("hotspot_x"));
if (!hotspotX.isDouble()) {
return std::nullopt;
}
const QJsonValue hotspotY = object.value(QLatin1String("hotspot_y"));
if (!hotspotY.isDouble()) {
return std::nullopt;
}
const QJsonValue frametime = object.value(QLatin1String("frametime"));
return SvgCursorMetaDataEntry{
.fileName = fileName.toString(),
.hotspot = QPointF(hotspotX.toDouble(), hotspotY.toDouble()),
.delay = std::chrono::milliseconds(frametime.toInt()),
};
}
struct SvgCursorMetaData
{
static std::optional<SvgCursorMetaData> parse(const QString &filePath);
QList<SvgCursorMetaDataEntry> entries;
};
std::optional<SvgCursorMetaData> SvgCursorMetaData::parse(const QString &filePath)
{
QFile metaDataFile(filePath);
if (!metaDataFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
return std::nullopt;
}
QJsonParseError jsonParseError;
const QJsonDocument document = QJsonDocument::fromJson(metaDataFile.readAll(), &jsonParseError);
if (jsonParseError.error) {
return std::nullopt;
}
QList<SvgCursorMetaDataEntry> entries;
if (document.isObject()) {
if (const auto entry = SvgCursorMetaDataEntry::parse(document.object())) {
entries.append(entry.value());
} else {
return std::nullopt;
}
} else if (document.isArray()) {
const QJsonArray array = document.array();
for (int i = 0; i < array.size(); ++i) {
const QJsonValue element = array.at(i);
if (!element.isObject()) {
return std::nullopt;
}
if (const auto entry = SvgCursorMetaDataEntry::parse(element.toObject())) {
entries.append(entry.value());
} else {
return std::nullopt;
}
}
} else {
return std::nullopt;
}
return SvgCursorMetaData{
.entries = entries,
};
}
QList<KXcursorSprite> SvgCursorReader::load(const QString &containerPath, int desiredSize, qreal devicePixelRatio)
{
const QDir containerDir(containerPath);
const QString metadataFilePath = containerDir.filePath(QStringLiteral("metadata.json"));
const auto metadata = SvgCursorMetaData::parse(metadataFilePath);
if (!metadata.has_value()) {
qCWarning(KWIN_CORE) << "Failed to parse" << metadataFilePath;
return {};
}
const qreal scale = desiredSize / 24.0;
QList<KXcursorSprite> sprites;
for (const SvgCursorMetaDataEntry &entry : metadata->entries) {
const QString filePath = containerDir.filePath(entry.fileName);
QSvgRenderer renderer(filePath);
if (!renderer.isValid()) {
qCWarning(KWIN_CORE) << "Failed to render" << filePath;
return {};
}
const QRect bounds(QPoint(0, 0), renderer.defaultSize() * scale);
QImage image(bounds.size() * devicePixelRatio, QImage::Format_ARGB32_Premultiplied);
image.fill(Qt::transparent);
image.setDevicePixelRatio(devicePixelRatio);
QPainter painter(&image);
renderer.render(&painter, bounds);
painter.end();
sprites.append(KXcursorSprite(image, (entry.hotspot * scale).toPoint(), entry.delay));
}
return sprites;
}
} // namespace KWin

View file

@ -0,0 +1,20 @@
/*
SPDX-FileCopyrightText: 2024 Vlad Zahorodnii <vlad.zahorodnii@kde.org>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#pragma once
#include "utils/xcursortheme.h"
namespace KWin
{
class SvgCursorReader
{
public:
static QList<KXcursorSprite> load(const QString &filePath, int desiredSize, qreal devicePixelRatio);
};
} // namespace KWin

View file

@ -0,0 +1,61 @@
/*
SPDX-FileCopyrightText: 2024 Vlad Zahorodnii <vlad.zahorodnii@kde.org>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "utils/xcursorreader.h"
#include "3rdparty/xcursor.h"
#include <QFile>
namespace KWin
{
QList<KXcursorSprite> XCursorReader::load(const QString &filePath, int desiredSize, qreal devicePixelRatio)
{
QFile file(filePath);
if (!file.open(QFile::ReadOnly)) {
return {};
}
XcursorFile reader {
.closure = &file,
.read = [](XcursorFile *file, uint8_t *buffer, int len) -> int {
QFile *device = static_cast<QFile *>(file->closure);
return device->read(reinterpret_cast<char *>(buffer), len);
},
.skip = [](XcursorFile *file, long offset) -> XcursorBool {
QFile *device = static_cast<QFile *>(file->closure);
return device->skip(offset) != -1;
},
.seek = [](XcursorFile *file, long offset) -> XcursorBool {
QFile *device = static_cast<QFile *>(file->closure);
return device->seek(offset);
},
};
XcursorImages *images = XcursorXcFileLoadImages(&reader, desiredSize * devicePixelRatio);
if (!images) {
return {};
}
QList<KXcursorSprite> sprites;
for (int i = 0; i < images->nimage; ++i) {
const XcursorImage *nativeCursorImage = images->images[i];
const qreal scale = std::max(qreal(1), qreal(nativeCursorImage->size) / desiredSize);
const QPoint hotspot(nativeCursorImage->xhot, nativeCursorImage->yhot);
const std::chrono::milliseconds delay(nativeCursorImage->delay);
QImage data(nativeCursorImage->width, nativeCursorImage->height, QImage::Format_ARGB32_Premultiplied);
data.setDevicePixelRatio(scale);
memcpy(data.bits(), nativeCursorImage->pixels, data.sizeInBytes());
sprites.append(KXcursorSprite(data, hotspot / scale, delay));
}
XcursorImagesDestroy(images);
return sprites;
}
} // namespace KWin

20
src/utils/xcursorreader.h Normal file
View file

@ -0,0 +1,20 @@
/*
SPDX-FileCopyrightText: 2024 Vlad Zahorodnii <vlad.zahorodnii@kde.org>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#pragma once
#include "utils/xcursortheme.h"
namespace KWin
{
class XCursorReader
{
public:
static QList<KXcursorSprite> load(const QString &filePath, int desiredSize, qreal devicePixelRatio);
};
} // namespace KWin

View file

@ -4,15 +4,15 @@
SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "xcursortheme.h"
#include "3rdparty/xcursor.h"
#include "utils/xcursortheme.h"
#include "utils/svgcursorreader.h"
#include "utils/xcursorreader.h"
#include <KConfig>
#include <KConfigGroup>
#include <KShell>
#include <QDir>
#include <QFile>
#include <QSet>
#include <QSharedData>
#include <QStack>
@ -29,14 +29,27 @@ public:
std::chrono::milliseconds delay;
};
struct KXcursorThemeXEntryInfo
{
QString path;
};
struct KXcursorThemeSvgEntryInfo
{
QString path;
};
using KXcursorThemeEntryInfo = std::variant<KXcursorThemeXEntryInfo,
KXcursorThemeSvgEntryInfo>;
class KXcursorThemeEntry
{
public:
explicit KXcursorThemeEntry(const QString &filePath);
explicit KXcursorThemeEntry(const KXcursorThemeEntryInfo &info);
void load(int size, qreal devicePixelRatio);
QString filePath;
KXcursorThemeEntryInfo info;
QList<KXcursorSprite> sprites;
};
@ -47,7 +60,8 @@ public:
KXcursorThemePrivate(const QString &themeName, int size, qreal devicePixelRatio);
void discover(const QStringList &searchPaths);
void discoverCursors(const QString &packagePath);
void discoverXCursors(const QString &packagePath);
void discoverSvgCursors(const QString &packagePath);
QString name;
int size = 0;
@ -111,65 +125,25 @@ KXcursorThemePrivate::KXcursorThemePrivate(const QString &themeName, int size, q
{
}
static QList<KXcursorSprite> loadCursor(const QString &filePath, int desiredSize, qreal devicePixelRatio)
{
QFile file(filePath);
if (!file.open(QFile::ReadOnly)) {
return {};
}
XcursorFile reader {
.closure = &file,
.read = [](XcursorFile *file, uint8_t *buffer, int len) -> int {
QFile *device = static_cast<QFile *>(file->closure);
return device->read(reinterpret_cast<char *>(buffer), len);
},
.skip = [](XcursorFile *file, long offset) -> XcursorBool {
QFile *device = static_cast<QFile *>(file->closure);
return device->skip(offset) != -1;
},
.seek = [](XcursorFile *file, long offset) -> XcursorBool {
QFile *device = static_cast<QFile *>(file->closure);
return device->seek(offset);
},
};
XcursorImages *images = XcursorXcFileLoadImages(&reader, desiredSize * devicePixelRatio);
if (!images) {
return {};
}
QList<KXcursorSprite> sprites;
for (int i = 0; i < images->nimage; ++i) {
const XcursorImage *nativeCursorImage = images->images[i];
const qreal scale = std::max(qreal(1), qreal(nativeCursorImage->size) / desiredSize);
const QPoint hotspot(nativeCursorImage->xhot, nativeCursorImage->yhot);
const std::chrono::milliseconds delay(nativeCursorImage->delay);
QImage data(nativeCursorImage->width, nativeCursorImage->height, QImage::Format_ARGB32_Premultiplied);
data.setDevicePixelRatio(scale);
memcpy(data.bits(), nativeCursorImage->pixels, data.sizeInBytes());
sprites.append(KXcursorSprite(data, hotspot / scale, delay));
}
XcursorImagesDestroy(images);
return sprites;
}
KXcursorThemeEntry::KXcursorThemeEntry(const QString &filePath)
: filePath(filePath)
KXcursorThemeEntry::KXcursorThemeEntry(const KXcursorThemeEntryInfo &info)
: info(info)
{
}
void KXcursorThemeEntry::load(int size, qreal devicePixelRatio)
{
if (sprites.isEmpty()) {
sprites = loadCursor(filePath, size, devicePixelRatio);
if (!sprites.isEmpty()) {
return;
}
if (const auto raster = std::get_if<KXcursorThemeXEntryInfo>(&info)) {
sprites = XCursorReader::load(raster->path, size, devicePixelRatio);
} else if (const auto svg = std::get_if<KXcursorThemeSvgEntryInfo>(&info)) {
sprites = SvgCursorReader::load(svg->path, size, devicePixelRatio);
}
}
void KXcursorThemePrivate::discoverCursors(const QString &packagePath)
void KXcursorThemePrivate::discoverXCursors(const QString &packagePath)
{
const QDir dir(packagePath);
QFileInfoList entries = dir.entryInfoList(QDir::Files | QDir::NoDotAndDotDot);
@ -191,7 +165,37 @@ void KXcursorThemePrivate::discoverCursors(const QString &packagePath)
}
}
}
registry.insert(shape, std::make_shared<KXcursorThemeEntry>(entry.absoluteFilePath()));
registry.insert(shape, std::make_shared<KXcursorThemeEntry>(KXcursorThemeXEntryInfo{
.path = entry.absoluteFilePath(),
}));
}
}
void KXcursorThemePrivate::discoverSvgCursors(const QString &packagePath)
{
const QDir dir(packagePath);
QFileInfoList entries = dir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot);
std::partition(entries.begin(), entries.end(), [](const QFileInfo &fileInfo) {
return !fileInfo.isSymLink();
});
for (const QFileInfo &entry : std::as_const(entries)) {
const QByteArray shape = QFile::encodeName(entry.fileName());
if (registry.contains(shape)) {
continue;
}
if (entry.isSymLink()) {
const QFileInfo symLinkInfo(entry.symLinkTarget());
if (symLinkInfo.absolutePath() == entry.absolutePath()) {
if (auto alias = registry.value(QFile::encodeName(symLinkInfo.fileName()))) {
registry.insert(shape, alias);
continue;
}
}
}
registry.insert(shape, std::make_shared<KXcursorThemeEntry>(KXcursorThemeSvgEntryInfo{
.path = entry.absoluteFilePath(),
}));
}
}
@ -240,7 +244,11 @@ void KXcursorThemePrivate::discover(const QStringList &searchPaths)
if (!dir.exists()) {
continue;
}
discoverCursors(dir.filePath(QStringLiteral("cursors")));
if (const QDir package = dir.filePath(QLatin1String("cursors_scalable")); package.exists()) {
discoverSvgCursors(package.path());
} else if (const QDir package = dir.filePath(QLatin1String("cursors")); package.exists()) {
discoverXCursors(package.path());
}
if (inherits.isEmpty()) {
const KConfig config(dir.filePath(QStringLiteral("index.theme")), KConfig::NoGlobals);
inherits << KConfigGroup(&config, QStringLiteral("Icon Theme")).readEntry("Inherits", QStringList());