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;