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 WaylandClient
Widgets Widgets
Sensors Sensors
Svg
) )
find_package(Qt6Test ${QT_MIN_VERSION} CONFIG QUIET) find_package(Qt6Test ${QT_MIN_VERSION} CONFIG QUIET)

@ -206,7 +206,9 @@ target_sources(kwin PRIVATE
tiles/tilemanager.cpp tiles/tilemanager.cpp
touch_input.cpp touch_input.cpp
useractions.cpp useractions.cpp
utils/svgcursorreader.cpp
utils/version.cpp utils/version.cpp
utils/xcursorreader.cpp
virtualdesktops.cpp virtualdesktops.cpp
virtualdesktopsdbustypes.cpp virtualdesktopsdbustypes.cpp
virtualkeyboard_dbus.cpp virtualkeyboard_dbus.cpp
@ -234,8 +236,9 @@ target_link_libraries(kwin
PRIVATE PRIVATE
Qt::Concurrent Qt::Concurrent
Qt::Sensors
Qt::GuiPrivate Qt::GuiPrivate
Qt::Sensors
Qt::Svg
KF6::ColorScheme KF6::ColorScheme
KF6::ConfigGui 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 SPDX-License-Identifier: GPL-2.0-or-later
*/ */
#include "xcursortheme.h" #include "utils/xcursortheme.h"
#include "3rdparty/xcursor.h" #include "utils/svgcursorreader.h"
#include "utils/xcursorreader.h"
#include <KConfig> #include <KConfig>
#include <KConfigGroup> #include <KConfigGroup>
#include <KShell> #include <KShell>
#include <QDir> #include <QDir>
#include <QFile>
#include <QSet> #include <QSet>
#include <QSharedData> #include <QSharedData>
#include <QStack> #include <QStack>
@ -29,14 +29,27 @@ public:
std::chrono::milliseconds delay; std::chrono::milliseconds delay;
}; };
struct KXcursorThemeXEntryInfo
{
QString path;
};
struct KXcursorThemeSvgEntryInfo
{
QString path;
};
using KXcursorThemeEntryInfo = std::variant<KXcursorThemeXEntryInfo,
KXcursorThemeSvgEntryInfo>;
class KXcursorThemeEntry class KXcursorThemeEntry
{ {
public: public:
explicit KXcursorThemeEntry(const QString &filePath); explicit KXcursorThemeEntry(const KXcursorThemeEntryInfo &info);
void load(int size, qreal devicePixelRatio); void load(int size, qreal devicePixelRatio);
QString filePath; KXcursorThemeEntryInfo info;
QList<KXcursorSprite> sprites; QList<KXcursorSprite> sprites;
}; };
@ -47,7 +60,8 @@ public:
KXcursorThemePrivate(const QString &themeName, int size, qreal devicePixelRatio); KXcursorThemePrivate(const QString &themeName, int size, qreal devicePixelRatio);
void discover(const QStringList &searchPaths); void discover(const QStringList &searchPaths);
void discoverCursors(const QString &packagePath); void discoverXCursors(const QString &packagePath);
void discoverSvgCursors(const QString &packagePath);
QString name; QString name;
int size = 0; 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 { void KXcursorThemeEntry::load(int size, qreal devicePixelRatio)
.closure = &file, {
.read = [](XcursorFile *file, uint8_t *buffer, int len) -> int { if (!sprites.isEmpty()) {
QFile *device = static_cast<QFile *>(file->closure); return;
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; if (const auto raster = std::get_if<KXcursorThemeXEntryInfo>(&info)) {
for (int i = 0; i < images->nimage; ++i) { sprites = XCursorReader::load(raster->path, size, devicePixelRatio);
const XcursorImage *nativeCursorImage = images->images[i]; } else if (const auto svg = std::get_if<KXcursorThemeSvgEntryInfo>(&info)) {
const qreal scale = std::max(qreal(1), qreal(nativeCursorImage->size) / desiredSize); sprites = SvgCursorReader::load(svg->path, size, devicePixelRatio);
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) void KXcursorThemePrivate::discoverXCursors(const QString &packagePath)
: filePath(filePath)
{ {
} 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) for (const QFileInfo &entry : std::as_const(entries)) {
{ const QByteArray shape = QFile::encodeName(entry.fileName());
if (sprites.isEmpty()) { if (registry.contains(shape)) {
sprites = loadCursor(filePath, size, devicePixelRatio); 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); 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) { std::partition(entries.begin(), entries.end(), [](const QFileInfo &fileInfo) {
return !fileInfo.isSymLink(); 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()) { if (!dir.exists()) {
continue; 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()) { if (inherits.isEmpty()) {
const KConfig config(dir.filePath(QStringLiteral("index.theme")), KConfig::NoGlobals); const KConfig config(dir.filePath(QStringLiteral("index.theme")), KConfig::NoGlobals);
inherits << KConfigGroup(&config, QStringLiteral("Icon Theme")).readEntry("Inherits", QStringList()); inherits << KConfigGroup(&config, QStringLiteral("Icon Theme")).readEntry("Inherits", QStringList());

Loading…
Cancel
Save