From 266c6ee855481fcc8a5bdc19e7d1f8677c445ce5 Mon Sep 17 00:00:00 2001 From: Vlad Zahorodnii Date: Wed, 31 Jul 2024 12:55:55 +0300 Subject: [PATCH] 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. --- CMakeLists.txt | 1 + src/CMakeLists.txt | 5 +- src/utils/svgcursorreader.cpp | 143 ++++++++++++++++++++++++++++++++++ src/utils/svgcursorreader.h | 20 +++++ src/utils/xcursorreader.cpp | 61 +++++++++++++++ src/utils/xcursorreader.h | 20 +++++ src/utils/xcursortheme.cpp | 120 +++++++++++++++------------- 7 files changed, 313 insertions(+), 57 deletions(-) create mode 100644 src/utils/svgcursorreader.cpp create mode 100644 src/utils/svgcursorreader.h create mode 100644 src/utils/xcursorreader.cpp create mode 100644 src/utils/xcursorreader.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 21aacc7b14..02cad62346 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 7ecd662b92..3419d1645d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -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 diff --git a/src/utils/svgcursorreader.cpp b/src/utils/svgcursorreader.cpp new file mode 100644 index 0000000000..330977bb8e --- /dev/null +++ b/src/utils/svgcursorreader.cpp @@ -0,0 +1,143 @@ +/* + SPDX-FileCopyrightText: 2024 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "utils/svgcursorreader.h" +#include "utils/common.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace KWin +{ + +struct SvgCursorMetaDataEntry +{ + static std::optional parse(const QJsonObject &object); + + QString fileName; + QPointF hotspot; + std::chrono::milliseconds delay; +}; + +std::optional 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 parse(const QString &filePath); + + QList entries; +}; + +std::optional 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 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 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 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 diff --git a/src/utils/svgcursorreader.h b/src/utils/svgcursorreader.h new file mode 100644 index 0000000000..06edbde8a4 --- /dev/null +++ b/src/utils/svgcursorreader.h @@ -0,0 +1,20 @@ +/* + SPDX-FileCopyrightText: 2024 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "utils/xcursortheme.h" + +namespace KWin +{ + +class SvgCursorReader +{ +public: + static QList load(const QString &filePath, int desiredSize, qreal devicePixelRatio); +}; + +} // namespace KWin diff --git a/src/utils/xcursorreader.cpp b/src/utils/xcursorreader.cpp new file mode 100644 index 0000000000..4bba84cb07 --- /dev/null +++ b/src/utils/xcursorreader.cpp @@ -0,0 +1,61 @@ +/* + SPDX-FileCopyrightText: 2024 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "utils/xcursorreader.h" +#include "3rdparty/xcursor.h" + +#include + +namespace KWin +{ + +QList 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(file->closure); + return device->read(reinterpret_cast(buffer), len); + }, + .skip = [](XcursorFile *file, long offset) -> XcursorBool { + QFile *device = static_cast(file->closure); + return device->skip(offset) != -1; + }, + .seek = [](XcursorFile *file, long offset) -> XcursorBool { + QFile *device = static_cast(file->closure); + return device->seek(offset); + }, + }; + + XcursorImages *images = XcursorXcFileLoadImages(&reader, desiredSize * devicePixelRatio); + if (!images) { + return {}; + } + + QList 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 diff --git a/src/utils/xcursorreader.h b/src/utils/xcursorreader.h new file mode 100644 index 0000000000..20b34cb899 --- /dev/null +++ b/src/utils/xcursorreader.h @@ -0,0 +1,20 @@ +/* + SPDX-FileCopyrightText: 2024 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "utils/xcursortheme.h" + +namespace KWin +{ + +class XCursorReader +{ +public: + static QList load(const QString &filePath, int desiredSize, qreal devicePixelRatio); +}; + +} // namespace KWin diff --git a/src/utils/xcursortheme.cpp b/src/utils/xcursortheme.cpp index 93754d0e51..acdac2dacd 100644 --- a/src/utils/xcursortheme.cpp +++ b/src/utils/xcursortheme.cpp @@ -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 #include #include #include -#include #include #include #include @@ -29,14 +29,27 @@ public: std::chrono::milliseconds delay; }; +struct KXcursorThemeXEntryInfo +{ + QString path; +}; + +struct KXcursorThemeSvgEntryInfo +{ + QString path; +}; + +using KXcursorThemeEntryInfo = std::variant; + class KXcursorThemeEntry { public: - explicit KXcursorThemeEntry(const QString &filePath); + explicit KXcursorThemeEntry(const KXcursorThemeEntryInfo &info); void load(int size, qreal devicePixelRatio); - QString filePath; + KXcursorThemeEntryInfo info; QList 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 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(file->closure); - return device->read(reinterpret_cast(buffer), len); - }, - .skip = [](XcursorFile *file, long offset) -> XcursorBool { - QFile *device = static_cast(file->closure); - return device->skip(offset) != -1; - }, - .seek = [](XcursorFile *file, long offset) -> XcursorBool { - QFile *device = static_cast(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 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(&info)) { + sprites = XCursorReader::load(raster->path, size, devicePixelRatio); + } else if (const auto svg = std::get_if(&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(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(entry.absoluteFilePath())); + registry.insert(shape, std::make_shared(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());