Files
waylight/src/IconRegistry.cpp
2025-10-15 03:33:35 +03:00

413 lines
10 KiB
C++

#include "IconRegistry.hpp"
#include <algorithm>
#include <cassert>
#include <cmath>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <print>
#include <ranges>
#include <stdexcept>
#include <lunasvg.h>
#include <mini/ini.h>
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<std::string> 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<int> 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<unsigned char> 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<std::filesystem::path> 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<int> optimal_size, bool symbolic, std::optional<Color> 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<int> 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));
}