wayland: Rework Xcursor theme loading

Xcursor loading code has hardcoded search paths, in order to take into
account distros installing app data in a different location,
libwayland-cursor sets the ICONDIR to the icon directory computed based
on the install prefix.

However, that won't work with gitlab CI because it relocates binaries. A
more robust way to find cursors would be to use QStandardPaths to find
all the icon directories on the system.

Another advantage of using own cursor loading code is that it allows us
to reuse cursor images that are symlinks. For example, with
breeze_cursors, almost half of the files in the cursors directory are
symlinks.

The main disadvantage of this approach is that we would have to keep the
search paths up to date. However, on the hand, there are not that many
of them, e.g. ~/.icons, ~/.local/share/icons, /usr/share/icons,
/usr/local/share/icons. The last three are implicitly handled by the
QStandardPaths.
This commit is contained in:
Vlad Zahorodnii 2022-01-27 22:47:11 +02:00
parent 7faa2587de
commit 0213661a7c
6 changed files with 111 additions and 482 deletions

View file

@ -56,9 +56,7 @@ static PlatformCursorImage loadReferenceThemeCursor(const QByteArray &name)
{
const Cursor *pointerCursor = Cursors::self()->mouse();
const KXcursorTheme theme = KXcursorTheme::fromTheme(pointerCursor->themeName(),
pointerCursor->themeSize(),
screens()->maxScale());
const KXcursorTheme theme(pointerCursor->themeName(), pointerCursor->themeSize(), screens()->maxScale());
if (theme.isEmpty()) {
return PlatformCursorImage();
}

435
src/3rdparty/xcursor.c vendored
View file

@ -238,7 +238,6 @@ XcursorImagesCreate (int size)
return NULL;
images->nimage = 0;
images->images = (XcursorImage **) (images + 1);
images->name = NULL;
return images;
}
@ -252,30 +251,9 @@ XcursorImagesDestroy (XcursorImages *images)
for (n = 0; n < images->nimage; n++)
XcursorImageDestroy (images->images[n]);
if (images->name)
free (images->name);
free (images);
}
static void
XcursorImagesSetName (XcursorImages *images, const char *name)
{
char *new;
if (!images || !name)
return;
new = malloc (strlen (name) + 1);
if (!new)
return;
strcpy (new, name);
if (images->name)
free (images->name);
images->name = new;
}
static XcursorBool
_XcursorReadUInt (XcursorFile *file, XcursorUInt *u)
{
@ -598,416 +576,19 @@ _XcursorStdioFileInitialize (FILE *stdfile, XcursorFile *file)
file->seek = _XcursorStdioFileSeek;
}
static XcursorImages *
XcursorFileLoadImages (FILE *file, int size)
XcursorImages *
XcursorFileLoadImages (const char *file, int size)
{
XcursorFile f;
XcursorImages *images;
if (!file)
FILE *fp = fopen(file, "r");
if (!fp)
return NULL;
_XcursorStdioFileInitialize (file, &f);
return XcursorXcFileLoadImages (&f, size);
}
_XcursorStdioFileInitialize (fp, &f);
images = XcursorXcFileLoadImages (&f, size);
fclose(fp);
/*
* From libXcursor/src/library.c
*/
#ifndef ICONDIR
#define ICONDIR "/usr/X11R6/lib/X11/icons"
#endif
#ifndef XCURSORPATH
#define XCURSORPATH "~/.icons:/usr/share/icons:/usr/share/pixmaps:~/.cursors:/usr/share/cursors/xorg-x11:"ICONDIR
#endif
#define XDG_DATA_HOME_FALLBACK "~/.local/share"
#define CURSORDIR "/icons"
/** Get search path for cursor themes
*
* This function builds the list of directories to look for cursor
* themes in. The format is PATH-like: directories are separated by
* colons.
*
* The memory block returned by this function is allocated on the heap
* and must be freed by the caller.
*/
static char *
XcursorLibraryPath (void)
{
const char *env_var;
char *path = NULL;
int pathlen = 0;
env_var = getenv ("XCURSOR_PATH");
if (env_var)
{
path = strdup (env_var);
}
else
{
env_var = getenv ("XDG_DATA_HOME");
if (env_var) {
pathlen = strlen (env_var) + strlen (CURSORDIR ":" XCURSORPATH) + 1;
path = malloc (pathlen);
snprintf (path, pathlen, "%s%s", env_var,
CURSORDIR ":" XCURSORPATH);
}
else
{
path = strdup (XDG_DATA_HOME_FALLBACK CURSORDIR ":" XCURSORPATH);
}
}
return path;
}
static void
_XcursorAddPathElt (char *path, const char *elt, int len)
{
int pathlen = strlen (path);
/* append / if the path doesn't currently have one */
if (path[0] == '\0' || path[pathlen - 1] != '/')
{
strcat (path, "/");
pathlen++;
}
if (len == -1)
len = strlen (elt);
/* strip leading slashes */
while (len && elt[0] == '/')
{
elt++;
len--;
}
strncpy (path + pathlen, elt, len);
path[pathlen + len] = '\0';
}
static char *
_XcursorBuildThemeDir (const char *dir, const char *theme)
{
const char *colon;
const char *tcolon;
char *full;
char *home;
int dirlen;
int homelen;
int themelen;
int len;
if (!dir || !theme)
return NULL;
colon = strchr (dir, ':');
if (!colon)
colon = dir + strlen (dir);
dirlen = colon - dir;
tcolon = strchr (theme, ':');
if (!tcolon)
tcolon = theme + strlen (theme);
themelen = tcolon - theme;
home = NULL;
homelen = 0;
if (*dir == '~')
{
home = getenv ("HOME");
if (!home)
return NULL;
homelen = strlen (home);
dir++;
dirlen--;
}
/*
* add space for any needed directory separators, one per component,
* and one for the trailing null
*/
len = 1 + homelen + 1 + dirlen + 1 + themelen + 1;
full = malloc (len);
if (!full)
return NULL;
full[0] = '\0';
if (home)
_XcursorAddPathElt (full, home, -1);
_XcursorAddPathElt (full, dir, dirlen);
_XcursorAddPathElt (full, theme, themelen);
return full;
}
static char *
_XcursorBuildFullname (const char *dir, const char *subdir, const char *file)
{
char *full;
if (!dir || !subdir || !file)
return NULL;
full = malloc (strlen (dir) + 1 + strlen (subdir) + 1 + strlen (file) + 1);
if (!full)
return NULL;
full[0] = '\0';
_XcursorAddPathElt (full, dir, -1);
_XcursorAddPathElt (full, subdir, -1);
_XcursorAddPathElt (full, file, -1);
return full;
}
static const char *
_XcursorNextPath (const char *path)
{
char *colon = strchr (path, ':');
if (!colon)
return NULL;
return colon + 1;
}
#define XcursorWhite(c) ((c) == ' ' || (c) == '\t' || (c) == '\n')
#define XcursorSep(c) ((c) == ';' || (c) == ',')
static char *
_XcursorThemeInherits (const char *full)
{
char line[8192];
char *result = NULL;
FILE *f;
if (!full)
return NULL;
f = fopen (full, "r");
if (f)
{
while (fgets (line, sizeof (line), f))
{
if (!strncmp (line, "Inherits", 8))
{
char *l = line + 8;
char *r;
while (*l == ' ') l++;
if (*l != '=') continue;
l++;
while (*l == ' ') l++;
result = malloc (strlen (l) + 1);
if (result)
{
r = result;
while (*l)
{
while (XcursorSep(*l) || XcursorWhite (*l)) l++;
if (!*l)
break;
if (r != result)
*r++ = ':';
while (*l && !XcursorWhite(*l) &&
!XcursorSep(*l))
*r++ = *l++;
}
*r++ = '\0';
}
break;
}
}
fclose (f);
}
return result;
}
static FILE *
XcursorScanTheme (const char *theme, const char *name)
{
FILE *f = NULL;
char *full;
char *dir;
const char *path;
char *inherits = NULL;
const char *i;
char *xcursor_path;
if (!theme || !name)
return NULL;
/*
* Scan this theme
*/
xcursor_path = XcursorLibraryPath ();
for (path = xcursor_path;
path && f == NULL;
path = _XcursorNextPath (path))
{
dir = _XcursorBuildThemeDir (path, theme);
if (dir)
{
full = _XcursorBuildFullname (dir, "cursors", name);
if (full)
{
f = fopen (full, "r");
free (full);
}
if (!f && !inherits)
{
full = _XcursorBuildFullname (dir, "", "index.theme");
if (full)
{
inherits = _XcursorThemeInherits (full);
free (full);
}
}
free (dir);
}
}
/*
* Recurse to scan inherited themes
*/
for (i = inherits; i && f == NULL; i = _XcursorNextPath (i))
f = XcursorScanTheme (i, name);
if (inherits != NULL)
free (inherits);
free (xcursor_path);
return f;
}
XcursorImages *
XcursorLibraryLoadImages (const char *file, const char *theme, int size)
{
FILE *f = NULL;
XcursorImages *images = NULL;
if (!file)
return NULL;
if (theme)
f = XcursorScanTheme (theme, file);
if (!f)
f = XcursorScanTheme ("default", file);
if (f)
{
images = XcursorFileLoadImages (f, size);
if (images)
XcursorImagesSetName (images, file);
fclose (f);
}
return images;
}
static void
load_all_cursors_from_dir(const char *path, int size,
void (*load_callback)(XcursorImages *, void *),
void *user_data)
{
FILE *f;
DIR *dir = opendir(path);
struct dirent *ent;
char *full;
XcursorImages *images;
if (!dir)
return;
for(ent = readdir(dir); ent; ent = readdir(dir)) {
#ifdef _DIRENT_HAVE_D_TYPE
if (ent->d_type != DT_UNKNOWN &&
(ent->d_type != DT_REG && ent->d_type != DT_LNK))
continue;
#endif
full = _XcursorBuildFullname(path, "", ent->d_name);
if (!full)
continue;
f = fopen(full, "r");
if (!f) {
free(full);
continue;
}
images = XcursorFileLoadImages(f, size);
if (images) {
XcursorImagesSetName(images, ent->d_name);
load_callback(images, user_data);
}
fclose (f);
free(full);
}
closedir(dir);
}
/** Load all the cursor of a theme
*
* This function loads all the cursor images of a given theme and its
* inherited themes. Each cursor is loaded into an XcursorImages object
* which is passed to the caller's load callback. If a cursor appears
* more than once across all the inherited themes, the load callback
* will be called multiple times, with possibly different XcursorImages
* object which have the same name. The user is expected to destroy the
* XcursorImages objects passed to the callback with
* XcursorImagesDestroy().
*
* \param theme The name of theme that should be loaded
* \param size The desired size of the cursor images
* \param load_callback A callback function that will be called
* for each cursor loaded. The first parameter is the XcursorImages
* object representing the loaded cursor and the second is a pointer
* to data provided by the user.
* \param user_data The data that should be passed to the load callback
*/
void
xcursor_load_theme(const char *theme, int size,
void (*load_callback)(XcursorImages *, void *),
void *user_data)
{
char *full, *dir;
char *inherits = NULL;
const char *path, *i;
char *xcursor_path;
if (!theme)
theme = "default";
xcursor_path = XcursorLibraryPath();
for (path = xcursor_path;
path;
path = _XcursorNextPath(path)) {
dir = _XcursorBuildThemeDir(path, theme);
if (!dir)
continue;
full = _XcursorBuildFullname(dir, "cursors", "");
if (full) {
load_all_cursors_from_dir(full, size, load_callback,
user_data);
free(full);
}
if (!inherits) {
full = _XcursorBuildFullname(dir, "", "index.theme");
if (full) {
inherits = _XcursorThemeInherits(full);
free(full);
}
}
free(dir);
}
for (i = inherits; i; i = _XcursorNextPath(i))
xcursor_load_theme(i, size, load_callback, user_data);
if (inherits)
free(inherits);
free (xcursor_path);
}

