From 81c31b0c4d0d9f9459baceeacc3ac687e809e7d0 Mon Sep 17 00:00:00 2001 From: Tim Jochen Kicker Date: Tue, 5 May 2026 23:55:55 +0200 Subject: [PATCH] swaybar/tray: replace icon lookup with libsfdo Drops the in-tree icon theme parser and lookup logic in favor of libsfdo-icon, which implements the icon theme spec. Absolute icon paths are handled in swaybar itself with an access() probe, since libsfdo intentionally doesn't deal with them. Pixmap fallback still kicks in if load_image() fails on the absolute path. KDE's IconThemePath is honored via sfdo_icon_theme_load_from() with a per-SNI override theme that gets dropped when the property changes. The min/max size cache per SNI is replaced by re-lookup when the rendered size changes; libsfdo caches the parsed theme internally and reads gtk's icon-theme.cache files, so this is cheap. Theme switching at runtime is preserved via a new tray_reload_icon_theme() called from parse_bar_config(). The old code worked here implicitly because all themes were preloaded. Closes: https://github.com/swaywm/sway/issues/8607 --- .builds/archlinux.yml | 1 + .builds/freebsd.yml | 3 +- include/swaybar/tray/icon.h | 43 --- include/swaybar/tray/item.h | 4 +- include/swaybar/tray/tray.h | 7 +- meson.build | 7 +- swaybar/ipc.c | 2 + swaybar/meson.build | 3 +- swaybar/tray/icon.c | 525 ------------------------------------ swaybar/tray/item.c | 104 +++++-- swaybar/tray/tray.c | 73 ++++- 11 files changed, 175 insertions(+), 597 deletions(-) delete mode 100644 include/swaybar/tray/icon.h delete mode 100644 swaybar/tray/icon.c diff --git a/.builds/archlinux.yml b/.builds/archlinux.yml index 2c8ffcf45..31854274d 100644 --- a/.builds/archlinux.yml +++ b/.builds/archlinux.yml @@ -7,6 +7,7 @@ packages: - libdisplay-info - libegl - libinput + - libsfdo - libxcb - libxkbcommon - meson diff --git a/.builds/freebsd.yml b/.builds/freebsd.yml index a3df06e6d..923b16d25 100644 --- a/.builds/freebsd.yml +++ b/.builds/freebsd.yml @@ -39,7 +39,8 @@ tasks: cd subprojects ln -s ../../wlroots wlroots cd .. - meson setup build --fatal-meson-warnings -Dtray=enabled -Dsd-bus-provider=basu + # libsfdo is not packaged for FreeBSD yet; tray is disabled here. + meson setup build --fatal-meson-warnings -Dtray=disabled - build: | cd sway ninja -C build diff --git a/include/swaybar/tray/icon.h b/include/swaybar/tray/icon.h deleted file mode 100644 index 3673674b9..000000000 --- a/include/swaybar/tray/icon.h +++ /dev/null @@ -1,43 +0,0 @@ -#ifndef _SWAYBAR_TRAY_ICON_H -#define _SWAYBAR_TRAY_ICON_H - -#include "list.h" - -struct icon_theme_subdir { - char *name; - int size; - - enum { - THRESHOLD, - SCALABLE, - FIXED - } type; - - int max_size; - int min_size; - int threshold; -}; - -struct icon_theme { - char *name; - char *comment; - list_t *inherits; // char * - list_t *directories; // char * - - char *dir; - list_t *subdirs; // struct icon_theme_subdir * -}; - -void init_themes(list_t **themes, list_t **basedirs); -void finish_themes(list_t *themes, list_t *basedirs); - -/* - * Finds an icon of a specified size given a list of themes and base directories. - * If the icon is found, the pointers min_size & max_size are set to minimum & - * maximum size that the icon can be scaled to, respectively. - * Returns: path of icon (which should be freed), or NULL if the icon is not found. - */ -char *find_icon(list_t *themes, list_t *basedirs, char *name, int size, - char *theme, int *min_size, int *max_size); - -#endif diff --git a/include/swaybar/tray/item.h b/include/swaybar/tray/item.h index 73937a0cc..955d9e3d1 100644 --- a/include/swaybar/tray/item.h +++ b/include/swaybar/tray/item.h @@ -28,9 +28,9 @@ struct swaybar_sni { // icon properties struct swaybar_tray *tray; cairo_surface_t *icon; - int min_size; - int max_size; + int icon_size; int target_size; + struct sfdo_icon_theme *icon_theme_override; // non-NULL if IconThemePath set // dbus properties char *watcher_id; diff --git a/include/swaybar/tray/tray.h b/include/swaybar/tray/tray.h index d2e80a6d4..5d7c42f87 100644 --- a/include/swaybar/tray/tray.h +++ b/include/swaybar/tray/tray.h @@ -10,6 +10,7 @@ #include #endif #include +#include #include #include "swaybar/tray/host.h" #include "list.h" @@ -30,13 +31,15 @@ struct swaybar_tray { struct swaybar_watcher *watcher_xdg; struct swaybar_watcher *watcher_kde; - list_t *basedirs; // char * - list_t *themes; // struct swaybar_theme * + struct sfdo_icon_ctx *icon_ctx; + struct sfdo_icon_theme *icon_theme; + char *icon_theme_name; }; struct swaybar_tray *create_tray(struct swaybar *bar); void destroy_tray(struct swaybar_tray *tray); void tray_in(int fd, short mask, void *data); +void tray_reload_icon_theme(struct swaybar_tray *tray, const char *name); uint32_t render_tray(cairo_t *cairo, struct swaybar_output *output, double *x); #endif diff --git a/meson.build b/meson.build index 17d65c334..7623ab699 100644 --- a/meson.build +++ b/meson.build @@ -97,9 +97,12 @@ else sdbus = dependency(get_option('sd-bus-provider'), required: get_option('tray')) endif -tray_deps_found = sdbus.found() +sfdo_icon = dependency('libsfdo-icon', version: '>=0.1.3', required: get_option('tray')) +sfdo_basedir = dependency('libsfdo-basedir', version: '>=0.1.3', required: get_option('tray')) + +tray_deps_found = sdbus.found() and sfdo_icon.found() and sfdo_basedir.found() if get_option('tray').enabled() and not tray_deps_found - error('Building with -Dtray=enabled, but sd-bus has not been not found') + error('Building with -Dtray=enabled, but sd-bus or libsfdo has not been found') endif have_tray = (not get_option('tray').disabled()) and tray_deps_found diff --git a/swaybar/ipc.c b/swaybar/ipc.c index 68d8dd32d..ef6fc80ef 100644 --- a/swaybar/ipc.c +++ b/swaybar/ipc.c @@ -524,6 +524,8 @@ static bool handle_barconfig_update(struct swaybar *bar, const char *payload, loop_remove_fd(bar->eventloop, bar->tray->fd); destroy_tray(bar->tray); bar->tray = NULL; + } else if (bar->tray) { + tray_reload_icon_theme(bar->tray, newcfg->icon_theme); } #endif diff --git a/swaybar/meson.build b/swaybar/meson.build index 34bbdeea9..2b02040b9 100644 --- a/swaybar/meson.build +++ b/swaybar/meson.build @@ -1,6 +1,5 @@ tray_files = have_tray ? [ 'tray/host.c', - 'tray/icon.c', 'tray/item.c', 'tray/tray.c', 'tray/watcher.c' @@ -18,7 +17,7 @@ swaybar_deps = [ wayland_cursor ] if have_tray - swaybar_deps += sdbus + swaybar_deps += [sdbus, sfdo_icon, sfdo_basedir] endif executable( diff --git a/swaybar/tray/icon.c b/swaybar/tray/icon.c deleted file mode 100644 index 659edd86f..000000000 --- a/swaybar/tray/icon.c +++ /dev/null @@ -1,525 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include "swaybar/tray/icon.h" -#include "config.h" -#include "list.h" -#include "log.h" -#include "stringop.h" - -static int cmp_id(const void *item, const void *cmp_to) { - return strcmp(item, cmp_to); -} - -static bool dir_exists(char *path) { - struct stat sb; - return stat(path, &sb) == 0 && S_ISDIR(sb.st_mode); -} - -static list_t *get_basedirs(void) { - list_t *basedirs = create_list(); - list_add(basedirs, strdup("$HOME/.icons")); // deprecated - - char *data_home = getenv("XDG_DATA_HOME"); - list_add(basedirs, strdup(data_home && *data_home ? - "$XDG_DATA_HOME/icons" : "$HOME/.local/share/icons")); - - list_add(basedirs, strdup("/usr/share/pixmaps")); - - char *data_dirs = getenv("XDG_DATA_DIRS"); - if (!(data_dirs && *data_dirs)) { - data_dirs = "/usr/local/share:/usr/share"; - } - data_dirs = strdup(data_dirs); - char *dir = strtok(data_dirs, ":"); - do { - char *path = format_str("%s/icons", dir); - list_add(basedirs, path); - } while ((dir = strtok(NULL, ":"))); - free(data_dirs); - - list_t *basedirs_expanded = create_list(); - for (int i = 0; i < basedirs->length; ++i) { - wordexp_t p; - if (wordexp(basedirs->items[i], &p, WRDE_UNDEF) == 0) { - if (dir_exists(p.we_wordv[0])) { - list_add(basedirs_expanded, strdup(p.we_wordv[0])); - } - wordfree(&p); - } - } - - list_free_items_and_destroy(basedirs); - - return basedirs_expanded; -} - -static void destroy_theme(struct icon_theme *theme) { - if (!theme) { - return; - } - free(theme->name); - free(theme->comment); - list_free_items_and_destroy(theme->inherits); - list_free_items_and_destroy(theme->directories); - free(theme->dir); - - for (int i = 0; i < theme->subdirs->length; ++i) { - struct icon_theme_subdir *subdir = theme->subdirs->items[i]; - free(subdir->name); - free(subdir); - } - list_free(theme->subdirs); - free(theme); -} - -static const char *group_handler(char *old_group, char *new_group, - struct icon_theme *theme) { - if (!old_group) { - return new_group && strcmp(new_group, "Icon Theme") == 0 ? NULL : - "first group must be 'Icon Theme'"; - } - - if (strcmp(old_group, "Icon Theme") == 0) { - if (!theme->name) { - return "missing required key 'Name'"; - } else if (!theme->comment) { - return "missing required key 'Comment'"; - } else if (!theme->directories) { - return "missing required key 'Directories'"; - } else { - for (char *c = theme->name; *c; ++c) { - if (*c == ',' || *c == ' ') { - return "malformed theme name"; - } - } - } - } else { - if (theme->subdirs->length == 0) { // skip - return NULL; - } - - struct icon_theme_subdir *subdir = - theme->subdirs->items[theme->subdirs->length - 1]; - if (!subdir->size) { - return "missing required key 'Size'"; - } - - switch (subdir->type) { - case FIXED: subdir->max_size = subdir->min_size = subdir->size; - break; - case SCALABLE: { - if (!subdir->max_size) subdir->max_size = subdir->size; - if (!subdir->min_size) subdir->min_size = subdir->size; - break; - } - case THRESHOLD: - subdir->max_size = subdir->size + subdir->threshold; - subdir->min_size = subdir->size - subdir->threshold; - } - } - - if (new_group && list_seq_find(theme->directories, cmp_id, new_group) != -1) { - struct icon_theme_subdir *subdir = calloc(1, sizeof(struct icon_theme_subdir)); - if (!subdir) { - return "out of memory"; - } - subdir->name = strdup(new_group); - subdir->threshold = 2; - list_add(theme->subdirs, subdir); - } - - return NULL; -} - -static const char *entry_handler(char *group, char *key, char *value, - struct icon_theme *theme) { - if (strcmp(group, "Icon Theme") == 0) { - if (strcmp(key, "Name") == 0) { - theme->name = strdup(value); - } else if (strcmp(key, "Comment") == 0) { - theme->comment = strdup(value); - } else if (strcmp(key, "Inherits") == 0) { - theme->inherits = split_string(value, ","); - } else if (strcmp(key, "Directories") == 0) { - theme->directories = split_string(value, ","); - } // Ignored: ScaledDirectories, Hidden, Example - } else { - if (theme->subdirs->length == 0) { // skip - return NULL; - } - - struct icon_theme_subdir *subdir = - theme->subdirs->items[theme->subdirs->length - 1]; - if (strcmp(subdir->name, group) != 0) { // skip - return NULL; - } - - if (strcmp(key, "Context") == 0) { - return NULL; // ignored, but explicitly handled to not fail parsing - } else if (strcmp(key, "Type") == 0) { - if (strcmp(value, "Fixed") == 0) { - subdir->type = FIXED; - } else if (strcmp(value, "Scalable") == 0) { - subdir->type = SCALABLE; - } else if (strcmp(value, "Threshold") == 0) { - subdir->type = THRESHOLD; - } else { - return "invalid value - expected 'Fixed', 'Scalable' or 'Threshold'"; - } - return NULL; - } - - char *end; - int n = strtol(value, &end, 10); - if (*end != '\0') { - return "invalid value - expected a number"; - } - - if (strcmp(key, "Size") == 0) { - subdir->size = n; - } else if (strcmp(key, "MaxSize") == 0) { - subdir->max_size = n; - } else if (strcmp(key, "MinSize") == 0) { - subdir->min_size = n; - } else if (strcmp(key, "Threshold") == 0) { - subdir->threshold = n; - } // Ignored: Scale - } - return NULL; -} - -/* - * This is a Freedesktop Desktop Entry parser (essentially INI) - * It calls entry_handler for every entry - * and group_handler between every group (as well as at both ends) - * Handlers return whether an error occurred, which stops parsing - */ -static struct icon_theme *read_theme_file(char *basedir, char *theme_name) { - // look for index.theme file - char *path = format_str("%s/%s/index.theme", basedir, theme_name); - FILE *theme_file = fopen(path, "r"); - free(path); - if (!theme_file) { - return NULL; - } - - struct icon_theme *theme = calloc(1, sizeof(struct icon_theme)); - if (!theme) { - fclose(theme_file); - return NULL; - } - theme->subdirs = create_list(); - - list_t *groups = create_list(); - - const char *error = NULL; - int line_no = 0; - char *full_line = NULL; - size_t full_len = 0; - ssize_t nread; - while ((nread = getline(&full_line, &full_len, theme_file)) != -1) { - ++line_no; - - char *line = full_line - 1; - while (isspace(*++line)) {} // remove leading whitespace - if (!*line || line[0] == '#') continue; // ignore blank lines & comments - - int len = nread - (line - full_line); - while (isspace(line[--len])) {} - line[++len] = '\0'; // remove trailing whitespace - - if (line[0] == '[') { // group header - // check well-formed - int i = 1; - for (; !iscntrl(line[i]) && line[i] != '[' && line[i] != ']'; ++i) {} - if (i != --len || line[i] != ']') { - error = "malformed group header"; - break; - } - - line[len] = '\0'; - - // check group is not duplicate - if (list_seq_find(groups, cmp_id, &line[1]) != -1) { - error = "duplicate group"; - break; - } - - // call handler - char *last_group = groups->length > 0 ? groups->items[groups->length - 1] : NULL; - error = group_handler(last_group, &line[1], theme); - if (error) { - break; - } - - list_add(groups, strdup(&line[1])); - } else { // key-value pair - if (groups->length == 0) { - error = "unexpected content before first header"; - break; - } - - // check well-formed - int eok = 0; - for (; isalnum(line[eok]) || line[eok] == '-'; ++eok) {} // TODO locale? - int i = eok - 1; - while (isspace(line[++i])) {} - if (line[i] != '=') { - error = "malformed key-value pair"; - break; - } - - line[eok] = '\0'; // split into key-value pair - char *value = &line[i]; - while (isspace(*++value)) {} - // TODO unescape value - - error = entry_handler(groups->items[groups->length - 1], line, - value, theme); - if (error) { - break; - } - } - } - - if (!error) { - if (groups->length > 0) { - error = group_handler(groups->items[groups->length - 1], NULL, theme); - } else { - error = "empty file"; - } - } - - if (!error) { - theme->dir = strdup(theme_name); - } else { - char *last_group = groups->length > 0 ? groups->items[groups->length-1] : "n/a"; - sway_log(SWAY_DEBUG, "Failed to load theme '%s' - parsing of file " - "'%s/%s/index.theme' failed on line %d (group '%s'): %s", - theme_name, basedir, theme_name, line_no, last_group, error); - destroy_theme(theme); - theme = NULL; - } - - free(full_line); - list_free_items_and_destroy(groups); - fclose(theme_file); - return theme; -} - -static list_t *load_themes_in_dir(char *basedir) { - DIR *dir; - if (!(dir = opendir(basedir))) { - return NULL; - } - - list_t *themes = create_list(); - struct dirent *entry; - while ((entry = readdir(dir))) { - if (entry->d_name[0] == '.') continue; - - struct icon_theme *theme = read_theme_file(basedir, entry->d_name); - if (theme) { - list_add(themes, theme); - } - } - closedir(dir); - return themes; -} - -static void log_loaded_themes(list_t *themes) { - if (themes->length == 0) { - sway_log(SWAY_INFO, "Warning: no icon themes loaded"); - return; - } - - const char sep[] = ", "; - size_t sep_len = strlen(sep); - - size_t len = 0; - for (int i = 0; i < themes->length; ++i) { - struct icon_theme *theme = themes->items[i]; - len += strlen(theme->name) + sep_len; - } - - char *str = malloc(len + 1); - if (!str) { - return; - } - char *p = str; - for (int i = 0; i < themes->length; ++i) { - if (i > 0) { - memcpy(p, sep, sep_len); - p += sep_len; - } - - struct icon_theme *theme = themes->items[i]; - size_t name_len = strlen(theme->name); - memcpy(p, theme->name, name_len); - p += name_len; - } - *p = '\0'; - - sway_log(SWAY_DEBUG, "Loaded icon themes: %s", str); - free(str); -} - -void init_themes(list_t **themes, list_t **basedirs) { - *basedirs = get_basedirs(); - - *themes = create_list(); - for (int i = 0; i < (*basedirs)->length; ++i) { - list_t *dir_themes = load_themes_in_dir((*basedirs)->items[i]); - if (dir_themes == NULL) { - continue; - } - list_cat(*themes, dir_themes); - list_free(dir_themes); - } - - log_loaded_themes(*themes); -} - -void finish_themes(list_t *themes, list_t *basedirs) { - for (int i = 0; i < themes->length; ++i) { - destroy_theme(themes->items[i]); - } - list_free(themes); - list_free_items_and_destroy(basedirs); -} - -static char *find_icon_in_subdir(char *name, char *basedir, char *theme, - char *subdir) { - static const char *extensions[] = { -#if HAVE_GDK_PIXBUF - "svg", -#endif - "png", -#if HAVE_GDK_PIXBUF - "xpm" // deprecated -#endif - }; - - for (size_t i = 0; i < sizeof(extensions) / sizeof(*extensions); ++i) { - char *path = format_str("%s/%s/%s/%s.%s", - basedir, theme, subdir, name, extensions[i]); - if (access(path, R_OK) == 0) { - return path; - } - free(path); - } - - return NULL; -} - -static bool theme_exists_in_basedir(char *theme, char *basedir) { - char *path = format_str("%s/%s", basedir, theme); - bool ret = dir_exists(path); - free(path); - return ret; -} - -static char *find_icon_with_theme(list_t *basedirs, list_t *themes, char *name, - int size, char *theme_name, int *min_size, int *max_size) { - struct icon_theme *theme = NULL; - for (int i = 0; i < themes->length; ++i) { - theme = themes->items[i]; - if (strcmp(theme->name, theme_name) == 0) { - break; - } - theme = NULL; - } - if (!theme) return NULL; - - char *icon = NULL; - for (int i = 0; i < basedirs->length; ++i) { - if (!theme_exists_in_basedir(theme->dir, basedirs->items[i])) { - continue; - } - // search backwards to hopefully hit scalable/larger icons first - for (int j = theme->subdirs->length - 1; j >= 0; --j) { - struct icon_theme_subdir *subdir = theme->subdirs->items[j]; - if (size >= subdir->min_size && size <= subdir->max_size) { - if ((icon = find_icon_in_subdir(name, basedirs->items[i], - theme->dir, subdir->name))) { - *min_size = subdir->min_size; - *max_size = subdir->max_size; - return icon; - } - } - } - } - - // inexact match - unsigned smallest_error = -1; // UINT_MAX - for (int i = 0; i < basedirs->length; ++i) { - if (!theme_exists_in_basedir(theme->dir, basedirs->items[i])) { - continue; - } - for (int j = theme->subdirs->length - 1; j >= 0; --j) { - struct icon_theme_subdir *subdir = theme->subdirs->items[j]; - unsigned error = (size > subdir->max_size ? size - subdir->max_size : 0) - + (size < subdir->min_size ? subdir->min_size - size : 0); - if (error < smallest_error) { - char *test_icon = find_icon_in_subdir(name, basedirs->items[i], - theme->dir, subdir->name); - if (test_icon) { - icon = test_icon; - smallest_error = error; - *min_size = subdir->min_size; - *max_size = subdir->max_size; - } - } - } - } - - if (!icon && theme->inherits) { - for (int i = 0; i < theme->inherits->length; ++i) { - icon = find_icon_with_theme(basedirs, themes, name, size, - theme->inherits->items[i], min_size, max_size); - if (icon) { - break; - } - } - } - - return icon; -} - -static char *find_fallback_icon(list_t *basedirs, char *name, int *min_size, - int *max_size) { - for (int i = 0; i < basedirs->length; ++i) { - char *icon = find_icon_in_subdir(name, basedirs->items[i], "", ""); - if (icon) { - *min_size = 1; - *max_size = 512; - return icon; - } - } - return NULL; -} - -char *find_icon(list_t *themes, list_t *basedirs, char *name, int size, - char *theme, int *min_size, int *max_size) { - // TODO https://specifications.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html#implementation_notes - char *icon = NULL; - if (theme) { - icon = find_icon_with_theme(basedirs, themes, name, size, theme, - min_size, max_size); - } - if (!icon && !(theme && strcmp(theme, "Hicolor") == 0)) { - icon = find_icon_with_theme(basedirs, themes, name, size, "Hicolor", - min_size, max_size); - } - if (!icon) { - icon = find_fallback_icon(basedirs, name, min_size, max_size); - } - return icon; -} diff --git a/swaybar/tray/item.c b/swaybar/tray/item.c index 12929743b..f5369015e 100644 --- a/swaybar/tray/item.c +++ b/swaybar/tray/item.c @@ -1,15 +1,17 @@ #include #include #include +#include #include #include #include +#include +#include "config.h" #include "swaybar/bar.h" #include "swaybar/config.h" #include "swaybar/image.h" #include "swaybar/input.h" #include "swaybar/tray/host.h" -#include "swaybar/tray/icon.h" #include "swaybar/tray/item.h" #include "swaybar/tray/tray.h" #include "cairo_util.h" @@ -28,7 +30,7 @@ static bool sni_ready(struct swaybar_sni *sni) { static void set_sni_dirty(struct swaybar_sni *sni) { if (sni_ready(sni)) { - sni->target_size = sni->min_size = sni->max_size = 0; // invalidate previous icon + sni->target_size = sni->icon_size = 0; // invalidate previous icon set_bar_dirty(sni->tray->bar); } } @@ -161,6 +163,13 @@ static int get_property_callback(sd_bus_message *msg, void *data, } } + // IconThemePath may change at runtime; the cached override theme is + // keyed on the old path, so drop it. + if (strcmp(prop, "IconThemePath") == 0) { + sfdo_icon_theme_destroy(sni->icon_theme_override); + sni->icon_theme_override = NULL; + } + if (strcmp(prop, "Status") == 0 || (sni->status && (sni->status[0] == 'N' ? prop[0] == 'A' : has_prefix(prop, "Icon")))) { set_sni_dirty(sni); @@ -313,6 +322,7 @@ void destroy_sni(struct swaybar_sni *sni) { } cairo_surface_destroy(sni->icon); + sfdo_icon_theme_destroy(sni->icon_theme_override); free(sni->watcher_id); free(sni->service); free(sni->path); @@ -415,25 +425,88 @@ static enum hotspot_event_handling icon_hotspot_callback( return HOTSPOT_PROCESS; } -static void reload_sni(struct swaybar_sni *sni, char *icon_theme, +// Some apps send "foo.png" as an icon name. The icon theme spec doesn't +// allow extensions in lookups; strip a known one before passing to libsfdo. +static size_t name_len_without_extension(const char *name) { + size_t len = strlen(name); + if (len >= 4 && name[len - 4] == '.') { + const char *ext = &name[len - 3]; + if (strcmp(ext, "png") == 0 || strcmp(ext, "svg") == 0 || + strcmp(ext, "xpm") == 0) { + return len - 4; + } + } + return len; +} + +static struct sfdo_icon_theme *get_sni_icon_theme(struct swaybar_sni *sni, + const char *theme_name) { + if (!sni->icon_theme_path) { + return sni->tray->icon_theme; + } + if (sni->icon_theme_override) { + return sni->icon_theme_override; + } + struct sfdo_string basedir = { + .data = sni->icon_theme_path, + .len = strlen(sni->icon_theme_path), + }; + int options = SFDO_ICON_THEME_LOAD_OPTION_ALLOW_MISSING | + SFDO_ICON_THEME_LOAD_OPTION_RELAXED; + sni->icon_theme_override = sfdo_icon_theme_load_from(sni->tray->icon_ctx, + theme_name, &basedir, 1, options); + if (!sni->icon_theme_override) { + sway_log(SWAY_DEBUG, "%s: failed to load theme '%s' from '%s', " + "falling back to global theme", sni->watcher_id, + theme_name ? theme_name : "(default)", sni->icon_theme_path); + return sni->tray->icon_theme; + } + return sni->icon_theme_override; +} + +static char *lookup_icon_path(struct swaybar_sni *sni, const char *icon_name, + const char *theme_name, int target_size) { + // Absolute paths are outside the XDG icon theme spec; handle directly + if (icon_name[0] == '/') { + return access(icon_name, R_OK) == 0 ? strdup(icon_name) : NULL; + } + + struct sfdo_icon_theme *theme = get_sni_icon_theme(sni, theme_name); + if (!theme) { + return NULL; + } + + int options = SFDO_ICON_THEME_LOOKUP_OPTIONS_DEFAULT; +#if !HAVE_GDK_PIXBUF + options |= SFDO_ICON_THEME_LOOKUP_OPTION_NO_SVG; +#endif + size_t name_len = name_len_without_extension(icon_name); + struct sfdo_icon_file *file = sfdo_icon_theme_lookup(theme, icon_name, + name_len, target_size, 1, options); + if (!file || file == SFDO_ICON_FILE_INVALID) { + return NULL; + } + char *path = strdup(sfdo_icon_file_get_path(file, NULL)); + sfdo_icon_file_destroy(file); + return path; +} + +static void reload_sni(struct swaybar_sni *sni, const char *icon_theme, int target_size) { char *icon_name = sni->status[0] == 'N' ? sni->attention_icon_name : sni->icon_name; if (icon_name) { - list_t *icon_search_paths = create_list(); - list_cat(icon_search_paths, sni->tray->basedirs); - if (sni->icon_theme_path) { - list_add(icon_search_paths, sni->icon_theme_path); - } - char *icon_path = find_icon(sni->tray->themes, icon_search_paths, - icon_name, target_size, icon_theme, - &sni->min_size, &sni->max_size); - list_free(icon_search_paths); + char *icon_path = lookup_icon_path(sni, icon_name, icon_theme, + target_size); if (icon_path) { cairo_surface_destroy(sni->icon); sni->icon = load_image(icon_path); free(icon_path); - return; + if (sni->icon) { + return; + } + // load_image() failed (unsupported format, file vanished, + // etc); fall through to pixmap fallback if available. } } @@ -463,11 +536,10 @@ uint32_t render_sni(cairo_t *cairo, struct swaybar_output *output, double *x, int padding = output->bar->config->tray_padding; int target_size = height - 2*padding; if (target_size != sni->target_size && sni_ready(sni)) { - // check if another icon should be loaded - if (target_size < sni->min_size || target_size > sni->max_size) { + if (target_size != sni->icon_size) { reload_sni(sni, output->bar->config->icon_theme, target_size); + sni->icon_size = target_size; } - sni->target_size = target_size; } diff --git a/swaybar/tray/tray.c b/swaybar/tray/tray.c index a4f382bfb..318a30cb8 100644 --- a/swaybar/tray/tray.c +++ b/swaybar/tray/tray.c @@ -1,11 +1,12 @@ #include #include +#include +#include #include #include #include #include "swaybar/config.h" #include "swaybar/bar.h" -#include "swaybar/tray/icon.h" #include "swaybar/tray/host.h" #include "swaybar/tray/item.h" #include "swaybar/tray/tray.h" @@ -34,6 +35,22 @@ static int handle_lost_watcher(sd_bus_message *msg, return 0; } +static struct sfdo_icon_theme *load_icon_theme(struct sfdo_icon_ctx *ctx, + const char *name) { + int options = SFDO_ICON_THEME_LOAD_OPTION_ALLOW_MISSING | + SFDO_ICON_THEME_LOAD_OPTION_RELAXED; + struct sfdo_icon_theme *theme = sfdo_icon_theme_load(ctx, name, options); + if (!theme) { + // _ALLOW_MISSING falls back to hicolor when the named theme is + // missing, but returns NULL when the theme is found but invalid. + // Manually retry with hicolor in that case. + sway_log(SWAY_DEBUG, "Failed to load icon theme '%s', " + "falling back to hicolor", name ? name : "(default)"); + theme = sfdo_icon_theme_load(ctx, "hicolor", options); + } + return theme; +} + struct swaybar_tray *create_tray(struct swaybar *bar) { sway_log(SWAY_DEBUG, "Initializing tray"); @@ -46,12 +63,29 @@ struct swaybar_tray *create_tray(struct swaybar *bar) { struct swaybar_tray *tray = calloc(1, sizeof(struct swaybar_tray)); if (!tray) { + sd_bus_flush_close_unref(bus); return NULL; } tray->bar = bar; tray->bus = bus; tray->fd = sd_bus_get_fd(tray->bus); + struct sfdo_basedir_ctx *basedir_ctx = sfdo_basedir_ctx_create(); + if (!basedir_ctx) { + sway_log(SWAY_ERROR, "Failed to create sfdo basedir context"); + goto error; + } + tray->icon_ctx = sfdo_icon_ctx_create(basedir_ctx); + sfdo_basedir_ctx_destroy(basedir_ctx); + if (!tray->icon_ctx) { + sway_log(SWAY_ERROR, "Failed to create sfdo icon context"); + goto error; + } + + const char *theme_name = bar->config->icon_theme; + tray->icon_theme = load_icon_theme(tray->icon_ctx, theme_name); + tray->icon_theme_name = theme_name ? strdup(theme_name) : NULL; + tray->watcher_xdg = create_watcher("freedesktop", tray->bus); tray->watcher_kde = create_watcher("kde", tray->bus); @@ -68,9 +102,13 @@ struct swaybar_tray *create_tray(struct swaybar *bar) { init_host(&tray->host_xdg, "freedesktop", tray); init_host(&tray->host_kde, "kde", tray); - init_themes(&tray->themes, &tray->basedirs); - return tray; + +error: + sfdo_icon_ctx_destroy(tray->icon_ctx); + sd_bus_flush_close_unref(tray->bus); + free(tray); + return NULL; } void destroy_tray(struct swaybar_tray *tray) { @@ -86,10 +124,37 @@ void destroy_tray(struct swaybar_tray *tray) { destroy_watcher(tray->watcher_xdg); destroy_watcher(tray->watcher_kde); sd_bus_flush_close_unref(tray->bus); - finish_themes(tray->themes, tray->basedirs); + sfdo_icon_theme_destroy(tray->icon_theme); + sfdo_icon_ctx_destroy(tray->icon_ctx); + free(tray->icon_theme_name); free(tray); } +void tray_reload_icon_theme(struct swaybar_tray *tray, const char *name) { + if ((!tray->icon_theme_name && !name) || + (tray->icon_theme_name && name && + strcmp(tray->icon_theme_name, name) == 0)) { + return; + } + sway_log(SWAY_DEBUG, "Reloading tray icon theme: '%s' -> '%s'", + tray->icon_theme_name ? tray->icon_theme_name : "(default)", + name ? name : "(default)"); + sfdo_icon_theme_destroy(tray->icon_theme); + tray->icon_theme = load_icon_theme(tray->icon_ctx, name); + free(tray->icon_theme_name); + tray->icon_theme_name = name ? strdup(name) : NULL; + + // invalidate per-SNI cached state so icons reload on next render + for (int i = 0; i < tray->items->length; ++i) { + struct swaybar_sni *sni = tray->items->items[i]; + sfdo_icon_theme_destroy(sni->icon_theme_override); + sni->icon_theme_override = NULL; + sni->icon_size = 0; + sni->target_size = 0; + } + set_bar_dirty(tray->bar); +} + void tray_in(int fd, short mask, void *data) { struct swaybar *bar = data; int ret;