#include "IconRegistry.hpp" #include #include #include #include #include #include #include #include #include #include #include static inline auto color_to_string(Color const &c) -> std::string { auto const r { c.r / 255.0 }, g { c.g / 255.0 }, b { c.b / 255.0 }; auto const maxv { std::fmax(r, std::fmax(g, b)) }; auto const minv { std::fmin(r, std::fmin(g, b)) }; auto const d { maxv - minv }; double h = 0.0; if (d > 1e-6) { if (maxv == r) h = 60.0 * std::fmod(((g - b) / d), 6.0); else if (maxv == g) h = 60.0 * (((b - r) / d) + 2.0); else h = 60.0 * (((r - g) / d) + 4.0); } if (h < 0.0) h += 360.0; if (h >= 345 || h < 15) return "red"; if (h < 45) return "orange"; if (h < 70) return "yellow"; if (h < 170) return "green"; if (h < 200) return "teal"; if (h < 250) return "cyan"; if (h < 290) return "blue"; if (h < 330) return "purple"; return "pink"; } static auto detect_desktop_environment() -> std::string const { if (auto const de { getenv("XDG_CURRENT_DESKTOP") }) return de; if (auto const sess { getenv("DESKTOP_SESSION") }) return sess; return "unknown"; } static auto kde_get_theme() -> std::string const { std::string home { getenv("HOME") ? getenv("HOME") : "" }; std::string const paths[] { home + "/.config/kdeglobals", home + "/.config/kdedefaults/kdeglobals", }; for (auto p : paths) { std::ifstream f(p); if (!f) continue; std::string line; auto in_icons { false }; while (std::getline(f, line)) { if (line == "[Icons]") { in_icons = true; continue; } if (line.starts_with("[")) in_icons = false; if (in_icons && line.starts_with("Theme=")) return line.substr(strlen("Theme=")); } } return {}; } static auto get_current_icon_theme() -> std::optional const { auto de { detect_desktop_environment() }; std::transform(de.begin(), de.end(), de.begin(), ::tolower); if (de.find("kde") != std::string::npos || de.find("plasma") != std::string::npos) { if (auto const t = kde_get_theme(); !t.empty()) { return t; } } return std::nullopt; } auto IconTheme::lookup(std::string_view const name, std::optional optimal_size) const -> Icon const { for (auto const &dir : m_directories) { if (optimal_size && *optimal_size < dir.size) continue; for (auto const &dir_entry : std::filesystem::recursive_directory_iterator(dir.path)) { if (!dir_entry.is_regular_file()) continue; if (dir_entry.path().stem() != name) continue; // This can be derived from the image filename. // But we probably won't need it either way... if (dir_entry.path().extension() == ".icon") continue; if (dir_entry.path().extension() == ".svg") { auto const document { lunasvg::Document::loadFromFile( dir_entry.path()) }; if (!document) { throw std::runtime_error("Failed to load SVG file"); } auto const bitmap { document->renderToBitmap() }; if (bitmap.width() == 0 || bitmap.height() == 0) continue; std::vector rgba( bitmap.width() * bitmap.height() * 4); auto *src = bitmap.data(); for (size_t i = 0, px = bitmap.width() * bitmap.height(); i < px; ++i) { uint8_t b = src[i * 4 + 0]; uint8_t g = src[i * 4 + 1]; uint8_t r = src[i * 4 + 2]; uint8_t a = src[i * 4 + 3]; if (a != 0) { r = (uint8_t)std::min( 255, (int)((r * 255 + a / 2) / a)); g = (uint8_t)std::min( 255, (int)((g * 255 + a / 2) / a)); b = (uint8_t)std::min( 255, (int)((b * 255 + a / 2) / a)); } rgba[i * 4 + 0] = r; rgba[i * 4 + 1] = g; rgba[i * 4 + 2] = b; rgba[i * 4 + 3] = a; } Image const img { .data = rgba.data(), .width = bitmap.width(), .height = bitmap.height(), .mipmaps = 1, .format = PIXELFORMAT_UNCOMPRESSED_R8G8B8A8, }; auto const tex { LoadTextureFromImage(img) }; if (!IsTextureValid(tex)) { throw std::runtime_error( "Failed to load texture from image"); } Icon const icon(dir_entry.path(), tex, dir.size); return icon; } else { auto const tex { LoadTexture(dir_entry.path().c_str()) }; if (!IsTextureValid(tex)) { throw std::runtime_error( "Failed to load texture from file"); } Icon const icon(dir_entry.path(), tex, dir.size); return icon; } } } if (optimal_size) { // We failed to find a icon big enough, try again with smaller sizes // than our optimal. return lookup(name, std::nullopt); } throw std::runtime_error( std::format("Failed to find icon `{}` in theme!", name)); } IconTheme::IconTheme(std::filesystem::path const &themes_directory_path) { for (auto const &dir : std::filesystem::directory_iterator(themes_directory_path)) { if (!dir.is_directory()) continue; auto const index_path = dir.path() / "index.theme"; if (!std::filesystem::is_regular_file(index_path)) continue; m_names.push_back(dir.path().filename().string()); mINI::INIFile ini_file(index_path); mINI::INIStructure ini; ini_file.read(ini); auto const &inherits { ini["Icon Theme"]["Inherits"] }; std::ranges::copy(std::string_view(inherits) | std::views::split(',') | std::views::transform( [](auto &&s) { return std::string(s.begin(), s.end()); }), std::back_inserter(m_inherits)); auto const &directories { ini["Icon Theme"]["Directories"] }; for (auto const &&dir_entry : directories | std::views::split(',')) { auto const dir_entry_str { std::string( dir_entry.begin(), dir_entry.end()) }; auto const path { std::filesystem::path(dir_entry_str) }; auto const path_actual { dir.path() / path }; if (!std::filesystem::is_directory(path_actual)) continue; auto const &type_raw { ini[dir_entry_str]["Type"] }; DirectoryEntry::Type type; if (type_raw == "Fixed") { type = DirectoryEntry::Type::Fixed; } else if (type_raw == "Scalable") { type = DirectoryEntry::Type::Scalable; } else if (type_raw == "Threshold") { type = DirectoryEntry::Type::Threshold; } else { continue; } auto const &context_raw { ini[dir_entry_str]["Context"] }; DirectoryEntry::Context context; if (context_raw == "Actions") { context = DirectoryEntry::Context::Actions; } else if (context_raw == "Devices") { context = DirectoryEntry::Context::Devices; } else if (context_raw == "FileSystems") { context = DirectoryEntry::Context::FileSystems; } else if (context_raw == "MimeTypes") { context = DirectoryEntry::Context::MimeTypes; } else if (context_raw == "Places") { context = DirectoryEntry::Context::Places; } else { continue; } int size { std::atoi(ini[dir_entry_str]["Size"].c_str()) }; if (size == 0) { if (type == DirectoryEntry::Type::Scalable) { int minSize = std::atoi(ini[dir_entry_str]["MinSize"].c_str()); int maxSize = std::atoi(ini[dir_entry_str]["MaxSize"].c_str()); size = std::max(minSize, maxSize); } if (size == 0) continue; } m_directories.push_back({ .path = path_actual, .size = size, .type = type, .context = context, }); } // Sort by biggest sizes first. This is important for the lookup // algorithm. Mess with this, change that. std::sort(m_directories.begin(), m_directories.end(), [](DirectoryEntry const &a, DirectoryEntry const &b) { return a.size > b.size; }); } } IconRegistry::IconRegistry() { m_preferred_theme = get_current_icon_theme(); std::vector theme_directory_paths; { auto const *env { getenv("HOME") }; if (env && *env) { theme_directory_paths.push_back( std::filesystem::path(env) / ".icons"); } } { auto const *env { getenv("XDG_DATA_DIRS") }; if (env && *env) { std::ranges::copy(std::string_view(env) | std::views::split(':') | std::views::transform([](auto &&s) { return std::filesystem::path(s.begin(), s.end()) / "icons"; }), std::back_inserter(theme_directory_paths)); } } { std::filesystem::path const paths[] { "/usr/share/pixmaps", "/usr/local/share/icons", "/usr/share/icons", }; for (auto const &path : paths) { if (std::filesystem::exists(path)) theme_directory_paths.push_back(path); } } for (auto &&path : theme_directory_paths | std::views::filter([](std::filesystem::path const &path) { return std::filesystem::is_directory(path); })) { try { m_themes.push_back({ path }); } catch (...) { } } if (m_themes.empty()) { throw std::runtime_error("Could not find any icon themes."); } if (m_preferred_theme) { TraceLog(LOG_INFO, std::format("Preferred theme: {}", *m_preferred_theme).c_str()); std::stable_partition( m_themes.begin(), m_themes.end(), [&](auto const &t) { bool found { false }; for (auto const &e : t.names()) { if (e == *m_preferred_theme) { found = true; break; } } return found; }); } } auto IconRegistry::lookup(std::string_view const name, std::optional optimal_size, bool symbolic, std::optional color) -> Icon const & { if (!color && m_color) color = m_color; std::string color_name {}; if (color) { auto const col { color_to_string(*color) }; if (!col.empty()) { color_name = "-" + col; } } if (symbolic) { try { auto const n { std::format("{}{}-symbolic", color_name, name) }; return lookup_cached(n, optimal_size); } catch (...) { return lookup(name, optimal_size, false, color); } } else { return lookup_cached( std::string_view(std::format("{}{}", name, color_name)), optimal_size); } } auto IconRegistry::lookup_cached(std::string_view const name, std::optional optimal_size) -> Icon const & { std::string name_s(name); if (m_cached_icons.contains(name_s)) { auto const &icon = m_cached_icons.at(name_s); if (optimal_size && icon.size() >= *optimal_size) return icon; } for (auto const &theme : m_themes) { try { auto const icon = theme.lookup(name, optimal_size); m_cached_icons.insert_or_assign(name_s, icon); return m_cached_icons.at(name_s); } catch (...) { } } throw std::runtime_error(std::format("Failed to find icon `{}`!", name)); }