413 lines
10 KiB
C++
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));
|
|
}
|