View file

@ -55,20 +55,14 @@ typedef struct _XcursorImage {
typedef struct _XcursorImages {
int nimage; /* number of images */
XcursorImage **images; /* array of XcursorImage pointers */
char *name; /* name used to load images */
} XcursorImages;
XcursorImages *
XcursorLibraryLoadImages (const char *file, const char *theme, int size);
XcursorFileLoadImages (const char *file, int size);
void
XcursorImagesDestroy (XcursorImages *images);
void
xcursor_load_theme(const char *theme, int size,
void (*load_callback)(XcursorImages *, void *),
void *user_data);
#ifdef __cplusplus
}
#endif

View file

@ -1262,14 +1262,12 @@ bool WaylandCursorImage::ensureCursorTheme()
const Cursor *pointerCursor = Cursors::self()->mouse();
const qreal targetDevicePixelRatio = screens()->maxScale();
m_cursorTheme = KXcursorTheme::fromTheme(pointerCursor->themeName(), pointerCursor->themeSize(),
targetDevicePixelRatio);
m_cursorTheme = KXcursorTheme(pointerCursor->themeName(), pointerCursor->themeSize(), targetDevicePixelRatio);
if (!m_cursorTheme.isEmpty()) {
return true;
}
m_cursorTheme = KXcursorTheme::fromTheme(Cursor::defaultThemeName(), Cursor::defaultThemeSize(),
targetDevicePixelRatio);
m_cursorTheme = KXcursorTheme(Cursor::defaultThemeName(), Cursor::defaultThemeSize(), targetDevicePixelRatio);
if (!m_cursorTheme.isEmpty()) {
return true;
}

