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.
wilder/Plasma/6.2
Vlad Zahorodnii 2 years ago
parent e7c1144e2c
commit 266c6ee855
  1. 1
      CMakeLists.txt
  2. 5
      src/CMakeLists.txt
  3. 143
      src/utils/svgcursorreader.cpp
  4. 20
      src/utils/svgcursorreader.h
  5. 61
      src/utils/xcursorreader.cpp
  6. 20
      src/utils/xcursorreader.h
  7. 120
      src/utils/xcursortheme.cpp

@ -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)

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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,68 +125,56 @@ KXcursorThemePrivate::KXcursorThemePrivate(const QString &themeName, int size, q
{
}
static QList<KXcursorSprite> loadCursor(const QString &filePath, int desiredSize, qreal devicePixelRatio)
KXcursorThemeEntry::KXcursorThemeEntry(const KXcursorThemeEntryInfo &info)
: info(info)
{
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 {};
void KXcursorThemeEntry::load(int size, qreal devicePixelRatio)
{
if (!sprites.isEmpty()) {
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));
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);
}
XcursorImagesDestroy(images);
return sprites;
}
KXcursorThemeEntry::KXcursorThemeEntry(const QString &filePath)
: filePath(filePath)
void KXcursorThemePrivate::discoverXCursors(const QString &packagePath)
{
}
const QDir dir(packagePath);
QFileInfoList entries = dir.entryInfoList(QDir::Files | QDir::NoDotAndDotDot);
std::partition(entries.begin(), entries.end(), [](const QFileInfo &fileInfo) {
return !fileInfo.isSymLink();
});
void KXcursorThemeEntry::load(int size, qreal devicePixelRatio)
{
if (sprites.isEmpty()) {
sprites = loadCursor(filePath, size, devicePixelRatio);
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>(KXcursorThemeXEntryInfo{
.path = entry.absoluteFilePath(),
}));
}
}
void KXcursorThemePrivate::discoverCursors(const QString &packagePath)
void KXcursorThemePrivate::discoverSvgCursors(const QString &packagePath)
{
const QDir dir(packagePath);
QFileInfoList entries = dir.entryInfoList(QDir::Files | QDir::NoDotAndDotDot);
QFileInfoList entries = dir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot);
std::partition(entries.begin(), entries.end(), [](const QFileInfo &fileInfo) {
return !fileInfo.isSymLink();
});
@ -191,7 +193,9 @@ void KXcursorThemePrivate::discoverCursors(const QString &packagePath)
}
}
}
registry.insert(shape, std::make_shared<KXcursorThemeEntry>(entry.absoluteFilePath()));
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());

Loading…
Cancel
Save