View file

@ -7,8 +7,13 @@
#include "xcursortheme.h"
#include "3rdparty/xcursor.h"
#include <QMap>
#include <KConfig>
#include <KConfigGroup>
#include <QDir>
#include <QFile>
#include <QSharedData>
#include <QStandardPaths>
namespace KWin
{
@ -24,7 +29,10 @@ public:
class KXcursorThemePrivate : public QSharedData
{
public:
QMap<QByteArray, QVector<KXcursorSprite>> registry;
void load(const QString &themeName, int size, qreal devicePixelRatio);
void loadCursors(const QString &packagePath, int size, qreal devicePixelRatio);
QHash<QByteArray, QVector<KXcursorSprite>> registry;
};
KXcursorSprite::KXcursorSprite()
@ -71,20 +79,17 @@ std::chrono::milliseconds KXcursorSprite::delay() const
return d->delay;
}
struct XcursorThemeClosure
static QVector<KXcursorSprite> loadCursor(const QString &filePath, int desiredSize, qreal devicePixelRatio)
{
QMap<QByteArray, QVector<KXcursorSprite>> registry;
int desiredSize;
};
XcursorImages *images = XcursorFileLoadImages(QFile::encodeName(filePath), desiredSize * devicePixelRatio);
if (!images) {
return {};
}
static void load_callback(XcursorImages *images, void *data)
{
XcursorThemeClosure *closure = static_cast<XcursorThemeClosure *>(data);
QVector<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) / closure->desiredSize);
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);
@ -95,10 +100,80 @@ static void load_callback(XcursorImages *images, void *data)
sprites.append(KXcursorSprite(data, hotspot / scale, delay));
}
if (!sprites.isEmpty()) {
closure->registry.insert(images->name, sprites);
}
XcursorImagesDestroy(images);
return sprites;
}
void KXcursorThemePrivate::loadCursors(const QString &packagePath, int size, qreal devicePixelRatio)
{
const QDir dir(packagePath);
QFileInfoList entries = dir.entryInfoList(QDir::Files | 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()) {
const auto sprites = registry.value(QFile::encodeName(symLinkInfo.fileName()));
if (!sprites.isEmpty()) {
registry.insert(shape, sprites);
continue;
}
}
}
const QVector<KXcursorSprite> sprites = loadCursor(entry.absoluteFilePath(), size, devicePixelRatio);
if (!sprites.isEmpty()) {
registry.insert(shape, sprites);
}
}
}
static QStringList searchPaths()
{
static QStringList paths;
if (paths.isEmpty()) {
if (const QString env = qEnvironmentVariable("XCURSOR_PATH"); !env.isEmpty()) {
paths.append(env.split(':', Qt::SkipEmptyParts));
} else {
const QString home = QDir::homePath();
if (!home.isEmpty()) {
paths.append(home + QLatin1String("/.icons"));
}
const QStringList dataDirs = QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation);
for (const QString &dataDir : dataDirs) {
paths.append(dataDir + QLatin1String("/icons"));
}
}
}
return paths;
}
void KXcursorThemePrivate::load(const QString &themeName, int size, qreal devicePixelRatio)
{
const QStringList paths = searchPaths();
QStringList inherits;
for (const QString &path : paths) {
const QDir dir(path + QLatin1Char('/') + themeName);
if (!dir.exists()) {
continue;
}
loadCursors(dir.filePath(QStringLiteral("cursors")), size, devicePixelRatio);
if (inherits.isEmpty()) {
const KConfig config(dir.filePath(QStringLiteral("index.theme")), KConfig::NoGlobals);
inherits << KConfigGroup(&config, "Icon Theme").readEntry("Inherits", QStringList());
}
}
for (const QString &inherit : inherits) {
load(inherit, size, devicePixelRatio);
}
}
KXcursorTheme::KXcursorTheme()
@ -106,10 +181,10 @@ KXcursorTheme::KXcursorTheme()
{
}
KXcursorTheme::KXcursorTheme(const QMap<QByteArray, QVector<KXcursorSprite>> &registry)
: KXcursorTheme()
KXcursorTheme::KXcursorTheme(const QString &themeName, int size, qreal devicePixelRatio)
: d(new KXcursorThemePrivate)
{
d->registry = registry;
d->load(themeName, size, devicePixelRatio);
}
KXcursorTheme::KXcursorTheme(const KXcursorTheme &other)
@ -137,20 +212,4 @@ QVector<KXcursorSprite> KXcursorTheme::shape(const QByteArray &name) const
return d->registry.value(name);
}
KXcursorTheme KXcursorTheme::fromTheme(const QString &themeName, int size, qreal dpr)
{
// Xcursors don't support HiDPI natively so we fake it by scaling the desired cursor
// size. The device pixel ratio argument acts only as a hint. The real scale factor
// of every cursor sprite will be computed in the loading closure.
XcursorThemeClosure closure;
closure.desiredSize = size;
xcursor_load_theme(themeName.toUtf8().constData(), size * dpr, load_callback, &closure);
if (closure.registry.isEmpty()) {
return KXcursorTheme();
}
return KXcursorTheme(closure.registry);
}
} // namespace KWin

View file

@ -84,6 +84,13 @@ public:
*/
KXcursorTheme();
/**
* Loads the Xcursor theme with the given @ themeName and the desired @a size.
* The @a dpr specifies the desired scale factor. If no theme with the provided
* name exists, the cursor theme will be empty.
*/
KXcursorTheme(const QString &theme, int size, qreal devicePixelRatio);
/**
* Constructs a copy of the KXcursorTheme object @a other.
*/
@ -109,15 +116,7 @@ public:
*/
QVector<KXcursorSprite> shape(const QByteArray &name) const;
/**
* Loads the Xcursor theme with the given @ themeName and the desired @a size.
* The @a dpr specifies the desired scale factor. If no theme with the provided
* name exists, an empty KXcursorTheme is returned.
*/
static KXcursorTheme fromTheme(const QString &themeName, int size, qreal dpr);
private:
KXcursorTheme(const QMap<QByteArray, QVector<KXcursorSprite>> &registry);
QSharedDataPointer<KXcursorThemePrivate> d;
};